mirror of
https://github.com/Microsoft/vscode
synced 2024-09-13 21:55:38 +00:00
testing: exploratory UI for followup actions
This adds an API that extensions can use to contribute 'followup' actions around test messages. Here just a dummy hello world using copilot, but extensions could have any action here, such as actions to update snapshots if a test failed: ![](https://memes.peet.io/img/24-05-c1f3e073-a2da-4f16-a033-c8f7e5cd4864.png) Implemented using a simple provider API.
This commit is contained in:
parent
71e25d9b3c
commit
e1dfc911ce
|
@ -41,6 +41,18 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||
const ctrl = vscode.tests.createTestController('selfhost-test-controller', 'VS Code Tests');
|
||||
const fileChangedEmitter = new vscode.EventEmitter<FileChangeEvent>();
|
||||
|
||||
// todo@connor4312: tidy this up and make it work
|
||||
// context.subscriptions.push(vscode.tests.registerTestFollowupProvider({
|
||||
// async provideFollowup(result, test, taskIndex, messageIndex, token) {
|
||||
// await new Promise(r => setTimeout(r, 2000));
|
||||
// return [{
|
||||
// title: '$(sparkle) Ask copilot for help',
|
||||
// command: 'asdf'
|
||||
// }];
|
||||
// },
|
||||
// }));
|
||||
|
||||
|
||||
ctrl.resolveHandler = async test => {
|
||||
if (!test) {
|
||||
context.subscriptions.push(await startWatchingWorkspace(ctrl, fileChangedEmitter));
|
||||
|
@ -62,7 +74,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||
});
|
||||
|
||||
const createRunHandler = (
|
||||
runnerCtor: { new (folder: vscode.WorkspaceFolder): VSCodeTestRunner },
|
||||
runnerCtor: { new(folder: vscode.WorkspaceFolder): VSCodeTestRunner },
|
||||
kind: vscode.TestRunProfileKind,
|
||||
args: string[] = []
|
||||
) => {
|
||||
|
|
|
@ -71,8 +71,6 @@ export class FailingDeepStrictEqualAssertFixer {
|
|||
},
|
||||
})
|
||||
);
|
||||
|
||||
tests.testResults;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
@ -190,7 +188,7 @@ class StrictEqualAssertion {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
constructor(private readonly expression: ts.CallExpression) {}
|
||||
constructor(private readonly expression: ts.CallExpression) { }
|
||||
|
||||
/** Gets the expected value */
|
||||
public get expectedValue(): ts.Expression | undefined {
|
||||
|
|
|
@ -18,13 +18,13 @@ import { TestId } from 'vs/workbench/contrib/testing/common/testId';
|
|||
import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService';
|
||||
import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult';
|
||||
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
|
||||
import { IMainThreadTestController, ITestRootProvider, ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
||||
import { IMainThreadTestController, ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
||||
import { CoverageDetails, ExtensionRunTestsRequest, IFileCoverage, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestResultState, TestRunProfileBitset, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes';
|
||||
import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers';
|
||||
import { ExtHostContext, ExtHostTestingShape, ILocationDto, ITestControllerPatch, MainContext, MainThreadTestingShape } from '../common/extHost.protocol';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadTesting)
|
||||
export class MainThreadTesting extends Disposable implements MainThreadTestingShape, ITestRootProvider {
|
||||
export class MainThreadTesting extends Disposable implements MainThreadTestingShape {
|
||||
private readonly proxy: ExtHostTestingShape;
|
||||
private readonly diffListener = this._register(new MutableDisposable());
|
||||
private readonly testProviderRegistrations = new Map<string, {
|
||||
|
@ -233,6 +233,9 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
|
|||
runTests: (reqs, token) => this.proxy.$runControllerTests(reqs, token),
|
||||
startContinuousRun: (reqs, token) => this.proxy.$startContinuousRun(reqs, token),
|
||||
expandTest: (testId, levels) => this.proxy.$expandTest(testId, isFinite(levels) ? levels : -1),
|
||||
provideTestFollowups: (req, token) => this.proxy.$provideTestFollowups(req, token),
|
||||
executeTestFollowup: id => this.proxy.$executeTestFollowup(id),
|
||||
disposeTestFollowups: ids => this.proxy.$disposeTestFollowups(ids),
|
||||
};
|
||||
|
||||
disposable.add(toDisposable(() => this.testProfiles.removeProfile(controllerId)));
|
||||
|
|
|
@ -453,6 +453,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
|||
checkProposedApiEnabled(extension, 'testObserver');
|
||||
return extHostTesting.runTests(provider);
|
||||
},
|
||||
registerTestFollowupProvider(provider) {
|
||||
checkProposedApiEnabled(extension, 'testObserver');
|
||||
return extHostTesting.registerTestFollowupProvider(provider);
|
||||
},
|
||||
get onDidChangeTestResults() {
|
||||
checkProposedApiEnabled(extension, 'testObserver');
|
||||
return _asExtensionEvent(extHostTesting.onResultsChanged);
|
||||
|
|
|
@ -65,7 +65,7 @@ import { InputValidationType } from 'vs/workbench/contrib/scm/common/scm';
|
|||
import { IWorkspaceSymbol, NotebookPriorityInfo } from 'vs/workbench/contrib/search/common/search';
|
||||
import { IRawClosedNotebookFileMatch } from 'vs/workbench/contrib/search/common/searchNotebookHelpers';
|
||||
import { IKeywordRecognitionEvent, ISpeechProviderMetadata, ISpeechToTextEvent, ITextToSpeechEvent } from 'vs/workbench/contrib/speech/common/speechService';
|
||||
import { CoverageDetails, ExtensionRunTestsRequest, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, IStartControllerTests, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes';
|
||||
import { CoverageDetails, ExtensionRunTestsRequest, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, IStartControllerTests, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestMessageFollowupRequest, TestMessageFollowupResponse, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes';
|
||||
import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline';
|
||||
import { TypeHierarchyItem } from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy';
|
||||
import { RelatedInformationResult, RelatedInformationType } from 'vs/workbench/services/aiRelatedInformation/common/aiRelatedInformation';
|
||||
|
@ -2703,8 +2703,6 @@ export interface ExtHostTestingShape {
|
|||
$cancelExtensionTestRun(runId: string | undefined): void;
|
||||
/** Handles a diff of tests, as a result of a subscribeToDiffs() call */
|
||||
$acceptDiff(diff: TestsDiffOp.Serialized[]): void;
|
||||
/** Publishes that a test run finished. */
|
||||
$publishTestResults(results: ISerializedTestResults[]): void;
|
||||
/** Expands a test item's children, by the given number of levels. */
|
||||
$expandTest(testId: string, levels: number): Promise<void>;
|
||||
/** Requests coverage details for a test run. Errors if not available. */
|
||||
|
@ -2719,6 +2717,17 @@ export interface ExtHostTestingShape {
|
|||
$syncTests(): Promise<void>;
|
||||
/** Sets the active test run profiles */
|
||||
$setDefaultRunProfiles(profiles: Record</* controller id */string, /* profile id */ number[]>): void;
|
||||
|
||||
// --- test results:
|
||||
|
||||
/** Publishes that a test run finished. */
|
||||
$publishTestResults(results: ISerializedTestResults[]): void;
|
||||
/** Requests followup actions for a test (failure) message */
|
||||
$provideTestFollowups(req: TestMessageFollowupRequest, token: CancellationToken): Promise<TestMessageFollowupResponse[]>;
|
||||
/** Actions a followup actions for a test (failure) message */
|
||||
$executeTestFollowup(id: number): Promise<void>;
|
||||
/** Disposes followup actions for a test (failure) message */
|
||||
$disposeTestFollowups(id: number[]): void;
|
||||
}
|
||||
|
||||
export interface ExtHostLocalizationShape {
|
||||
|
|
|
@ -27,7 +27,7 @@ import { TestRunProfileKind, TestRunRequest, FileCoverage } from 'vs/workbench/a
|
|||
import { TestCommandId } from 'vs/workbench/contrib/testing/common/constants';
|
||||
import { TestId, TestIdPathParts, TestPosition } from 'vs/workbench/contrib/testing/common/testId';
|
||||
import { InvalidTestItemError } from 'vs/workbench/contrib/testing/common/testItemCollection';
|
||||
import { AbstractIncrementalTestCollection, CoverageDetails, ICallProfileRunHandler, ISerializedTestResults, IStartControllerTests, IStartControllerTestsResult, ITestErrorMessage, ITestItem, ITestItemContext, ITestMessageMenuArgs, ITestRunProfile, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp, isStartControllerTests } from 'vs/workbench/contrib/testing/common/testTypes';
|
||||
import { AbstractIncrementalTestCollection, CoverageDetails, ICallProfileRunHandler, ISerializedTestResults, IStartControllerTests, IStartControllerTestsResult, ITestErrorMessage, ITestItem, ITestItemContext, ITestMessageMenuArgs, ITestRunProfile, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestMessageFollowupRequest, TestMessageFollowupResponse, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp, isStartControllerTests } from 'vs/workbench/contrib/testing/common/testTypes';
|
||||
import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import type * as vscode from 'vscode';
|
||||
|
||||
|
@ -41,6 +41,10 @@ interface ControllerInfo {
|
|||
|
||||
type DefaultProfileChangeEvent = Map</* controllerId */ string, Map< /* profileId */number, boolean>>;
|
||||
|
||||
let followupCounter = 0;
|
||||
|
||||
const testResultInternalIDs = new WeakMap<vscode.TestRunResult, string>();
|
||||
|
||||
export class ExtHostTesting extends Disposable implements ExtHostTestingShape {
|
||||
private readonly resultsChangedEmitter = this._register(new Emitter<void>());
|
||||
protected readonly controllers = new Map</* controller ID */ string, ControllerInfo>();
|
||||
|
@ -48,14 +52,16 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape {
|
|||
private readonly runTracker: TestRunCoordinator;
|
||||
private readonly observer: TestObservers;
|
||||
private readonly defaultProfilesChangedEmitter = this._register(new Emitter<DefaultProfileChangeEvent>());
|
||||
private readonly followupProviders = new Set<vscode.TestFollowupProvider>();
|
||||
private readonly testFollowups = new Map<number, vscode.Command>();
|
||||
|
||||
public onResultsChanged = this.resultsChangedEmitter.event;
|
||||
public results: ReadonlyArray<vscode.TestRunResult> = [];
|
||||
|
||||
constructor(
|
||||
@IExtHostRpcService rpc: IExtHostRpcService,
|
||||
@ILogService logService: ILogService,
|
||||
commands: ExtHostCommands,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
private readonly commands: ExtHostCommands,
|
||||
private readonly editors: ExtHostDocumentsAndEditors,
|
||||
) {
|
||||
super();
|
||||
|
@ -222,6 +228,14 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape {
|
|||
}, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements vscode.test.registerTestFollowupProvider
|
||||
*/
|
||||
public registerTestFollowupProvider(provider: vscode.TestFollowupProvider): vscode.Disposable {
|
||||
this.followupProviders.add(provider);
|
||||
return { dispose: () => { this.followupProviders.delete(provider); } };
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
@ -292,7 +306,11 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape {
|
|||
public $publishTestResults(results: ISerializedTestResults[]): void {
|
||||
this.results = Object.freeze(
|
||||
results
|
||||
.map(Convert.TestResults.to)
|
||||
.map(r => {
|
||||
const o = Convert.TestResults.to(r);
|
||||
testResultInternalIDs.set(o, r.id);
|
||||
return o;
|
||||
})
|
||||
.concat(this.results)
|
||||
.sort((a, b) => b.completedAt - a.completedAt)
|
||||
.slice(0, 32),
|
||||
|
@ -348,6 +366,52 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape {
|
|||
return res;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public async $provideTestFollowups(req: TestMessageFollowupRequest, token: CancellationToken): Promise<TestMessageFollowupResponse[]> {
|
||||
const results = this.results.find(r => testResultInternalIDs.get(r) === req.resultId);
|
||||
const test = results && findTestInResultSnapshot(TestId.fromString(req.extId), results?.results);
|
||||
if (!test) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let followups: vscode.Command[] = [];
|
||||
await Promise.all([...this.followupProviders].map(async provider => {
|
||||
try {
|
||||
const r = await provider.provideFollowup(results, test, req.taskIndex, req.messageIndex, token);
|
||||
if (r) {
|
||||
followups = followups.concat(r);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(`Error thrown while providing followup for test message`, e);
|
||||
}
|
||||
}));
|
||||
|
||||
if (token.isCancellationRequested) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return followups.map(command => {
|
||||
const id = followupCounter++;
|
||||
this.testFollowups.set(id, command);
|
||||
return { title: command.title, id };
|
||||
});
|
||||
}
|
||||
|
||||
$disposeTestFollowups(id: number[]): void {
|
||||
for (const i of id) {
|
||||
this.testFollowups.delete(i);
|
||||
}
|
||||
}
|
||||
|
||||
$executeTestFollowup(id: number): Promise<void> {
|
||||
const command = this.testFollowups.get(id);
|
||||
if (!command) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this.commands.executeCommand(command.command, ...(command.arguments || []));
|
||||
}
|
||||
|
||||
private async runControllerTestRequest(req: ICallProfileRunHandler | ICallProfileRunHandler, isContinuous: boolean, token: CancellationToken): Promise<IStartControllerTestsResult> {
|
||||
const lookup = this.controllers.get(req.controllerId);
|
||||
if (!lookup) {
|
||||
|
@ -1202,3 +1266,20 @@ const profileGroupToBitset: { [K in TestRunProfileKind]: TestRunProfileBitset }
|
|||
[TestRunProfileKind.Debug]: TestRunProfileBitset.Debug,
|
||||
[TestRunProfileKind.Run]: TestRunProfileBitset.Run,
|
||||
};
|
||||
|
||||
function findTestInResultSnapshot(extId: TestId, snapshot: readonly Readonly<vscode.TestResultSnapshot>[]) {
|
||||
for (let i = 0; i < extId.path.length; i++) {
|
||||
const item = snapshot.find(s => s.id === extId.path[i]);
|
||||
if (!item) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (i === extId.path.length - 1) {
|
||||
return item;
|
||||
}
|
||||
|
||||
snapshot = item.children;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
@ -267,6 +267,48 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.testing-followup-action {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 22px;
|
||||
right: 22px;
|
||||
margin-top: -25px;
|
||||
line-height: 25px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(transparent, var(--vscode-peekViewEditor-background) 50%);
|
||||
|
||||
&.animated {
|
||||
animation: fadeIn 150ms ease-out;
|
||||
}
|
||||
|
||||
> a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
width: fit-content;
|
||||
|
||||
&, .codicon {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--vscode-textLink-activeForeground);
|
||||
}
|
||||
|
||||
&[aria-disabled="true"] {
|
||||
color: inherit;
|
||||
cursor: default;
|
||||
|
||||
.codicon {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** -- filter */
|
||||
.monaco-action-bar.testing-filter-action-bar {
|
||||
flex-shrink: 0;
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { alert } from 'vs/base/browser/ui/aria/aria';
|
||||
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
|
||||
|
@ -16,6 +17,7 @@ import { ITreeContextMenuEvent, ITreeNode } from 'vs/base/browser/ui/tree/tree';
|
|||
import { Action, IAction, Separator } from 'vs/base/common/actions';
|
||||
import { Delayer, Limiter, RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
|
@ -97,7 +99,7 @@ import { ITestExplorerFilterState } from 'vs/workbench/contrib/testing/common/te
|
|||
import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService';
|
||||
import { ITaskRawOutput, ITestResult, ITestRunTaskResults, LiveTestResult, TestResultItemChange, TestResultItemChangeReason, maxCountPriority, resultItemParents } from 'vs/workbench/contrib/testing/common/testResult';
|
||||
import { ITestResultService, ResultChangeEvent } from 'vs/workbench/contrib/testing/common/testResultService';
|
||||
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
||||
import { ITestFollowup, ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
||||
import { IRichLocation, ITestErrorMessage, ITestItem, ITestItemContext, ITestMessage, ITestMessageMenuArgs, ITestRunTask, ITestTaskState, InternalTestItem, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset, getMarkId, testResultStateToContextValues } from 'vs/workbench/contrib/testing/common/testTypes';
|
||||
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
|
||||
import { IShowResultOptions, ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener';
|
||||
|
@ -769,11 +771,82 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo
|
|||
}
|
||||
}
|
||||
|
||||
const FOLLOWUP_ANIMATION_MIN_TIME = 500;
|
||||
|
||||
class FollowupActionWidget extends Disposable {
|
||||
private readonly el = dom.h('div.testing-followup-action', []);
|
||||
private readonly visibleStore = this._register(new DisposableStore());
|
||||
|
||||
constructor(
|
||||
private readonly container: HTMLElement,
|
||||
@ITestService private readonly testService: ITestService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public show(subject: InspectSubject) {
|
||||
this.visibleStore.clear();
|
||||
if (subject instanceof MessageSubject) {
|
||||
this.showMessage(subject);
|
||||
}
|
||||
}
|
||||
|
||||
private async showMessage(subject: MessageSubject) {
|
||||
const cts = this.visibleStore.add(new CancellationTokenSource());
|
||||
const start = Date.now();
|
||||
const followups = await this.testService.provideTestFollowups({
|
||||
extId: subject.test.extId,
|
||||
messageIndex: subject.messageIndex,
|
||||
resultId: subject.result.id,
|
||||
taskIndex: subject.taskIndex,
|
||||
}, cts.token);
|
||||
|
||||
|
||||
if (!followups.followups.length || cts.token.isCancellationRequested) {
|
||||
followups.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
this.visibleStore.add(followups);
|
||||
|
||||
dom.clearNode(this.el.root);
|
||||
this.el.root.classList.toggle('animated', Date.now() - start > FOLLOWUP_ANIMATION_MIN_TIME);
|
||||
for (const fu of followups.followups) {
|
||||
const link = document.createElement('a');
|
||||
link.tabIndex = 0;
|
||||
dom.reset(link, ...renderLabelWithIcons(fu.message));
|
||||
|
||||
this.visibleStore.add(dom.addDisposableListener(link, 'click', () => this.actionFollowup(link, fu)));
|
||||
this.visibleStore.add(dom.addDisposableListener(link, 'keydown', e => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) {
|
||||
this.actionFollowup(link, fu);
|
||||
}
|
||||
}));
|
||||
|
||||
this.el.root.appendChild(link);
|
||||
}
|
||||
|
||||
this.container.appendChild(this.el.root);
|
||||
this.visibleStore.add(toDisposable(() => {
|
||||
this.el.root.parentElement?.removeChild(this.el.root);
|
||||
}));
|
||||
}
|
||||
|
||||
private actionFollowup(link: HTMLAnchorElement, fu: ITestFollowup) {
|
||||
if (link.ariaDisabled !== 'true') {
|
||||
link.ariaDisabled = 'true';
|
||||
fu.execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TestResultsViewContent extends Disposable {
|
||||
private static lastSplitWidth?: number;
|
||||
|
||||
private readonly didReveal = this._register(new Emitter<{ subject: InspectSubject; preserveFocus: boolean }>());
|
||||
private readonly currentSubjectStore = this._register(new DisposableStore());
|
||||
private followupWidget!: FollowupActionWidget;
|
||||
private messageContextKeyService!: IContextKeyService;
|
||||
private contextKeyTestMessage!: IContextKey<string>;
|
||||
private contextKeyResultOutdated!: IContextKey<boolean>;
|
||||
|
@ -810,6 +883,7 @@ class TestResultsViewContent extends Disposable {
|
|||
const { historyVisible, showRevealLocationOnMessages } = this.options;
|
||||
const isInPeekView = this.editor !== undefined;
|
||||
const messageContainer = this.messageContainer = dom.append(containerElement, dom.$('.test-output-peek-message-container'));
|
||||
this.followupWidget = this._register(this.instantiationService.createInstance(FollowupActionWidget, messageContainer));
|
||||
this.contentProviders = [
|
||||
this._register(this.instantiationService.createInstance(DiffContentProvider, this.editor, messageContainer)),
|
||||
this._register(this.instantiationService.createInstance(MarkdownTestMessagePeek, messageContainer)),
|
||||
|
@ -883,7 +957,7 @@ class TestResultsViewContent extends Disposable {
|
|||
this.current = opts.subject;
|
||||
return this.contentProvidersUpdateLimiter.queue(async () => {
|
||||
await Promise.all(this.contentProviders.map(p => p.update(opts.subject)));
|
||||
|
||||
this.followupWidget.show(opts.subject);
|
||||
this.currentSubjectStore.clear();
|
||||
this.populateFloatingClick(opts.subject);
|
||||
});
|
||||
|
|
|
@ -12,7 +12,7 @@ import { URI } from 'vs/base/common/uri';
|
|||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
|
||||
import { IObservableValue, MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue';
|
||||
import { AbstractIncrementalTestCollection, ICallProfileRunHandler, IncrementalTestCollectionItem, InternalTestItem, ITestItemContext, ResolvedTestRunRequest, IStartControllerTests, IStartControllerTestsResult, TestItemExpandState, TestRunProfileBitset, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes';
|
||||
import { AbstractIncrementalTestCollection, ICallProfileRunHandler, IncrementalTestCollectionItem, InternalTestItem, ITestItemContext, ResolvedTestRunRequest, IStartControllerTests, IStartControllerTestsResult, TestItemExpandState, TestRunProfileBitset, TestsDiff, TestMessageFollowupResponse, TestMessageFollowupRequest } from 'vs/workbench/contrib/testing/common/testTypes';
|
||||
import { TestExclusions } from 'vs/workbench/contrib/testing/common/testExclusions';
|
||||
import { TestId } from 'vs/workbench/contrib/testing/common/testId';
|
||||
import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult';
|
||||
|
@ -29,6 +29,9 @@ export interface IMainThreadTestController {
|
|||
expandTest(id: string, levels: number): Promise<void>;
|
||||
startContinuousRun(request: ICallProfileRunHandler[], token: CancellationToken): Promise<IStartControllerTestsResult[]>;
|
||||
runTests(request: IStartControllerTests[], token: CancellationToken): Promise<IStartControllerTestsResult[]>;
|
||||
provideTestFollowups(req: TestMessageFollowupRequest, token: CancellationToken): Promise<TestMessageFollowupResponse[]>;
|
||||
executeTestFollowup(id: number): Promise<void>;
|
||||
disposeTestFollowups(ids: number[]): void;
|
||||
}
|
||||
|
||||
export interface IMainThreadTestCollection extends AbstractIncrementalTestCollection<IncrementalTestCollectionItem> {
|
||||
|
@ -213,14 +216,6 @@ export const testsUnderUri = async function* (testService: ITestService, ident:
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* An instance of the RootProvider should be registered for each extension
|
||||
* host.
|
||||
*/
|
||||
export interface ITestRootProvider {
|
||||
// todo: nothing, yet
|
||||
}
|
||||
|
||||
/**
|
||||
* A run request that expresses the intent of the request and allows the
|
||||
* test service to resolve the specifics of the group.
|
||||
|
@ -236,6 +231,15 @@ export interface AmbiguousRunTestsRequest {
|
|||
continuous?: boolean;
|
||||
}
|
||||
|
||||
export interface ITestFollowup {
|
||||
message: string;
|
||||
execute(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ITestFollowups extends IDisposable {
|
||||
followups: ITestFollowup[];
|
||||
}
|
||||
|
||||
export interface ITestService {
|
||||
readonly _serviceBrand: undefined;
|
||||
/**
|
||||
|
@ -304,6 +308,11 @@ export interface ITestService {
|
|||
*/
|
||||
runResolvedTests(req: ResolvedTestRunRequest, token?: CancellationToken): Promise<ITestResult>;
|
||||
|
||||
/**
|
||||
* Provides followup actions for a test run.
|
||||
*/
|
||||
provideTestFollowups(req: TestMessageFollowupRequest, token: CancellationToken): Promise<ITestFollowups>;
|
||||
|
||||
/**
|
||||
* Ensures the test diff from the remote ext host is flushed and waits for
|
||||
* any "busy" tests to become idle before resolving.
|
||||
|
|
|
@ -27,8 +27,8 @@ import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingC
|
|||
import { canUseProfileWithTest, ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService';
|
||||
import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult';
|
||||
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
|
||||
import { AmbiguousRunTestsRequest, IMainThreadTestController, ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
||||
import { ResolvedTestRunRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes';
|
||||
import { AmbiguousRunTestsRequest, IMainThreadTestController, ITestFollowups, ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
||||
import { ResolvedTestRunRequest, TestDiffOpType, TestMessageFollowupRequest, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
|
||||
export class TestService extends Disposable implements ITestService {
|
||||
|
@ -264,6 +264,32 @@ export class TestService extends Disposable implements ITestService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public async provideTestFollowups(req: TestMessageFollowupRequest, token: CancellationToken): Promise<ITestFollowups> {
|
||||
const reqs = await Promise.all([...this.testControllers.values()]
|
||||
.map(async ctrl => ({ ctrl, followups: await ctrl.provideTestFollowups(req, token) })));
|
||||
|
||||
const followups: ITestFollowups = {
|
||||
followups: reqs.flatMap(({ ctrl, followups }) => followups.map(f => ({
|
||||
message: f.title,
|
||||
execute: () => ctrl.executeTestFollowup(f.id)
|
||||
}))),
|
||||
dispose: () => {
|
||||
for (const { ctrl, followups } of reqs) {
|
||||
ctrl.disposeTestFollowups(followups.map(f => f.id));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (token.isCancellationRequested) {
|
||||
followups.dispose();
|
||||
}
|
||||
|
||||
return followups;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
|
|
@ -474,6 +474,20 @@ export const applyTestItemUpdate = (internal: InternalTestItem | ITestItemUpdate
|
|||
}
|
||||
};
|
||||
|
||||
/** Request to an ext host to get followup messages for a test failure. */
|
||||
export interface TestMessageFollowupRequest {
|
||||
resultId: string;
|
||||
extId: string;
|
||||
taskIndex: number;
|
||||
messageIndex: number;
|
||||
}
|
||||
|
||||
/** Request to an ext host to get followup messages for a test failure. */
|
||||
export interface TestMessageFollowupResponse {
|
||||
id: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test result item used in the main thread.
|
||||
*/
|
||||
|
|
|
@ -15,6 +15,11 @@ declare module 'vscode' {
|
|||
*/
|
||||
export function runTests(run: TestRunRequest, token?: CancellationToken): Thenable<void>;
|
||||
|
||||
/**
|
||||
* Registers a provider that can provide follow-up actions for a test failure.
|
||||
*/
|
||||
export function registerTestFollowupProvider(provider: TestFollowupProvider): Disposable;
|
||||
|
||||
/**
|
||||
* Returns an observer that watches and can request tests.
|
||||
*/
|
||||
|
@ -31,6 +36,10 @@ declare module 'vscode' {
|
|||
export const onDidChangeTestResults: Event<void>;
|
||||
}
|
||||
|
||||
export interface TestFollowupProvider {
|
||||
provideFollowup(result: TestRunResult, test: TestResultSnapshot, taskIndex: number, messageIndex: number, token: CancellationToken): ProviderResult<Command[]>;
|
||||
}
|
||||
|
||||
export interface TestObserver {
|
||||
/**
|
||||
* List of tests returned by test provider for files in the workspace.
|
||||
|
|
Loading…
Reference in a new issue