Skip to content

Commit f578c2b

Browse files
committed
testing: improve area below filter
With results, the area is both more compact and useful. Passed/Total tests are shown on the left, with an icon summarizing the view. On the right, the wall-clock duration of the tests and a button to rerun tests. The alt action on the button reruns the tests with the debugger. Previously the rerun action was not very discoverable--had a few issues about it over the course of time. ![](https://memes.peet.io/img/23-08-8bb85d55-6304-4acb-bef3-9c1126da8c49.png) There's also no awkward gap when there's no test results: ![](https://memes.peet.io/img/23-08-f5bcda56-60d7-4c8d-9822-e311b68079d7.png) Fixes #188841 cc @eleanorjboyd
1 parent 9ed20df commit f578c2b

File tree

7 files changed

+169
-151
lines changed

7 files changed

+169
-151
lines changed

src/vs/workbench/contrib/testing/browser/icons.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { TestResultState } from 'vs/workbench/contrib/testing/common/testTypes';
1414
export const testingViewIcon = registerIcon('test-view-icon', Codicon.beaker, localize('testViewIcon', 'View icon of the test view.'));
1515
export const testingResultsIcon = registerIcon('test-results-icon', Codicon.checklist, localize('testingResultsIcon', 'Icons for test results.'));
1616
export const testingRunIcon = registerIcon('testing-run-icon', Codicon.run, localize('testingRunIcon', 'Icon of the "run test" action.'));
17+
export const testingRerunIcon = registerIcon('testing-rerun-icon', Codicon.refresh, localize('testingRerunIcon', 'Icon of the "rerun tests" action.'));
1718
export const testingRunAllIcon = registerIcon('testing-run-all-icon', Codicon.runAll, localize('testingRunAllIcon', 'Icon of the "run all tests" action.'));
1819
// todo: https://github.com/microsoft/vscode-codicons/issues/72
1920
export const testingDebugAllIcon = registerIcon('testing-debug-all-icon', Codicon.debugAltSmall, localize('testingDebugAllIcon', 'Icon of the "debug all tests" action.'));

src/vs/workbench/contrib/testing/browser/media/testing.css

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,21 @@
128128
opacity: 0.8;
129129
}
130130

131-
.test-explorer .test-explorer-messages {
131+
.test-explorer .result-summary-container {
132132
padding: 0 12px 8px;
133133
font-variant-numeric: tabular-nums;
134+
height: 27px;
135+
box-sizing: border-box;
136+
}
137+
138+
.test-explorer .result-summary {
139+
display: flex;
140+
align-items: center;
141+
gap: 2px;
142+
}
143+
144+
.test-explorer .result-summary > span {
145+
flex-grow: 1;
134146
}
135147

136148
.monaco-workbench

src/vs/workbench/contrib/testing/browser/testing.contribution.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { testingResultsIcon, testingViewIcon } from 'vs/workbench/contrib/testin
2424
import { TestingDecorationService, TestingDecorations } from 'vs/workbench/contrib/testing/browser/testingDecorations';
2525
import { TestingExplorerView } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
2626
import { CloseTestPeek, GoToNextMessageAction, GoToPreviousMessageAction, OpenMessageInEditorAction, TestResultsView, TestingOutputPeekController, TestingPeekOpener, ToggleTestingPeekHistory } from 'vs/workbench/contrib/testing/browser/testingOutputPeek';
27-
import { ITestingProgressUiService, TestingProgressTrigger, TestingProgressUiService } from 'vs/workbench/contrib/testing/browser/testingProgressUiService';
27+
import { TestingProgressTrigger } from 'vs/workbench/contrib/testing/browser/testingProgressUiService';
2828
import { TestingViewPaneContainer } from 'vs/workbench/contrib/testing/browser/testingViewPaneContainer';
2929
import { testingConfiguration } from 'vs/workbench/contrib/testing/common/configuration';
3030
import { TestCommandId, Testing } from 'vs/workbench/contrib/testing/common/constants';
@@ -52,7 +52,6 @@ registerSingleton(ITestingContinuousRunService, TestingContinuousRunService, Ins
5252
registerSingleton(ITestResultService, TestResultService, InstantiationType.Delayed);
5353
registerSingleton(ITestExplorerFilterState, TestExplorerFilterState, InstantiationType.Delayed);
5454
registerSingleton(ITestingPeekOpener, TestingPeekOpener, InstantiationType.Delayed);
55-
registerSingleton(ITestingProgressUiService, TestingProgressUiService, InstantiationType.Delayed);
5655
registerSingleton(ITestingDecorationsService, TestingDecorationService, InstantiationType.Delayed);
5756

5857
const viewContainer = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({

src/vs/workbench/contrib/testing/browser/testingExplorerView.ts

Lines changed: 134 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelega
1212
import { DefaultKeyboardNavigationDelegate, IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
1313
import { ITreeContextMenuEvent, ITreeFilter, ITreeNode, ITreeRenderer, ITreeSorter, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree';
1414
import { Action, ActionRunner, IAction, Separator } from 'vs/base/common/actions';
15+
import { mapFind } from 'vs/base/common/arrays';
1516
import { RunOnceScheduler, disposableTimeout } from 'vs/base/common/async';
1617
import { Color, RGBA } from 'vs/base/common/color';
1718
import { Emitter, Event } from 'vs/base/common/event';
1819
import { FuzzyScore } from 'vs/base/common/filters';
1920
import { KeyCode } from 'vs/base/common/keyCodes';
20-
import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
21+
import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle';
2122
import { fuzzyContains } from 'vs/base/common/strings';
2223
import { ThemeIcon } from 'vs/base/common/themables';
2324
import { isDefined } from 'vs/base/common/types';
@@ -26,7 +27,7 @@ import 'vs/css!./media/testing';
2627
import { MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer';
2728
import { localize } from 'vs/nls';
2829
import { DropdownWithPrimaryActionViewItem } from 'vs/platform/actions/browser/dropdownWithPrimaryActionViewItem';
29-
import { MenuEntryActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
30+
import { MenuEntryActionViewItem, createActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
3031
import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions';
3132
import { ICommandService } from 'vs/platform/commands/common/commands';
3233
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
@@ -40,38 +41,40 @@ import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } fro
4041
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
4142
import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles';
4243
import { foreground } from 'vs/platform/theme/common/colorRegistry';
44+
import { spinningLoading } from 'vs/platform/theme/common/iconRegistry';
4345
import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
4446
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
47+
import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands';
4548
import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane';
4649
import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
4750
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
4851
import { IViewDescriptorService } from 'vs/workbench/common/views';
49-
import { TreeProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/treeProjection';
50-
import { ListProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/listProjection';
5152
import { ITestTreeProjection, TestExplorerTreeElement, TestItemTreeElement, TestTreeErrorMessage } from 'vs/workbench/contrib/testing/browser/explorerProjections/index';
53+
import { ListProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/listProjection';
5254
import { getTestItemContextOverlay } from 'vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay';
5355
import { TestingObjectTree } from 'vs/workbench/contrib/testing/browser/explorerProjections/testingObjectTree';
5456
import { ISerializedTestTreeCollapseState } from 'vs/workbench/contrib/testing/browser/explorerProjections/testingViewState';
57+
import { TreeProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/treeProjection';
5558
import * as icons from 'vs/workbench/contrib/testing/browser/icons';
59+
import { DebugLastRun, ReRunLastRun } from 'vs/workbench/contrib/testing/browser/testExplorerActions';
5660
import { TestingExplorerFilter } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter';
57-
import { CountSummary, ITestingProgressUiService } from 'vs/workbench/contrib/testing/browser/testingProgressUiService';
61+
import { CountSummary, collectTestStateCounts, getTestProgressText } from 'vs/workbench/contrib/testing/browser/testingProgressUiService';
5862
import { TestingConfigKeys, TestingCountBadge, getTestingConfiguration } from 'vs/workbench/contrib/testing/common/configuration';
5963
import { TestCommandId, TestExplorerViewMode, TestExplorerViewSorting, Testing, labelForTestInState } from 'vs/workbench/contrib/testing/common/constants';
6064
import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue';
6165
import { ITestExplorerFilterState, TestExplorerFilterState, TestFilterTerm } from 'vs/workbench/contrib/testing/common/testExplorerFilterState';
6266
import { TestId } from 'vs/workbench/contrib/testing/common/testId';
6367
import { ITestProfileService, canUseProfileWithTest } from 'vs/workbench/contrib/testing/common/testProfileService';
64-
import { TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult';
68+
import { LiveTestResult, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult';
6569
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
6670
import { IMainThreadTestCollection, ITestService, testCollectionIsEmpty } from 'vs/workbench/contrib/testing/common/testService';
6771
import { ITestRunProfile, InternalTestItem, TestItemExpandState, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes';
6872
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
6973
import { ITestingContinuousRunService } from 'vs/workbench/contrib/testing/common/testingContinuousRunService';
7074
import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener';
71-
import { cmpPriority, isFailedState, isStateWithResult } from 'vs/workbench/contrib/testing/common/testingStates';
75+
import { cmpPriority, isFailedState, isStateWithResult, statesInOrder } from 'vs/workbench/contrib/testing/common/testingStates';
7276
import { IActivityService, IconBadge, NumberBadge } from 'vs/workbench/services/activity/common/activity';
7377
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
74-
import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands';
7578

7679
const enum LastFocusState {
7780
Input,
@@ -83,10 +86,8 @@ export class TestingExplorerView extends ViewPane {
8386
private filterActionBar = this._register(new MutableDisposable());
8487
private container!: HTMLElement;
8588
private treeHeader!: HTMLElement;
86-
private countSummary: CountSummary | undefined;
8789
private discoveryProgress = this._register(new MutableDisposable<UnmanagedProgress>());
8890
private readonly filter = this._register(new MutableDisposable<TestingExplorerFilter>());
89-
private readonly badgeDisposable = this._register(new MutableDisposable<IDisposable>());
9091
private readonly filterFocusListener = this._register(new MutableDisposable());
9192
private readonly dimensions = { width: 0, height: 0 };
9293
private lastFocusState = LastFocusState.Input;
@@ -97,7 +98,6 @@ export class TestingExplorerView extends ViewPane {
9798

9899
constructor(
99100
options: IViewletViewOptions,
100-
@IActivityService private readonly activityService: IActivityService,
101101
@IContextMenuService contextMenuService: IContextMenuService,
102102
@IKeybindingService keybindingService: IKeybindingService,
103103
@IConfigurationService configurationService: IConfigurationService,
@@ -108,10 +108,8 @@ export class TestingExplorerView extends ViewPane {
108108
@IThemeService themeService: IThemeService,
109109
@ITestService private readonly testService: ITestService,
110110
@ITelemetryService telemetryService: ITelemetryService,
111-
@ITestingProgressUiService private readonly testProgressService: ITestingProgressUiService,
112111
@ITestProfileService private readonly testProfileService: ITestProfileService,
113112
@ICommandService private readonly commandService: ICommandService,
114-
@ITestingContinuousRunService private readonly crService: ITestingContinuousRunService,
115113
) {
116114
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
117115

@@ -127,9 +125,6 @@ export class TestingExplorerView extends ViewPane {
127125
}));
128126

129127
this._register(testProfileService.onDidChange(() => this.updateActions()));
130-
const onDidChangeTestingCountBadge = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('testing.countBadge'));
131-
this._register(onDidChangeTestingCountBadge(this.renderActivityBadge, this));
132-
this._register(crService.onDidChange(this.renderActivityBadge, this));
133128
}
134129

135130
public override shouldShowWelcome() {
@@ -278,21 +273,8 @@ export class TestingExplorerView extends ViewPane {
278273
this.treeHeader = dom.append(this.container, dom.$('.test-explorer-header'));
279274
this.filterActionBar.value = this.createFilterActionBar();
280275

281-
const messagesContainer = dom.append(this.treeHeader, dom.$('.test-explorer-messages'));
282-
this._register(this.testProgressService.onTextChange(text => {
283-
const hadText = !!messagesContainer.innerText;
284-
const hasText = !!text;
285-
messagesContainer.innerText = text;
286-
287-
if (hadText !== hasText) {
288-
this.layoutBody();
289-
}
290-
}));
291-
this._register(this.testProgressService.onCountChange((text: CountSummary) => {
292-
this.countSummary = text;
293-
this.renderActivityBadge();
294-
}));
295-
this.testProgressService.update();
276+
const messagesContainer = dom.append(this.treeHeader, dom.$('.result-summary-container'));
277+
this._register(this.instantiationService.createInstance(ResultSummaryView, messagesContainer));
296278

297279
const listContainer = dom.append(this.container, dom.$('.test-explorer-tree'));
298280
this.viewModel = this.instantiationService.createInstance(TestingExplorerViewModel, listContainer, this.onDidChangeBodyVisibility);
@@ -441,17 +423,130 @@ export class TestingExplorerView extends ViewPane {
441423
}
442424
}
443425

444-
private renderActivityBadge() {
445-
const countBadgeType = this.configurationService.getValue<TestingCountBadge>(TestingConfigKeys.CountBadge);
446-
if (this.countSummary && countBadgeType !== TestingCountBadge.Off && this.countSummary[countBadgeType] !== 0) {
447-
const badge = new NumberBadge(this.countSummary[countBadgeType], num => this.getLocalizedBadgeString(countBadgeType, num));
448-
this.badgeDisposable.value = this.activityService.showViewActivity(Testing.ExplorerViewId, { badge });
426+
/**
427+
* @override
428+
*/
429+
protected override layoutBody(height = this.dimensions.height, width = this.dimensions.width): void {
430+
super.layoutBody(height, width);
431+
this.dimensions.height = height;
432+
this.dimensions.width = width;
433+
this.container.style.height = `${height}px`;
434+
this.viewModel.layout(height - this.treeHeader.clientHeight, width);
435+
this.filter.value?.layout(width);
436+
}
437+
}
438+
439+
const SUMMARY_RENDER_INTERVAL = 200;
440+
441+
class ResultSummaryView extends Disposable {
442+
private elementsWereAttached = false;
443+
private badgeType: TestingCountBadge;
444+
private lastBadge?: NumberBadge | IconBadge;
445+
private readonly badgeDisposable = this._register(new MutableDisposable());
446+
private readonly renderLoop = this._register(new RunOnceScheduler(() => this.render(), SUMMARY_RENDER_INTERVAL));
447+
private readonly elements = dom.h('div.result-summary', [
448+
dom.h('div@status'),
449+
dom.h('div@count'),
450+
dom.h('div@count'),
451+
dom.h('span'),
452+
dom.h('duration@duration'),
453+
dom.h('a@rerun'),
454+
]);
455+
456+
constructor(
457+
private readonly container: HTMLElement,
458+
@ITestResultService private readonly resultService: ITestResultService,
459+
@IActivityService private readonly activityService: IActivityService,
460+
@ITestingContinuousRunService private readonly crService: ITestingContinuousRunService,
461+
@IConfigurationService configurationService: IConfigurationService,
462+
@IInstantiationService instantiationService: IInstantiationService,
463+
) {
464+
super();
465+
466+
this.badgeType = configurationService.getValue<TestingCountBadge>(TestingConfigKeys.CountBadge);
467+
this._register(resultService.onResultsChanged(this.render, this));
468+
this._register(configurationService.onDidChangeConfiguration(e => {
469+
if (e.affectsConfiguration(TestingConfigKeys.CountBadge)) {
470+
this.badgeType = configurationService.getValue(TestingConfigKeys.CountBadge);
471+
this.render();
472+
}
473+
}));
474+
475+
const ab = this._register(new ActionBar(this.elements.rerun, {
476+
actionViewItemProvider: action => createActionViewItem(instantiationService, action),
477+
}));
478+
ab.push(instantiationService.createInstance(MenuItemAction,
479+
{ ...new ReRunLastRun().desc, icon: icons.testingRerunIcon },
480+
{ ...new DebugLastRun().desc, icon: icons.testingDebugIcon },
481+
{},
482+
undefined,
483+
), { icon: true, label: false });
484+
485+
this.render();
486+
}
487+
488+
private render() {
489+
const { results } = this.resultService;
490+
const { count, root, status, duration, rerun } = this.elements;
491+
if (!results.length) {
492+
this.container.removeChild(root);
493+
this.container.innerText = localize('noResults', 'No test results yet.');
494+
this.elementsWereAttached = false;
495+
return;
496+
}
497+
498+
const live = results.filter(r => !r.completedAt) as LiveTestResult[];
499+
let counts: CountSummary;
500+
if (live.length) {
501+
status.className = ThemeIcon.asClassName(spinningLoading);
502+
counts = collectTestStateCounts(true, live);
503+
this.renderLoop.schedule();
504+
505+
const last = live[live.length - 1];
506+
duration.textContent = formatDuration(Date.now() - last.startedAt);
507+
rerun.style.display = 'none';
508+
} else {
509+
const last = results[0];
510+
const dominantState = mapFind(statesInOrder, s => last.counts[s] > 0 ? s : undefined);
511+
status.className = ThemeIcon.asClassName(icons.testingStatesToIcons.get(dominantState ?? TestResultState.Unset)!);
512+
counts = collectTestStateCounts(true, [last]);
513+
duration.textContent = last instanceof LiveTestResult ? formatDuration(last.completedAt! - last.startedAt) : '';
514+
rerun.style.display = 'block';
515+
}
516+
517+
count.textContent = `${counts.passed}/${counts.totalWillBeRun}`;
518+
count.title = getTestProgressText(counts);
519+
this.renderActivityBadge(counts);
520+
521+
if (!this.elementsWereAttached) {
522+
dom.clearNode(this.container);
523+
this.container.appendChild(root);
524+
this.elementsWereAttached = true;
525+
}
526+
}
527+
528+
private renderActivityBadge(countSummary: CountSummary) {
529+
if (countSummary && this.badgeType !== TestingCountBadge.Off && countSummary[this.badgeType] !== 0) {
530+
if (this.lastBadge instanceof NumberBadge && this.lastBadge.number === countSummary[this.badgeType]) {
531+
return;
532+
}
533+
534+
this.lastBadge = new NumberBadge(countSummary[this.badgeType], num => this.getLocalizedBadgeString(this.badgeType, num));
449535
} else if (this.crService.isEnabled()) {
450-
const badge = new IconBadge(icons.testingContinuousIsOn, () => localize('testingContinuousBadge', 'Tests are being watched for changes'));
451-
this.badgeDisposable.value = this.activityService.showViewActivity(Testing.ExplorerViewId, { badge });
536+
if (this.lastBadge instanceof IconBadge && this.lastBadge.icon === icons.testingContinuousIsOn) {
537+
return;
538+
}
539+
540+
this.lastBadge = new IconBadge(icons.testingContinuousIsOn, () => localize('testingContinuousBadge', 'Tests are being watched for changes'));
452541
} else {
453-
this.badgeDisposable.value = undefined;
542+
if (!this.lastBadge) {
543+
return;
544+
}
545+
546+
this.lastBadge = undefined;
454547
}
548+
549+
this.badgeDisposable.value = this.lastBadge && this.activityService.showViewActivity(Testing.ExplorerViewId, { badge: this.lastBadge });
455550
}
456551

457552
private getLocalizedBadgeString(countBadgeType: TestingCountBadge, count: number): string {
@@ -464,18 +559,6 @@ export class TestingExplorerView extends ViewPane {
464559
return localize('testingCountBadgeFailed', '{0} failed tests', count);
465560
}
466561
}
467-
468-
/**
469-
* @override
470-
*/
471-
protected override layoutBody(height = this.dimensions.height, width = this.dimensions.width): void {
472-
super.layoutBody(height, width);
473-
this.dimensions.height = height;
474-
this.dimensions.width = width;
475-
this.container.style.height = `${height}px`;
476-
this.viewModel.layout(height - this.treeHeader.clientHeight, width);
477-
this.filter.value?.layout(width);
478-
}
479562
}
480563

481564
const enum WelcomeExperience {

0 commit comments

Comments
 (0)