Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

flag error in status bar and added a reveal-terminal button in TestExplorer #985

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ shows active workspace has an execution error.
### How to use the Test Explorer?
Users with `vscode` v1.59 and `vscode-jest` v4.1 and up will start to see tests appearing in the test explorer automatically. Test explorer provides a "test-centric" view, allows users to run/debug tests directly from the explorer, and provides a native terminal output experience (with colors!):

<img src="images/testExplorer.png" alt="testExplorer.png" width="800"/>
<img src="images/testExplorer-5.1.1.png" alt="testExplorer.png" width="800"/>

<a id='how-to-toggle-auto-run'>**How to toggle autoRun for the workspace?**</a>
- In TestExplorer, click on the root of the test tree, i.e. the one with the workspace name and the current autoRun mode. You will see a list of buttons to its right.
Expand Down
2 changes: 2 additions & 0 deletions __mocks__/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const ViewColumn = {

const TestMessage = jest.fn();
const TestRunRequest = jest.fn();
const ThemeColor = jest.fn();

const EventEmitter = jest.fn().mockImplementation(() => {
return {
Expand All @@ -100,6 +101,7 @@ const QuickPickItemKind = {
};

export = {
ThemeColor,
CodeLens,
languages,
StatusBarAlignment,
Expand Down
Binary file added images/status-bar-error.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/testExplorer-5.1.1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 14 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,11 @@
"title": "Toggle Coverage On",
"icon": "$(color-mode)"
},
{
"command": "io.orta.jest.test-item.reveal-output",
"title": "Reveal Test Output",
"icon": "$(terminal)"
},
{
"command": "io.orta.jest.test-item.view-snapshot",
"title": "View Snapshot",
Expand Down Expand Up @@ -296,24 +301,29 @@
"testing/item/context": [
{
"command": "io.orta.jest.test-item.auto-run.toggle-off",
"group": "inline",
"group": "inline@1",
"when": "testId in jest.autoRun.on"
},
{
"command": "io.orta.jest.test-item.auto-run.toggle-on",
"group": "inline",
"group": "inline@1",
"when": "testId in jest.autoRun.off"
},
{
"command": "io.orta.jest.test-item.coverage.toggle-off",
"group": "inline",
"group": "inline@2",
"when": "testId in jest.coverage.on"
},
{
"command": "io.orta.jest.test-item.coverage.toggle-on",
"group": "inline",
"group": "inline@2",
"when": "testId in jest.coverage.off"
},
{
"command": "io.orta.jest.test-item.reveal-output",
"group": "inline@3",
"when": "testId in jest.workspaceRoot"
},
{
"command": "io.orta.jest.test-item.update-snapshot"
}
Expand Down
14 changes: 11 additions & 3 deletions src/JestExt/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,14 @@ export class JestExt {
this.updateStatusBar({ state: 'running' });
break;
}
case 'end':
this.updateStatusBar({ state: 'done' });
case 'end': {
const state = event.error ? 'exec-error' : 'done';
this.updateStatusBar({ state });
break;
}
case 'exit':
if (event.error) {
this.updateStatusBar({ state: 'stopped' });
this.updateStatusBar({ state: 'exec-error' });
messaging.systemErrorMessage(
prefixWorkspace(this.extContext, event.error),
...this.buildMessageActions(['wizard', 'disable-folder', 'help'])
Expand All @@ -214,6 +216,12 @@ export class JestExt {
this.updateStatusBar({ state: 'done' });
}
break;
case 'data': {
if (event.isError) {
this.updateStatusBar({ state: 'exec-error' });
}
break;
}
case 'long-run': {
const msg = prefixWorkspace(this.extContext, this.longRunMessage(event));
messaging.systemWarningMessage(msg, ...this.buildMessageActions(['help-long-run']));
Expand Down
13 changes: 2 additions & 11 deletions src/JestExt/process-listeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,17 +340,8 @@ export class RunTestListener extends AbstractProcessListener {
this.runEnded();

// possible no output will be generated
const matched = output.match(RUN_EXEC_ERROR);
if (matched) {
this.onRunEvent.fire({
type: 'data',
process,
text: matched[1],
newLine: true,
isError: true,
});
}
this.onRunEvent.fire({ type: 'end', process });
const error = output.match(RUN_EXEC_ERROR)?.[1];
this.onRunEvent.fire({ type: 'end', process, error });
}
}
protected onExecutableOutput(process: JestProcess, output: string, raw: string): void {
Expand Down
2 changes: 1 addition & 1 deletion src/JestExt/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export type JestRunEvent = RunEventBase &
| { type: 'data'; text: string; raw?: string; newLine?: boolean; isError?: boolean }
| { type: 'process-start' }
| { type: 'start' }
| { type: 'end' }
| { type: 'end'; error?: string }
| { type: 'exit'; error?: string; code?: number }
| { type: 'long-run'; threshold: number; numTotalTestSuites?: number }
);
Expand Down
153 changes: 83 additions & 70 deletions src/StatusBar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export enum StatusType {
summary,
}

export type ProcessState = 'running' | 'failed' | 'success' | 'stopped' | 'initial' | 'done';
export type ProcessState = 'running' | 'success' | 'exec-error' | 'stopped' | 'initial' | 'done';
export type AutoRunMode =
| 'auto-run-watch'
| 'auto-run-on-save'
Expand All @@ -18,7 +18,7 @@ export type Mode = AutoRunMode | 'coverage';

type SummaryState = 'summary-warning' | 'summary-pass' | 'stats-not-sync';

export type SBTestStats = TestStats & { isDirty?: boolean };
export type SBTestStats = TestStats & { isDirty?: boolean; state?: ProcessState };
export interface ExtensionStatus {
mode?: Mode[];
stats?: SBTestStats;
Expand All @@ -35,61 +35,46 @@ export type StatusBarUpdate = Partial<ExtensionStatus>;
export interface StatusBarUpdateRequest {
update: (status: StatusBarUpdate) => void;
}
interface SpinnableStatusBarItem
extends Pick<vscode.StatusBarItem, 'command' | 'text' | 'tooltip'> {
interface TypedStatusBarItem {
actual: vscode.StatusBarItem;
readonly type: StatusType;
show(): void;
hide(): void;
}

const createStatusBarItem = (type: StatusType, priority: number): SpinnableStatusBarItem => {
const item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, priority);
type BGColor = 'error' | 'warning';

interface StateInfo {
label: string;
backgroundColor?: BGColor;
}

const createStatusBarItem = (type: StatusType, priority: number): TypedStatusBarItem => {
return {
type,
show: () => item.show(),
hide: () => item.hide(),

get command() {
return item.command;
},
get text() {
return item.text;
},
get tooltip() {
return item.tooltip;
},

set command(_command) {
item.command = _command;
},
set text(_text: string) {
item.text = _text;
},
set tooltip(_tooltip: string | vscode.MarkdownString | undefined) {
item.tooltip = _tooltip;
},
actual: vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, priority),
};
};

// The bottom status bar
export class StatusBar {
private activeStatusItem = createStatusBarItem(StatusType.active, 2);
private summaryStatusItem = createStatusBarItem(StatusType.summary, 1);
private warningColor = new vscode.ThemeColor('statusBarItem.warningBackground');
private errorColor = new vscode.ThemeColor('statusBarItem.errorBackground');

private sourceStatusMap = new Map<string, SourceStatus>();
private _activeFolder?: string;
private summaryOutput?: vscode.OutputChannel;

constructor() {
this.summaryStatusItem.tooltip = 'Jest status summary of the workspace';
this.activeStatusItem.tooltip = 'Jest status of the active folder';
this.summaryStatusItem.actual.tooltip = 'Jest status summary of the workspace';
this.activeStatusItem.actual.tooltip = 'Jest status of the active folder';
}

register(getExtension: (name: string) => JestExt | undefined): vscode.Disposable[] {
const showSummaryOutput = `${extensionName}.show-summary-output`;
const showActiveOutput = `${extensionName}.show-active-output`;
this.summaryStatusItem.command = showSummaryOutput;
this.activeStatusItem.command = showActiveOutput;
this.summaryStatusItem.actual.command = showSummaryOutput;
this.activeStatusItem.actual.command = showActiveOutput;

return [
vscode.commands.registerCommand(showSummaryOutput, () => {
Expand Down Expand Up @@ -162,9 +147,10 @@ export class StatusBar {

if (ss) {
const tooltip = this.getModes(ss.status.mode, false);
this.render(this.buildSourceStatusString(ss), tooltip, this.activeStatusItem);
const stateInfo = this.buildSourceStatusString(ss);
this.render(stateInfo, tooltip, this.activeStatusItem);
} else {
this.activeStatusItem.hide();
this.activeStatusItem.actual.hide();
}
}

Expand All @@ -182,15 +168,25 @@ export class StatusBar {
this.updateSummaryOutput();

const summaryStats: SBTestStats = { fail: 0, success: 0, unknown: 0 };
for (const r of this.sourceStatusMap.values()) {
this.updateSummaryStats(r, summaryStats);
let backgroundColor: BGColor | undefined;
for (const ss of this.sourceStatusMap.values()) {
this.updateSummaryStats(ss, summaryStats);
if (!backgroundColor) {
const color = ss.status.state && this.getStateInfo(ss.status.state).backgroundColor;
if (color) {
backgroundColor = 'warning';
}
}
}

const tooltip = this.buildStatsString(summaryStats, false);
this.render(this.buildStatsString(summaryStats), tooltip, this.summaryStatusItem);
this.render(
{ label: this.buildStatsString(summaryStats), backgroundColor },
tooltip,
this.summaryStatusItem
);
return;
}
this.summaryStatusItem.hide();
this.summaryStatusItem.actual.hide();
}
private buildStatsString(stats: SBTestStats, showIcon = true, alwaysShowDetails = false): string {
const summary: SummaryState = stats.isDirty
Expand All @@ -211,29 +207,42 @@ export class StatusBar {
return output.filter((s) => s).join(' | ');
}

private buildSourceStatusString(ss: SourceStatus): string {
private buildSourceStatusString(ss: SourceStatus): StateInfo {
const stateInfo = ss.status.state && this.getStateInfo(ss.status.state);

const parts: string[] = [
ss.status.state ? this.getMessageByState(ss.status.state) : '',
stateInfo?.label ?? '',
ss.status.mode ? this.getModes(ss.status.mode) : '',
];
return parts.filter((s) => s.length > 0).join(' | ');
return {
label: parts.filter((s) => s.length > 0).join(' | '),
backgroundColor: stateInfo?.backgroundColor,
};
}

private render(text: string, tooltip: string, statusBarItem: SpinnableStatusBarItem) {
private toThemeColor(color?: BGColor): vscode.ThemeColor | undefined {
switch (color) {
case 'error':
return this.errorColor;
case 'warning':
return this.warningColor;
}
}
private render(stateInfo: StateInfo, tooltip: string, statusBarItem: TypedStatusBarItem) {
switch (statusBarItem.type) {
case StatusType.active: {
statusBarItem.text = `Jest: ${text}`;
statusBarItem.tooltip = `'${this.activeFolder}' Jest: ${tooltip}`;
statusBarItem.actual.text = `Jest: ${stateInfo.label}`;
statusBarItem.actual.tooltip = `'${this.activeFolder}' Jest: ${tooltip}`;
statusBarItem.actual.backgroundColor = this.toThemeColor(stateInfo.backgroundColor);
break;
}
case StatusType.summary:
statusBarItem.text = `Jest-WS: ${text}`;
statusBarItem.tooltip = `Workspace(s) stats: ${tooltip}`;
statusBarItem.actual.text = `Jest-WS: ${stateInfo.label}`;
statusBarItem.actual.tooltip = `Workspace(s) stats: ${tooltip}`;
statusBarItem.actual.backgroundColor = this.toThemeColor(stateInfo.backgroundColor);
break;
default:
throw new Error(`unexpected statusType: ${statusBarItem.type}`);
}
statusBarItem.show();
statusBarItem.actual.show();
}

private updateSummaryOutput() {
Expand All @@ -259,36 +268,44 @@ export class StatusBar {
return this.sourceStatusMap.size > 0;
}

private getMessageByState(
private getStateInfo(
state: ProcessState | TestStatsCategory | SummaryState,
showIcon = true
): string {
): StateInfo {
switch (state) {
case 'running':
return showIcon ? '$(sync~spin)' : state;
return { label: showIcon ? '$(sync~spin)' : state };
case 'fail':
return showIcon ? '$(error)' : state;
return { label: showIcon ? '$(error)' : state };
case 'summary-warning':
return showIcon ? '' : 'warning';
case 'failed':
return showIcon ? '$(alert)' : state;
return { label: showIcon ? '' : 'warning' };
case 'exec-error':
return { label: showIcon ? '$(alert)' : state, backgroundColor: 'error' };
case 'stopped':
return { label: state, backgroundColor: 'error' };
case 'success':
return showIcon ? '$(pass)' : state;
return { label: showIcon ? '$(pass)' : state };
case 'initial':
return showIcon ? '...' : state;
return { label: showIcon ? '...' : state };
case 'unknown':
return showIcon ? '$(question)' : state;
return { label: showIcon ? '$(question)' : state };
case 'done':
return showIcon ? '' : 'idle';
return { label: showIcon ? '' : 'idle' };
case 'summary-pass':
return showIcon ? '$(check)' : 'pass';
return { label: showIcon ? '$(check)' : 'pass' };
case 'stats-not-sync':
return showIcon ? '$(sync-ignored)' : state;
return { label: showIcon ? '$(sync-ignored)' : state, backgroundColor: 'warning' };

default:
return state;
return { label: state };
}
}
private getMessageByState(
state: ProcessState | TestStatsCategory | SummaryState,
showIcon: boolean
): string {
return this.getStateInfo(state, showIcon).label;
}
private getModes(modes?: Mode[], showIcon = true): string {
if (!modes || modes.length <= 0) {
return '';
Expand All @@ -308,10 +325,6 @@ export class StatusBar {
return '$(save)';
case 'auto-run-off':
return '$(wrench)';

default:
console.error(`unrecognized mode: ${m}`);
return '';
}
});
return modesStrings.join(showIcon ? ' ' : ', ');
Expand Down
Loading