testing: initial implementation of test decorations
This commit is contained in:
parent
3e55989cca
commit
add5b32d95
|
@ -32,7 +32,7 @@
|
|||
"testing.enableProblemDiagnostics": {
|
||||
"description": "%config.enableProblemDiagnostics%",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ class TestingConfig implements IDisposable {
|
|||
}
|
||||
|
||||
public get diagnostics() {
|
||||
return this.section.get(Constants.EnableDiagnosticsConfig, true);
|
||||
return this.section.get(Constants.EnableDiagnosticsConfig, false);
|
||||
}
|
||||
|
||||
public get isEnabled() {
|
||||
|
|
|
@ -106,6 +106,7 @@ export abstract class PeekViewWidget extends ZoneWidget {
|
|||
|
||||
private readonly _onDidClose = new Emitter<PeekViewWidget>();
|
||||
readonly onDidClose = this._onDidClose.event;
|
||||
private disposed?: true;
|
||||
|
||||
protected _headElement?: HTMLDivElement;
|
||||
protected _primaryHeading?: HTMLElement;
|
||||
|
@ -124,8 +125,11 @@ export abstract class PeekViewWidget extends ZoneWidget {
|
|||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
this._onDidClose.fire(this);
|
||||
if (!this.disposed) {
|
||||
this.disposed = true; // prevent consumers who dispose on onDidClose from looping
|
||||
super.dispose();
|
||||
this._onDidClose.fire(this);
|
||||
}
|
||||
}
|
||||
|
||||
style(styles: IPeekViewStyles): void {
|
||||
|
|
|
@ -172,6 +172,24 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi
|
|||
this.setDecorationsScheduler.schedule();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns context menu actions at the line number if breakpoints can be
|
||||
* set. This is used by the {@link TestingDecorations} to allow breakpoint
|
||||
* setting on lines where breakpoint "run" actions are present.
|
||||
*/
|
||||
public getContextMenuActionsAtPosition(lineNumber: number, model: ITextModel) {
|
||||
if (!this.debugService.getAdapterManager().hasDebuggers()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!this.debugService.canSetBreakpointsIn(model)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const breakpoints = this.debugService.getModel().getBreakpoints({ lineNumber, uri: model.uri });
|
||||
return this.getContextMenuActions(breakpoints, model.uri, lineNumber);
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this.toDispose.push(this.editor.onMouseDown(async (e: IEditorMouseEvent) => {
|
||||
if (!this.debugService.getAdapterManager().hasDebuggers()) {
|
||||
|
@ -376,7 +394,8 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi
|
|||
const decorations = this.editor.getLineDecorations(line);
|
||||
if (decorations) {
|
||||
for (const { options } of decorations) {
|
||||
if (options.glyphMarginClassName && options.glyphMarginClassName.indexOf('codicon-') === -1) {
|
||||
const clz = options.glyphMarginClassName;
|
||||
if (clz && (!clz.includes('codicon-') || clz.includes('codicon-testing-'))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configur
|
|||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes';
|
||||
import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
|
||||
export const VIEWLET_ID = 'workbench.view.debug';
|
||||
|
||||
|
@ -951,6 +952,7 @@ export interface IDebugEditorContribution extends editorCommon.IEditorContributi
|
|||
export interface IBreakpointEditorContribution extends editorCommon.IEditorContribution {
|
||||
showBreakpointWidget(lineNumber: number, column: number | undefined, context?: BreakpointWidgetContext): void;
|
||||
closeBreakpointWidget(): void;
|
||||
getContextMenuActionsAtPosition(lineNumber: number, model: EditorIModel): IAction[];
|
||||
}
|
||||
|
||||
// temporary debug helper service
|
||||
|
|
|
@ -46,14 +46,8 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes
|
|||
this._register(listener.onFolderChange(this.applyFolderChange, this));
|
||||
|
||||
for (const [folder, collection] of listener.workspaceFolderCollections) {
|
||||
const queue = [collection.rootIds];
|
||||
while (queue.length) {
|
||||
for (const id of queue.pop()!) {
|
||||
const node = collection.getNodeById(id)!;
|
||||
const item = this.createItem(node, folder.folder);
|
||||
this.storeItem(item);
|
||||
queue.push(node.children);
|
||||
}
|
||||
for (const node of collection.all) {
|
||||
this.storeItem(this.createItem(node, folder.folder));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,11 +8,11 @@ import { localize } from 'vs/nls';
|
|||
import { registerIcon } from 'vs/platform/theme/common/iconRegistry';
|
||||
import { registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService';
|
||||
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
|
||||
import { testStatesToIconColors } from 'vs/workbench/contrib/testing/browser/theme';
|
||||
import { testingColorRunAction, testStatesToIconColors } from 'vs/workbench/contrib/testing/browser/theme';
|
||||
|
||||
export const testingViewIcon = registerIcon('test-view-icon', Codicon.beaker, localize('testViewIcon', 'View icon of the test view.'));
|
||||
export const testingRunIcon = registerIcon('testing-run-icon', Codicon.run, localize('testingRunIcon', 'Icon of the "run test" action.'));
|
||||
export const testingRunAllIcon = registerIcon('testing-run-icon', Codicon.runAll, localize('testingRunAllIcon', 'Icon of the "run all tests" action.'));
|
||||
export const testingRunAllIcon = registerIcon('testing-run-all-icon', Codicon.runAll, localize('testingRunAllIcon', 'Icon of the "run all tests" action.'));
|
||||
export const testingDebugIcon = registerIcon('testing-debug-icon', Codicon.debugAlt, localize('testingDebugIcon', 'Icon of the "debug test" action.'));
|
||||
export const testingCancelIcon = registerIcon('testing-cancel-icon', Codicon.close, localize('testingCancelIcon', 'Icon to cancel ongoing test runs.'));
|
||||
|
||||
|
@ -24,7 +24,7 @@ export const testingStatesToIcons = new Map<TestRunState, ThemeIcon>([
|
|||
[TestRunState.Failed, registerIcon('testing-failed-icon', Codicon.close, localize('testingFailedIcon', 'Icon shown for tests that failed.'))],
|
||||
[TestRunState.Passed, registerIcon('testing-passed-icon', Codicon.pass, localize('testingPassedIcon', 'Icon shown for tests that passed.'))],
|
||||
[TestRunState.Queued, registerIcon('testing-queued-icon', Codicon.watch, localize('testingQueuedIcon', 'Icon shown for tests that are queued.'))],
|
||||
[TestRunState.Running, Codicon.loading],
|
||||
[TestRunState.Running, ThemeIcon.modify(Codicon.loading, 'spin')],
|
||||
[TestRunState.Skipped, registerIcon('testing-skipped-icon', Codicon.debugStepOver, localize('testingSkippedIcon', 'Icon shown for tests that are skipped.'))],
|
||||
[TestRunState.Unset, registerIcon('testing-unset-icon', Codicon.circleOutline, localize('testingUnsetIcon', 'Icon shown for tests that are in an unset state.'))],
|
||||
]);
|
||||
|
@ -39,4 +39,11 @@ registerThemingParticipant((theme, collector) => {
|
|||
color: ${theme.getColor(color)} !important;
|
||||
}`);
|
||||
}
|
||||
|
||||
collector.addRule(`
|
||||
.monaco-editor ${ThemeIcon.asCSSSelector(testingRunIcon)},
|
||||
.monaco-editor ${ThemeIcon.asCSSSelector(testingRunAllIcon)} {
|
||||
color: ${theme.getColor(testingColorRunAction)};
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -71,7 +71,6 @@
|
|||
max-height: 29px;
|
||||
}
|
||||
|
||||
|
||||
/** -- filter */
|
||||
.testing-filter-action-bar {
|
||||
flex-shrink: 0;
|
||||
|
@ -92,3 +91,13 @@
|
|||
.testing-filter-action-item .testing-filter-wrapper {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/** -- decorations */
|
||||
|
||||
.monaco-editor .testing-run-glyph {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.monaco-editor .testing-inline-message {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { localize } from 'vs/nls';
|
|||
import { Action2, MenuId } from 'vs/platform/actions/common/actions';
|
||||
import { ContextKeyAndExpr, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol';
|
||||
|
@ -20,10 +21,9 @@ import * as icons from 'vs/workbench/contrib/testing/browser/icons';
|
|||
import { TestingExplorerView, TestingExplorerViewModel } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
|
||||
import { TestExplorerViewGrouping, TestExplorerViewMode, Testing } from 'vs/workbench/contrib/testing/common/constants';
|
||||
import { EMPTY_TEST_RESULT, InternalTestItem, RunTestsResult, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { IWorkspaceTestCollectionService } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
|
||||
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
|
||||
import { ITestService, waitForAllRoots } from 'vs/workbench/contrib/testing/common/testService';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IWorkspaceTestCollectionService } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
|
||||
|
||||
const category = localize('testing.category', 'Test');
|
||||
|
||||
|
@ -197,18 +197,18 @@ abstract class RunOrDebugAllAllAction extends Action2 {
|
|||
|
||||
const tests: TestIdWithProvider[] = [];
|
||||
await Promise.all(workspace.getWorkspace().folders.map(async (folder) => {
|
||||
const handle = testService.subscribeToDiffs(ExtHostTestingResource.Workspace, folder.uri);
|
||||
const ref = testService.subscribeToDiffs(ExtHostTestingResource.Workspace, folder.uri);
|
||||
try {
|
||||
await waitForAllRoots(handle.collection);
|
||||
await waitForAllRoots(ref.object);
|
||||
|
||||
for (const root of handle.collection.rootIds) {
|
||||
const node = handle.collection.getNodeById(root);
|
||||
for (const root of ref.object.rootIds) {
|
||||
const node = ref.object.getNodeById(root);
|
||||
if (node && (this.debug ? node.item.debuggable : node.item.runnable)) {
|
||||
tests.push({ testId: node.id, providerId: node.providerId });
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
handle.dispose();
|
||||
ref.dispose();
|
||||
}
|
||||
}));
|
||||
|
||||
|
|
|
@ -5,8 +5,7 @@
|
|||
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorAction2, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||
import { localize } from 'vs/nls';
|
||||
import { registerAction2 } from 'vs/platform/actions/common/actions';
|
||||
|
@ -20,6 +19,7 @@ import { Registry } from 'vs/platform/registry/common/platform';
|
|||
import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
|
||||
import { Extensions as ViewContainerExtensions, IViewContainersRegistry, IViewsRegistry, ViewContainerLocation } from 'vs/workbench/common/views';
|
||||
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 { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek';
|
||||
|
@ -32,7 +32,6 @@ import { ITestResultService, TestResultService } from 'vs/workbench/contrib/test
|
|||
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 { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
|
||||
import * as Action from './testExplorerActions';
|
||||
|
||||
|
@ -99,6 +98,7 @@ registerAction2(Action.DebugAllAction);
|
|||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingContentProvider, LifecyclePhase.Eventually);
|
||||
|
||||
registerEditorContribution(Testing.OutputPeekContributionId, TestingOutputPeekController);
|
||||
registerEditorContribution(Testing.DecorationsContributionId, TestingDecorations);
|
||||
|
||||
CommandsRegistry.registerCommand({
|
||||
id: 'vscode.runTests',
|
||||
|
@ -117,28 +117,10 @@ CommandsRegistry.registerCommand({
|
|||
});
|
||||
|
||||
CommandsRegistry.registerCommand({
|
||||
id: 'vscode.revealTestMessage',
|
||||
handler: async (accessor: ServicesAccessor, testRef: TestIdWithProvider, messageIndex: number) => {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const testService = accessor.get(ITestService);
|
||||
|
||||
const test = await testService.lookupTest(testRef);
|
||||
const message = test?.item.state.messages[messageIndex];
|
||||
if (!test || !message?.location) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pane = await editorService.openEditor({
|
||||
resource: URI.revive(message.location.uri),
|
||||
options: { selection: message.location.range }
|
||||
});
|
||||
|
||||
const control = pane?.getControl();
|
||||
if (!isCodeEditor(control)) {
|
||||
return;
|
||||
}
|
||||
|
||||
TestingOutputPeekController.get(control).show(test, messageIndex);
|
||||
id: 'vscode.revealTestInExplorer',
|
||||
handler: async (accessor: ServicesAccessor, path: string[]) => {
|
||||
accessor.get(ITestExplorerFilterState).reveal = path;
|
||||
await new Action.ShowTestView().run(accessor);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
328
src/vs/workbench/contrib/testing/browser/testingDecorations.ts
Normal file
328
src/vs/workbench/contrib/testing/browser/testingDecorations.ts
Normal file
|
@ -0,0 +1,328 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Action, IAction, Separator } from 'vs/base/common/actions';
|
||||
import { MarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { Disposable, dispose, IDisposable, IReference, MutableDisposable } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { IRange } from 'vs/editor/common/core/range';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { IModelDeltaDecoration, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model';
|
||||
import { Location as ModeLocation } from 'vs/editor/common/modes';
|
||||
import { overviewRulerError, overviewRulerInfo, overviewRulerWarning } from 'vs/editor/common/view/editorColorRegistry';
|
||||
import { localize } from 'vs/nls';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IThemeService, themeColorFromId, ThemeIcon } from 'vs/platform/theme/common/themeService';
|
||||
import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { TestMessageSeverity, TestRunState } from 'vs/workbench/api/common/extHostTypes';
|
||||
import { BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { testingRunAllIcon, testingRunIcon, testingStatesToIcons } from 'vs/workbench/contrib/testing/browser/icons';
|
||||
import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek';
|
||||
import { testMessageSeverityColors } from 'vs/workbench/contrib/testing/browser/theme';
|
||||
import { IncrementalTestCollectionItem, ITestMessage } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { maxPriority } from 'vs/workbench/contrib/testing/common/testingStates';
|
||||
import { buildTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
|
||||
import { IMainThreadTestCollection, ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
||||
|
||||
export class TestingDecorations extends Disposable implements IEditorContribution {
|
||||
private collection = this._register(new MutableDisposable<IReference<IMainThreadTestCollection>>());
|
||||
private lastDecorations: ITestDecoration[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly editor: ICodeEditor,
|
||||
@ITestService private readonly testService: ITestService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
) {
|
||||
super();
|
||||
this.attachModel(editor.getModel()?.uri);
|
||||
this._register(this.editor.onDidChangeModel(e => this.attachModel(e.newModelUrl || undefined)));
|
||||
this._register(this.editor.onMouseDown(e => {
|
||||
for (const decoration of this.lastDecorations) {
|
||||
if (decoration.click(e)) {
|
||||
e.event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private attachModel(uri?: URI) {
|
||||
if (!uri) {
|
||||
this.collection.value = undefined;
|
||||
this.clearDecorations();
|
||||
return;
|
||||
}
|
||||
|
||||
this.collection.value = this.testService.subscribeToDiffs(ExtHostTestingResource.TextDocument, uri, () => this.setDecorations(uri));
|
||||
this.setDecorations(uri);
|
||||
}
|
||||
|
||||
private setDecorations(uri: URI): void {
|
||||
const ref = this.collection.value;
|
||||
if (!ref) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.editor.changeDecorations(accessor => {
|
||||
const newDecorations: ITestDecoration[] = [];
|
||||
for (const test of ref.object.all) {
|
||||
if (hasValidLocation(uri, test.item)) {
|
||||
newDecorations.push(this.instantiationService.createInstance(
|
||||
RunTestDecoration, test, ref.object, test.item.location, this.editor));
|
||||
}
|
||||
|
||||
for (let i = 0; i < test.item.state.messages.length; i++) {
|
||||
const m = test.item.state.messages[i];
|
||||
if (hasValidLocation(uri, m)) {
|
||||
const uri = buildTestUri({
|
||||
type: TestUriType.LiveMessage,
|
||||
messageIndex: i,
|
||||
providerId: test.providerId,
|
||||
testId: test.id,
|
||||
});
|
||||
|
||||
newDecorations.push(this.instantiationService.createInstance(TestMessageDecoration, m, uri, m.location, this.editor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
accessor
|
||||
.deltaDecorations(this.lastDecorations.map(d => d.id), newDecorations.map(d => d.editorDecoration))
|
||||
.forEach((id, i) => newDecorations[i].id = id);
|
||||
|
||||
this.lastDecorations = newDecorations;
|
||||
});
|
||||
}
|
||||
|
||||
private clearDecorations(): void {
|
||||
this.editor.changeDecorations(accessor => {
|
||||
for (const decoration of this.lastDecorations) {
|
||||
accessor.removeDecoration(decoration.id);
|
||||
}
|
||||
|
||||
this.lastDecorations = [];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface ITestDecoration extends IDisposable {
|
||||
/**
|
||||
* ID of the decoration after being added to the editor, set after the
|
||||
* decoration is applied.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
readonly editorDecoration: IModelDeltaDecoration;
|
||||
|
||||
/**
|
||||
* Handles a click event, returns true if it was handled.
|
||||
*/
|
||||
click(e: IEditorMouseEvent): boolean;
|
||||
}
|
||||
|
||||
const hasValidLocation = <T extends { location?: ModeLocation }>(editorUri: URI, t: T): t is T & { location: ModeLocation } =>
|
||||
t.location?.uri.toString() === editorUri.toString();
|
||||
|
||||
const firstLineRange = (editor: ICodeEditor, originalRange: IRange) => {
|
||||
const model = editor.getModel();
|
||||
const endColumn = model?.getLineMaxColumn(originalRange.startLineNumber);
|
||||
return {
|
||||
startLineNumber: originalRange.startLineNumber,
|
||||
endLineNumber: originalRange.startLineNumber,
|
||||
startColumn: 0,
|
||||
endColumn: endColumn || 1,
|
||||
};
|
||||
};
|
||||
|
||||
class RunTestDecoration implements ITestDecoration {
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
id = '';
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public readonly editorDecoration: IModelDeltaDecoration;
|
||||
|
||||
private line: number;
|
||||
|
||||
constructor(
|
||||
private readonly test: IncrementalTestCollectionItem,
|
||||
private readonly collection: IMainThreadTestCollection,
|
||||
private readonly location: ModeLocation,
|
||||
private readonly editor: ICodeEditor,
|
||||
@ITestService private readonly testService: ITestService,
|
||||
@IContextMenuService private readonly contextMenuService: IContextMenuService,
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
) {
|
||||
this.line = location.range.startLineNumber;
|
||||
|
||||
const queue = [test.children];
|
||||
let state = this.test.item.state.runState;
|
||||
while (queue.length) {
|
||||
for (const child of queue.pop()!) {
|
||||
const node = collection.getNodeById(child);
|
||||
if (node) {
|
||||
state = maxPriority(node.item.state.runState, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const icon = state !== TestRunState.Unset
|
||||
? testingStatesToIcons.get(state)!
|
||||
: test.children.size > 0 ? testingRunAllIcon : testingRunIcon;
|
||||
|
||||
this.editorDecoration = {
|
||||
range: firstLineRange(this.editor, this.location.range),
|
||||
options: {
|
||||
isWholeLine: true,
|
||||
glyphMarginClassName: ThemeIcon.asClassName(icon) + ' testing-run-glyph',
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
glyphMarginHoverMessage: new MarkdownString().appendText(localize('testing.clickToRun', 'Click to run tests, right click for more options')),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public click(e: IEditorMouseEvent): boolean {
|
||||
if (e.target.position?.lineNumber !== this.line || e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.event.rightButton) {
|
||||
const actions = this.getContextMenu();
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => ({ x: e.event.posx, y: e.event.posy }),
|
||||
getActions: () => actions,
|
||||
onHide: () => dispose(actions),
|
||||
});
|
||||
} else {
|
||||
// todo: customize click behavior
|
||||
this.testService.runTests({ tests: [{ testId: this.test.id, providerId: this.test.providerId }], debug: false });
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
private getContextMenu() {
|
||||
const model = this.editor.getModel();
|
||||
if (!model) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const testActions: IAction[] = [];
|
||||
if (this.test.item.runnable) {
|
||||
testActions.push(new Action('testing.run', localize('run test', 'Run Test'), undefined, undefined, () => this.testService.runTests({
|
||||
debug: false,
|
||||
tests: [{ providerId: this.test.providerId, testId: this.test.id }],
|
||||
})));
|
||||
}
|
||||
|
||||
if (this.test.item.debuggable) {
|
||||
testActions.push(new Action('testing.debug', localize('debug test', 'Debug Test'), undefined, undefined, () => this.testService.runTests({
|
||||
debug: true,
|
||||
tests: [{ providerId: this.test.providerId, testId: this.test.id }],
|
||||
})));
|
||||
}
|
||||
|
||||
testActions.push(new Action('testing.reveal', localize('reveal test', 'Reveal in Test Explorer'), undefined, undefined, async () => {
|
||||
const path = [];
|
||||
for (let id: string | null = this.test.id; id;) {
|
||||
const node = this.collection.getNodeById(id);
|
||||
if (!node) {
|
||||
break;
|
||||
}
|
||||
|
||||
path.unshift(node.item.label);
|
||||
id = node.parent;
|
||||
}
|
||||
|
||||
await this.commandService.executeCommand('vscode.revealTestInExplorer', this.test);
|
||||
}));
|
||||
|
||||
const breakpointActions = this.editor
|
||||
.getContribution<IBreakpointEditorContribution>(BREAKPOINT_EDITOR_CONTRIBUTION_ID)
|
||||
.getContextMenuActionsAtPosition(this.line, model);
|
||||
|
||||
return breakpointActions.length ? [...testActions, new Separator(), ...breakpointActions] : testActions;
|
||||
}
|
||||
}
|
||||
|
||||
class TestMessageDecoration implements ITestDecoration {
|
||||
public id = '';
|
||||
|
||||
public readonly editorDecoration: IModelDeltaDecoration;
|
||||
private readonly decorationId = `testmessage-${generateUuid()}`;
|
||||
|
||||
constructor(
|
||||
{ message, severity }: ITestMessage,
|
||||
private readonly messageUri: URI,
|
||||
location: ModeLocation,
|
||||
private readonly editor: ICodeEditor,
|
||||
@ICodeEditorService private readonly editorService: ICodeEditorService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
) {
|
||||
severity = severity || TestMessageSeverity.Error;
|
||||
|
||||
const colorTheme = themeService.getColorTheme();
|
||||
editorService.registerDecorationType(this.decorationId, {
|
||||
after: {
|
||||
contentText: message.toString(),
|
||||
color: `${colorTheme.getColor(testMessageSeverityColors[severity].decorationForeground)}`,
|
||||
fontSize: `${editor.getOption(EditorOption.fontSize)}px`,
|
||||
fontFamily: editor.getOption(EditorOption.fontFamily),
|
||||
padding: `0px 12px`,
|
||||
},
|
||||
}, undefined, editor);
|
||||
|
||||
const options = editorService.resolveDecorationOptions(this.decorationId, true);
|
||||
options.hoverMessage = typeof message === 'string' ? new MarkdownString().appendText(message) : message;
|
||||
options.afterContentClassName = `${options.afterContentClassName} testing-inline-message`;
|
||||
options.zIndex = 10; // todo: in spite of the z-index, this appears behind gitlens
|
||||
|
||||
const rulerColor = severity === TestMessageSeverity.Error
|
||||
? overviewRulerError
|
||||
: severity === TestMessageSeverity.Warning
|
||||
? overviewRulerWarning
|
||||
: severity === TestMessageSeverity.Information
|
||||
? overviewRulerInfo
|
||||
: undefined;
|
||||
|
||||
if (rulerColor) {
|
||||
options.overviewRuler = { color: themeColorFromId(rulerColor), position: OverviewRulerLane.Right };
|
||||
}
|
||||
|
||||
this.editorDecoration = { range: firstLineRange(editor, location.range), options };
|
||||
}
|
||||
|
||||
click(e: IEditorMouseEvent): boolean {
|
||||
if (e.event.rightButton) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.target.element?.className.includes(this.decorationId)) {
|
||||
TestingOutputPeekController.get(this.editor).show(this.messageUri);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.editorService.removeDecorationType(this.decorationId);
|
||||
}
|
||||
}
|
|
@ -27,19 +27,34 @@ import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingC
|
|||
export interface ITestExplorerFilterState {
|
||||
_serviceBrand: undefined;
|
||||
readonly onDidChange: Event<string>;
|
||||
readonly onDidRequestReveal: Event<string[]>;
|
||||
value: string;
|
||||
reveal: string[] | undefined;
|
||||
}
|
||||
|
||||
export const ITestExplorerFilterState = createDecorator<ITestExplorerFilterState>('testingFilterState');
|
||||
|
||||
export class TestExplorerFilterState implements ITestExplorerFilterState {
|
||||
declare _serviceBrand: undefined;
|
||||
|
||||
private readonly revealRequest = new Emitter<string[]>();
|
||||
private readonly changeEmitter = new Emitter<string>();
|
||||
private _value = '';
|
||||
private _reveal?: string[];
|
||||
|
||||
public readonly onDidRequestReveal = this.revealRequest.event;
|
||||
public readonly onDidChange = this.changeEmitter.event;
|
||||
|
||||
public get reveal() {
|
||||
return this._reveal;
|
||||
}
|
||||
|
||||
public set reveal(v: string[] | undefined) {
|
||||
this._reveal = v;
|
||||
if (v !== undefined) {
|
||||
this.revealRequest.fire(v);
|
||||
}
|
||||
}
|
||||
|
||||
public get value() {
|
||||
return this._value;
|
||||
}
|
||||
|
|
|
@ -56,6 +56,7 @@ import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browse
|
|||
import { TestExplorerViewGrouping, TestExplorerViewMode, Testing } from 'vs/workbench/contrib/testing/common/constants';
|
||||
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
|
||||
import { cmpPriority, isFailedState } from 'vs/workbench/contrib/testing/common/testingStates';
|
||||
import { buildTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
|
||||
import { ITestResultService, sumCounts, TestStateCount } from 'vs/workbench/contrib/testing/common/testResultService';
|
||||
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
||||
import { IWorkspaceTestCollectionService, TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
|
||||
|
@ -388,7 +389,13 @@ export class TestingExplorerViewModel extends Disposable {
|
|||
return false;
|
||||
}
|
||||
|
||||
TestingOutputPeekController.get(control).show(item.test, index);
|
||||
TestingOutputPeekController.get(control).show(buildTestUri({
|
||||
type: TestUriType.LiveMessage,
|
||||
messageIndex: index,
|
||||
providerId: item.test.providerId,
|
||||
testId: item.test.id,
|
||||
}));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -648,10 +655,6 @@ class TestsRenderer implements ITreeRenderer<ITestTreeElement, FuzzyScore, TestT
|
|||
const state = getComputedState(element);
|
||||
const icon = testingStatesToIcons.get(state);
|
||||
data.icon.className = 'computed-state ' + (icon ? ThemeIcon.asClassName(icon) : '');
|
||||
if (state === TestRunState.Running) {
|
||||
data.icon.className += ' codicon-modifier-spin';
|
||||
}
|
||||
|
||||
const test = element.test;
|
||||
if (test) {
|
||||
if (test.item.location) {
|
||||
|
|
|
@ -5,12 +5,13 @@
|
|||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { DisposableStore, IDisposable, IReference } from 'vs/base/common/lifecycle';
|
||||
import { IReference, MutableDisposable } from 'vs/base/common/lifecycle';
|
||||
import { clamp } from 'vs/base/common/numbers';
|
||||
import { count } from 'vs/base/common/strings';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget';
|
||||
import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions';
|
||||
import { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget';
|
||||
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 { IPeekViewService, peekViewTitleBackground, peekViewTitleForeground, peekViewTitleInfoForeground, PeekViewWidget } from 'vs/editor/contrib/peekView/peekView';
|
||||
|
@ -22,7 +23,17 @@ import { testingPeekBorder } from 'vs/workbench/contrib/testing/browser/theme';
|
|||
import { Testing } from 'vs/workbench/contrib/testing/common/constants';
|
||||
import { InternalTestItem, ITestMessage } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
|
||||
import { buildTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
|
||||
import { buildTestUri, parseTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
|
||||
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
|
||||
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
||||
|
||||
interface ITestDto {
|
||||
messageIndex: number;
|
||||
test: InternalTestItem;
|
||||
expectedUri: URI;
|
||||
actualUri: URI;
|
||||
messageUri: URI;
|
||||
}
|
||||
|
||||
export class TestingOutputPeekController implements IEditorContribution {
|
||||
/**
|
||||
|
@ -35,7 +46,7 @@ export class TestingOutputPeekController implements IEditorContribution {
|
|||
/**
|
||||
* Currently-shown peek view.
|
||||
*/
|
||||
private peek?: TestingOutputPeek;
|
||||
private readonly peek = new MutableDisposable<TestingOutputPeek>();
|
||||
|
||||
/**
|
||||
* Context key updated when the peek is visible/hidden.
|
||||
|
@ -45,6 +56,8 @@ export class TestingOutputPeekController implements IEditorContribution {
|
|||
constructor(
|
||||
private readonly editor: ICodeEditor,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@ITestResultService private readonly testResults: ITestResultService,
|
||||
@ITestService private readonly testService: ITestService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
) {
|
||||
this.visible = TestingContextKeys.peekVisible.bindTo(contextKeyService);
|
||||
|
@ -60,54 +73,92 @@ export class TestingOutputPeekController implements IEditorContribution {
|
|||
/**
|
||||
* Shows a peek for the message in th editor.
|
||||
*/
|
||||
public async show(test: InternalTestItem, messageIndex: number) {
|
||||
const message = test?.item.state.messages[messageIndex];
|
||||
if (!test || !message?.location) {
|
||||
public async show(uri: URI) {
|
||||
const dto = await this.retrieveTest(uri);
|
||||
if (!dto) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.peek) {
|
||||
this.peek = this.instantiationService.createInstance(TestingOutputPeek, this.editor);
|
||||
const message = dto.test.item.state.messages[dto.messageIndex];
|
||||
if (!message?.location) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.visible.set(true);
|
||||
this.peek.setModel(test, messageIndex);
|
||||
this.peek.onDidClose(() => {
|
||||
this.visible.set(false);
|
||||
this.peek = undefined;
|
||||
});
|
||||
const ctor = message.actualOutput !== undefined && message.expectedOutput !== undefined
|
||||
? TestingDiffOutputPeek : TestingMessageOutputPeek;
|
||||
const isNew = !(this.peek.value instanceof ctor);
|
||||
if (isNew) {
|
||||
this.peek.value = this.instantiationService.createInstance(ctor, this.editor);
|
||||
this.peek.value.onDidClose(() => {
|
||||
this.visible.set(false);
|
||||
this.peek.value = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
if (isNew) {
|
||||
this.visible.set(true);
|
||||
this.peek.value!.create();
|
||||
}
|
||||
|
||||
this.peek.value!.setModel(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes the peek view, if any.
|
||||
*/
|
||||
public removePeek() {
|
||||
if (this.peek) {
|
||||
this.peek.dispose();
|
||||
this.peek = undefined;
|
||||
this.peek.value = undefined;
|
||||
}
|
||||
|
||||
private async retrieveTest(uri: URI): Promise<ITestDto | undefined> {
|
||||
const parts = parseTestUri(uri);
|
||||
if (!parts) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if ('resultId' in parts) {
|
||||
const test = this.testResults.lookup(parts.resultId)?.tests.find(t => t.id === parts.testId);
|
||||
return test && {
|
||||
test,
|
||||
messageIndex: parts.messageIndex,
|
||||
expectedUri: buildTestUri({ ...parts, type: TestUriType.ResultExpectedOutput }),
|
||||
actualUri: buildTestUri({ ...parts, type: TestUriType.ResultActualOutput }),
|
||||
messageUri: buildTestUri({ ...parts, type: TestUriType.ResultMessage }),
|
||||
};
|
||||
}
|
||||
|
||||
const test = await this.testService.lookupTest({ providerId: parts.providerId, testId: parts.testId });
|
||||
if (!test) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
test,
|
||||
messageIndex: parts.messageIndex,
|
||||
expectedUri: buildTestUri({ ...parts, type: TestUriType.LiveActualOutput }),
|
||||
actualUri: buildTestUri({ ...parts, type: TestUriType.LiveExpectedOutput }),
|
||||
messageUri: buildTestUri({ ...parts, type: TestUriType.LiveMessage }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class TestingOutputPeek extends PeekViewWidget {
|
||||
private readonly disposable = new DisposableStore();
|
||||
private diff?: EmbeddedDiffEditorWidget;
|
||||
private model?: IDisposable;
|
||||
private dimension?: dom.Dimension;
|
||||
abstract class TestingOutputPeek extends PeekViewWidget {
|
||||
protected model = new MutableDisposable();
|
||||
protected dimension?: dom.Dimension;
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IPeekViewService peekViewService: IPeekViewService,
|
||||
@IInstantiationService protected readonly instantiationService: IInstantiationService,
|
||||
@ITextModelService private readonly modelService: ITextModelService,
|
||||
@ITextModelService protected readonly modelService: ITextModelService,
|
||||
) {
|
||||
super(editor, { showFrame: false, showArrow: true, isResizeable: true, isAccessible: true, className: 'test-output-peek' }, instantiationService);
|
||||
|
||||
this._disposables.add(themeService.onDidColorThemeChange(this.applyTheme, this));
|
||||
this._disposables.add(this.model);
|
||||
this.applyTheme(themeService.getColorTheme());
|
||||
peekViewService.addExclusiveWidget(editor, this);
|
||||
this.create();
|
||||
}
|
||||
|
||||
private applyTheme(theme: IColorTheme) {
|
||||
|
@ -121,67 +172,83 @@ export class TestingOutputPeek extends PeekViewWidget {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the test to be shown.
|
||||
*/
|
||||
public abstract setModel(dto: ITestDto): Promise<void>;
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
public dispose() {
|
||||
super.dispose();
|
||||
this.model?.dispose();
|
||||
this.disposable.dispose();
|
||||
protected _doLayoutBody(height: number, width: number) {
|
||||
super._doLayoutBody(height, width);
|
||||
this.dimension = new dom.Dimension(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
const commonEditorOptions: IEditorOptions = {
|
||||
scrollBeyondLastLine: false,
|
||||
scrollbar: {
|
||||
verticalScrollbarSize: 14,
|
||||
horizontal: 'auto',
|
||||
useShadows: true,
|
||||
verticalHasArrows: false,
|
||||
horizontalHasArrows: false,
|
||||
alwaysConsumeMouseWheel: false
|
||||
},
|
||||
fixedOverflowWidgets: true,
|
||||
readOnly: true,
|
||||
minimap: {
|
||||
enabled: false
|
||||
},
|
||||
};
|
||||
|
||||
const diffEditorOptions: IDiffEditorOptions = {
|
||||
...commonEditorOptions,
|
||||
enableSplitViewResizing: true,
|
||||
isInEmbeddedEditor: true,
|
||||
renderOverviewRuler: false,
|
||||
ignoreTrimWhitespace: false,
|
||||
renderSideBySide: true,
|
||||
};
|
||||
|
||||
class TestingDiffOutputPeek extends TestingOutputPeek {
|
||||
private readonly diff = this._disposables.add(new MutableDisposable<EmbeddedDiffEditorWidget>());
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
protected _fillBody(containerElement: HTMLElement): void {
|
||||
const diffContainer = dom.append(containerElement, dom.$('div.preview.inline'));
|
||||
let options: IDiffEditorOptions = {
|
||||
scrollBeyondLastLine: false,
|
||||
scrollbar: {
|
||||
verticalScrollbarSize: 14,
|
||||
horizontal: 'auto',
|
||||
useShadows: true,
|
||||
verticalHasArrows: false,
|
||||
horizontalHasArrows: false,
|
||||
alwaysConsumeMouseWheel: false
|
||||
},
|
||||
fixedOverflowWidgets: true,
|
||||
readOnly: true,
|
||||
minimap: {
|
||||
enabled: false
|
||||
},
|
||||
enableSplitViewResizing: true,
|
||||
isInEmbeddedEditor: true,
|
||||
renderOverviewRuler: false,
|
||||
ignoreTrimWhitespace: false,
|
||||
renderSideBySide: true,
|
||||
};
|
||||
|
||||
const preview = this.diff = this.instantiationService.createInstance(EmbeddedDiffEditorWidget, diffContainer, options, this.editor);
|
||||
this.disposable.add(preview);
|
||||
const preview = this.diff.value = this.instantiationService.createInstance(EmbeddedDiffEditorWidget, diffContainer, diffEditorOptions, this.editor);
|
||||
|
||||
if (this.dimension) {
|
||||
preview.layout(this.dimension);
|
||||
}
|
||||
}
|
||||
|
||||
public async setModel(test: InternalTestItem, messageIndex: number) {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
public async setModel({ test, messageIndex, expectedUri, actualUri }: ITestDto) {
|
||||
const message = test.item.state.messages[messageIndex];
|
||||
if (!message?.location) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.show(message.location.range, hintPeekHeight(message));
|
||||
|
||||
if (this.model) {
|
||||
this.model.dispose();
|
||||
}
|
||||
|
||||
this.show(message.location.range, hintDiffPeekHeight(message));
|
||||
this.setTitle(message.message.toString(), test.item.label);
|
||||
if (message.actualOutput !== undefined && message.expectedOutput !== undefined) {
|
||||
await this.showDiffInEditor(test, messageIndex);
|
||||
|
||||
const [original, modified] = await Promise.all([
|
||||
this.modelService.createModelReference(expectedUri),
|
||||
this.modelService.createModelReference(actualUri),
|
||||
]);
|
||||
|
||||
const model = this.model.value = new SimpleDiffEditorModel(original, modified);
|
||||
if (!this.diff.value) {
|
||||
this.model.value = undefined;
|
||||
} else {
|
||||
await this.showMessageInEditor(test, messageIndex);
|
||||
this.diff.value.setModel(model);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -190,37 +257,58 @@ export class TestingOutputPeek extends PeekViewWidget {
|
|||
*/
|
||||
protected _doLayoutBody(height: number, width: number) {
|
||||
super._doLayoutBody(height, width);
|
||||
this.dimension = new dom.Dimension(width, height);
|
||||
this.diff?.layout(this.dimension);
|
||||
}
|
||||
|
||||
private async showMessageInEditor(test: InternalTestItem, messageIndex: number) {
|
||||
// todo? not sure if this is a useful experience
|
||||
this.model?.dispose();
|
||||
this.diff?.setModel(null);
|
||||
}
|
||||
|
||||
private async showDiffInEditor(test: InternalTestItem, messageIndex: number) {
|
||||
const uriParts = { messageIndex, testId: test.id, providerId: test.providerId };
|
||||
const [original, modified] = await Promise.all([
|
||||
this.modelService.createModelReference(buildTestUri({ ...uriParts, type: TestUriType.ExpectedOutput })),
|
||||
this.modelService.createModelReference(buildTestUri({ ...uriParts, type: TestUriType.ActualOutput })),
|
||||
]);
|
||||
|
||||
this.model?.dispose();
|
||||
const model = this.model = new SimpleDiffEditorModel(original, modified);
|
||||
if (!this.diff) {
|
||||
model.dispose();
|
||||
} else {
|
||||
this.diff.setModel(model);
|
||||
}
|
||||
this.diff.value?.layout(this.dimension);
|
||||
}
|
||||
}
|
||||
|
||||
const hintPeekHeight = (message: ITestMessage) => {
|
||||
const lines = Math.max(count(message.actualOutput || '', '\n'), count(message.expectedOutput || '', '\n'));
|
||||
return clamp(lines, 5, 20);
|
||||
};
|
||||
class TestingMessageOutputPeek extends TestingOutputPeek {
|
||||
private readonly preview = this._disposables.add(new MutableDisposable<EmbeddedCodeEditorWidget>());
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
protected _fillBody(containerElement: HTMLElement): void {
|
||||
const diffContainer = dom.append(containerElement, dom.$('div.preview.inline'));
|
||||
const preview = this.preview.value = this.instantiationService.createInstance(EmbeddedCodeEditorWidget, diffContainer, commonEditorOptions, this.editor);
|
||||
|
||||
if (this.dimension) {
|
||||
preview.layout(this.dimension);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
public async setModel({ test, messageIndex, messageUri }: ITestDto) {
|
||||
const message = test.item.state.messages[messageIndex];
|
||||
if (!message?.location) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.show(message.location.range, hintPeekStrHeight(message.message.toString()));
|
||||
this.setTitle(message.message.toString(), test.item.label);
|
||||
|
||||
const modelRef = this.model.value = await this.modelService.createModelReference(messageUri);
|
||||
if (this.preview.value) {
|
||||
this.preview.value.setModel(modelRef.object.textEditorModel);
|
||||
} else {
|
||||
this.model.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
protected _doLayoutBody(height: number, width: number) {
|
||||
super._doLayoutBody(height, width);
|
||||
this.preview.value?.layout(this.dimension);
|
||||
}
|
||||
}
|
||||
|
||||
const hintDiffPeekHeight = (message: ITestMessage) =>
|
||||
Math.max(hintPeekStrHeight(message.actualOutput), hintPeekStrHeight(message.expectedOutput));
|
||||
|
||||
const hintPeekStrHeight = (str: string | undefined) => clamp(count(str || '', '\n'), 5, 20);
|
||||
|
||||
class SimpleDiffEditorModel extends EditorModel {
|
||||
public readonly original = this._original.object.textEditorModel;
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { editorErrorForeground, registerColor } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
|
||||
import { editorErrorForeground, editorForeground, editorHintForeground, editorInfoForeground, editorWarningForeground, registerColor } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { TestMessageSeverity, TestRunState } from 'vs/workbench/api/common/extHostTypes';
|
||||
|
||||
export const testingColorIconFailed = registerColor('testing.iconFailed', {
|
||||
dark: '#f14c4c',
|
||||
|
@ -19,13 +19,18 @@ export const testingColorIconErrored = registerColor('testing.iconErrored', {
|
|||
hc: '#000000'
|
||||
}, localize('testing.iconErrored', "Color for the 'Errored' icon in the test explorer."));
|
||||
|
||||
|
||||
export const testingColorIconPassed = registerColor('testing.iconPassed', {
|
||||
dark: '#73c991',
|
||||
light: '#73c991',
|
||||
hc: '#000000'
|
||||
}, localize('testing.iconPassed', "Color for the 'passed' icon in the test explorer."));
|
||||
|
||||
export const testingColorRunAction = registerColor('testing.runAction', {
|
||||
dark: testingColorIconPassed,
|
||||
light: testingColorIconPassed,
|
||||
hc: testingColorIconPassed
|
||||
}, localize('testing.runAction', "Color for 'run' icons in the editor."));
|
||||
|
||||
export const testingColorIconQueued = registerColor('testing.iconQueued', {
|
||||
dark: '#cca700',
|
||||
light: '#cca700',
|
||||
|
@ -50,6 +55,41 @@ export const testingPeekBorder = registerColor('testing.peekBorder', {
|
|||
hc: editorErrorForeground,
|
||||
}, localize('testing.peekBorder', 'Color of the peek view borders and arrow.'));
|
||||
|
||||
export const testMessageSeverityColors: {
|
||||
[K in TestMessageSeverity]: {
|
||||
decorationForeground: string,
|
||||
};
|
||||
} = {
|
||||
[TestMessageSeverity.Error]: {
|
||||
decorationForeground: registerColor(
|
||||
'testing.message.error.decorationForeground',
|
||||
{ dark: editorErrorForeground, light: editorErrorForeground, hc: editorForeground },
|
||||
localize('testing.message.error.decorationForeground', 'Text color of test error messages shown inline in the editor.')
|
||||
),
|
||||
},
|
||||
[TestMessageSeverity.Warning]: {
|
||||
decorationForeground: registerColor(
|
||||
'testing.message.warning.decorationForeground',
|
||||
{ dark: editorWarningForeground, light: editorWarningForeground, hc: editorForeground },
|
||||
localize('testing.message.warning.decorationForeground', 'Text color of test warning messages shown inline in the editor.')
|
||||
),
|
||||
},
|
||||
[TestMessageSeverity.Information]: {
|
||||
decorationForeground: registerColor(
|
||||
'testing.message.info.decorationForeground',
|
||||
{ dark: editorInfoForeground, light: editorInfoForeground, hc: editorForeground },
|
||||
localize('testing.message.info.decorationForeground', 'Text color of test info messages shown inline in the editor.')
|
||||
),
|
||||
},
|
||||
[TestMessageSeverity.Hint]: {
|
||||
decorationForeground: registerColor(
|
||||
'testing.message.hint.decorationForeground',
|
||||
{ dark: editorHintForeground, light: editorHintForeground, hc: editorForeground },
|
||||
localize('testing.message.hint.decorationForeground', 'Text color of test hint messages shown inline in the editor.')
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const testStatesToIconColors: { [K in TestRunState]?: string } = {
|
||||
[TestRunState.Errored]: testingColorIconErrored,
|
||||
[TestRunState.Failed]: testingColorIconFailed,
|
||||
|
|
|
@ -11,6 +11,7 @@ export const enum Testing {
|
|||
ViewletId = 'workbench.view.extension.test',
|
||||
ExplorerViewId = 'workbench.view.testing',
|
||||
OutputPeekContributionId = 'editor.contrib.testingOutputPeek',
|
||||
DecorationsContributionId = 'editor.contrib.testingDecorations',
|
||||
FilterActionId = 'workbench.actions.treeView.testExplorer.filter',
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
|
||||
|
@ -88,6 +89,11 @@ export class TestResult {
|
|||
public onChange = this.changeEmitter.event;
|
||||
public onComplete = this.completeEmitter.event;
|
||||
|
||||
/**
|
||||
* Unique ID for referring to this set of test results.
|
||||
*/
|
||||
public readonly id = generateUuid();
|
||||
|
||||
/**
|
||||
* Gets whether the test run has finished.
|
||||
*/
|
||||
|
@ -175,11 +181,16 @@ export interface ITestResultService {
|
|||
* Adds a new test result to the collection.
|
||||
*/
|
||||
push(result: TestResult): TestResult;
|
||||
|
||||
/**
|
||||
* Looks up a set of test results by ID.
|
||||
*/
|
||||
lookup(resultId: string): TestResult | undefined;
|
||||
}
|
||||
|
||||
export const ITestResultService = createDecorator<ITestResultService>('testResultService');
|
||||
|
||||
const RETAIN_LAST_RESULTS = 10;
|
||||
const RETAIN_LAST_RESULTS = 16;
|
||||
|
||||
export class TestResultService implements ITestResultService {
|
||||
declare _serviceBrand: undefined;
|
||||
|
@ -216,6 +227,13 @@ export class TestResultService implements ITestResultService {
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public lookup(id: string) {
|
||||
return this.results.find(r => r.id === id);
|
||||
}
|
||||
|
||||
private onComplete() {
|
||||
// move the complete test run down behind any still-running ones
|
||||
for (let i = 0; i < this.results.length - 2; i++) {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IDisposable, IReference } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol';
|
||||
|
@ -39,6 +39,11 @@ export interface IMainThreadTestCollection extends AbstractIncrementalTestCollec
|
|||
*/
|
||||
rootIds: ReadonlySet<string>;
|
||||
|
||||
/**
|
||||
* Iterates over every test in the collection.
|
||||
*/
|
||||
all: Iterable<IncrementalTestCollectionItem>;
|
||||
|
||||
/**
|
||||
* Gets a node in the collection by ID.
|
||||
*/
|
||||
|
@ -82,10 +87,7 @@ export interface ITestService {
|
|||
runTests(req: RunTestsRequest, token?: CancellationToken): Promise<RunTestsResult>;
|
||||
cancelTestRun(req: RunTestsRequest): void;
|
||||
publishDiff(resource: ExtHostTestingResource, uri: URI, diff: TestsDiff): void;
|
||||
subscribeToDiffs(resource: ExtHostTestingResource, uri: URI, acceptDiff?: TestDiffListener): {
|
||||
collection: IMainThreadTestCollection;
|
||||
dispose(): void;
|
||||
};
|
||||
subscribeToDiffs(resource: ExtHostTestingResource, uri: URI, acceptDiff?: TestDiffListener): IReference<IMainThreadTestCollection>;
|
||||
|
||||
/**
|
||||
* Updates the number of sources who provide test roots when subscription
|
||||
|
|
|
@ -7,7 +7,7 @@ import { groupBy } from 'vs/base/common/arrays';
|
|||
import { disposableTimeout } from 'vs/base/common/async';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, IDisposable, IReference } from 'vs/base/common/lifecycle';
|
||||
import { isDefined } from 'vs/base/common/types';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { localize } from 'vs/nls';
|
||||
|
@ -131,7 +131,7 @@ export class TestService extends Disposable implements ITestService {
|
|||
const subscriptions = [...this.testSubscriptions.values()]
|
||||
.filter(v => req.tests.some(t => v.collection.getNodeById(t.testId)))
|
||||
.map(s => this.subscribeToDiffs(s.ident.resource, s.ident.uri, () => result?.notifyChanged()));
|
||||
result = this.testResults.push(TestResult.from(subscriptions.map(s => s.collection), req.tests));
|
||||
result = this.testResults.push(TestResult.from(subscriptions.map(s => s.object), req.tests));
|
||||
|
||||
try {
|
||||
const tests = groupBy(req.tests, (a, b) => a.providerId === b.providerId ? 0 : 1);
|
||||
|
@ -175,7 +175,7 @@ export class TestService extends Disposable implements ITestService {
|
|||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public subscribeToDiffs(resource: ExtHostTestingResource, uri: URI, acceptDiff?: TestDiffListener) {
|
||||
public subscribeToDiffs(resource: ExtHostTestingResource, uri: URI, acceptDiff?: TestDiffListener): IReference<IMainThreadTestCollection> {
|
||||
const subscriptionKey = getTestSubscriptionKey(resource, uri);
|
||||
let subscription = this.testSubscriptions.get(subscriptionKey);
|
||||
if (!subscription) {
|
||||
|
@ -200,7 +200,7 @@ export class TestService extends Disposable implements ITestService {
|
|||
|
||||
const listener = acceptDiff && subscription.onDiff.event(acceptDiff);
|
||||
return {
|
||||
collection: subscription.collection,
|
||||
object: subscription.collection,
|
||||
dispose: () => {
|
||||
listener?.dispose();
|
||||
|
||||
|
@ -277,6 +277,14 @@ class MainThreadTestCollection extends AbstractIncrementalTestCollection<Increme
|
|||
return this.roots;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public get all() {
|
||||
return this.getIterator();
|
||||
}
|
||||
|
||||
|
||||
public readonly onPendingRootProvidersChange = this.pendingRootChangeEmitter.event;
|
||||
public readonly onBusyProvidersChange = this.busyProvidersChangeEmitter.event;
|
||||
|
||||
|
@ -350,4 +358,15 @@ class MainThreadTestCollection extends AbstractIncrementalTestCollection<Increme
|
|||
protected createItem(internal: InternalTestItem): IncrementalTestCollectionItem {
|
||||
return { ...internal, children: new Set() };
|
||||
}
|
||||
|
||||
private *getIterator() {
|
||||
const queue = [this.rootIds];
|
||||
while (queue.length) {
|
||||
for (const id of queue.pop()!) {
|
||||
const node = this.getNodeById(id)!;
|
||||
yield node;
|
||||
queue.push(node.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import { IModelService } from 'vs/editor/common/services/modelService';
|
|||
import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||
import { parseTestUri, TestUriType, TEST_DATA_SCHEME } from 'vs/workbench/contrib/testing/common/testingUri';
|
||||
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
|
||||
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
||||
|
||||
/**
|
||||
|
@ -20,6 +21,7 @@ export class TestingContentProvider implements IWorkbenchContribution, ITextMode
|
|||
@ITextModelService textModelResolverService: ITextModelService,
|
||||
@IModelService private readonly modelService: IModelService,
|
||||
@ITestService private readonly testService: ITestService,
|
||||
@ITestService private readonly resultService: ITestResultService,
|
||||
) {
|
||||
textModelResolverService.registerTextModelContentProvider(TEST_DATA_SCHEME, this);
|
||||
}
|
||||
|
@ -38,20 +40,26 @@ export class TestingContentProvider implements IWorkbenchContribution, ITextMode
|
|||
return null;
|
||||
}
|
||||
|
||||
const test = await this.testService.lookupTest({ providerId: parsed.providerId, testId: parsed.testId });
|
||||
const test = 'providerId' in parsed
|
||||
? await this.testService.lookupTest({ providerId: parsed.providerId, testId: parsed.testId })
|
||||
: this.resultService.lookup(parsed.resultId)?.tests.find(t => t.id === parsed.testId);
|
||||
|
||||
if (!test) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let text: string | undefined;
|
||||
switch (parsed.type) {
|
||||
case TestUriType.ActualOutput:
|
||||
case TestUriType.ResultActualOutput:
|
||||
case TestUriType.LiveActualOutput:
|
||||
text = test.item.state.messages[parsed.messageIndex]?.actualOutput;
|
||||
break;
|
||||
case TestUriType.ExpectedOutput:
|
||||
case TestUriType.ResultExpectedOutput:
|
||||
case TestUriType.LiveExpectedOutput:
|
||||
text = test.item.state.messages[parsed.messageIndex]?.expectedOutput;
|
||||
break;
|
||||
case TestUriType.Message:
|
||||
case TestUriType.ResultMessage:
|
||||
case TestUriType.LiveMessage:
|
||||
text = test.item.state.messages[parsed.messageIndex]?.message.toString();
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ export const statePriority: { [K in TestRunState]: number } = {
|
|||
[TestRunState.Unset]: 0,
|
||||
};
|
||||
|
||||
|
||||
export const isFailedState = (s: TestRunState) => s === TestRunState.Errored || s === TestRunState.Failed;
|
||||
|
||||
export const stateNodes = Object.entries(statePriority).reduce(
|
||||
|
|
|
@ -8,29 +8,54 @@ import { URI } from 'vs/base/common/uri';
|
|||
export const TEST_DATA_SCHEME = 'vscode-test-data';
|
||||
|
||||
export const enum TestUriType {
|
||||
Message,
|
||||
ActualOutput,
|
||||
ExpectedOutput,
|
||||
LiveMessage,
|
||||
LiveActualOutput,
|
||||
LiveExpectedOutput,
|
||||
ResultMessage,
|
||||
ResultActualOutput,
|
||||
ResultExpectedOutput,
|
||||
}
|
||||
|
||||
interface IGenericTestUri {
|
||||
interface ILiveTestUri {
|
||||
providerId: string;
|
||||
testId: string;
|
||||
}
|
||||
|
||||
interface ITestMessageReference extends IGenericTestUri {
|
||||
type: TestUriType.Message;
|
||||
interface ILiveTestMessageReference extends ILiveTestUri {
|
||||
type: TestUriType.LiveMessage;
|
||||
messageIndex: number;
|
||||
}
|
||||
|
||||
interface ITestOutputReference extends IGenericTestUri {
|
||||
type: TestUriType.ActualOutput | TestUriType.ExpectedOutput;
|
||||
interface ILiveTestOutputReference extends ILiveTestUri {
|
||||
type: TestUriType.LiveActualOutput | TestUriType.LiveExpectedOutput;
|
||||
messageIndex: number;
|
||||
}
|
||||
|
||||
export type ParsedTestUri = ITestMessageReference | ITestOutputReference;
|
||||
interface IResultTestUri {
|
||||
resultId: string;
|
||||
testId: string;
|
||||
}
|
||||
|
||||
interface IResultTestMessageReference extends IResultTestUri {
|
||||
type: TestUriType.ResultMessage;
|
||||
messageIndex: number;
|
||||
}
|
||||
|
||||
interface IResultTestOutputReference extends IResultTestUri {
|
||||
type: TestUriType.ResultActualOutput | TestUriType.ResultExpectedOutput;
|
||||
messageIndex: number;
|
||||
}
|
||||
|
||||
export type ParsedTestUri =
|
||||
| IResultTestMessageReference
|
||||
| IResultTestOutputReference
|
||||
| ILiveTestMessageReference
|
||||
| ILiveTestOutputReference;
|
||||
|
||||
const enum TestUriParts {
|
||||
Results = 'results',
|
||||
Live = 'live',
|
||||
|
||||
Messages = 'message',
|
||||
Text = 'text',
|
||||
ActualOutput = 'actualOutput',
|
||||
|
@ -38,21 +63,30 @@ const enum TestUriParts {
|
|||
}
|
||||
|
||||
export const parseTestUri = (uri: URI): ParsedTestUri | undefined => {
|
||||
const providerId = uri.authority;
|
||||
const [testId, ...request] = uri.path.slice(1).split('/');
|
||||
const type = uri.authority;
|
||||
const [locationId, testId, ...request] = uri.path.slice(1).split('/');
|
||||
|
||||
if (request[0] === TestUriParts.Messages) {
|
||||
const index = Number(request[1]);
|
||||
const part = request[2];
|
||||
switch (part) {
|
||||
case TestUriParts.Text:
|
||||
return { providerId, testId, messageIndex: index, type: TestUriType.Message };
|
||||
case TestUriParts.ActualOutput:
|
||||
return { providerId, testId, messageIndex: index, type: TestUriType.ActualOutput };
|
||||
case TestUriParts.ExpectedOutput:
|
||||
return { providerId, testId, messageIndex: index, type: TestUriType.ExpectedOutput };
|
||||
default:
|
||||
return undefined;
|
||||
if (type === TestUriParts.Results) {
|
||||
switch (part) {
|
||||
case TestUriParts.Text:
|
||||
return { resultId: locationId, testId, messageIndex: index, type: TestUriType.ResultMessage };
|
||||
case TestUriParts.ActualOutput:
|
||||
return { resultId: locationId, testId, messageIndex: index, type: TestUriType.ResultActualOutput };
|
||||
case TestUriParts.ExpectedOutput:
|
||||
return { resultId: locationId, testId, messageIndex: index, type: TestUriType.ResultExpectedOutput };
|
||||
}
|
||||
} else if (type === TestUriParts.Live) {
|
||||
switch (part) {
|
||||
case TestUriParts.Text:
|
||||
return { providerId: locationId, testId, messageIndex: index, type: TestUriType.LiveMessage };
|
||||
case TestUriParts.ActualOutput:
|
||||
return { providerId: locationId, testId, messageIndex: index, type: TestUriType.LiveActualOutput };
|
||||
case TestUriParts.ExpectedOutput:
|
||||
return { providerId: locationId, testId, messageIndex: index, type: TestUriType.LiveExpectedOutput };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,17 +94,29 @@ export const parseTestUri = (uri: URI): ParsedTestUri | undefined => {
|
|||
};
|
||||
|
||||
export const buildTestUri = (parsed: ParsedTestUri): URI => {
|
||||
const uriParts = { scheme: TEST_DATA_SCHEME, authority: parsed.testId };
|
||||
const msgRef = (index: number, ...remaining: string[]) =>
|
||||
URI.from({ ...uriParts, path: ['', parsed.testId, TestUriParts.Messages, index, ...remaining].join('/') });
|
||||
const uriParts = {
|
||||
scheme: TEST_DATA_SCHEME,
|
||||
authority: 'resultId' in parsed ? TestUriParts.Results : TestUriParts.Live
|
||||
};
|
||||
const msgRef = (locationId: string, index: number, ...remaining: string[]) =>
|
||||
URI.from({
|
||||
...uriParts,
|
||||
path: ['', locationId, parsed.testId, TestUriParts.Messages, index, ...remaining].join('/'),
|
||||
});
|
||||
|
||||
switch (parsed.type) {
|
||||
case TestUriType.ActualOutput:
|
||||
return msgRef(parsed.messageIndex, TestUriParts.ActualOutput);
|
||||
case TestUriType.ExpectedOutput:
|
||||
return msgRef(parsed.messageIndex, TestUriParts.ExpectedOutput);
|
||||
case TestUriType.Message:
|
||||
return msgRef(parsed.messageIndex, TestUriParts.Text);
|
||||
case TestUriType.ResultActualOutput:
|
||||
return msgRef(parsed.resultId, parsed.messageIndex, TestUriParts.ActualOutput);
|
||||
case TestUriType.ResultExpectedOutput:
|
||||
return msgRef(parsed.resultId, parsed.messageIndex, TestUriParts.ExpectedOutput);
|
||||
case TestUriType.ResultMessage:
|
||||
return msgRef(parsed.resultId, parsed.messageIndex, TestUriParts.Text);
|
||||
case TestUriType.LiveActualOutput:
|
||||
return msgRef(parsed.providerId, parsed.messageIndex, TestUriParts.ActualOutput);
|
||||
case TestUriType.LiveExpectedOutput:
|
||||
return msgRef(parsed.providerId, parsed.messageIndex, TestUriParts.ExpectedOutput);
|
||||
case TestUriType.LiveMessage:
|
||||
return msgRef(parsed.providerId, parsed.messageIndex, TestUriParts.Text);
|
||||
default:
|
||||
throw new Error('Invalid test uri');
|
||||
}
|
||||
|
|
|
@ -195,8 +195,8 @@ class TestSubscription extends Disposable {
|
|||
const folderNode: ITestSubscriptionFolder = {
|
||||
folder,
|
||||
getChildren: function* () {
|
||||
for (const rootId of listener.collection.rootIds) {
|
||||
const node = listener.collection.getNodeById(rootId);
|
||||
for (const rootId of ref.object.rootIds) {
|
||||
const node = ref.object.getNodeById(rootId);
|
||||
if (node) {
|
||||
yield node;
|
||||
}
|
||||
|
@ -204,7 +204,7 @@ class TestSubscription extends Disposable {
|
|||
},
|
||||
};
|
||||
|
||||
const listener = this.testService.subscribeToDiffs(
|
||||
const ref = this.testService.subscribeToDiffs(
|
||||
ExtHostTestingResource.Workspace,
|
||||
folder.uri,
|
||||
diff => {
|
||||
|
@ -215,16 +215,15 @@ class TestSubscription extends Disposable {
|
|||
);
|
||||
|
||||
const disposable = new DisposableStore();
|
||||
disposable.add(listener);
|
||||
disposable.add(listener.collection.onBusyProvidersChange(
|
||||
disposable.add(ref);
|
||||
disposable.add(ref.object.onBusyProvidersChange(
|
||||
() => this.pendingRootChangeEmitter.fire(this.pendingRootProviders)));
|
||||
disposable.add(listener.collection.onBusyProvidersChange(
|
||||
disposable.add(ref.object.onBusyProvidersChange(
|
||||
() => this.busyProvidersChangeEmitter.fire(this.busyProviders)));
|
||||
|
||||
|
||||
this.collectionsForWorkspaces.set(folder.uri.toString(), {
|
||||
listener: disposable,
|
||||
collection: listener.collection,
|
||||
collection: ref.object,
|
||||
folder: folderNode,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { buildTestUri, ParsedTestUri, parseTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
|
||||
|
||||
suite('Workbench - Testing URIs', () => {
|
||||
test('round trip', () => {
|
||||
const uris: ParsedTestUri[] = [
|
||||
{ type: TestUriType.LiveActualOutput, messageIndex: 42, providerId: 'p', testId: 't' },
|
||||
{ type: TestUriType.LiveExpectedOutput, messageIndex: 42, providerId: 'p', testId: 't' },
|
||||
{ type: TestUriType.LiveMessage, messageIndex: 42, providerId: 'p', testId: 't' },
|
||||
{ type: TestUriType.ResultActualOutput, messageIndex: 42, resultId: 'r', testId: 't' },
|
||||
{ type: TestUriType.ResultExpectedOutput, messageIndex: 42, resultId: 'r', testId: 't' },
|
||||
{ type: TestUriType.ResultMessage, messageIndex: 42, resultId: 'r', testId: 't' },
|
||||
];
|
||||
|
||||
for (const uri of uris) {
|
||||
const serialized = buildTestUri(uri);
|
||||
assert.deepStrictEqual(uri, parseTestUri(serialized));
|
||||
}
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue