Skip to content

Commit cc3db73

Browse files
authored
feat: implement TestController refreshHandler API (#1865)
1 parent 1d825f1 commit cc3db73

12 files changed

Lines changed: 173 additions & 37 deletions

File tree

packages/core-browser/src/contextkey/testing.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export const TestingHasAnyResults = new RawContextKey('testing.hasAnyResults', f
66
export const TestingIsRunning = new RawContextKey('testing.isRunning', false);
77
export const TestingIsInPeek = new RawContextKey('testing.isInPeek', true);
88
export const TestingIsPeekVisible = new RawContextKey('testing.isPeekVisible', false);
9+
export const TestingCanRefreshTests = new RawContextKey('testing.canRefresh', false);

packages/extension/src/browser/vscode/api/main.thread.tests.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,9 @@
22
import { Injectable, Optional, Autowired } from '@opensumi/di';
33
import { IRPCProtocol } from '@opensumi/ide-connection';
44
import { Logger } from '@opensumi/ide-core-browser';
5-
import {
6-
CancellationToken,
7-
Disposable,
8-
DisposableStore,
9-
IDisposable,
10-
URI,
11-
} from '@opensumi/ide-core-common';
5+
import { CancellationToken, Disposable, DisposableStore, IDisposable, URI } from '@opensumi/ide-core-common';
126
import { ITestController, ITestService, TestServiceToken } from '@opensumi/ide-testing';
7+
import { ObservableValue } from '@opensumi/ide-testing/lib/common/observableValue';
138
import { ITestProfileService, TestProfileServiceToken } from '@opensumi/ide-testing/lib/common/test-profile';
149
import {
1510
ITestResult,
@@ -31,7 +26,7 @@ import {
3126
} from '@opensumi/ide-testing/lib/common/testCollection';
3227
import { Range } from '@opensumi/monaco-editor-core/esm/vs/editor/common/core/range';
3328

34-
import { ExtHostAPIIdentifier, IExtHostTests, IMainThreadTesting } from '../../../common/vscode';
29+
import { ExtHostAPIIdentifier, IExtHostTests, IMainThreadTesting, ITestControllerPatch } from '../../../common/vscode';
3530

3631
const reviveDiff = (diff: TestsDiff) => {
3732
for (const entry of diff) {
@@ -59,6 +54,7 @@ export class MainThreadTestsImpl extends Disposable implements IMainThreadTestin
5954
{
6055
instance: ITestController;
6156
label: string;
57+
canRefresh: ObservableValue<boolean>;
6258
disposable: IDisposable;
6359
}
6460
>();
@@ -77,12 +73,16 @@ export class MainThreadTestsImpl extends Disposable implements IMainThreadTestin
7773
this.proxy = rpcProtocol.getProxy(ExtHostAPIIdentifier.ExtHostTests);
7874
}
7975

80-
$registerTestController(controllerId: string, label: string): void {
76+
$registerTestController(controllerId: string, label: string, canRefreshValue: boolean): void {
8177
const disposable = new DisposableStore();
78+
const canRefresh = disposable.add(new ObservableValue(canRefreshValue));
79+
8280
const controller: ITestController = {
8381
id: controllerId,
8482
label,
83+
canRefresh,
8584
configureRunProfile: (id) => this.proxy.$configureRunProfile(controllerId, id),
85+
refreshTests: (token) => this.proxy.$refreshTests(controllerId, token),
8686
runTests: (req, token) => this.proxy.$runControllerTests(req, token),
8787
expandTest: (testId, levels) => this.proxy.$expandTest(testId, isFinite(levels) ? levels : -1),
8888
};
@@ -93,12 +93,24 @@ export class MainThreadTestsImpl extends Disposable implements IMainThreadTestin
9393
this.testProviderRegistrations.set(controllerId, {
9494
instance: controller,
9595
label,
96+
canRefresh,
9697
disposable,
9798
});
9899
}
99100

100-
$updateControllerLabel(controllerId: string, label: string): void {
101-
this.logger.warn('test: updateControllerLabel>>', controllerId, label);
101+
$updateController(controllerId: string, patch: ITestControllerPatch): void {
102+
const controller = this.testProviderRegistrations.get(controllerId);
103+
if (!controller) {
104+
return;
105+
}
106+
107+
if (patch.label !== undefined) {
108+
controller.label = patch.label;
109+
}
110+
111+
if (patch.canRefresh !== undefined) {
112+
controller.canRefresh.value = patch.canRefresh;
113+
}
102114
}
103115

104116
$unregisterTestController(controllerId: string): void {

packages/extension/src/common/vscode/tests.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ import {
2424
} from '@opensumi/ide-testing/lib/common/testCollection';
2525

2626
export interface IExtHostTests {
27-
createTestController(controllerId: string, label: string): vscode.TestController;
27+
createTestController(
28+
controllerId: string,
29+
label: string,
30+
refreshHandler?: ((token: CancellationToken) => Thenable<void> | void) | undefined,
31+
): vscode.TestController;
2832

2933
// #region API for main thread
3034
$runControllerTests(req: RunTestForControllerRequest, token: CancellationToken): Promise<void>;
@@ -49,16 +53,23 @@ export interface IExtHostTests {
4953
): Promise<CoverageDetails[]>;
5054
/** Configures a test run config. */
5155
$configureRunProfile(controllerId: string, configId: number): void;
56+
/** Asks the controller to refresh its tests */
57+
$refreshTests(controllerId: string, token: CancellationToken): Promise<void>;
5258
// #endregion
5359
}
5460

61+
export interface ITestControllerPatch {
62+
label?: string;
63+
canRefresh?: boolean;
64+
}
65+
5566
export interface IMainThreadTesting {
5667
// --- test lifecycle:
5768

5869
/** Registers that there's a test controller with the given ID */
59-
$registerTestController(controllerId: string, label: string): void;
70+
$registerTestController(controllerId: string, label: string, canRefresh: boolean): void;
6071
/** Updates the label of an existing test controller. */
61-
$updateControllerLabel(controllerId: string, label: string): void;
72+
$updateController(controllerId: string, patch: ITestControllerPatch): void;
6273
/** Disposes of the test controller with the given ID */
6374
$unregisterTestController(controllerId: string): void;
6475
/** Requests tests published to VS Code. */

packages/extension/src/hosted/api/vscode/ext.host.api.impl.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,8 @@ export function createApiFactory(
232232
},
233233
},
234234
tests: {
235-
createTestController(controllerId: string, label: string) {
236-
return extHostTests.createTestController(controllerId, label);
235+
createTestController(controllerId: string, label: string, refreshHandler?: () => Thenable<void> | void) {
236+
return extHostTests.createTestController(controllerId, label, refreshHandler);
237237
},
238238
},
239239
// 类型定义

packages/extension/src/hosted/api/vscode/ext.host.tests.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,17 @@ export class ExtHostTestsImpl implements IExtHostTests {
189189
this.controllers.get(controllerId)?.profiles.get(profileId)?.configureHandler?.();
190190
}
191191

192+
async $refreshTests(controllerId: string, token: CancellationToken) {
193+
await this.controllers.get(controllerId)?.controller.refreshHandler?.(token);
194+
}
195+
192196
// #endregion
193197

194-
createTestController(controllerId: string, label: string): TestController {
198+
createTestController(
199+
controllerId: string,
200+
label: string,
201+
refreshHandler?: (token: CancellationToken) => Thenable<void> | void,
202+
): TestController {
195203
if (this.controllers.has(controllerId)) {
196204
throw new Error(`Attempt to insert a duplicate controller with ID "${controllerId}"`);
197205
}
@@ -211,7 +219,14 @@ export class ExtHostTestsImpl implements IExtHostTests {
211219
set label(value: string) {
212220
label = value;
213221
collection.root.label = value;
214-
proxy.$updateControllerLabel(controllerId, label);
222+
proxy.$updateController(controllerId, { label });
223+
},
224+
get refreshHandler() {
225+
return refreshHandler;
226+
},
227+
set refreshHandler(value: ((token: CancellationToken) => Thenable<void> | void) | undefined) {
228+
refreshHandler = value;
229+
proxy.$updateController(controllerId, { canRefresh: !!value });
215230
},
216231
get id() {
217232
return controllerId;
@@ -256,7 +271,7 @@ export class ExtHostTestsImpl implements IExtHostTests {
256271
// back compat:
257272
(controller as any).createRunConfiguration = controller.createRunProfile;
258273

259-
proxy.$registerTestController(controllerId, label);
274+
proxy.$registerTestController(controllerId, label, !!refreshHandler);
260275
disposable.add(toDisposable(() => proxy.$unregisterTestController(controllerId)));
261276

262277
const info: ControllerInfo = { controller, collection, profiles };

packages/testing/src/browser/icons/icons.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const testingStatesToIcons = new Map<TestResultState, string>([
2525

2626
export const testingRunIcon = getExternalIcon('run');
2727
export const testingRunAllIcon = getExternalIcon('run-all');
28+
export const testingRefreshTests = getExternalIcon('refresh');
2829

2930
export const getIconWithColor = (state: TestResultState) =>
3031
`${testingStatesToIcons.get(state)} ${testStatesToIconColors[state]}` || '';

packages/testing/src/browser/test.service.ts

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
SlotLocation,
1111
} from '@opensumi/ide-core-browser';
1212
import { IContextKey, IContextKeyService } from '@opensumi/ide-core-browser/lib/context-key';
13-
import { TestingServiceProviderCount } from '@opensumi/ide-core-browser/lib/contextkey/testing';
13+
import { TestingCanRefreshTests, TestingServiceProviderCount } from '@opensumi/ide-core-browser/lib/contextkey/testing';
1414
import { IMainLayoutService } from '@opensumi/ide-main-layout';
1515

1616
import { AmbiguousRunTestsRequest, ITestController, ITestService, TestId } from '../common';
@@ -27,6 +27,7 @@ import { TestingView } from './components/testing.view';
2727
export class TestServiceImpl extends Disposable implements ITestService {
2828
private controllers = new Map<string, ITestController>();
2929
private controllerCount: IContextKey<number>;
30+
private canRefreshTests: IContextKey<boolean>;
3031

3132
private readonly processDiffEmitter = new Emitter<TestsDiff>();
3233
private viewId = '';
@@ -49,6 +50,24 @@ export class TestServiceImpl extends Disposable implements ITestService {
4950
constructor() {
5051
super();
5152
this.controllerCount = TestingServiceProviderCount.bind(this.contextKeyService);
53+
this.canRefreshTests = TestingCanRefreshTests.bind(this.contextKeyService);
54+
}
55+
56+
public getTestController(controllerId: string): ITestController | undefined {
57+
return this.controllers.get(controllerId);
58+
}
59+
60+
public async refreshTests(controllerId?: string): Promise<void> {
61+
const cts = new CancellationTokenSource();
62+
try {
63+
if (controllerId) {
64+
await this.controllers.get(controllerId)?.refreshTests(cts.token);
65+
} else {
66+
await Promise.all([...this.controllers.values()].map((c) => c.refreshTests(cts.token)));
67+
}
68+
} finally {
69+
cts.dispose(true);
70+
}
5271
}
5372

5473
private registerTestingExplorerView(): string {
@@ -70,27 +89,41 @@ export class TestServiceImpl extends Disposable implements ITestService {
7089
registerTestController(id: string, testController: ITestController): IDisposable {
7190
this.controllers.set(id, testController);
7291
this.controllerCount.set(this.controllers.size);
92+
this.updateCanRefresh();
7393

7494
if (this.controllers.size > 0 && !this.viewId) {
7595
this.viewId = this.registerTestingExplorerView();
7696
}
7797

78-
return Disposable.create(() => {
79-
const diff: TestsDiff = [];
80-
for (const root of this.collection.rootItems) {
81-
if (root.controllerId === id) {
82-
diff.push([TestDiffOpType.Remove, root.item.extId]);
83-
}
84-
}
85-
this.publishDiff(id, diff);
98+
const disposable = new Disposable();
8699

87-
if (this.controllers.delete(id)) {
88-
this.controllerCount.set(this.controllers.size);
89-
if (this.controllers.size === 0 && this.viewId) {
90-
this.mainlayoutService.disposeContainer(this.viewId);
100+
disposable.addDispose(
101+
Disposable.create(() => {
102+
const diff: TestsDiff = [];
103+
for (const root of this.collection.rootItems) {
104+
if (root.controllerId === id) {
105+
diff.push([TestDiffOpType.Remove, root.item.extId]);
106+
}
91107
}
92-
}
93-
});
108+
this.publishDiff(id, diff);
109+
110+
if (this.controllers.delete(id)) {
111+
this.controllerCount.set(this.controllers.size);
112+
this.updateCanRefresh();
113+
if (this.controllers.size === 0 && this.viewId) {
114+
this.mainlayoutService.disposeContainer(this.viewId);
115+
}
116+
}
117+
}),
118+
);
119+
120+
disposable.addDispose(testController.canRefresh.onDidChange(this.updateCanRefresh, this));
121+
122+
return disposable;
123+
}
124+
125+
private updateCanRefresh() {
126+
this.canRefreshTests.set(Array.from(this.controllers.values()).some((c) => c.canRefresh));
94127
}
95128

96129
public async expandTest(id: string, levels: number) {

packages/testing/src/browser/testing.contribution.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
ToolbarRegistry,
1919
URI,
2020
} from '@opensumi/ide-core-browser';
21-
import { TestingIsPeekVisible } from '@opensumi/ide-core-browser/lib/contextkey/testing';
21+
import { TestingCanRefreshTests, TestingIsPeekVisible } from '@opensumi/ide-core-browser/lib/contextkey/testing';
2222
import { IMenuRegistry, MenuContribution, MenuId } from '@opensumi/ide-core-browser/lib/menu/next';
2323
import { Emitter, IMarkdownString } from '@opensumi/ide-core-common';
2424
import {
@@ -47,6 +47,7 @@ import {
4747
GoToTestCommand,
4848
OpenMessageInEditor,
4949
PeekTestError,
50+
RefreshTestsCommand,
5051
RuntAllTestCommand,
5152
RuntTestCommand,
5253
TestingDebugCurrentFile,
@@ -334,6 +335,12 @@ export class TestingContribution
334335
await this.testService.runTests({ tests: roots, group });
335336
};
336337

338+
commands.registerCommand(RefreshTestsCommand, {
339+
execute: async () => {
340+
await this.testService.refreshTests();
341+
},
342+
});
343+
337344
commands.registerCommand(RuntAllTestCommand, {
338345
execute: async () => {
339346
await runOrDebugAllTestsAction(TestRunProfileBitset.Run);
@@ -398,6 +405,12 @@ export class TestingContribution
398405
}
399406

400407
registerToolbarItems(registry: ToolbarRegistry): void {
408+
registry.registerItem({
409+
id: RefreshTestsCommand.id,
410+
command: RefreshTestsCommand.id,
411+
viewId: Testing.ExplorerViewId,
412+
when: TestingCanRefreshTests.equalsTo(true),
413+
});
401414
registry.registerItem({
402415
id: RuntAllTestCommand.id,
403416
command: RuntAllTestCommand.id,

packages/testing/src/common/commands.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,9 @@ export const DebugAllTestCommand: Command = {
7070
label: 'Debug All Test',
7171
iconClass: getIcon('debug-alt-small'),
7272
};
73+
74+
export const RefreshTestsCommand: Command = {
75+
id: 'testing.refresshTests',
76+
label: 'Refresh Tests',
77+
iconClass: getIcon('refresh'),
78+
};

packages/testing/src/common/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Event, IDisposable, CancellationToken } from '@opensumi/ide-core-browser';
22

3+
import { ObservableValue } from './observableValue';
34
import { ITestResult } from './test-result';
45
import {
56
InternalTestItem,
@@ -15,9 +16,10 @@ export * from './testId';
1516
export interface ITestController {
1617
readonly id: string;
1718
readonly label: string;
18-
19+
readonly canRefresh: ObservableValue<boolean>;
1920
configureRunProfile(profileId: number): void;
2021
expandTest(testId: string, levels: number): Promise<void>;
22+
refreshTests(token: CancellationToken): Promise<void>;
2123
runTests(request: RunTestForControllerRequest, token: CancellationToken): Promise<void>;
2224
}
2325

@@ -36,8 +38,9 @@ export interface ITestService {
3638
runTests(req: AmbiguousRunTestsRequest, token?: CancellationToken): Promise<ITestResult>;
3739
publishDiff(controllerId: string, diff: TestsDiff): void;
3840
runResolvedTests(req: ResolvedTestRunRequest, token?: CancellationToken): Promise<ITestResult>;
39-
4041
onDidProcessDiff: Event<TestsDiff>;
42+
getTestController(controllerId: string): ITestController | undefined;
43+
refreshTests(controllerId?: string): Promise<void>;
4144
}
4245

4346
export interface AmbiguousRunTestsRequest {

0 commit comments

Comments
 (0)