@@ -12,12 +12,13 @@ import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelega
12
12
import { DefaultKeyboardNavigationDelegate , IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget' ;
13
13
import { ITreeContextMenuEvent , ITreeFilter , ITreeNode , ITreeRenderer , ITreeSorter , TreeFilterResult , TreeVisibility } from 'vs/base/browser/ui/tree/tree' ;
14
14
import { Action , ActionRunner , IAction , Separator } from 'vs/base/common/actions' ;
15
+ import { mapFind } from 'vs/base/common/arrays' ;
15
16
import { RunOnceScheduler , disposableTimeout } from 'vs/base/common/async' ;
16
17
import { Color , RGBA } from 'vs/base/common/color' ;
17
18
import { Emitter , Event } from 'vs/base/common/event' ;
18
19
import { FuzzyScore } from 'vs/base/common/filters' ;
19
20
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' ;
21
22
import { fuzzyContains } from 'vs/base/common/strings' ;
22
23
import { ThemeIcon } from 'vs/base/common/themables' ;
23
24
import { isDefined } from 'vs/base/common/types' ;
@@ -26,7 +27,7 @@ import 'vs/css!./media/testing';
26
27
import { MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer' ;
27
28
import { localize } from 'vs/nls' ;
28
29
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' ;
30
31
import { IMenuService , MenuId , MenuItemAction } from 'vs/platform/actions/common/actions' ;
31
32
import { ICommandService } from 'vs/platform/commands/common/commands' ;
32
33
import { IConfigurationService } from 'vs/platform/configuration/common/configuration' ;
@@ -40,38 +41,40 @@ import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } fro
40
41
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry' ;
41
42
import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles' ;
42
43
import { foreground } from 'vs/platform/theme/common/colorRegistry' ;
44
+ import { spinningLoading } from 'vs/platform/theme/common/iconRegistry' ;
43
45
import { IThemeService , registerThemingParticipant } from 'vs/platform/theme/common/themeService' ;
44
46
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' ;
47
+ import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands' ;
45
48
import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane' ;
46
49
import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet' ;
47
50
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput' ;
48
51
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' ;
51
52
import { ITestTreeProjection , TestExplorerTreeElement , TestItemTreeElement , TestTreeErrorMessage } from 'vs/workbench/contrib/testing/browser/explorerProjections/index' ;
53
+ import { ListProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/listProjection' ;
52
54
import { getTestItemContextOverlay } from 'vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay' ;
53
55
import { TestingObjectTree } from 'vs/workbench/contrib/testing/browser/explorerProjections/testingObjectTree' ;
54
56
import { ISerializedTestTreeCollapseState } from 'vs/workbench/contrib/testing/browser/explorerProjections/testingViewState' ;
57
+ import { TreeProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/treeProjection' ;
55
58
import * as icons from 'vs/workbench/contrib/testing/browser/icons' ;
59
+ import { DebugLastRun , ReRunLastRun } from 'vs/workbench/contrib/testing/browser/testExplorerActions' ;
56
60
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' ;
58
62
import { TestingConfigKeys , TestingCountBadge , getTestingConfiguration } from 'vs/workbench/contrib/testing/common/configuration' ;
59
63
import { TestCommandId , TestExplorerViewMode , TestExplorerViewSorting , Testing , labelForTestInState } from 'vs/workbench/contrib/testing/common/constants' ;
60
64
import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue' ;
61
65
import { ITestExplorerFilterState , TestExplorerFilterState , TestFilterTerm } from 'vs/workbench/contrib/testing/common/testExplorerFilterState' ;
62
66
import { TestId } from 'vs/workbench/contrib/testing/common/testId' ;
63
67
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' ;
65
69
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService' ;
66
70
import { IMainThreadTestCollection , ITestService , testCollectionIsEmpty } from 'vs/workbench/contrib/testing/common/testService' ;
67
71
import { ITestRunProfile , InternalTestItem , TestItemExpandState , TestResultState , TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes' ;
68
72
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys' ;
69
73
import { ITestingContinuousRunService } from 'vs/workbench/contrib/testing/common/testingContinuousRunService' ;
70
74
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' ;
72
76
import { IActivityService , IconBadge , NumberBadge } from 'vs/workbench/services/activity/common/activity' ;
73
77
import { IEditorService } from 'vs/workbench/services/editor/common/editorService' ;
74
- import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands' ;
75
78
76
79
const enum LastFocusState {
77
80
Input ,
@@ -83,10 +86,8 @@ export class TestingExplorerView extends ViewPane {
83
86
private filterActionBar = this . _register ( new MutableDisposable ( ) ) ;
84
87
private container ! : HTMLElement ;
85
88
private treeHeader ! : HTMLElement ;
86
- private countSummary : CountSummary | undefined ;
87
89
private discoveryProgress = this . _register ( new MutableDisposable < UnmanagedProgress > ( ) ) ;
88
90
private readonly filter = this . _register ( new MutableDisposable < TestingExplorerFilter > ( ) ) ;
89
- private readonly badgeDisposable = this . _register ( new MutableDisposable < IDisposable > ( ) ) ;
90
91
private readonly filterFocusListener = this . _register ( new MutableDisposable ( ) ) ;
91
92
private readonly dimensions = { width : 0 , height : 0 } ;
92
93
private lastFocusState = LastFocusState . Input ;
@@ -97,7 +98,6 @@ export class TestingExplorerView extends ViewPane {
97
98
98
99
constructor (
99
100
options : IViewletViewOptions ,
100
- @IActivityService private readonly activityService : IActivityService ,
101
101
@IContextMenuService contextMenuService : IContextMenuService ,
102
102
@IKeybindingService keybindingService : IKeybindingService ,
103
103
@IConfigurationService configurationService : IConfigurationService ,
@@ -108,10 +108,8 @@ export class TestingExplorerView extends ViewPane {
108
108
@IThemeService themeService : IThemeService ,
109
109
@ITestService private readonly testService : ITestService ,
110
110
@ITelemetryService telemetryService : ITelemetryService ,
111
- @ITestingProgressUiService private readonly testProgressService : ITestingProgressUiService ,
112
111
@ITestProfileService private readonly testProfileService : ITestProfileService ,
113
112
@ICommandService private readonly commandService : ICommandService ,
114
- @ITestingContinuousRunService private readonly crService : ITestingContinuousRunService ,
115
113
) {
116
114
super ( options , keybindingService , contextMenuService , configurationService , contextKeyService , viewDescriptorService , instantiationService , openerService , themeService , telemetryService ) ;
117
115
@@ -127,9 +125,6 @@ export class TestingExplorerView extends ViewPane {
127
125
} ) ) ;
128
126
129
127
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 ) ) ;
133
128
}
134
129
135
130
public override shouldShowWelcome ( ) {
@@ -278,21 +273,8 @@ export class TestingExplorerView extends ViewPane {
278
273
this . treeHeader = dom . append ( this . container , dom . $ ( '.test-explorer-header' ) ) ;
279
274
this . filterActionBar . value = this . createFilterActionBar ( ) ;
280
275
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 ) ) ;
296
278
297
279
const listContainer = dom . append ( this . container , dom . $ ( '.test-explorer-tree' ) ) ;
298
280
this . viewModel = this . instantiationService . createInstance ( TestingExplorerViewModel , listContainer , this . onDidChangeBodyVisibility ) ;
@@ -441,17 +423,130 @@ export class TestingExplorerView extends ViewPane {
441
423
}
442
424
}
443
425
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 ) ) ;
449
535
} 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' ) ) ;
452
541
} else {
453
- this . badgeDisposable . value = undefined ;
542
+ if ( ! this . lastBadge ) {
543
+ return ;
544
+ }
545
+
546
+ this . lastBadge = undefined ;
454
547
}
548
+
549
+ this . badgeDisposable . value = this . lastBadge && this . activityService . showViewActivity ( Testing . ExplorerViewId , { badge : this . lastBadge } ) ;
455
550
}
456
551
457
552
private getLocalizedBadgeString ( countBadgeType : TestingCountBadge , count : number ) : string {
@@ -464,18 +559,6 @@ export class TestingExplorerView extends ViewPane {
464
559
return localize ( 'testingCountBadgeFailed' , '{0} failed tests' , count ) ;
465
560
}
466
561
}
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
- }
479
562
}
480
563
481
564
const enum WelcomeExperience {
0 commit comments