Skip to content

Commit

Permalink
flag error in status bar and added a reveal-terminal button in TestEx…
Browse files Browse the repository at this point in the history
…plorer (#985)

* added revealOutput

* fix lint issues

* rename setting to autoRevealOutput and add an "off" option

* added reveal-output item menu and show error in statusBar

* update readme
  • Loading branch information
connectdotz committed Jan 11, 2023
1 parent 249deb8 commit 1ca2d51
Show file tree
Hide file tree
Showing 18 changed files with 329 additions and 116 deletions.
2 changes: 1 addition & 1 deletion README.md
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
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
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
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
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
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
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
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
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

0 comments on commit 1ca2d51

Please sign in to comment.