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

testing: migrate from actions to menu contributions

Related to https://github.com/microsoft/vscode/issues/92038
上级 52bdc14c
......@@ -6,14 +6,19 @@
import { Action } from 'vs/base/common/actions';
import { Emitter } from 'vs/base/common/event';
import { Iterable } from 'vs/base/common/iterator';
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 { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane';
import * as icons from 'vs/workbench/contrib/testing/browser/icons';
import { ITestingCollectionService } from 'vs/workbench/contrib/testing/browser/testingCollectionService';
import { TestingExplorerViewModel } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
import { EMPTY_TEST_RESULT, InternalTestItem, RunTestsResult } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestingExplorerView, TestingExplorerViewModel } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
import { TestExplorerViewMode, Testing } from 'vs/workbench/contrib/testing/common/constants';
import { EMPTY_TEST_RESULT, InternalTestItem, RunTestsResult, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
export class FilterableAction extends Action {
......@@ -46,6 +51,9 @@ export class DebugAction extends Action {
);
}
/**
* @override
*/
public run(): Promise<any> {
return this.testService.runTests({
tests: [{ testId: this.test.id, providerId: this.test.providerId }],
......@@ -67,6 +75,9 @@ export class RunAction extends Action {
);
}
/**
* @override
*/
public run(): Promise<any> {
return this.testService.runTests({
tests: [{ testId: this.test.id, providerId: this.test.providerId }],
......@@ -75,61 +86,57 @@ export class RunAction extends Action {
}
}
abstract class RunOrDebugAction extends FilterableAction {
constructor(
private readonly viewModel: TestingExplorerViewModel,
id: string,
label: string,
className: string,
@ITestingCollectionService private readonly testCollection: ITestingCollectionService,
@ITestService private readonly testService: ITestService,
) {
super(
abstract class RunOrDebugAction extends ViewAction<TestingExplorerView> {
constructor(id: string, title: string, icon: ThemeIcon) {
super({
id,
label,
'test-action ' + className,
/* enabled= */ Iterable.first(testService.testRuns) === undefined,
);
this._register(testService.onTestRunStarted(this.updateVisibility, this));
this._register(testService.onTestRunCompleted(this.updateVisibility, this));
this._register(viewModel.onDidChangeSelection(this.updateEnablementState, this));
title,
icon,
viewId: Testing.ExplorerViewId,
menu: {
id: MenuId.ViewTitle,
order: 10,
group: 'navigation',
when: ContextKeyAndExpr.create([
ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId),
ContextKeyEqualsExpr.create(TestingContextKeys.isRunning.serialize(), false),
])
}
});
}
public run(): Promise<RunTestsResult> {
const tests = [...this.getActionableTests()];
/**
* @override
*/
public runInView(accessor: ServicesAccessor, view: TestingExplorerView): Promise<RunTestsResult> {
const tests = this.getActionableTests(accessor.get(ITestingCollectionService), view.viewModel);
if (!tests.length) {
return Promise.resolve(EMPTY_TEST_RESULT);
}
return this.testService.runTests({ tests, debug: this.debug() });
return accessor.get(ITestService).runTests({ tests, debug: this.debug() });
}
private updateVisibility() {
this._setVisible(Iterable.isEmpty(this.testService.testRuns));
}
private updateEnablementState() {
this._setEnabled(!Iterable.isEmpty(this.getActionableTests()));
}
private *getActionableTests() {
const selected = this.viewModel.getSelectedTests();
private getActionableTests(testCollection: ITestingCollectionService, viewModel: TestingExplorerViewModel) {
const selected = viewModel.getSelectedTests();
const tests: TestIdWithProvider[] = [];
if (!selected.length) {
for (const folder of this.testCollection.workspaceFolders()) {
for (const folder of testCollection.workspaceFolders()) {
for (const child of folder.getChildren()) {
if (this.filter(child)) {
yield { testId: child.id, providerId: child.providerId };
tests.push({ testId: child.id, providerId: child.providerId });
}
}
}
} else {
for (const item of selected) {
if (item?.test && this.filter(item.test)) {
yield { testId: item.test.id, providerId: item.test.providerId };
tests.push({ testId: item.test.id, providerId: item.test.providerId });
}
}
}
return tests;
}
protected abstract debug(): boolean;
......@@ -138,130 +145,128 @@ abstract class RunOrDebugAction extends FilterableAction {
export class RunSelectedAction extends RunOrDebugAction {
constructor(
viewModel: TestingExplorerViewModel,
@ITestingCollectionService testCollection: ITestingCollectionService,
@ITestService testService: ITestService,
) {
super(
viewModel,
'action.runSelected',
localize('runSelectedTests', 'Run Selected Tests'),
ThemeIcon.asClassName(icons.testingRunIcon),
testCollection,
testService,
icons.testingRunIcon,
);
}
/**
* @override
*/
public debug() {
return false;
}
/**
* @override
*/
public filter({ item }: InternalTestItem) {
return item.runnable;
}
}
export class DebugSelectedAction extends RunOrDebugAction {
constructor(
viewModel: TestingExplorerViewModel,
@ITestingCollectionService testCollection: ITestingCollectionService,
@ITestService testService: ITestService,
) {
constructor() {
super(
viewModel,
'action.debugSelected',
localize('debugSelectedTests', 'Debug Selected Tests'),
ThemeIcon.asClassName(icons.testingDebugIcon),
testCollection,
testService,
icons.testingDebugIcon,
);
}
/**
* @override
*/
public debug() {
return true;
}
/**
* @override
*/
public filter({ item }: InternalTestItem) {
return item.debuggable;
}
}
export class CancelTestRunAction extends FilterableAction {
constructor(@ITestService private readonly testService: ITestService) {
super(
'action.cancelRun',
localize('cancelRunTests', 'Cancel Test Run'),
ThemeIcon.asClassName(icons.testingCancelIcon),
);
this._register(testService.onTestRunStarted(this.updateVisibility, this));
this._register(testService.onTestRunCompleted(this.updateVisibility, this));
this.updateVisibility();
}
private updateVisibility() {
this._setVisible(!Iterable.isEmpty(this.testService.testRuns));
export class CancelTestRunAction extends Action2 {
constructor() {
super({
id: 'testing.cancelRun',
title: localize('testing.cancelRun', "Cancel Test Run"),
icon: icons.testingCancelIcon,
menu: {
id: MenuId.ViewTitle,
order: 10,
group: 'navigation',
when: ContextKeyAndExpr.create([
ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId),
ContextKeyEqualsExpr.create(TestingContextKeys.isRunning.serialize(), true),
])
}
});
}
public async run(): Promise<void> {
for (const run of this.testService.testRuns) {
this.testService.cancelTestRun(run);
/**
* @override
*/
public async run(accessor: ServicesAccessor) {
const testService = accessor.get(ITestService);
for (const run of testService.testRuns) {
testService.cancelTestRun(run);
}
}
}
export const enum ViewMode {
List,
Tree
}
export const enum ViewGrouping {
ByTree,
ByStatus,
}
export class ToggleViewModeAction extends Action {
constructor(private readonly viewModel: TestingExplorerViewModel) {
super(
'workbench.testing.action.toggleViewMode',
localize('toggleViewMode', "View as List"),
);
this._register(viewModel.onViewModeChange(this.onDidChangeMode, this));
this.onDidChangeMode(this.viewModel.viewMode);
}
async run(): Promise<void> {
this.viewModel.viewMode = this.viewModel.viewMode === ViewMode.List
? ViewMode.Tree
: ViewMode.List;
export class TestingViewAsListAction extends ViewAction<TestingExplorerView> {
constructor() {
super({
id: 'testing.viewAsList',
viewId: Testing.ExplorerViewId,
title: localize('testing.viewAsList', "View as List"),
f1: false,
toggled: TestingContextKeys.viewMode.isEqualTo(TestExplorerViewMode.List),
menu: {
id: MenuId.ViewTitle,
order: 10,
group: 'viewAs',
when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId)
}
});
}
private onDidChangeMode(mode: ViewMode): void {
const iconClass = ThemeIcon.asClassName(mode === ViewMode.List ? icons.testingShowAsList : icons.testingShowAsTree);
this.class = iconClass;
this.checked = mode === ViewMode.List;
/**
* @override
*/
public runInView(_accessor: ServicesAccessor, view: TestingExplorerView) {
view.viewModel.viewMode = TestExplorerViewMode.List;
}
}
export class ToggleViewGroupingAction extends Action {
constructor(private readonly viewModel: TestingExplorerViewModel) {
super(
'workbench.testing.action.toggleViewMode',
localize('toggleViewMode', "View as List"),
);
this._register(viewModel.onViewModeChange(this.onDidChangeMode, this));
this.onDidChangeMode(this.viewModel.viewMode);
}
async run(): Promise<void> {
this.viewModel.viewMode = this.viewModel.viewMode === ViewMode.List
? ViewMode.Tree
: ViewMode.List;
export class TestingViewAsTreeAction extends ViewAction<TestingExplorerView> {
constructor() {
super({
id: 'testing.viewAsTree',
viewId: Testing.ExplorerViewId,
title: localize('testing.viewAsTree', "View as Tree"),
f1: false,
toggled: TestingContextKeys.viewMode.isEqualTo(TestExplorerViewMode.Tree),
menu: {
id: MenuId.ViewTitle,
order: 10,
group: 'viewAs',
when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId)
}
});
}
private onDidChangeMode(mode: ViewMode): void {
const iconClass = ThemeIcon.asClassName(mode === ViewMode.List ? icons.testingShowAsList : icons.testingShowAsTree);
this.class = iconClass;
this.checked = mode === ViewMode.List;
/**
* @override
*/
public runInView(_accessor: ServicesAccessor, view: TestingExplorerView) {
view.viewModel.viewMode = TestExplorerViewMode.Tree;
}
}
......@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { registerAction2 } from 'vs/platform/actions/common/actions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
......@@ -11,18 +12,19 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { Extensions as ViewContainerExtensions, IViewContainersRegistry, IViewsRegistry, ViewContainerLocation } from 'vs/workbench/common/views';
import { testingViewIcon } from 'vs/workbench/contrib/testing/browser/icons';
import { ITestingCollectionService, TestingCollectionService } from 'vs/workbench/contrib/testing/browser/testingCollectionService';
import { TestingExplorerView, TESTING_EXPLORER_VIEW_ID } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
import { TestingExplorerView } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
import { TestingViewPaneContainer } from 'vs/workbench/contrib/testing/browser/testingViewPaneContainer';
import { Testing } from 'vs/workbench/contrib/testing/common/constants';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
import { TestService } from 'vs/workbench/contrib/testing/common/testServiceImpl';
import { TESTING_VIEWLET_ID } from 'vs/workbench/contrib/testing/common/testViews';
import * as Action from './testExplorerActions';
registerSingleton(ITestService, TestService);
registerSingleton(ITestingCollectionService, TestingCollectionService);
const viewContainer = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({
id: TESTING_VIEWLET_ID,
id: Testing.ViewletId,
name: localize('testing', "Testing"),
ctorDescriptor: new SyncDescriptor(TestingViewPaneContainer),
icon: testingViewIcon,
......@@ -34,11 +36,11 @@ const viewContainer = Registry.as<IViewContainersRegistry>(ViewContainerExtensio
const viewsRegistry = Registry.as<IViewsRegistry>(ViewContainerExtensions.ViewsRegistry);
viewsRegistry.registerViewWelcomeContent(TESTING_EXPLORER_VIEW_ID, {
viewsRegistry.registerViewWelcomeContent(Testing.ExplorerViewId, {
content: localize('noTestProvidersRegistered', "No test providers are registered for this workspace."),
});
viewsRegistry.registerViewWelcomeContent(TESTING_EXPLORER_VIEW_ID, {
viewsRegistry.registerViewWelcomeContent(Testing.ExplorerViewId, {
content: localize(
{
key: 'searchMarketplaceForTestExtensions',
......@@ -50,7 +52,7 @@ viewsRegistry.registerViewWelcomeContent(TESTING_EXPLORER_VIEW_ID, {
});
viewsRegistry.registerViews([{
id: TESTING_EXPLORER_VIEW_ID,
id: Testing.ExplorerViewId,
name: localize('testExplorer', "Test Explorer"),
ctorDescriptor: new SyncDescriptor(TestingExplorerView),
canToggleVisibility: true,
......@@ -62,3 +64,9 @@ viewsRegistry.registerViews([{
// temporary until release, at which point we can show the welcome view:
when: ContextKeyExpr.greater(TestingContextKeys.providerCount.serialize(), 0),
}], viewContainer);
registerAction2(Action.TestingViewAsListAction);
registerAction2(Action.TestingViewAsTreeAction);
registerAction2(Action.CancelTestRunAction);
registerAction2(Action.RunSelectedAction);
registerAction2(Action.DebugSelectedAction);
......@@ -9,7 +9,6 @@ import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelega
import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree';
import { ITreeElement, ITreeEvent, ITreeFilter, ITreeNode, ITreeRenderer, ITreeSorter, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree';
import { Action } from 'vs/base/common/actions';
import { throttle } from 'vs/base/common/decorators';
import { Emitter, Event } from 'vs/base/common/event';
import { FuzzyScore } from 'vs/base/common/filters';
......@@ -41,17 +40,15 @@ import { IViewDescriptorService } from 'vs/workbench/common/views';
import { testingStatesToIcons } from 'vs/workbench/contrib/testing/browser/icons';
import { maxPriority, statePriority } from 'vs/workbench/contrib/testing/browser/testExplorerTree';
import { ITestingCollectionService, TestSubscriptionListener } from 'vs/workbench/contrib/testing/browser/testingCollectionService';
import { TestExplorerViewMode } from 'vs/workbench/contrib/testing/common/constants';
import { InternalTestItem, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { CancelTestRunAction, DebugAction, DebugSelectedAction, FilterableAction, filterVisibleActions, RunAction, RunSelectedAction, ToggleViewModeAction, ViewMode } from './testExplorerActions';
export const TESTING_EXPLORER_VIEW_ID = 'workbench.view.testing';
import { DebugAction, RunAction } from './testExplorerActions';
export class TestingExplorerView extends ViewPane {
private primaryActions: Action[] = [];
private secondaryActions: Action[] = [];
private viewModel!: TestingExplorerViewModel;
public viewModel!: TestingExplorerViewModel;
private currentSubscription?: TestSubscriptionListener;
private listContainer!: HTMLElement;
......@@ -90,24 +87,6 @@ export class TestingExplorerView extends ViewPane {
this.viewModel = this.instantiationService.createInstance(TestingExplorerViewModel, this.listContainer, this.onDidChangeBodyVisibility, this.currentSubscription);
this._register(this.viewModel);
this.secondaryActions = [
this.instantiationService.createInstance(ToggleViewModeAction, this.viewModel)
];
this.secondaryActions.forEach(this._register, this);
this.primaryActions = [
this.instantiationService.createInstance(RunSelectedAction, this.viewModel),
this.instantiationService.createInstance(DebugSelectedAction, this.viewModel),
this.instantiationService.createInstance(CancelTestRunAction),
];
this.primaryActions.forEach(this._register, this);
for (const action of [...this.primaryActions, ...this.secondaryActions]) {
if (action instanceof FilterableAction) {
action.onDidChangeVisibility(this.updateActions, this);
}
}
this._register(this.onDidChangeBodyVisibility(visible => {
if (!visible && this.currentSubscription) {
this.currentSubscription.dispose();
......@@ -120,20 +99,6 @@ export class TestingExplorerView extends ViewPane {
}));
}
/**
* @override
*/
public getActions() {
return [...filterVisibleActions(this.primaryActions), ...super.getActions()];
}
/**
* @override
*/
public getSecondaryActions() {
return [...filterVisibleActions(this.secondaryActions), ...super.getSecondaryActions()];
}
/**
* @override
......@@ -153,8 +118,9 @@ export class TestingExplorerViewModel extends Disposable {
private tree: ObjectTree<ITestTreeElement, FuzzyScore>;
private filter: TestsFilter;
private projection!: ITestTreeProjection;
private _viewMode = Number(this.storageService.get('testing.viewMode', StorageScope.WORKSPACE, String(ViewMode.Tree))) as ViewMode;
private viewModeChangeEmitter = new Emitter<ViewMode>();
private readonly _viewMode = TestingContextKeys.viewMode.bindTo(this.contextKeyService);
private viewModeChangeEmitter = new Emitter<TestExplorerViewMode>();
/**
* Fires when the tree view mode changes.
......@@ -167,15 +133,15 @@ export class TestingExplorerViewModel extends Disposable {
public readonly onDidChangeSelection: Event<ITreeEvent<ITestTreeElement | null>>;
public get viewMode() {
return this._viewMode;
return this._viewMode.get() ?? TestExplorerViewMode.Tree;
}
public set viewMode(newMode: ViewMode) {
if (newMode === this._viewMode) {
public set viewMode(newMode: TestExplorerViewMode) {
if (newMode === this._viewMode.get()) {
return;
}
this._viewMode = newMode;
this._viewMode.set(newMode);
this.updatePreferredProjection();
this.storageService.store('testing.viewMode', newMode, StorageScope.WORKSPACE, StorageTarget.USER);
this.viewModeChangeEmitter.fire(newMode);
......@@ -188,8 +154,11 @@ export class TestingExplorerViewModel extends Disposable {
@IInstantiationService instantiationService: IInstantiationService,
@IEditorService editorService: IEditorService,
@IStorageService private readonly storageService: IStorageService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
) {
super();
this._viewMode.set(this.storageService.get('testing.viewMode', StorageScope.WORKSPACE, TestExplorerViewMode.Tree) as TestExplorerViewMode);
const labels = this._register(instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: onDidChangeVisibility }));
this.filter = new TestsFilter();
......@@ -249,7 +218,7 @@ export class TestingExplorerViewModel extends Disposable {
return;
}
if (this._viewMode === ViewMode.List) {
if (this._viewMode.get() === TestExplorerViewMode.List) {
this.projection = new ListProjection(this.listener);
} else {
this.projection = new HierarchalProjection(this.listener);
......
......@@ -3,4 +3,17 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const TESTING_VIEWLET_ID = 'workbench.view.testing';
export const enum Testing {
ViewletId = 'workbench.view.testing',
ExplorerViewId = 'workbench.view.testing',
}
export const enum TestExplorerViewMode {
List = 'list',
Tree = 'true'
}
export const enum TestExplorerViewGrouping {
ByLocation = 'location',
ByStatus = 'status',
}
......@@ -31,6 +31,7 @@ export class TestService extends Disposable implements ITestService {
private readonly unsubscribeEmitter = new Emitter<{ resource: ExtHostTestingResource, uri: URI }>();
private readonly changeProvidersEmitter = new Emitter<{ delta: number }>();
private readonly providerCount: IContextKey<number>;
private readonly isRunning: IContextKey<boolean>;
private readonly runStartedEmitter = new Emitter<RunTestsRequest>();
private readonly runCompletedEmitter = new Emitter<{ req: RunTestsRequest, result: RunTestsResult }>();
private readonly runningTests = new Map<RunTestsRequest, CancellationTokenSource>();
......@@ -41,6 +42,7 @@ export class TestService extends Disposable implements ITestService {
constructor(@IContextKeyService contextKeyService: IContextKeyService, @INotificationService private readonly notificationService: INotificationService) {
super();
this.providerCount = TestingContextKeys.providerCount.bindTo(contextKeyService);
this.isRunning = TestingContextKeys.isRunning.bindTo(contextKeyService);
}
/**
......@@ -104,9 +106,13 @@ export class TestService extends Disposable implements ITestService {
this.runningTests.set(req, cancelSource);
this.runStartedEmitter.fire(req);
const result = await collectTestResults(await Promise.all(requests));
this.isRunning.set(true);
const result = collectTestResults(await Promise.all(requests));
this.runningTests.delete(req);
this.runCompletedEmitter.fire({ req, result });
this.isRunning.set(this.runningTests.size > 0);
return result;
}
......
......@@ -4,7 +4,11 @@
*--------------------------------------------------------------------------------------------*/
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { TestExplorerViewMode, TestExplorerViewGrouping } from 'vs/workbench/contrib/testing/common/constants';
export namespace TestingContextKeys {
export const providerCount = new RawContextKey<number>('testingProviderCount', 0);
export const providerCount = new RawContextKey('testingProviderCount', 0);
export const viewMode = new RawContextKey('testExplorerViewMode', TestExplorerViewMode.List);
export const viewGrouping = new RawContextKey('testExplorerViewGrouping', TestExplorerViewGrouping.ByLocation);
export const isRunning = new RawContextKey('testIsrunning', false);
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册