From 244b48af73de6f31e7be1b1e2f70b5ed2e9e41bc Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 26 May 2021 13:00:37 -0700 Subject: [PATCH] testing: additional actions and better theming for peek Fixes #124642 --- .../contrib/testing/browser/media/testing.css | 6 +- .../testing/browser/testExplorerActions.ts | 82 ++++++----- .../testing/browser/testing.contribution.ts | 28 +++- .../testing/browser/testingExplorerView.ts | 2 +- .../testing/browser/testingOutputPeek.ts | 130 +++++++++++++----- .../testing/common/testingPeekOpener.ts | 27 ++++ 6 files changed, 190 insertions(+), 85 deletions(-) create mode 100644 src/vs/workbench/contrib/testing/common/testingPeekOpener.ts diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index d7de6f4b83f..94ed31fb9df 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -26,7 +26,7 @@ .test-explorer .monaco-list-row .codicon-testing-hidden { display: none; flex-shrink: 0; - margin-right: 1px; + margin-right: 0.8em; } .test-explorer .monaco-list-row:hover .monaco-action-bar, @@ -128,7 +128,8 @@ border-bottom-width: 2px; } -.monaco-editor .zone-widget.test-output-peek .test-output-peek-message-container { +.monaco-editor .zone-widget.test-output-peek .test-output-peek-message-container, +.monaco-editor .zone-widget.test-output-peek .test-output-peek-tree { height: 100%; } @@ -144,6 +145,7 @@ .monaco-editor .zone-widget.test-output-peek .preview-text p:last-child { margin-bottom: 0; } + /** -- filter */ .testing-filter-action-bar { flex-shrink: 0; diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index e5662ce40e2..174963dd1df 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -8,7 +8,6 @@ import { Codicon } from 'vs/base/common/codicons'; import { Iterable } from 'vs/base/common/iterator'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { isDefined } from 'vs/base/common/types'; -import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { Range } from 'vs/editor/common/core/range'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { localize } from 'vs/nls'; @@ -31,12 +30,12 @@ import { IActionableTestTreeElement, TestItemTreeElement } from 'vs/workbench/co import * as icons from 'vs/workbench/contrib/testing/browser/icons'; import { ITestExplorerFilterState } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter'; import { TestingExplorerView, TestingExplorerViewModel } from 'vs/workbench/contrib/testing/browser/testingExplorerView'; -import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; import { ITestingOutputTerminalService } from 'vs/workbench/contrib/testing/browser/testingOutputTerminalService'; import { TestExplorerViewMode, TestExplorerViewSorting, Testing } from 'vs/workbench/contrib/testing/common/constants'; import { InternalTestItem, ITestItem, TestIdPath, TestIdWithSrc } from 'vs/workbench/contrib/testing/common/testCollection'; import { ITestingAutoRun } from 'vs/workbench/contrib/testing/common/testingAutoRun'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; +import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; import { isFailedState } from 'vs/workbench/contrib/testing/common/testingStates'; import { getPathForTestInResult, ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; @@ -625,6 +624,7 @@ export class GoToTest extends Action2 { const { range, uri, extId } = element.test.item; accessor.get(ITestExplorerFilterState).reveal.value = [extId]; + accessor.get(ITestingPeekOpener).closeAllPeeks(); let isFile = true; try { @@ -640,7 +640,7 @@ export class GoToTest extends Action2 { return; } - const pane = await editorService.openEditor({ + await editorService.openEditor({ resource: uri, options: { selection: range @@ -649,12 +649,6 @@ export class GoToTest extends Action2 { preserveFocus: preserveFocus === true, }, }); - - // if the user selected a failed test and now they didn't, hide the peek - const control = pane?.getControl(); - if (isCodeEditor(control)) { - TestingOutputPeekController.get(control).removePeek(); - } } /** @@ -680,6 +674,7 @@ export class GoToTest extends Action2 { const editorService = accessor.get(IEditorService); accessor.get(ITestExplorerFilterState).reveal.value = [test.extId]; + accessor.get(ITestingPeekOpener).closeAllPeeks(); let isFile = true; try { @@ -695,7 +690,7 @@ export class GoToTest extends Action2 { return; } - const pane = await editorService.openEditor({ + await editorService.openEditor({ resource: test.uri, options: { selection: test.range @@ -704,12 +699,6 @@ export class GoToTest extends Action2 { preserveFocus, }, }); - - // if the user selected a failed test and now they didn't, hide the peek - const control = pane?.getControl(); - if (isCodeEditor(control)) { - TestingOutputPeekController.get(control).removePeek(); - } } } @@ -954,41 +943,47 @@ export class DebugCurrentFile extends RunOrDebugCurrentFile { } } -abstract class RunOrDebugExtsById extends Action2 { +export const runTestsByPath = async ( + workspaceTests: IWorkspaceTestCollectionService, + progress: IProgressService, + paths: ReadonlyArray, + runTests: (tests: ReadonlyArray) => Promise, +): Promise => { + const subscription = workspaceTests.subscribeToWorkspaceTests(); + try { + const todo = Promise.all([...subscription.workspaceFolderCollections.values()].map( + c => Promise.all(paths.map(p => getTestByPath(c, p))), + )); + + const tests = flatten(await showDiscoveringWhile(progress, todo)).filter(isDefined); + return tests.length ? await runTests(tests) : undefined; + } finally { + subscription.dispose(); + } +}; + +abstract class RunOrDebugExtsByPath extends Action2 { /** * @override */ - public async run(accessor: ServicesAccessor) { + public async run(accessor: ServicesAccessor, ...args: unknown[]) { const testService = accessor.get(ITestService); - const paths = [...this.getTestExtIdsToRun(accessor)]; - if (paths.length === 0) { - return; - } - - const workspaceTests = accessor.get(IWorkspaceTestCollectionService).subscribeToWorkspaceTests(); - - try { - const todo = Promise.all([...workspaceTests.workspaceFolderCollections.values()].map( - c => Promise.all(paths.map(p => getTestByPath(c, p))), - )); - - const tests = flatten(await showDiscoveringWhile(accessor.get(IProgressService), todo)).filter(isDefined); - if (tests.length) { - await this.runTest(testService, tests); - } - } finally { - workspaceTests.dispose(); - } + await runTestsByPath( + accessor.get(IWorkspaceTestCollectionService), + accessor.get(IProgressService), + [...this.getTestExtIdsToRun(accessor, ...args)], + tests => this.runTest(testService, tests), + ); } - protected abstract getTestExtIdsToRun(accessor: ServicesAccessor): Iterable; + protected abstract getTestExtIdsToRun(accessor: ServicesAccessor, ...args: unknown[]): Iterable; protected abstract filter(node: InternalTestItem): boolean; - protected abstract runTest(service: ITestService, node: InternalTestItem[]): Promise; + protected abstract runTest(service: ITestService, node: readonly InternalTestItem[]): Promise; } -abstract class RunOrDebugFailedTests extends RunOrDebugExtsById { +abstract class RunOrDebugFailedTests extends RunOrDebugExtsByPath { /** * @inheritdoc */ @@ -1012,12 +1007,13 @@ abstract class RunOrDebugFailedTests extends RunOrDebugExtsById { } } -abstract class RunOrDebugLastRun extends RunOrDebugExtsById { +abstract class RunOrDebugLastRun extends RunOrDebugExtsByPath { /** * @inheritdoc */ - protected *getTestExtIdsToRun(accessor: ServicesAccessor): Iterable { - const lastResult = accessor.get(ITestResultService).results[0]; + protected *getTestExtIdsToRun(accessor: ServicesAccessor, runId?: string): Iterable { + const resultService = accessor.get(ITestResultService); + const lastResult = runId ? resultService.results.find(r => r.id === runId) : resultService.results[0]; if (!lastResult) { return; } diff --git a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts index 527b889a0ea..545f5c5e593 100644 --- a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts +++ b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts @@ -12,6 +12,7 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IProgressService } from 'vs/platform/progress/common/progress'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { Extensions as ViewContainerExtensions, IViewContainersRegistry, IViewsRegistry, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; @@ -19,23 +20,24 @@ import { testingViewIcon } from 'vs/workbench/contrib/testing/browser/icons'; import { TestingDecorations } from 'vs/workbench/contrib/testing/browser/testingDecorations'; import { ITestExplorerFilterState, TestExplorerFilterState } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter'; import { TestingExplorerView } from 'vs/workbench/contrib/testing/browser/testingExplorerView'; -import { CloseTestPeek, ITestingPeekOpener, TestingOutputPeekController, TestingPeekOpener } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; +import { CloseTestPeek, TestingOutputPeekController, TestingPeekOpener } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; import { ITestingOutputTerminalService, TestingOutputTerminalService } from 'vs/workbench/contrib/testing/browser/testingOutputTerminalService'; import { ITestingProgressUiService, TestingProgressUiService } from 'vs/workbench/contrib/testing/browser/testingProgressUiService'; import { TestingViewPaneContainer } from 'vs/workbench/contrib/testing/browser/testingViewPaneContainer'; import { testingConfiguation } from 'vs/workbench/contrib/testing/common/configuration'; import { Testing } from 'vs/workbench/contrib/testing/common/constants'; -import { TestIdPath, TestIdWithSrc } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestIdPath, TestIdWithMaybeSrc, TestIdWithSrc } from 'vs/workbench/contrib/testing/common/testCollection'; import { ITestingAutoRun, TestingAutoRun } from 'vs/workbench/contrib/testing/common/testingAutoRun'; import { TestingContentProvider } from 'vs/workbench/contrib/testing/common/testingContentProvider'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; +import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; import { ITestResultService, TestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { ITestResultStorage, TestResultStorage } from 'vs/workbench/contrib/testing/common/testResultStorage'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { TestService } from 'vs/workbench/contrib/testing/common/testServiceImpl'; import { IWorkspaceTestCollectionService, WorkspaceTestCollectionService } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { allTestActions } from './testExplorerActions'; +import { allTestActions, runTestsByPath } from './testExplorerActions'; registerSingleton(ITestService, TestService); registerSingleton(ITestResultStorage, TestResultStorage); @@ -108,9 +110,9 @@ registerEditorContribution(Testing.DecorationsContributionId, TestingDecorations CommandsRegistry.registerCommand({ id: 'vscode.runTests', - handler: async (accessor: ServicesAccessor, tests: TestIdWithSrc[]) => { + handler: async (accessor: ServicesAccessor, tests: TestIdWithMaybeSrc[]) => { const testService = accessor.get(ITestService); - testService.runTests({ debug: false, tests: tests.filter(t => t.src && t.testId) }); + testService.runTests({ debug: false, tests: tests.filter(t => !!t.testId) }); } }); @@ -140,4 +142,20 @@ CommandsRegistry.registerCommand({ } }); +CommandsRegistry.registerCommand({ + id: 'vscode.runTestsByPath', + handler: async (accessor: ServicesAccessor, debug: boolean, ...pathToTests: TestIdPath[]) => { + const testService = accessor.get(ITestService); + await runTestsByPath( + accessor.get(IWorkspaceTestCollectionService), + accessor.get(IProgressService), + pathToTests, + tests => testService.runTests({ + debug: false, + tests: tests.map(t => ({ testId: t.item.extId, src: t.src })), + }), + ); + } +}); + Registry.as(ConfigurationExtensions.Configuration).registerConfiguration(testingConfiguation); diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index bbe818ebf29..dee91b50691 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -51,7 +51,7 @@ import { HierarchicalByNameProjection } from 'vs/workbench/contrib/testing/brows import { IActionableTestTreeElement, isActionableTestTreeElement, ITestTreeProjection, TestExplorerTreeElement, TestItemTreeElement, TestTreeErrorMessage, TestTreeWorkspaceFolder } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; import { testingHiddenIcon, testingStatesToIcons } from 'vs/workbench/contrib/testing/browser/icons'; import { ITestExplorerFilterState, TestExplorerFilterState, TestingExplorerFilter } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter'; -import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; +import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; import { ITestingProgressUiService } from 'vs/workbench/contrib/testing/browser/testingProgressUiService'; import { getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; import { TestExplorerStateFilter, TestExplorerViewMode, TestExplorerViewSorting, Testing, testStateNames } from 'vs/workbench/contrib/testing/common/constants'; diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 80e1c124015..ff148d0cf3d 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -34,7 +34,7 @@ import { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/br import { IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; -import { getOuterEditor, IPeekViewService, peekViewTitleBackground, peekViewTitleForeground, peekViewTitleInfoForeground, PeekViewWidget } from 'vs/editor/contrib/peekView/peekView'; +import { getOuterEditor, IPeekViewService, peekViewResultsBackground, peekViewResultsMatchForeground, peekViewResultsSelectionBackground, peekViewResultsSelectionForeground, peekViewTitleBackground, peekViewTitleForeground, peekViewTitleInfoForeground, PeekViewWidget } from 'vs/editor/contrib/peekView/peekView'; import { localize } from 'vs/nls'; import { createAndFillInActionBarActions, MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; @@ -43,21 +43,23 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; -import { createDecorator, IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; -import { IColorTheme, IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; +import { IColorTheme, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { TestResultState } from 'vs/workbench/api/common/extHostTypes'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { flatTestItemDelimiter } from 'vs/workbench/contrib/testing/browser/explorerProjections/display'; -import { testingStatesToIcons, testMessageSeverityToIcons } from 'vs/workbench/contrib/testing/browser/icons'; +import * as icons from 'vs/workbench/contrib/testing/browser/icons'; import { ITestingOutputTerminalService } from 'vs/workbench/contrib/testing/browser/testingOutputTerminalService'; import { testingPeekBorder } from 'vs/workbench/contrib/testing/browser/theme'; import { AutoOpenPeekViewWhen, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; import { Testing } from 'vs/workbench/contrib/testing/common/constants'; import { IRichLocation, ITestItem, ITestMessage, ITestRunTask, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; +import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; import { isFailedState } from 'vs/workbench/contrib/testing/common/testingStates'; import { buildTestUri, parseTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri'; import { getPathForTestInResult, ITestResult, maxCountPriority, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; @@ -84,18 +86,6 @@ class TestDto { } } -export interface ITestingPeekOpener { - _serviceBrand: undefined; - - /** - * Tries to peek the first test error, if the item is in a failed state. - * @returns a boolean indicating whether a peek was opened - */ - tryPeekFirstError(result: ITestResult, test: TestResultItem, options?: Partial): Promise; -} - -export const ITestingPeekOpener = createDecorator('testingPeekOpener'); - export class TestingPeekOpener extends Disposable implements ITestingPeekOpener { declare _serviceBrand: undefined; @@ -109,10 +99,7 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener this._register(testResults.onTestChanged(this.openPeekOnFailure, this)); } - /** - * Tries to peek the first test error, if the item is in a failed state. - * @returns a boolean if a peek was opened - */ + /** @inheritdoc */ public async tryPeekFirstError(result: ITestResult, test: TestResultItem, options?: Partial) { const candidate = this.getCandidateMessage(test); if (!candidate) { @@ -141,6 +128,13 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener return true; } + /** @inheritdoc */ + public closeAllPeeks() { + for (const editor of this.codeEditorService.listCodeEditors()) { + TestingOutputPeekController.get(editor).removePeek(); + } + } + /** * Opens the peek view on a test failure, based on user preferences. */ @@ -766,7 +760,7 @@ export class TestResultElement implements ITreeElement { public readonly label = this.value.name; public get icon() { - return testingStatesToIcons.get( + return icons.testingStatesToIcons.get( this.value.completedAt === undefined ? TestResultState.Running : maxCountPriority(this.value.counts) @@ -778,22 +772,22 @@ export class TestResultElement implements ITreeElement { export class TestCaseElement implements ITreeElement { public readonly type = 'test'; - public readonly context = this.value.item.extId; - public readonly id = `${this.results.id}/${this.value.item.extId}`; - public readonly label = this.value.item.label; + public readonly context = this.test.item.extId; + public readonly id = `${this.results.id}/${this.test.item.extId}`; + public readonly label = this.test.item.label; public readonly description?: string; public get icon() { - return testingStatesToIcons.get(this.value.computedState); + return icons.testingStatesToIcons.get(this.test.computedState); } public get path() { - return getPathForTestInResult(this.value, this.results); + return getPathForTestInResult(this.test, this.results); } constructor( private readonly results: ITestResult, - public readonly value: TestResultItem, + public readonly test: TestResultItem, ) { for (const parent of this.parents()) { this.description = this.description @@ -804,7 +798,7 @@ export class TestCaseElement implements ITreeElement { private *parents() { for ( - let parent = this.value.parent && this.results.getStateById(this.value.parent); + let parent = this.test.parent && this.results.getStateById(this.test.parent); parent; parent = parent.parent && this.results.getStateById(parent.parent) ) { @@ -825,7 +819,7 @@ class TestTaskElement implements ITreeElement { return getPathForTestInResult(this.test, this.results); } - constructor(private readonly results: ITestResult, private readonly test: TestResultItem, index: number) { + constructor(private readonly results: ITestResult, public readonly test: TestResultItem, index: number) { this.id = `${results.id}/${test.item.extId}/${index}`; this.task = results.tasks[index]; this.context = String(index); @@ -861,7 +855,7 @@ class TestMessageElement implements ITreeElement { this.id = this.uri.toString(); this.label = firstLine(renderStringAsPlaintext(message)); - this.icon = testMessageSeverityToIcons.get(severity); + this.icon = icons.testMessageSeverityToIcons.get(severity); } } @@ -1155,9 +1149,9 @@ class TreeActionsProvider { ) { } public provideActionBar(element: ITreeElement) { - const test = element instanceof TestCaseElement ? element.value : undefined; + const test = element instanceof TestCaseElement ? element.test : undefined; const contextOverlay = this.contextKeyService.createOverlay([ - ['view', Testing.OutputPeekContributionId], + ['peek', Testing.OutputPeekContributionId], [TestingContextKeys.peekItemType.key, element.type], [TestingContextKeys.testItemExtId.key, test?.item.extId], [TestingContextKeys.testItemHasUri.key, !!test?.item.uri], @@ -1172,23 +1166,62 @@ class TreeActionsProvider { if (element instanceof TestResultElement) { primary.push(new Action( - 'testing.showResultOutput', + 'testing.outputPeek.showResultOutput', localize('testing.showResultOutput', "Show Result Output"), Codicon.terminal.classNames, undefined, () => this.testTerminalService.open(element.value) )); + + primary.push(new Action( + 'testing.outputPeek.reRunLastRun', + localize('testing.reRunLastRun', "Rerun Test Run"), + ThemeIcon.asClassName(icons.testingRunIcon), + undefined, + () => this.commandService.executeCommand('testing.reRunLastRun', element.value.id), + )); + + if (Iterable.some(element.value.tests, t => t.item.debuggable)) { + primary.push(new Action( + 'testing.outputPeek.debugLastRun', + localize('testing.debugLastRun', "Debug Test Run"), + ThemeIcon.asClassName(icons.testingDebugIcon), + undefined, + () => this.commandService.executeCommand('testing.debugLastRun', element.value.id), + )); + } } if (element instanceof TestCaseElement || element instanceof TestTaskElement) { primary.push(new Action( - 'testing.revealInExplorer', + 'testing.outputPeek.revealInExplorer', localize('testing.revealInExplorer', "Reveal in Test Explorer"), Codicon.listTree.classNames, undefined, () => this.commandService.executeCommand('vscode.revealTestInExplorer', element.path), )); + + if (element.test.item.runnable) { + primary.push(new Action( + 'testing.outputPeek.runTest', + localize('run test', 'Run Test'), + ThemeIcon.asClassName(icons.testingRunIcon), + undefined, + () => this.commandService.executeCommand('vscode.runTestsByPath', false, element.path), + )); + } + + if (element.test.item.debuggable) { + primary.push(new Action( + 'testing.outputPeek.debugTest', + localize('debug test', 'Debug Test'), + ThemeIcon.asClassName(icons.testingDebugIcon), + undefined, + () => this.commandService.executeCommand('vscode.runTestsByPath', true, element.path), + )); + } } + const result = { primary, secondary }; const actionsDisposable = createAndFillInActionBarActions(menu, { shouldForwardArgs: true, @@ -1200,3 +1233,32 @@ class TreeActionsProvider { } } } + +registerThemingParticipant((theme, collector) => { + const resultsBackground = theme.getColor(peekViewResultsBackground); + if (resultsBackground) { + collector.addRule(`.monaco-editor .test-output-peek .test-output-peek-tree { background-color: ${resultsBackground}; }`); + } + const resultsMatchForeground = theme.getColor(peekViewResultsMatchForeground); + if (resultsMatchForeground) { + collector.addRule(`.monaco-editor .test-output-peek .test-output-peek-tree { color: ${resultsMatchForeground}; }`); + } + const resultsSelectedBackground = theme.getColor(peekViewResultsSelectionBackground); + if (resultsSelectedBackground) { + collector.addRule(`.monaco-editor .test-output-peek .test-output-peek-tree .monaco-list:focus .monaco-list-rows > .monaco-list-row.selected:not(.highlighted) { background-color: ${resultsSelectedBackground}; }`); + } + const resultsSelectedForeground = theme.getColor(peekViewResultsSelectionForeground); + if (resultsSelectedForeground) { + collector.addRule(`.monaco-editor .test-output-peek .test-output-peek-tree .monaco-list:focus .monaco-list-rows > .monaco-list-row.selected:not(.highlighted) { color: ${resultsSelectedForeground} !important; }`); + } + + const textLinkForegroundColor = theme.getColor(textLinkForeground); + if (textLinkForegroundColor) { + collector.addRule(`.monaco-editor .test-output-peek .test-output-peek-message-container a { color: ${textLinkForegroundColor}; }`); + } + + const textLinkActiveForegroundColor = theme.getColor(textLinkActiveForeground); + if (textLinkActiveForegroundColor) { + collector.addRule(`.monaco-editor .test-output-peek .test-output-peek-message-container a :hover { color: ${textLinkActiveForegroundColor}; }`); + } +}); diff --git a/src/vs/workbench/contrib/testing/common/testingPeekOpener.ts b/src/vs/workbench/contrib/testing/common/testingPeekOpener.ts new file mode 100644 index 00000000000..208ab9a22f2 --- /dev/null +++ b/src/vs/workbench/contrib/testing/common/testingPeekOpener.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; + +export interface ITestingPeekOpener { + _serviceBrand: undefined; + + /** + * Tries to peek the first test error, if the item is in a failed state. + * @returns a boolean indicating whether a peek was opened + */ + tryPeekFirstError(result: ITestResult, test: TestResultItem, options?: Partial): Promise; + + /** + * Closes peeks for all visible editors. + */ + closeAllPeeks(): void; +} + +export const ITestingPeekOpener = createDecorator('testingPeekOpener'); +