mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 17:19:48 +01:00
testing: enable continuous run for selected tests (#179215)
feat: enable auto-run for selected tests For #178973. Demo in https://github.com/microsoft/vscode/issues/178973#issuecomment-1496713352
This commit is contained in:
@@ -28,7 +28,8 @@ export const testingShowAsTree = registerIcon('testing-show-as-list-icon', Codic
|
||||
export const testingUpdateProfiles = registerIcon('testing-update-profiles', Codicon.gear, localize('testingUpdateProfiles', 'Icon shown to update test profiles.'));
|
||||
export const testingRefreshTests = registerIcon('testing-refresh-tests', Codicon.refresh, localize('testingRefreshTests', 'Icon on the button to refresh tests.'));
|
||||
export const testingTurnContinuousRunOn = registerIcon('testing-turn-continuous-run-on', Codicon.eye, localize('testingTurnContinuousRunOn', 'Icon to turn continuous test runs on.'));
|
||||
export const testingTurnContinuousRunOff = registerIcon('testing-turn-continuous-run-pff', Codicon.eyeClosed, localize('testingTurnContinuousRunOff', 'Icon to turn continuous test runs off.'));
|
||||
export const testingTurnContinuousRunOff = registerIcon('testing-turn-continuous-run-off', Codicon.eyeClosed, localize('testingTurnContinuousRunOff', 'Icon to turn continuous test runs off.'));
|
||||
export const testingContinuousIsOn = registerIcon('testing-continuous-is-on', Codicon.eye, localize('testingTurnContinuousRunIsOn', 'Icon when continuous run is on for a test ite,.'));
|
||||
export const testingCancelRefreshTests = registerIcon('testing-cancel-refresh-tests', Codicon.stop, localize('testingCancelRefreshTests', 'Icon on the button to cancel refreshing tests.'));
|
||||
|
||||
export const testingStatesToIcons = new Map<TestResultState, ThemeIcon>([
|
||||
|
||||
@@ -70,9 +70,26 @@
|
||||
margin-right: 0.8em;
|
||||
}
|
||||
|
||||
.test-explorer .monaco-list-row .monaco-action-bar .codicon-testing-continuous-is-on {
|
||||
color: var(--vscode-inputOption-activeForeground);
|
||||
border-color: var(--vscode-inputOption-activeBorder);
|
||||
background: var(--vscode-inputOption-activeBackground);
|
||||
border: 1px solid var(--vscode-inputOption-activeBorder);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.test-explorer .monaco-list-row:not(.focused, :hover) .monaco-action-bar.testing-is-continuous-run .action-item {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.test-explorer .monaco-list-row .monaco-action-bar.testing-is-continuous-run .action-item:last-child {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.test-explorer .monaco-list-row:hover .monaco-action-bar,
|
||||
.test-output-peek-tree .monaco-list-row:hover .monaco-action-bar,
|
||||
.test-explorer .monaco-list-row.focused .monaco-action-bar,
|
||||
.test-explorer .monaco-list-row .monaco-action-bar.testing-is-continuous-run,
|
||||
.test-output-peek-tree .monaco-list-row:hover .monaco-action-bar,
|
||||
.test-output-peek-tree .monaco-list-row.focused .monaco-action-bar {
|
||||
display: initial;
|
||||
}
|
||||
@@ -124,7 +141,7 @@
|
||||
.monaco-action-bar
|
||||
.action-item
|
||||
> .action-label {
|
||||
padding: 2px;
|
||||
padding: 1px 2px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ import { TestCommandId, TestExplorerViewMode, TestExplorerViewSorting, Testing,
|
||||
import { ITestProfileService, canUseProfileWithTest } from 'vs/workbench/contrib/testing/common/testProfileService';
|
||||
import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult';
|
||||
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
|
||||
import { IMainThreadTestCollection, ITestService, expandAndGetTestById, testsInFile } from 'vs/workbench/contrib/testing/common/testService';
|
||||
import { IMainThreadTestCollection, IMainThreadTestController, ITestService, expandAndGetTestById, testsInFile } from 'vs/workbench/contrib/testing/common/testService';
|
||||
import { ExtTestRunProfileKind, ITestRunProfile, InternalTestItem, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes';
|
||||
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
|
||||
import { ITestingContinuousRunService } from 'vs/workbench/contrib/testing/common/testingContinuousRunService';
|
||||
@@ -64,6 +64,7 @@ const enum ActionOrder {
|
||||
Sort,
|
||||
GoToTest,
|
||||
HideTest,
|
||||
ContinuousRunTest = -1 >>> 1, // max int, always at the end to avoid shifting on hover
|
||||
}
|
||||
|
||||
const hasAnyTestProvider = ContextKeyGreaterExpr.create(TestingContextKeys.providerCount.key, 0);
|
||||
@@ -250,6 +251,91 @@ export class SelectDefaultTestProfiles extends Action2 {
|
||||
}
|
||||
}
|
||||
|
||||
export class ContinuousRunTestAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: TestCommandId.ToggleContinousRunForTest,
|
||||
title: localize('testing.toggleContinuousRunOn', 'Turn on Continuous Run'),
|
||||
icon: icons.testingTurnContinuousRunOn,
|
||||
precondition: ContextKeyExpr.or(
|
||||
TestingContextKeys.isContinuousModeOn.isEqualTo(true),
|
||||
TestingContextKeys.isParentRunningContinuously.isEqualTo(false)
|
||||
),
|
||||
toggled: {
|
||||
condition: TestingContextKeys.isContinuousModeOn.isEqualTo(true),
|
||||
icon: icons.testingContinuousIsOn,
|
||||
title: localize('testing.toggleContinuousRunOff', 'Turn off Continuous Run'),
|
||||
},
|
||||
menu: testItemInlineAndInContext(ActionOrder.ContinuousRunTest, TestingContextKeys.supportsContinuousRun.isEqualTo(true)),
|
||||
});
|
||||
}
|
||||
|
||||
public override async run(accessor: ServicesAccessor, ...elements: IActionableTestTreeElement[]): Promise<any> {
|
||||
const crService = accessor.get(ITestingContinuousRunService);
|
||||
const profileService = accessor.get(ITestProfileService);
|
||||
for (const element of elements) {
|
||||
if (!(element instanceof TestItemTreeElement)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const id = element.test.item.extId;
|
||||
if (crService.isSpecificallyEnabledFor(id)) {
|
||||
crService.stop(id);
|
||||
continue;
|
||||
}
|
||||
|
||||
const profiles = profileService.getGroupDefaultProfiles(TestRunProfileBitset.Run)
|
||||
.filter(p => p.supportsContinuousRun && p.controllerId === element.test.controllerId);
|
||||
if (!profiles.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
crService.start(profiles, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ContinuousRunUsingProfileTestAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: TestCommandId.ContinousRunUsingForTest,
|
||||
title: localize('testing.startContinuousRunUsing', 'Start Continous Run Using...'),
|
||||
icon: icons.testingDebugIcon,
|
||||
menu: [
|
||||
{
|
||||
id: MenuId.TestItem,
|
||||
order: ActionOrder.RunContinuous,
|
||||
group: 'builtin@2',
|
||||
when: ContextKeyExpr.and(
|
||||
TestingContextKeys.supportsContinuousRun.isEqualTo(true),
|
||||
TestingContextKeys.isContinuousModeOn.isEqualTo(false),
|
||||
)
|
||||
}
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
public override async run(accessor: ServicesAccessor, ...elements: IActionableTestTreeElement[]): Promise<any> {
|
||||
const crService = accessor.get(ITestingContinuousRunService);
|
||||
const profileService = accessor.get(ITestProfileService);
|
||||
const notificationService = accessor.get(INotificationService);
|
||||
const quickInputService = accessor.get(IQuickInputService);
|
||||
|
||||
for (const element of elements) {
|
||||
if (!(element instanceof TestItemTreeElement)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const selected = await selectContinuousRunProfiles(crService, notificationService, quickInputService,
|
||||
[{ profiles: profileService.getControllerProfiles(element.test.controllerId) }]);
|
||||
|
||||
if (selected.length) {
|
||||
crService.start(selected, element.test.item.extId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfigureTestProfilesAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
@@ -314,6 +400,79 @@ class StopContinuousRunAction extends Action2 {
|
||||
}
|
||||
}
|
||||
|
||||
function selectContinuousRunProfiles(
|
||||
crs: ITestingContinuousRunService,
|
||||
notificationService: INotificationService,
|
||||
quickInputService: IQuickInputService,
|
||||
profilesToPickFrom: Iterable<Readonly<{
|
||||
controller?: IMainThreadTestController;
|
||||
profiles: ITestRunProfile[];
|
||||
}>>,
|
||||
): Promise<ITestRunProfile[]> {
|
||||
type ItemType = IQuickPickItem & { profile: ITestRunProfile };
|
||||
|
||||
const items: ItemType[] = [];
|
||||
for (const { controller, profiles } of profilesToPickFrom) {
|
||||
for (const profile of profiles) {
|
||||
if (profile.supportsContinuousRun) {
|
||||
items.push({
|
||||
label: profile.label || controller?.label.value || '',
|
||||
description: controller?.label.value,
|
||||
profile,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
notificationService.info(localize('testing.noProfiles', 'No test continuous run-enabled profiles were found'));
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
// special case: don't bother to quick a pickpick if there's only a single profile
|
||||
if (items.length === 1) {
|
||||
return Promise.resolve([items[0].profile]);
|
||||
}
|
||||
|
||||
const qpItems: (ItemType | IQuickPickSeparator)[] = [];
|
||||
const selectedItems: ItemType[] = [];
|
||||
const lastRun = crs.lastRunProfileIds;
|
||||
|
||||
items.sort((a, b) => a.profile.group - b.profile.group
|
||||
|| a.profile.controllerId.localeCompare(b.profile.controllerId)
|
||||
|| a.label.localeCompare(b.label));
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (i === 0 || items[i - 1].profile.group !== item.profile.group) {
|
||||
qpItems.push({ type: 'separator', label: testConfigurationGroupNames[item.profile.group] });
|
||||
}
|
||||
|
||||
qpItems.push(item);
|
||||
if (lastRun.has(item.profile.profileId)) {
|
||||
selectedItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
const quickpick = quickInputService.createQuickPick<IQuickPickItem & { profile: ITestRunProfile }>();
|
||||
quickpick.title = localize('testing.selectContinuousProfiles', 'Select profiles to run when files change:');
|
||||
quickpick.canSelectMany = true;
|
||||
quickpick.items = qpItems;
|
||||
quickpick.selectedItems = selectedItems;
|
||||
quickpick.show();
|
||||
return new Promise((resolve, reject) => {
|
||||
quickpick.onDidAccept(() => {
|
||||
resolve(quickpick.selectedItems.map(i => i.profile));
|
||||
quickpick.dispose();
|
||||
});
|
||||
|
||||
quickpick.onDidHide(() => {
|
||||
resolve([]);
|
||||
quickpick.dispose();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class StartContinuousRunAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
@@ -324,69 +483,12 @@ class StartContinuousRunAction extends Action2 {
|
||||
menu: continuousMenus(false),
|
||||
});
|
||||
}
|
||||
run(accessor: ServicesAccessor, ...args: any[]): void {
|
||||
const controllerProfiles = accessor.get(ITestProfileService).all();
|
||||
const notificationService = accessor.get(INotificationService);
|
||||
async run(accessor: ServicesAccessor, ...args: any[]): Promise<void> {
|
||||
const crs = accessor.get(ITestingContinuousRunService);
|
||||
|
||||
type ItemType = IQuickPickItem & { profile: ITestRunProfile };
|
||||
|
||||
const items: ItemType[] = [];
|
||||
for (const { controller, profiles } of controllerProfiles) {
|
||||
for (const profile of profiles) {
|
||||
if (profile.supportsContinuousRun) {
|
||||
items.push({
|
||||
label: profile.label || controller.label.value,
|
||||
description: controller.label.value,
|
||||
profile,
|
||||
});
|
||||
}
|
||||
}
|
||||
const selected = await selectContinuousRunProfiles(crs, accessor.get(INotificationService), accessor.get(IQuickInputService), accessor.get(ITestProfileService).all());
|
||||
if (selected.length) {
|
||||
crs.start(selected);
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
notificationService.info(localize('testing.noProfiles', 'No test continuous run-enabled profiles were found'));
|
||||
return;
|
||||
}
|
||||
|
||||
// special case: don't bother to quick a pickpick if there's only a single profile
|
||||
if (items.length === 1) {
|
||||
return crs.start([items[0].profile]);
|
||||
}
|
||||
|
||||
const qpItems: (ItemType | IQuickPickSeparator)[] = [];
|
||||
const selectedItems: ItemType[] = [];
|
||||
const lastRun = crs.lastRunProfileIds;
|
||||
|
||||
items.sort((a, b) => a.profile.group - b.profile.group
|
||||
|| a.profile.controllerId.localeCompare(b.profile.controllerId)
|
||||
|| a.label.localeCompare(b.label));
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (i === 0 || items[i - 1].profile.group !== item.profile.group) {
|
||||
qpItems.push({ type: 'separator', label: testConfigurationGroupNames[item.profile.group] });
|
||||
}
|
||||
|
||||
qpItems.push(item);
|
||||
if (lastRun.has(item.profile.profileId)) {
|
||||
selectedItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
const quickpick = accessor.get(IQuickInputService).createQuickPick<IQuickPickItem & { profile: ITestRunProfile }>();
|
||||
quickpick.title = localize('testing.selectContinuousProfiles', 'Select profiles to run when files change:');
|
||||
quickpick.canSelectMany = true;
|
||||
quickpick.items = qpItems;
|
||||
quickpick.selectedItems = selectedItems;
|
||||
quickpick.show();
|
||||
|
||||
quickpick.onDidAccept(() => {
|
||||
if (quickpick.selectedItems.length) {
|
||||
crs.start(quickpick.selectedItems.map(i => i.profile));
|
||||
quickpick.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1367,6 +1469,8 @@ export const allTestActions = [
|
||||
ClearTestResultsAction,
|
||||
CollapseAllAction,
|
||||
ConfigureTestProfilesAction,
|
||||
ContinuousRunTestAction,
|
||||
ContinuousRunUsingProfileTestAction,
|
||||
DebugAction,
|
||||
DebugAllAction,
|
||||
DebugAtCursor,
|
||||
|
||||
@@ -69,6 +69,7 @@ import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingC
|
||||
import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener';
|
||||
import { cmpPriority, isFailedState, isStateWithResult } from 'vs/workbench/contrib/testing/common/testingStates';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { ITestingContinuousRunService } from 'vs/workbench/contrib/testing/common/testingContinuousRunService';
|
||||
|
||||
const enum LastFocusState {
|
||||
Input,
|
||||
@@ -513,6 +514,7 @@ class TestingExplorerViewModel extends Disposable {
|
||||
@ITestResultService private readonly testResults: ITestResultService,
|
||||
@ITestingPeekOpener private readonly peekOpener: ITestingPeekOpener,
|
||||
@ITestProfileService private readonly testProfileService: ITestProfileService,
|
||||
@ITestingContinuousRunService private readonly crService: ITestingContinuousRunService,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -563,6 +565,14 @@ class TestingExplorerViewModel extends Disposable {
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this.crService.onDidChange(testId => {
|
||||
if (testId) {
|
||||
// a continuous run test will sort to the top:
|
||||
const elem = this.projection.value?.getElementByTestId(testId);
|
||||
this.tree.resort(elem?.parent && this.tree.hasElement(elem.parent) ? elem.parent : null, false);
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(onDidChangeVisibility(visible => {
|
||||
if (visible) {
|
||||
this.ensureProjection();
|
||||
@@ -781,7 +791,7 @@ class TestingExplorerViewModel extends Disposable {
|
||||
return;
|
||||
}
|
||||
|
||||
const actions = getActionableElementActions(this.contextKeyService, this.menuService, this.testService, this.testProfileService, element);
|
||||
const { actions } = getActionableElementActions(this.contextKeyService, this.menuService, this.testService, this.crService, this.testProfileService, element);
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => evt.anchor,
|
||||
getActions: () => actions.secondary,
|
||||
@@ -1007,13 +1017,21 @@ class TestsFilter implements ITreeFilter<TestExplorerTreeElement> {
|
||||
}
|
||||
|
||||
class TreeSorter implements ITreeSorter<TestExplorerTreeElement> {
|
||||
constructor(private readonly viewModel: TestingExplorerViewModel) { }
|
||||
constructor(
|
||||
private readonly viewModel: TestingExplorerViewModel,
|
||||
@ITestingContinuousRunService private readonly crService: ITestingContinuousRunService,
|
||||
) { }
|
||||
|
||||
public compare(a: TestExplorerTreeElement, b: TestExplorerTreeElement): number {
|
||||
if (a instanceof TestTreeErrorMessage || b instanceof TestTreeErrorMessage) {
|
||||
return (a instanceof TestTreeErrorMessage ? -1 : 0) + (b instanceof TestTreeErrorMessage ? 1 : 0);
|
||||
}
|
||||
|
||||
const crDelta = +this.crService.isSpecificallyEnabledFor(b.test.item.extId) - +this.crService.isSpecificallyEnabledFor(a.test.item.extId);
|
||||
if (crDelta !== 0) {
|
||||
return crDelta;
|
||||
}
|
||||
|
||||
const durationDelta = (b.duration || 0) - (a.duration || 0);
|
||||
if (this.viewModel.viewSorting === TestExplorerViewSorting.ByDuration && durationDelta !== 0) {
|
||||
return durationDelta;
|
||||
@@ -1178,6 +1196,7 @@ class ErrorRenderer implements ITreeRenderer<TestTreeErrorMessage, FuzzyScore, I
|
||||
}
|
||||
|
||||
interface IActionableElementTemplateData {
|
||||
current?: TestItemTreeElement;
|
||||
label: HTMLElement;
|
||||
icon: HTMLElement;
|
||||
wrapper: HTMLElement;
|
||||
@@ -1186,8 +1205,10 @@ interface IActionableElementTemplateData {
|
||||
templateDisposable: IDisposable[];
|
||||
}
|
||||
|
||||
abstract class ActionableItemTemplateData<T extends TestItemTreeElement> extends Disposable
|
||||
implements ITreeRenderer<T, FuzzyScore, IActionableElementTemplateData> {
|
||||
class TestItemRenderer extends Disposable
|
||||
implements ITreeRenderer<TestItemTreeElement, FuzzyScore, IActionableElementTemplateData> {
|
||||
public static readonly ID = 'testItem';
|
||||
|
||||
constructor(
|
||||
private readonly actionRunner: TestExplorerActionRunner,
|
||||
@IMenuService private readonly menuService: IMenuService,
|
||||
@@ -1195,6 +1216,7 @@ abstract class ActionableItemTemplateData<T extends TestItemTreeElement> extends
|
||||
@ITestProfileService protected readonly profiles: ITestProfileService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@ITestingContinuousRunService private readonly crService: ITestingContinuousRunService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -1202,7 +1224,7 @@ abstract class ActionableItemTemplateData<T extends TestItemTreeElement> extends
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
abstract get templateId(): string;
|
||||
public readonly templateId = TestItemRenderer.ID;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
@@ -1222,14 +1244,14 @@ abstract class ActionableItemTemplateData<T extends TestItemTreeElement> extends
|
||||
: undefined
|
||||
});
|
||||
|
||||
return { wrapper, label, actionBar, icon, elementDisposable: [], templateDisposable: [actionBar] };
|
||||
}
|
||||
const crListener = this.crService.onDidChange(changed => {
|
||||
if (templateData.current && (!changed || changed === templateData.current.test.item.extId)) {
|
||||
this.fillActionBar(templateData.current, templateData);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public renderElement({ element }: ITreeNode<T, FuzzyScore>, _: number, data: IActionableElementTemplateData): void {
|
||||
this.fillActionBar(element, data);
|
||||
const templateData: IActionableElementTemplateData = { wrapper, label, actionBar, icon, elementDisposable: [], templateDisposable: [actionBar, crListener] };
|
||||
return templateData;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1243,34 +1265,25 @@ abstract class ActionableItemTemplateData<T extends TestItemTreeElement> extends
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
disposeElement(_element: ITreeNode<T, FuzzyScore>, _: number, templateData: IActionableElementTemplateData): void {
|
||||
disposeElement(_element: ITreeNode<TestItemTreeElement, FuzzyScore>, _: number, templateData: IActionableElementTemplateData): void {
|
||||
dispose(templateData.elementDisposable);
|
||||
templateData.elementDisposable = [];
|
||||
}
|
||||
|
||||
private fillActionBar(element: T, data: IActionableElementTemplateData) {
|
||||
const actions = getActionableElementActions(this.contextKeyService, this.menuService, this.testService, this.profiles, element);
|
||||
private fillActionBar(element: TestItemTreeElement, data: IActionableElementTemplateData) {
|
||||
const { actions, contextOverlay } = getActionableElementActions(this.contextKeyService, this.menuService, this.testService, this.crService, this.profiles, element);
|
||||
data.actionBar.domNode.classList.toggle('testing-is-continuous-run', !!contextOverlay.getContextKeyValue(TestingContextKeys.isContinuousModeOn.key));
|
||||
data.actionBar.clear();
|
||||
data.actionBar.context = element;
|
||||
data.actionBar.push(actions.primary, { icon: true, label: false });
|
||||
}
|
||||
}
|
||||
|
||||
class TestItemRenderer extends ActionableItemTemplateData<TestItemTreeElement> {
|
||||
public static readonly ID = 'testItem';
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
get templateId(): string {
|
||||
return TestItemRenderer.ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public override renderElement(node: ITreeNode<TestItemTreeElement, FuzzyScore>, depth: number, data: IActionableElementTemplateData): void {
|
||||
super.renderElement(node, depth, data);
|
||||
public renderElement(node: ITreeNode<TestItemTreeElement, FuzzyScore>, _depth: number, data: IActionableElementTemplateData): void {
|
||||
data.current = node.element;
|
||||
this.fillActionBar(node.element, data);
|
||||
|
||||
const testHidden = this.testService.excluded.contains(node.element.test);
|
||||
data.wrapper.classList.toggle('test-is-hidden', testHidden);
|
||||
@@ -1321,6 +1334,7 @@ const getActionableElementActions = (
|
||||
contextKeyService: IContextKeyService,
|
||||
menuService: IMenuService,
|
||||
testService: ITestService,
|
||||
crService: ITestingContinuousRunService,
|
||||
profiles: ITestProfileService,
|
||||
element: TestItemTreeElement,
|
||||
) => {
|
||||
@@ -1328,13 +1342,23 @@ const getActionableElementActions = (
|
||||
const contextKeys: [string, unknown][] = getTestItemContextOverlay(test, test ? profiles.capabilitiesForTest(test) : 0);
|
||||
contextKeys.push(['view', Testing.ExplorerViewId]);
|
||||
if (test) {
|
||||
const ctrl = testService.getTestController(test.controllerId);
|
||||
const supportsCr = !!ctrl && profiles.getControllerProfiles(ctrl.id).some(p => p.supportsContinuousRun);
|
||||
contextKeys.push([
|
||||
TestingContextKeys.canRefreshTests.key,
|
||||
TestId.isRoot(test.item.extId) && testService.getTestController(test.item.extId)?.canRefresh.value
|
||||
]);
|
||||
contextKeys.push([
|
||||
!!ctrl?.canRefresh.value && TestId.isRoot(test.item.extId),
|
||||
], [
|
||||
TestingContextKeys.testItemIsHidden.key,
|
||||
testService.excluded.contains(test)
|
||||
], [
|
||||
TestingContextKeys.isContinuousModeOn.key,
|
||||
supportsCr && crService.isSpecificallyEnabledFor(test.item.extId)
|
||||
], [
|
||||
TestingContextKeys.isParentRunningContinuously.key,
|
||||
supportsCr && crService.isEnabledForAParentOf(test.item.extId)
|
||||
], [
|
||||
TestingContextKeys.supportsContinuousRun.key,
|
||||
supportsCr,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1349,7 +1373,7 @@ const getActionableElementActions = (
|
||||
shouldForwardArgs: true,
|
||||
}, result, 'inline');
|
||||
|
||||
return result;
|
||||
return { actions: result, contextOverlay };
|
||||
} finally {
|
||||
menu.dispose();
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ export const enum TestCommandId {
|
||||
ClearTestResultsAction = 'testing.clearTestResults',
|
||||
CollapseAllAction = 'testing.collapseAll',
|
||||
ConfigureTestProfilesAction = 'testing.configureProfile',
|
||||
ContinousRunUsingForTest = 'testing.continuousRunUsingForTest',
|
||||
DebugAction = 'testing.debug',
|
||||
DebugAllAction = 'testing.debugAll',
|
||||
DebugAtCursor = 'testing.debugAtCursor',
|
||||
@@ -88,6 +89,7 @@ export const enum TestCommandId {
|
||||
TestingSortByStatusAction = 'testing.sortByStatus',
|
||||
TestingViewAsListAction = 'testing.viewAsList',
|
||||
TestingViewAsTreeAction = 'testing.viewAsTree',
|
||||
ToggleContinousRunForTest = 'testing.toggleContinuousRunForTest',
|
||||
ToggleInlineTestOutput = 'testing.toggleInlineTestOutput',
|
||||
UnhideAllTestsAction = 'testing.unhideAllTests',
|
||||
UnhideTestAction = 'testing.unhideTest',
|
||||
|
||||
@@ -19,6 +19,7 @@ export namespace TestingContextKeys {
|
||||
export const hasNonDefaultProfile = new RawContextKey('testing.hasNonDefaultProfile', false, { type: 'boolean', description: localize('testing.hasNonDefaultConfig', 'Indicates whether any test controller has registered a non-default configuration') });
|
||||
export const hasConfigurableProfile = new RawContextKey('testing.hasConfigurableProfile', false, { type: 'boolean', description: localize('testing.hasConfigurableConfig', 'Indicates whether any test configuration can be configured') });
|
||||
export const supportsContinuousRun = new RawContextKey('testing.supportsContinuousRun', false, { type: 'boolean', description: localize('testing.supportsContinuousRun', 'Indicates whether continous test running is supported') });
|
||||
export const isParentRunningContinuously = new RawContextKey('testing.isParentRunningContinuously', false, { type: 'boolean', description: localize('testing.isParentRunningContinuously', 'Indicates whether the parent of a test is continuously running, set in the menu context of test items') });
|
||||
export const activeEditorHasTests = new RawContextKey('testing.activeEditorHasTests', false, { type: 'boolean', description: localize('testing.activeEditorHasTests', 'Indicates whether any tests are present in the current editor') });
|
||||
|
||||
export const capabilityToContextKey: { [K in TestRunProfileBitset]: RawContextKey<boolean> } = {
|
||||
@@ -36,7 +37,6 @@ export namespace TestingContextKeys {
|
||||
export const isRunning = new RawContextKey<boolean>('testing.isRunning', false);
|
||||
export const isInPeek = new RawContextKey<boolean>('testing.isInPeek', true);
|
||||
export const isPeekVisible = new RawContextKey<boolean>('testing.isPeekVisible', false);
|
||||
export const autoRun = new RawContextKey<boolean>('testing.autoRun', false);
|
||||
|
||||
export const peekItemType = new RawContextKey<string | undefined>('peekItemType', undefined, {
|
||||
type: 'string',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||
@@ -13,6 +13,8 @@ import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingC
|
||||
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
||||
import { TestService } from 'vs/workbench/contrib/testing/common/testServiceImpl';
|
||||
import { ITestRunProfile } from 'vs/workbench/contrib/testing/common/testTypes';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { TestId } from 'vs/workbench/contrib/testing/common/testId';
|
||||
|
||||
export const ITestingContinuousRunService = createDecorator<ITestingContinuousRunService>('testingContinuousRunService');
|
||||
|
||||
@@ -25,22 +27,44 @@ export interface ITestingContinuousRunService {
|
||||
readonly lastRunProfileIds: ReadonlySet<number>;
|
||||
|
||||
/**
|
||||
* Starts a continuous auto run with a specific profile or set of profiles.
|
||||
* Fired when a test is added or removed from continous run, or when
|
||||
* enablement is changed globally.
|
||||
*/
|
||||
start(profile: ITestRunProfile[]): void;
|
||||
onDidChange: Event<string | undefined>;
|
||||
|
||||
/**
|
||||
* Stops any continuous run.
|
||||
* Gets whether continous run is specifically enabled for the given test ID.
|
||||
*/
|
||||
stop(): void;
|
||||
isSpecificallyEnabledFor(testId: string): boolean;
|
||||
|
||||
/**
|
||||
* Gets whether continous run is specifically enabled for
|
||||
* the given test ID, or any of its parents.
|
||||
*/
|
||||
isEnabledForAParentOf(testId: string): boolean;
|
||||
|
||||
/**
|
||||
* Starts a continuous auto run with a specific profile or set of profiles.
|
||||
* Globally if no test is given, for a specific test otherwise.
|
||||
*/
|
||||
start(profile: ITestRunProfile[], testId?: string): void;
|
||||
|
||||
/**
|
||||
* Stops any continuous run
|
||||
* Globally if no test is given, for a specific test otherwise.
|
||||
*/
|
||||
stop(testId?: string): void;
|
||||
}
|
||||
|
||||
export class TestingContinuousRunService extends Disposable implements ITestingContinuousRunService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly changeEmitter = new Emitter<string | undefined>();
|
||||
private readonly running = new Map<string | undefined, CancellationTokenSource>();
|
||||
private readonly lastRun: StoredValue<Set<number>>;
|
||||
private readonly cancellation = this._register(new MutableDisposable<CancellationTokenSource>());
|
||||
private readonly isOn: IContextKey<boolean>;
|
||||
private readonly isGloballyOn: IContextKey<boolean>;
|
||||
|
||||
public readonly onDidChange = this.changeEmitter.event;
|
||||
|
||||
public get lastRunProfileIds() {
|
||||
return this.lastRun.get(new Set());
|
||||
@@ -52,7 +76,7 @@ export class TestingContinuousRunService extends Disposable implements ITestingC
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
) {
|
||||
super();
|
||||
this.isOn = TestingContextKeys.isContinuousModeOn.bindTo(contextKeyService);
|
||||
this.isGloballyOn = TestingContextKeys.isContinuousModeOn.bindTo(contextKeyService);
|
||||
this.lastRun = new StoredValue<Set<number>>({
|
||||
key: 'lastContinuousRunProfileIds',
|
||||
scope: StorageScope.WORKSPACE,
|
||||
@@ -65,26 +89,63 @@ export class TestingContinuousRunService extends Disposable implements ITestingC
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public start(profile: ITestRunProfile[]): void {
|
||||
this.cancellation.value?.cancel();
|
||||
const cts = this.cancellation.value = new CancellationTokenSource();
|
||||
public isSpecificallyEnabledFor(testId: string): boolean {
|
||||
return this.running.has(testId);
|
||||
}
|
||||
|
||||
this.isOn.set(true);
|
||||
/** @inheritdoc */
|
||||
public isEnabledForAParentOf(testId: string): boolean {
|
||||
if (!this.running.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.running.has(undefined)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const part of TestId.fromString(testId).idsFromRoot()) {
|
||||
if (this.running.has(part.toString())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public start(profile: ITestRunProfile[], testId?: string): void {
|
||||
const cts = new CancellationTokenSource();
|
||||
|
||||
if (testId === undefined) {
|
||||
this.isGloballyOn.set(true);
|
||||
}
|
||||
|
||||
this.running.get(testId)?.dispose(true);
|
||||
this.running.set(testId, cts);
|
||||
this.lastRun.store(new Set(profile.map(p => p.profileId)));
|
||||
|
||||
this.testService.startContinuousRun({
|
||||
continuous: true,
|
||||
targets: profile.map(p => ({
|
||||
testIds: [p.controllerId], // root id
|
||||
testIds: [testId ?? p.controllerId],
|
||||
controllerId: p.controllerId,
|
||||
profileGroup: p.group,
|
||||
profileId: p.profileId
|
||||
})),
|
||||
}, cts.token);
|
||||
|
||||
this.changeEmitter.fire(testId);
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.isOn.set(false);
|
||||
this.cancellation.value?.cancel();
|
||||
this.cancellation.value = undefined;
|
||||
/** @inheritdoc */
|
||||
public stop(testId?: string): void {
|
||||
this.running.get(testId)?.dispose(true);
|
||||
this.running.delete(testId);
|
||||
|
||||
if (testId === undefined) {
|
||||
this.isGloballyOn.set(false);
|
||||
}
|
||||
|
||||
this.changeEmitter.fire(testId);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user