未验证 提交 f91663e8 编写于 作者: C Connor Peet

testing: add explicit 'peek error' action to testitems

Fixes #125240
上级 3b8141a1
...@@ -30,6 +30,7 @@ import { testingConfiguation } from 'vs/workbench/contrib/testing/common/configu ...@@ -30,6 +30,7 @@ import { testingConfiguation } from 'vs/workbench/contrib/testing/common/configu
import { Testing } from 'vs/workbench/contrib/testing/common/constants'; import { Testing } from 'vs/workbench/contrib/testing/common/constants';
import { identifyTest, ITestIdWithSrc, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; import { identifyTest, ITestIdWithSrc, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestProfileService, TestProfileService } from 'vs/workbench/contrib/testing/common/testConfigurationService'; import { ITestProfileService, TestProfileService } from 'vs/workbench/contrib/testing/common/testConfigurationService';
import { TestId, TestPosition } from 'vs/workbench/contrib/testing/common/testId';
import { ITestingAutoRun, TestingAutoRun } from 'vs/workbench/contrib/testing/common/testingAutoRun'; import { ITestingAutoRun, TestingAutoRun } from 'vs/workbench/contrib/testing/common/testingAutoRun';
import { TestingContentProvider } from 'vs/workbench/contrib/testing/common/testingContentProvider'; import { TestingContentProvider } from 'vs/workbench/contrib/testing/common/testingContentProvider';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
...@@ -142,9 +143,23 @@ CommandsRegistry.registerCommand({ ...@@ -142,9 +143,23 @@ CommandsRegistry.registerCommand({
id: 'vscode.peekTestError', id: 'vscode.peekTestError',
handler: async (accessor: ServicesAccessor, extId: string) => { handler: async (accessor: ServicesAccessor, extId: string) => {
const lookup = accessor.get(ITestResultService).getStateById(extId); const lookup = accessor.get(ITestResultService).getStateById(extId);
if (lookup) { if (!lookup) {
accessor.get(ITestingPeekOpener).tryPeekFirstError(lookup[0], lookup[1]); return false;
} }
const [result, ownState] = lookup;
const opener = accessor.get(ITestingPeekOpener);
if (opener.tryPeekFirstError(result, ownState)) { // fast path
return true;
}
for (const test of result.tests) {
if (TestId.compare(ownState.item.extId, test.item.extId) === TestPosition.IsChild && opener.tryPeekFirstError(result, test)) {
return true;
}
}
return false;
} }
}); });
......
...@@ -32,10 +32,10 @@ import { DefaultGutterClickAction, getTestingConfiguration, TestingConfigKeys } ...@@ -32,10 +32,10 @@ import { DefaultGutterClickAction, getTestingConfiguration, TestingConfigKeys }
import { labelForTestInState } from 'vs/workbench/contrib/testing/common/constants'; import { labelForTestInState } from 'vs/workbench/contrib/testing/common/constants';
import { identifyTest, IncrementalTestCollectionItem, InternalTestItem, IRichLocation, ITestMessage, ITestRunProfile, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; import { identifyTest, IncrementalTestCollectionItem, InternalTestItem, IRichLocation, ITestMessage, ITestRunProfile, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testConfigurationService'; import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testConfigurationService';
import { maxPriority } from 'vs/workbench/contrib/testing/common/testingStates'; import { isFailedState, maxPriority } from 'vs/workbench/contrib/testing/common/testingStates';
import { buildTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri'; import { buildTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
import { IMainThreadTestCollection, ITestService, testsInFile } from 'vs/workbench/contrib/testing/common/testService'; import { ITestService, testsInFile } from 'vs/workbench/contrib/testing/common/testService';
function isOriginalInDiffEditor(codeEditorService: ICodeEditorService, codeEditor: ICodeEditor): boolean { function isOriginalInDiffEditor(codeEditorService: ICodeEditorService, codeEditor: ICodeEditor): boolean {
const diffEditors = codeEditorService.listDiffEditors(); const diffEditors = codeEditorService.listDiffEditors();
...@@ -402,7 +402,7 @@ abstract class RunTestDecoration extends Disposable { ...@@ -402,7 +402,7 @@ abstract class RunTestDecoration extends Disposable {
/** /**
* Gets context menu actions relevant for a singel test. * Gets context menu actions relevant for a singel test.
*/ */
protected getTestContextMenuActions(collection: IMainThreadTestCollection, test: InternalTestItem) { protected getTestContextMenuActions(test: InternalTestItem, resultItem?: TestResultItem) {
const testActions: IAction[] = []; const testActions: IAction[] = [];
const capabilities = this.testProfileService.controllerCapabilities(test.controllerId); const capabilities = this.testProfileService.controllerCapabilities(test.controllerId);
if (capabilities & TestRunProfileBitset.Run) { if (capabilities & TestRunProfileBitset.Run) {
...@@ -437,6 +437,11 @@ abstract class RunTestDecoration extends Disposable { ...@@ -437,6 +437,11 @@ abstract class RunTestDecoration extends Disposable {
})); }));
} }
if (resultItem && isFailedState(resultItem.computedState)) {
testActions.push(new Action('testing.gutter.peekFailure', localize('peek failure', 'Peek Error'), undefined, undefined,
() => this.commandService.executeCommand('vscode.peekTestError', test.item.extId)));
}
testActions.push(new Action('testing.gutter.reveal', localize('reveal test', 'Reveal in Test Explorer'), undefined, undefined, testActions.push(new Action('testing.gutter.reveal', localize('reveal test', 'Reveal in Test Explorer'), undefined, undefined,
() => this.commandService.executeCommand('vscode.revealTestInExplorer', test.item.extId))); () => this.commandService.executeCommand('vscode.revealTestInExplorer', test.item.extId)));
...@@ -476,8 +481,8 @@ class MultiRunTestDecoration extends RunTestDecoration implements ITestDecoratio ...@@ -476,8 +481,8 @@ class MultiRunTestDecoration extends RunTestDecoration implements ITestDecoratio
allActions.push(new Action('testing.gutter.debugAll', localize('debug all test', 'Debug All Tests'), undefined, undefined, () => this.defaultDebug())); allActions.push(new Action('testing.gutter.debugAll', localize('debug all test', 'Debug All Tests'), undefined, undefined, () => this.defaultDebug()));
} }
const testSubmenus = this.tests.map(({ test }) => const testSubmenus = this.tests.map(({ test, resultItem }) =>
new SubmenuAction(test.item.extId, test.item.label, this.getTestContextMenuActions(this.testService.collection, test))); new SubmenuAction(test.item.extId, test.item.label, this.getTestContextMenuActions(test, resultItem)));
return Separator.join(allActions, testSubmenus); return Separator.join(allActions, testSubmenus);
} }
...@@ -519,7 +524,7 @@ class RunSingleTestDecoration extends RunTestDecoration implements ITestDecorati ...@@ -519,7 +524,7 @@ class RunSingleTestDecoration extends RunTestDecoration implements ITestDecorati
} }
protected override getContextMenuActions(e: IEditorMouseEvent) { protected override getContextMenuActions(e: IEditorMouseEvent) {
return this.getTestContextMenuActions(this.testService.collection, this.test); return this.getTestContextMenuActions(this.test, this.resultItem);
} }
protected override defaultRun() { protected override defaultRun() {
......
...@@ -470,7 +470,7 @@ export class TestingExplorerViewModel extends Disposable { ...@@ -470,7 +470,7 @@ export class TestingExplorerViewModel extends Disposable {
} }
})); }));
this._register(this.tree.onDidChangeSelection(async evt => { this._register(this.tree.onDidChangeSelection(evt => {
if (evt.browserEvent instanceof MouseEvent && (evt.browserEvent.altKey || evt.browserEvent.shiftKey)) { if (evt.browserEvent instanceof MouseEvent && (evt.browserEvent.altKey || evt.browserEvent.shiftKey)) {
return; // don't focus when alt-clicking to multi select return; // don't focus when alt-clicking to multi select
} }
...@@ -478,7 +478,7 @@ export class TestingExplorerViewModel extends Disposable { ...@@ -478,7 +478,7 @@ export class TestingExplorerViewModel extends Disposable {
const selected = evt.elements[0]; const selected = evt.elements[0];
if (selected && evt.browserEvent && selected instanceof TestItemTreeElement if (selected && evt.browserEvent && selected instanceof TestItemTreeElement
&& selected.children.size === 0 && selected.test.expand === TestItemExpandState.NotExpandable) { && selected.children.size === 0 && selected.test.expand === TestItemExpandState.NotExpandable) {
await this.tryPeekError(selected); this.tryPeekError(selected);
} }
})); }));
...@@ -629,7 +629,7 @@ export class TestingExplorerViewModel extends Disposable { ...@@ -629,7 +629,7 @@ export class TestingExplorerViewModel extends Disposable {
/** /**
* Tries to peek the first test error, if the item is in a failed state. * Tries to peek the first test error, if the item is in a failed state.
*/ */
private async tryPeekError(item: TestItemTreeElement) { private tryPeekError(item: TestItemTreeElement) {
const lookup = item.test && this.testResults.getStateById(item.test.item.extId); const lookup = item.test && this.testResults.getStateById(item.test.item.extId);
return lookup && lookup[1].tasks.some(s => isFailedState(s.state)) return lookup && lookup[1].tasks.some(s => isFailedState(s.state))
? this.peekOpener.tryPeekFirstError(lookup[0], lookup[1], { preserveFocus: true }) ? this.peekOpener.tryPeekFirstError(lookup[0], lookup[1], { preserveFocus: true })
......
...@@ -154,14 +154,14 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener ...@@ -154,14 +154,14 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener
} }
/** @inheritdoc */ /** @inheritdoc */
public async tryPeekFirstError(result: ITestResult, test: TestResultItem, options?: Partial<ITextEditorOptions>) { public tryPeekFirstError(result: ITestResult, test: TestResultItem, options?: Partial<ITextEditorOptions>) {
const candidate = this.getFailedCandidateMessage(test); const candidate = this.getFailedCandidateMessage(test);
if (!candidate) { if (!candidate) {
return false; return false;
} }
const message = candidate.message; const message = candidate.message;
return this.showPeekFromUri({ this.showPeekFromUri({
type: TestUriType.ResultMessage, type: TestUriType.ResultMessage,
documentUri: message.location!.uri, documentUri: message.location!.uri,
taskIndex: candidate.taskId, taskIndex: candidate.taskId,
...@@ -169,6 +169,7 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener ...@@ -169,6 +169,7 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener
resultId: result.id, resultId: result.id,
testExtId: test.item.extId, testExtId: test.item.extId,
}, { selection: message.location!.range, ...options }); }, { selection: message.location!.range, ...options });
return true;
} }
/** @inheritdoc */ /** @inheritdoc */
......
...@@ -69,6 +69,25 @@ export class TestId { ...@@ -69,6 +69,25 @@ export class TestId {
return base.toString() + TestIdPathParts.Delimiter + b; return base.toString() + TestIdPathParts.Delimiter + b;
} }
/**
* Compares the position of the two ID strings.
*/
public static compare(a: string, b: string) {
if (a === b) {
return TestPosition.IsSame;
}
if (b.startsWith(a + TestIdPathParts.Delimiter)) {
return TestPosition.IsChild;
}
if (a.startsWith(b + TestIdPathParts.Delimiter)) {
return TestPosition.IsParent;
}
return TestPosition.Disconnected;
}
constructor( constructor(
public readonly path: readonly string[], public readonly path: readonly string[],
private readonly viewEnd = path.length, private readonly viewEnd = path.length,
...@@ -124,7 +143,11 @@ export class TestId { ...@@ -124,7 +143,11 @@ export class TestId {
/** /**
* Compares the other test ID with this one. * Compares the other test ID with this one.
*/ */
public compare(other: TestId) { public compare(other: TestId | string) {
if (typeof other === 'string') {
return TestId.compare(this.toString(), other);
}
for (let i = 0; i < other.viewEnd && i < this.viewEnd; i++) { for (let i = 0; i < other.viewEnd && i < this.viewEnd; i++) {
if (other.path[i] !== this.path[i]) { if (other.path[i] !== this.path[i]) {
return TestPosition.Disconnected; return TestPosition.Disconnected;
......
...@@ -15,7 +15,7 @@ export interface ITestingPeekOpener { ...@@ -15,7 +15,7 @@ export interface ITestingPeekOpener {
* Tries to peek the first test error, if the item is in a failed state. * Tries to peek the first test error, if the item is in a failed state.
* @returns a boolean indicating whether a peek was opened * @returns a boolean indicating whether a peek was opened
*/ */
tryPeekFirstError(result: ITestResult, test: TestResultItem, options?: Partial<ITextEditorOptions>): Promise<boolean>; tryPeekFirstError(result: ITestResult, test: TestResultItem, options?: Partial<ITextEditorOptions>): boolean;
/** /**
* Opens the peek. Shows any available message. * Opens the peek. Shows any available message.
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册