testing: allow following running test

Closes #117893
This commit is contained in:
Connor Peet 2021-04-26 14:30:56 -07:00
parent 480f3c0b35
commit 6014c7781e
No known key found for this signature in database
GPG key ID: CF8FD2EA0DBC61BD
5 changed files with 70 additions and 31 deletions

View file

@ -34,11 +34,11 @@ import { TestingExplorerView, TestingExplorerViewModel } from 'vs/workbench/cont
import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek';
import { ITestingOutputTerminalService } from 'vs/workbench/contrib/testing/browser/testingOutputTerminalService';
import { TestExplorerViewMode, TestExplorerViewSorting, Testing } from 'vs/workbench/contrib/testing/common/constants';
import { InternalTestItem, ITestItem, TestIdPath, TestIdWithSrc, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection';
import { InternalTestItem, ITestItem, TestIdPath, TestIdWithSrc } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestingAutoRun } from 'vs/workbench/contrib/testing/common/testingAutoRun';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { isFailedState } from 'vs/workbench/contrib/testing/common/testingStates';
import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult';
import { getPathForTestInResult, ITestResult } from 'vs/workbench/contrib/testing/common/testResult';
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
import { getAllTestsInHierarchy, getTestByPath, ITestService, waitForAllRoots } from 'vs/workbench/contrib/testing/common/testService';
import { IWorkspaceTestCollectionService } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
@ -853,21 +853,6 @@ abstract class RunOrDebugExtsById extends Action2 {
protected abstract filter(node: InternalTestItem): boolean;
protected abstract runTest(service: ITestService, node: InternalTestItem[]): Promise<ITestResult>;
protected getPathForTest(test: TestResultItem, results: ITestResult) {
const path = [test];
while (true) {
const parentId = path[0].parent;
const parent = parentId && results.getStateById(parentId);
if (!parent) {
break;
}
path.unshift(parent);
}
return path.map(t => t.item.extId);
}
}
abstract class RunOrDebugFailedTests extends RunOrDebugExtsById {
@ -881,7 +866,7 @@ abstract class RunOrDebugFailedTests extends RunOrDebugExtsById {
for (let i = results.length - 1; i >= 0; i--) {
const resultSet = results[i];
for (const test of resultSet.tests) {
const path = this.getPathForTest(test, resultSet).join(sep);
const path = getPathForTestInResult(test, resultSet).join(sep);
if (isFailedState(test.ownComputedState)) {
paths.add(path);
} else {
@ -906,7 +891,7 @@ abstract class RunOrDebugLastRun extends RunOrDebugExtsById {
for (const test of lastResult.tests) {
if (test.direct) {
yield this.getPathForTest(test, lastResult);
yield getPathForTestInResult(test, lastResult);
}
}
}

View file

@ -12,7 +12,7 @@ import { DefaultKeyboardNavigationDelegate, IListAccessibilityProvider } from 'v
import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree';
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 { RunOnceScheduler } from 'vs/base/common/async';
import { disposableTimeout, RunOnceScheduler } from 'vs/base/common/async';
import { Color, RGBA } from 'vs/base/common/color';
import { Event } from 'vs/base/common/event';
import { FuzzyScore } from 'vs/base/common/filters';
@ -53,10 +53,12 @@ import { testingHiddenIcon, testingStatesToIcons } from 'vs/workbench/contrib/te
import { ITestExplorerFilterState, TestExplorerFilterState, TestingExplorerFilter } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter';
import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/browser/testingOutputPeek';
import { ITestingProgressUiService } from 'vs/workbench/contrib/testing/browser/testingProgressUiService';
import { getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration';
import { TestExplorerStateFilter, TestExplorerViewMode, TestExplorerViewSorting, Testing, testStateNames } from 'vs/workbench/contrib/testing/common/constants';
import { TestIdPath, TestItemExpandState } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { cmpPriority, isFailedState } from 'vs/workbench/contrib/testing/common/testingStates';
import { cmpPriority, isFailedState, isStateWithResult } from 'vs/workbench/contrib/testing/common/testingStates';
import { getPathForTestInResult, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult';
import { ITestResultService } 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';
@ -223,6 +225,7 @@ export class TestingExplorerViewModel extends Disposable {
public projection = this._register(new MutableDisposable<ITestTreeProjection>());
private readonly emptyTestsWidget: EmptyTestsWidget;
private readonly revealTimeout = new MutableDisposable();
private readonly _viewMode = TestingContextKeys.viewMode.bindTo(this.contextKeyService);
private readonly _viewSorting = TestingContextKeys.viewSorting.bindTo(this.contextKeyService);
@ -272,6 +275,7 @@ export class TestingExplorerViewModel extends Disposable {
listContainer: HTMLElement,
onDidChangeVisibility: Event<boolean>,
private listener: TestSubscriptionListener | undefined,
@IConfigurationService configurationService: IConfigurationService,
@IMenuService private readonly menuService: IMenuService,
@IContextMenuService private readonly contextMenuService: IContextMenuService,
@ITestService private readonly testService: ITestService,
@ -379,6 +383,29 @@ export class TestingExplorerViewModel extends Disposable {
}
}));
let followRunningTests = getTestingConfiguration(configurationService, TestingConfigKeys.FollowRunningTest);
this._register(configurationService.onDidChangeConfiguration(() => {
followRunningTests = getTestingConfiguration(configurationService, TestingConfigKeys.FollowRunningTest);
}));
this._register(testResults.onTestChanged(evt => {
if (!followRunningTests) {
return;
}
if (evt.reason !== TestResultItemChangeReason.OwnStateChange) {
return;
}
// follow running tests, or tests whose state changed. Tests that
// complete very fast may not enter the running state at all.
if (evt.item.ownComputedState !== TestResultState.Running && !(evt.previous === TestResultState.Queued && isStateWithResult(evt.item.ownComputedState))) {
return;
}
this.revealByIdPath(getPathForTestInResult(evt.item, evt.result), false, false);
}));
this._register(testResults.onResultsChanged(() => {
this.tree.resort(null);
}));
@ -403,7 +430,7 @@ export class TestingExplorerViewModel extends Disposable {
* Tries to reveal by extension ID. Queues the request if the extension
* ID is not currently available.
*/
private revealByIdPath(idPath: TestIdPath | undefined) {
private revealByIdPath(idPath: TestIdPath | undefined, expand = true, focus = true) {
if (!idPath) {
this.hasPendingReveal = false;
return;
@ -423,19 +450,21 @@ export class TestingExplorerViewModel extends Disposable {
continue;
}
// If this 'if' is true, we're at the clostest-visible parent to the node
// If this 'if' is true, we're at the closest-visible parent to the node
// we want to expand. Expand that, and then start the loop again because
// we might already have children for it.
if (i < idPath.length - 1) {
this.tree.expand(element);
expandToLevel = i + 1; // avoid an infinite loop if the test does not exist
i = idPath.length - 1; // restart the loop since new children may now be visible
continue;
if (expand) {
this.tree.expand(element);
expandToLevel = i + 1; // avoid an infinite loop if the test does not exist
i = idPath.length - 1; // restart the loop since new children may now be visible
continue;
}
}
// Otherwise, we've arrived!
// If the node or any of its children are exlcuded, flip on the 'show
// If the node or any of its children are excluded, flip on the 'show
// excluded tests' checkbox automatically.
for (let n: TestItemTreeElement | TestTreeWorkspaceFolder = element; n instanceof TestItemTreeElement; n = n.parent) {
if (n.test && this.testService.excludeTests.value.has(n.test.item.extId)) {
@ -446,9 +475,11 @@ export class TestingExplorerViewModel extends Disposable {
this.filterState.reveal.value = undefined;
this.hasPendingReveal = false;
this.tree.domFocus();
if (focus) {
this.tree.domFocus();
}
setTimeout(() => {
this.revealTimeout.value = disposableTimeout(() => {
// Don't scroll to the item if it's already visible
if (this.tree.getRelativeTop(element) === null) {
this.tree.reveal(element, 0.5);

View file

@ -12,6 +12,7 @@ export const enum TestingConfigKeys {
AutoRunMode = 'testing.autoRun.mode',
AutoOpenPeekView = 'testing.automaticallyOpenPeekView',
AutoOpenPeekViewDuringAutoRun = 'testing.automaticallyOpenPeekViewDuringAutoRun',
FollowRunningTest = 'testing.followRunningTest',
}
export const enum AutoOpenPeekViewWhen {
@ -65,6 +66,11 @@ export const testingConfiguation: IConfigurationNode = {
type: 'boolean',
default: false,
},
[TestingConfigKeys.FollowRunningTest]: {
description: localize('testing.followRunningTest', 'Controls whether the running test should be followed in the test explorer view'),
type: 'boolean',
default: true,
},
}
};
@ -73,6 +79,7 @@ export interface ITestingConfiguration {
[TestingConfigKeys.AutoRunDelay]: number;
[TestingConfigKeys.AutoOpenPeekView]: AutoOpenPeekViewWhen;
[TestingConfigKeys.AutoOpenPeekViewDuringAutoRun]: boolean;
[TestingConfigKeys.FollowRunningTest]: boolean;
}
export const getTestingConfiguration = <K extends TestingConfigKeys>(config: IConfigurationService, key: K) => config.getValue<ITestingConfiguration[K]>(key);

View file

@ -11,7 +11,7 @@ import { URI } from 'vs/base/common/uri';
import { Range } from 'vs/editor/common/core/range';
import { TestResultState } from 'vs/workbench/api/common/extHostTypes';
import { IComputedStateAccessor, refreshComputedState } from 'vs/workbench/contrib/testing/common/getComputedState';
import { ExtensionRunTestsRequest, ISerializedTestResults, ITestItem, ITestMessage, ITestRunTask, ITestTaskState, RunTestsRequest, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection';
import { ExtensionRunTestsRequest, ISerializedTestResults, ITestItem, ITestMessage, ITestRunTask, ITestTaskState, RunTestsRequest, TestIdPath, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection';
import { maxPriority, statesInOrder } from 'vs/workbench/contrib/testing/common/testingStates';
export interface ITestResult {
@ -63,6 +63,21 @@ export interface ITestResult {
toJSON(): ISerializedTestResults | undefined;
}
export const getPathForTestInResult = (test: TestResultItem, results: ITestResult): TestIdPath => {
const path = [test];
while (true) {
const parentId = path[0].parent;
const parent = parentId && results.getStateById(parentId);
if (!parent) {
break;
}
path.unshift(parent);
}
return path.map(t => t.item.extId);
};
/**
* Count of the number of tests in each run state.
*/

View file

@ -23,6 +23,7 @@ export const statePriority: { [K in TestResultState]: number } = {
};
export const isFailedState = (s: TestResultState) => s === TestResultState.Errored || s === TestResultState.Failed;
export const isStateWithResult = (s: TestResultState) => s === TestResultState.Errored || s === TestResultState.Failed || s === TestResultState.Passed;
export const stateNodes = Object.entries(statePriority).reduce(
(acc, [stateStr, priority]) => {