testing: allow contributing menu items to test view

Fixes #116806
This commit is contained in:
Connor Peet
2021-02-18 15:28:37 -08:00
parent 7aff64c42d
commit 45dc2f5fa0
4 changed files with 104 additions and 36 deletions
@@ -121,6 +121,7 @@ export class MenuId {
static readonly SCMTitle = new MenuId('SCMTitle');
static readonly SearchContext = new MenuId('SearchContext');
static readonly StatusBarWindowIndicatorMenu = new MenuId('StatusBarWindowIndicatorMenu');
static readonly TestItem = new MenuId('TestItem');
static readonly TouchBarContext = new MenuId('TouchBarContext');
static readonly TitleBarContext = new MenuId('TitleBarContext');
static readonly TunnelContext = new MenuId('TunnelContext');
@@ -166,6 +166,12 @@ const apiMenus: IAPIMenu[] = [
description: localize('notebook.cell.title', "The contributed notebook cell title menu"),
proposed: true
},
{
key: 'testing/item/context',
id: MenuId.TestItem,
description: localize('testing.item.title', "The contributed test item menu"),
proposed: true
},
{
key: 'extension/context',
id: MenuId.ExtensionContext,
@@ -10,9 +10,8 @@ import * as aria from 'vs/base/browser/ui/aria/aria';
import { Button } from 'vs/base/browser/ui/button/button';
import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { DefaultKeyboardNavigationDelegate, IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree';
import { ITreeEvent, ITreeFilter, ITreeNode, ITreeRenderer, ITreeSorter, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree';
import { ITreeContextMenuEvent, ITreeEvent, ITreeFilter, ITreeNode, ITreeRenderer, ITreeSorter, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree';
import { Action, IAction } from 'vs/base/common/actions';
import { DeferredPromise, RunOnceScheduler } from 'vs/base/common/async';
import { Color, RGBA } from 'vs/base/common/color';
@@ -22,15 +21,15 @@ import { FuzzyScore } from 'vs/base/common/filters';
import { splitGlobAware } from 'vs/base/common/glob';
import { Iterable } from 'vs/base/common/iterator';
import { KeyCode } from 'vs/base/common/keyCodes';
import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { Disposable, DisposableStore, dispose, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { isDefined } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import 'vs/css!./media/testing';
import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { localize } from 'vs/nls';
import { MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { MenuItemAction } from 'vs/platform/actions/common/actions';
import { createAndFillInActionBarActions, MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
@@ -256,8 +255,8 @@ export class TestingExplorerViewModel extends Disposable {
listContainer: HTMLElement,
onDidChangeVisibility: Event<boolean>,
private listener: TestSubscriptionListener | undefined,
@ICommandService commandService: ICommandService,
@IThemeService themeService: IThemeService,
@IMenuService private readonly menuService: IMenuService,
@IContextMenuService private readonly contextMenuService: IContextMenuService,
@ITestService private readonly testService: ITestService,
@ITestExplorerFilterState filterState: TestExplorerFilterState,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@@ -304,6 +303,8 @@ export class TestingExplorerViewModel extends Disposable {
this.tree.refilter();
}));
this._register(this.tree.onContextMenu(e => this.onContextMenu(e)));
this._register(editorService.onDidActiveEditorChange(() => {
if (filterState.currentDocumentOnly.value && editorService.activeEditor?.resource) {
if (this.projection.hasTestInDocument(editorService.activeEditor.resource)) {
@@ -450,6 +451,20 @@ export class TestingExplorerViewModel extends Disposable {
: false;
}
private onContextMenu(evt: ITreeContextMenuEvent<ITestTreeElement | null>) {
if (!evt.element) {
return;
}
const actions = getTestItemActions(this.instantiationService, this.contextKeyService, this.menuService, evt.element);
this.contextMenuService.showContextMenu({
getAnchor: () => evt.anchor,
getActions: () => actions.value.secondary,
getActionsContext: () => evt.element?.test?.item.extId,
onHide: () => actions.dispose(),
});
}
private handleExecuteKeypress(evt: IKeyboardEvent) {
const focused = this.tree.getFocus();
const selected = this.tree.getSelection();
@@ -747,25 +762,32 @@ interface TestTemplateData {
label: IResourceLabel;
icon: HTMLElement;
actionBar: ActionBar;
elementDisposable: IDisposable[];
templateDisposable: IDisposable[];
}
class TestsRenderer implements ITreeRenderer<ITestTreeElement, FuzzyScore, TestTemplateData> {
class TestsRenderer extends Disposable implements ITreeRenderer<ITestTreeElement, FuzzyScore, TestTemplateData> {
public static readonly ID = 'testExplorer';
constructor(
private labels: ResourceLabels,
@IMenuService private readonly menuService: IMenuService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IInstantiationService private readonly instantiationService: IInstantiationService
) { }
renderCompressedElements(node: ITreeNode<ICompressedTreeNode<ITestTreeElement>, FuzzyScore>, index: number, templateData: TestTemplateData): void {
const element = node.element.elements[node.element.elements.length - 1];
this.renderElementDirect(element, templateData);
) {
super();
}
/**
* @inheritdoc
*/
get templateId(): string {
return TestsRenderer.ID;
}
/**
* @inheritdoc
*/
public renderTemplate(container: HTMLElement): TestTemplateData {
const wrapper = dom.append(container, dom.$('.test-item'));
@@ -780,14 +802,13 @@ class TestsRenderer implements ITreeRenderer<ITestTreeElement, FuzzyScore, TestT
: undefined
});
return { label, actionBar, icon };
return { label, actionBar, icon, elementDisposable: [], templateDisposable: [label, actionBar] };
}
public renderElement(node: ITreeNode<ITestTreeElement, FuzzyScore>, index: number, data: TestTemplateData): void {
this.renderElementDirect(node.element, data);
}
private renderElementDirect(element: ITestTreeElement, data: TestTemplateData) {
/**
* @inheritdoc
*/
public renderElement({ element }: ITreeNode<ITestTreeElement, FuzzyScore>, _: number, data: TestTemplateData): void {
const label: IResourceLabelProps = { name: element.label };
const options: IResourceLabelOptions = {};
data.actionBar.clear();
@@ -816,30 +837,65 @@ class TestsRenderer implements ITreeRenderer<ITestTreeElement, FuzzyScore, TestT
options.fileKind = FileKind.ROOT_FOLDER;
}
const running = element.state === TestRunState.Running;
if (!Iterable.isEmpty(element.runnable)) {
data.actionBar.push(
this.instantiationService.createInstance(RunAction, element.runnable, running),
{ icon: true, label: false },
);
}
if (!Iterable.isEmpty(element.debuggable)) {
data.actionBar.push(
this.instantiationService.createInstance(DebugAction, element.debuggable, running),
{ icon: true, label: false },
);
}
this.fillActionBar(element, data);
data.label.setResource(label, options);
}
/**
* @inheritdoc
*/
disposeTemplate(templateData: TestTemplateData): void {
templateData.label.dispose();
templateData.actionBar.dispose();
dispose(templateData.templateDisposable);
templateData.templateDisposable = [];
}
/**
* @inheritdoc
*/
disposeElement(_element: ITreeNode<ITestTreeElement, FuzzyScore>, _: number, templateData: TestTemplateData): void {
dispose(templateData.elementDisposable);
templateData.elementDisposable = [];
}
private fillActionBar(element: ITestTreeElement, data: TestTemplateData) {
const actions = getTestItemActions(this.instantiationService, this.contextKeyService, this.menuService, element);
data.elementDisposable.push(actions);
data.actionBar.clear();
data.actionBar.push(actions.value.primary, { icon: true, label: false });
}
}
const getTestItemActions = (instantionService: IInstantiationService, contextKeyService: IContextKeyService, menuService: IMenuService, element: ITestTreeElement) => {
const contextOverlay = contextKeyService.createOverlay([
['view', Testing.ExplorerViewId],
[TestingContextKeys.testItemExtId.key, element.test?.item.extId]
]);
const menu = menuService.createMenu(MenuId.TestItem, contextOverlay);
try {
const primary: IAction[] = [];
const running = element.state === TestRunState.Running;
if (!Iterable.isEmpty(element.runnable)) {
primary.push(instantionService.createInstance(RunAction, element.runnable, running));
}
if (!Iterable.isEmpty(element.debuggable)) {
primary.push(instantionService.createInstance(DebugAction, element.debuggable, running));
}
const secondary: IAction[] = [];
const result = { primary, secondary };
const actionsDisposable = createAndFillInActionBarActions(menu, {
arg: element.test?.item.extId,
shouldForwardArgs: true,
}, result, g => /^inline/.test(g));
return { value: result, dispose: () => actionsDisposable.dispose };
} finally {
menu.dispose();
}
};
type CountSummary = ReturnType<typeof collectCounts>;
const collectCounts = (count: TestStateCount) => {
@@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { ViewContainerLocation } from 'vs/workbench/common/views';
import { TestExplorerViewMode, TestExplorerViewSorting } from 'vs/workbench/contrib/testing/common/constants';
@@ -18,4 +19,8 @@ export namespace TestingContextKeys {
export const isPeekVisible = new RawContextKey('testing.isPeekVisible', false);
export const explorerLocation = new RawContextKey('testing.explorerLocation', ViewContainerLocation.Sidebar);
export const autoRun = new RawContextKey('testing.autoRun', false);
export const testItemExtId = new RawContextKey<string | undefined>('testId', undefined, {
type: 'string',
description: localize('testing.testId', 'ID of the current test item, set when creating or opening menus on test items')
});
}