Skip to content

Commit

Permalink
[vscode] Support TestRunRequest.preserveFocus (#13839)
Browse files Browse the repository at this point in the history
Add a progress service for tests to support activation of views
Forward Test Run to be started event to progress service to activate view if needed

fixes #13759

contributed on behalf of STMicroelectronics

Signed-off-by: Remi Schnekenburger <rschnekenburger@eclipsesource.com>
  • Loading branch information
rschnekenbu authored Jun 27, 2024
1 parent 63c0684 commit 0e50cfc
Show file tree
Hide file tree
Showing 12 changed files with 149 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<!-- ## not yet released
- [plugin] Avoid pollution of all toolbars by actions contributed by tree views in extensions [#13768](https://github.com/eclipse-theia/theia/pull/13768) - contributed on behalf of STMicroelectronics
- [plugin] support TestRunRequest preserveFocus API [#13839](https://github.com/eclipse-theia/theia/pull/13839) - contributed on behalf of STMicroelectronics
- [debug] stub activeStackItem and related change event in debug namespace [#13847](https://github.com/eclipse-theia/theia/pull/13847) - contributed on behalf of STMicroelectronics
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2265,7 +2265,7 @@ export interface TestingMain {

// Test runs

$notifyTestRunCreated(controllerId: string, run: TestRunDTO): void;
$notifyTestRunCreated(controllerId: string, run: TestRunDTO, preserveFocus: boolean): void;
$notifyTestStateChanged(controllerId: string, runId: string, stateChanges: TestStateChangeDTO[], outputChanges: TestOutputDTO[]): void;
$notifyTestRunEnded(controllerId: string, runId: string): void;
}
Expand Down
1 change: 1 addition & 0 deletions packages/plugin-ext/src/common/test-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export interface TestRunRequestDTO {
name: string;
includedTests: string[][]; // array of paths
excludedTests: string[][]; // array of paths
preserveFocus: boolean;
}

export interface TestItemReference {
Expand Down
11 changes: 8 additions & 3 deletions packages/plugin-ext/src/main/browser/test-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { SimpleObservableCollection, TreeCollection, observableProperty } from '
import {
TestController, TestItem, TestOutputItem, TestRun, TestRunProfile, TestService, TestState, TestStateChangedEvent
} from '@theia/test/lib/browser/test-service';
import { TestExecutionProgressService } from '@theia/test/lib/browser/test-execution-progress-service';
import { AccumulatingTreeDeltaEmitter, CollectionDelta, DeltaKind, TreeDelta, TreeDeltaBuilder } from '@theia/test/lib/common/tree-delta';
import { Emitter, Location, Range } from '@theia/core/shared/vscode-languageserver-protocol';
import { Range as PluginRange, Location as PluginLocation } from '../../common/plugin-api-rpc-model';
Expand Down Expand Up @@ -245,13 +246,14 @@ class TestRunProfileImpl implements TestRunProfile {
this.proxy.$onConfigureRunProfile(this.controllerId, this.id);
}

run(name: string, included: TestItem[], excluded: TestItem[]): void {
run(name: string, included: TestItem[], excluded: TestItem[], preserveFocus: boolean): void {
this.proxy.$onRunControllerTests([{
controllerId: this.controllerId,
name,
profileId: this.id,
includedTests: included.map(item => itemToPath(item)),
excludedTests: excluded.map(item => itemToPath(item))
excludedTests: excluded.map(item => itemToPath(item)),
preserveFocus
}]);
}
}
Expand Down Expand Up @@ -551,12 +553,14 @@ class TestControllerImpl implements TestController {

export class TestingMainImpl implements TestingMain {
private testService: TestService;
private testExecutionProgressService: TestExecutionProgressService;
private controllerRegistrations = new Map<string, [TestControllerImpl, Disposable]>();
private proxy: TestingExt;
canRefresh: boolean;

constructor(rpc: RPCProtocol, container: interfaces.Container, commandRegistry: CommandRegistryMainImpl) {
this.testService = container.get(TestService);
this.testExecutionProgressService = container.get(TestExecutionProgressService);
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.TESTING_EXT);
commandRegistry.registerArgumentProcessor({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -614,7 +618,8 @@ export class TestingMainImpl implements TestingMain {
$removeTestRunProfile(controllerId: string, profileId: string): void {
this.withController(controllerId).removeProfile(profileId);
}
$notifyTestRunCreated(controllerId: string, run: TestRunDTO): void {
$notifyTestRunCreated(controllerId: string, run: TestRunDTO, preserveFocus: boolean): void {
this.testExecutionProgressService.onTestRunRequested(preserveFocus);
this.withController(controllerId).addRun(run.id, run.name, run.isRunning);
}
$notifyTestStateChanged(controllerId: string, runId: string, stateChanges: TestStateChangeDTO[], outputChanges: TestOutputDTO[]): void {
Expand Down
15 changes: 8 additions & 7 deletions packages/plugin-ext/src/plugin/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export class TestControllerImpl implements theia.TestController {
}

createTestRun(request: theia.TestRunRequest, name?: string, persist: boolean = true): theia.TestRun {
return this.testRunStarted(request, name || '', persist, true);
return this.testRunStarted(request, name || '', persist, true);
}

dispose() {
Expand All @@ -149,7 +149,7 @@ export class TestControllerImpl implements theia.TestController {
return existing;
}

const run = new TestRun(this, this.proxy, name, persist, isRunning);
const run = new TestRun(this, this.proxy, name, persist, isRunning, request.preserveFocus);
const endListener = run.onWillFlush(() => {
// make sure we notify the front end of test item changes before test run state is sent
this.deltaBuilder.flush();
Expand All @@ -162,7 +162,7 @@ export class TestControllerImpl implements theia.TestController {
return run;
}

runTestsForUI(profileId: string, name: string, includedTests: string[][], excludedTests: string[][]): void {
runTestsForUI(profileId: string, name: string, includedTests: string[][], excludedTests: string[][], preserveFocus: boolean): void {
const profile = this.getProfile(profileId);
if (!profile) {
console.error(`No test run profile found for controller ${this.id} with id ${profileId} `);
Expand Down Expand Up @@ -197,7 +197,7 @@ export class TestControllerImpl implements theia.TestController {
.filter(isDefined);

const request = new TestRunRequest(
includeTests, excludeTests, profile, false // don't support continuous run yet
includeTests, excludeTests, profile, false /* don't support continuous run yet */, preserveFocus
);

const run = this.testRunStarted(request, name, false, false);
Expand Down Expand Up @@ -258,13 +258,14 @@ class TestRun implements theia.TestRun {
private readonly proxy: TestingMain,
readonly name: string,
readonly isPersisted: boolean,
isRunning: boolean) {
isRunning: boolean,
preserveFocus: boolean) {
this.id = generateUuid();

this.tokenSource = new CancellationTokenSource();
this.token = this.tokenSource.token;

this.proxy.$notifyTestRunCreated(this.controller.id, { id: this.id, name: this.name, isRunning });
this.proxy.$notifyTestRunCreated(this.controller.id, { id: this.id, name: this.name, isRunning }, preserveFocus);
}
enqueued(test: theia.TestItem): void {
this.updateTestState(test, { itemPath: checkTestInstance(test).path, state: TestExecutionState.Queued });
Expand Down Expand Up @@ -444,7 +445,7 @@ export class TestingExtImpl implements TestingExt {
}

runTestsForUI(req: TestRunRequestDTO): void {
this.withController(req.controllerId).runTestsForUI(req.profileId, req.name, req.includedTests, req.excludedTests);
this.withController(req.controllerId).runTestsForUI(req.profileId, req.name, req.includedTests, req.excludedTests, req.preserveFocus);
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/plugin-ext/src/plugin/types-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3337,6 +3337,7 @@ export class TestRunRequest implements theia.TestRunRequest {
public readonly exclude: theia.TestItem[] | undefined = undefined,
public readonly profile: theia.TestRunProfile | undefined = undefined,
public readonly continuous: boolean | undefined = undefined,
public readonly preserveFocus: boolean = true
) { }
}

Expand Down
11 changes: 10 additions & 1 deletion packages/plugin/src/theia.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16312,13 +16312,22 @@ export module '@theia/plugin' {
*/
readonly continuous?: boolean;

/**
* Controls how test Test Results view is focused. If true, the editor
* will keep the maintain the user's focus. If false, the editor will
* prefer to move focus into the Test Results view, although
* this may be configured by users.
*/
readonly preserveFocus: boolean;

/**
* @param include Array of specific tests to run, or undefined to run all tests
* @param exclude An array of tests to exclude from the run.
* @param profile The run profile used for this request.
* @param continuous Whether to run tests continuously as source changes.
* @param preserveFocus Whether to preserve the user's focus when the run is started
*/
constructor(include?: readonly TestItem[], exclude?: readonly TestItem[], profile?: TestRunProfile, continuous?: boolean);
constructor(include?: readonly TestItem[], exclude?: readonly TestItem[], profile?: TestRunProfile, continuous?: boolean, preserveFocus?: boolean);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export class PreferenceTreeGenerator {
['search', 'features'],
['task', 'features'],
['terminal', 'features'],
['testing', 'features'],
['toolbar', 'features'],
['webview', 'features'],
['workspace', 'application'],
Expand Down
53 changes: 53 additions & 0 deletions packages/test/src/browser/test-execution-progress-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// *****************************************************************************
// Copyright (C) 2024 STMicroelectronics and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { inject, injectable } from '@theia/core/shared/inversify';
import { Widget } from '@theia/core/lib/browser';
import { TestResultViewContribution } from './view/test-result-view-contribution';
import { TestViewContribution } from './view/test-view-contribution';
import { TestPreferences } from './test-preferences';

export interface TestExecutionProgressService {
onTestRunRequested(preserveFocus: boolean): Promise<void>;
}

export const TestExecutionProgressService = Symbol('TestExecutionProgressService');

@injectable()
export class DefaultTestExecutionProgressService implements TestExecutionProgressService {

@inject(TestResultViewContribution)
protected readonly testResultView: TestResultViewContribution;

@inject(TestViewContribution)
protected readonly testView: TestViewContribution;

@inject(TestPreferences)
protected readonly testPreferences: TestPreferences;

async onTestRunRequested(preserveFocus: boolean): Promise<void> {
if (!preserveFocus) {
const openTesting = this.testPreferences['testing.openTesting'];
if (openTesting === 'openOnTestStart') {
this.openTestResultView();
}
}
}

async openTestResultView(): Promise<Widget> {
return this.testResultView.openView({ activate: true });
}
}
58 changes: 58 additions & 0 deletions packages/test/src/browser/test-preferences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// *****************************************************************************
// Copyright (C) 2024 STMicroelectronics and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { interfaces } from '@theia/core/shared/inversify';
import { createPreferenceProxy, PreferenceProxy, PreferenceService, PreferenceContribution, PreferenceSchema } from '@theia/core/lib/browser';
import { nls } from '@theia/core/lib/common/nls';

export const TestConfigSchema: PreferenceSchema = {
type: 'object',
properties: {
'testing.openTesting': {
type: 'string',
enum: ['neverOpen', 'openOnTestStart'],
enumDescriptions: [
nls.localizeByDefault('Never automatically open the testing views'),
nls.localizeByDefault('Open the test results view when tests start'),
],
description: nls.localizeByDefault('Controls when the testing view should open.'),
default: 'neverOpen',
scope: 'resource',
}
}
};

export interface TestConfiguration {
'testing.openTesting': 'neverOpen' | 'openOnTestStart';
}

export const TestPreferenceContribution = Symbol('TestPreferenceContribution');
export const TestPreferences = Symbol('TestPreferences');
export type TestPreferences = PreferenceProxy<TestConfiguration>;

export function createTestPreferences(preferences: PreferenceService, schema: PreferenceSchema = TestConfigSchema): TestPreferences {
return createPreferenceProxy(preferences, schema);
}

export const bindTestPreferences = (bind: interfaces.Bind): void => {
bind(TestPreferences).toDynamicValue(ctx => {
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
const contribution = ctx.container.get<PreferenceContribution>(TestPreferenceContribution);
return createTestPreferences(preferences, contribution.schema);
}).inSingletonScope();
bind(TestPreferenceContribution).toConstantValue({ schema: TestConfigSchema });
bind(PreferenceContribution).toService(TestPreferenceContribution);
};
6 changes: 3 additions & 3 deletions packages/test/src/browser/test-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export interface TestRunProfile {
isDefault: boolean;
readonly canConfigure: boolean;
readonly tag: string;
run(name: string, included: readonly TestItem[], excluded: readonly TestItem[]): void;
run(name: string, included: readonly TestItem[], excluded: readonly TestItem[], preserveFocus: boolean): void;
configure(): void;
}

Expand Down Expand Up @@ -287,7 +287,7 @@ export class DefaultTestService implements TestService {
}
}
if (activeProfile) {
activeProfile.run(`Test run #${this.testRunCounter++}`, items, []);
activeProfile.run(`Test run #${this.testRunCounter++}`, items, [], true);
}
}

Expand Down Expand Up @@ -343,7 +343,7 @@ export class DefaultTestService implements TestService {
if (controller) {
this.pickProfile(controller.testRunProfiles, nls.localizeByDefault('Pick a test profile to use')).then(activeProfile => {
if (activeProfile) {
activeProfile.run(`Test run #${this.testRunCounter++}`, items, []);
activeProfile.run(`Test run #${this.testRunCounter++}`, items, [], true);
}
});
}
Expand Down
6 changes: 4 additions & 2 deletions packages/test/src/browser/view/test-view-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ import { TestRunTree, TestRunTreeWidget } from './test-run-widget';
import { TestResultViewContribution } from './test-result-view-contribution';
import { TEST_RUNS_CONTEXT_MENU, TestRunViewContribution } from './test-run-view-contribution';
import { TestContextKeyService } from './test-context-key-service';
import { DefaultTestExecutionProgressService, TestExecutionProgressService } from '../test-execution-progress-service';
import { bindTestPreferences } from '../test-preferences';

export default new ContainerModule(bind => {

bindTestPreferences(bind);
bindContributionProvider(bind, TestContribution);
bind(TestContextKeyService).toSelf().inSingletonScope();
bind(TestService).to(DefaultTestService).inSingletonScope();
Expand Down Expand Up @@ -105,7 +107,7 @@ export default new ContainerModule(bind => {
bind(TabBarToolbarContribution).toService(TestRunViewContribution);
bind(TestExecutionStateManager).toSelf().inSingletonScope();
bind(TestOutputUIModel).toSelf().inSingletonScope();

bind(TestExecutionProgressService).to(DefaultTestExecutionProgressService).inSingletonScope();
});

export function createTestTreeContainer(parent: interfaces.Container): Container {
Expand Down

0 comments on commit 0e50cfc

Please sign in to comment.