@@ -12,12 +12,13 @@ import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelega
1212import { DefaultKeyboardNavigationDelegate , IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget' ;
1313import { ITreeContextMenuEvent , ITreeFilter , ITreeNode , ITreeRenderer , ITreeSorter , TreeFilterResult , TreeVisibility } from 'vs/base/browser/ui/tree/tree' ;
1414import { Action , ActionRunner , IAction , Separator } from 'vs/base/common/actions' ;
15+ import { mapFind } from 'vs/base/common/arrays' ;
1516import { RunOnceScheduler , disposableTimeout } from 'vs/base/common/async' ;
1617import { Color , RGBA } from 'vs/base/common/color' ;
1718import { Emitter , Event } from 'vs/base/common/event' ;
1819import { FuzzyScore } from 'vs/base/common/filters' ;
1920import { 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' ;
2122import { fuzzyContains } from 'vs/base/common/strings' ;
2223import { ThemeIcon } from 'vs/base/common/themables' ;
2324import { isDefined } from 'vs/base/common/types' ;
@@ -26,7 +27,7 @@ import 'vs/css!./media/testing';
2627import { MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer' ;
2728import { localize } from 'vs/nls' ;
2829import { 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' ;
3031import { IMenuService , MenuId , MenuItemAction } from 'vs/platform/actions/common/actions' ;
3132import { ICommandService } from 'vs/platform/commands/common/commands' ;
3233import { IConfigurationService } from 'vs/platform/configuration/common/configuration' ;
@@ -40,38 +41,40 @@ import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } fro
4041import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry' ;
4142import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles' ;
4243import { foreground } from 'vs/platform/theme/common/colorRegistry' ;
44+ import { spinningLoading } from 'vs/platform/theme/common/iconRegistry' ;
4345import { IThemeService , registerThemingParticipant } from 'vs/platform/theme/common/themeService' ;
4446import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' ;
47+ import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands' ;
4548import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane' ;
4649import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet' ;
4750import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput' ;
4851import { 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' ;
5152import { ITestTreeProjection , TestExplorerTreeElement , TestItemTreeElement , TestTreeErrorMessage } from 'vs/workbench/contrib/testing/browser/explorerProjections/index' ;
53+ import { ListProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/listProjection' ;
5254import { getTestItemContextOverlay } from 'vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay' ;
5355import { TestingObjectTree } from 'vs/workbench/contrib/testing/browser/explorerProjections/testingObjectTree' ;
5456import { ISerializedTestTreeCollapseState } from 'vs/workbench/contrib/testing/browser/explorerProjections/testingViewState' ;
57+ import { TreeProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/treeProjection' ;
5558import * as icons from 'vs/workbench/contrib/testing/browser/icons' ;
59+ import { DebugLastRun , ReRunLastRun } from 'vs/workbench/contrib/testing/browser/testExplorerActions' ;
5660import { 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' ;
5862import { TestingConfigKeys , TestingCountBadge , getTestingConfiguration } from 'vs/workbench/contrib/testing/common/configuration' ;
5963import { TestCommandId , TestExplorerViewMode , TestExplorerViewSorting , Testing , labelForTestInState } from 'vs/workbench/contrib/testing/common/constants' ;
6064import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue' ;
6165import { ITestExplorerFilterState , TestExplorerFilterState , TestFilterTerm } from 'vs/workbench/contrib/testing/common/testExplorerFilterState' ;
6266import { TestId } from 'vs/workbench/contrib/testing/common/testId' ;
6367import { 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' ;
6569import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService' ;
6670import { IMainThreadTestCollection , ITestService , testCollectionIsEmpty } from 'vs/workbench/contrib/testing/common/testService' ;
6771import { ITestRunProfile , InternalTestItem , TestItemExpandState , TestResultState , TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes' ;
6872import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys' ;
6973import { ITestingContinuousRunService } from 'vs/workbench/contrib/testing/common/testingContinuousRunService' ;
7074import { 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' ;
7276import { IActivityService , IconBadge , NumberBadge } from 'vs/workbench/services/activity/common/activity' ;
7377import { IEditorService } from 'vs/workbench/services/editor/common/editorService' ;
74- import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands' ;
7578
7679const 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
481564const enum WelcomeExperience {
0 commit comments