From 6e3ab98395c4674c166b448c2c1e8a5ce62ee3c7 Mon Sep 17 00:00:00 2001 From: Benny Neugebauer Date: Mon, 10 Aug 2026 23:38:19 +0200 Subject: [PATCH] fix: prefer nested config profiles for shared tests --- packages/extension/src/api.ts | 9 +++- packages/extension/src/extension.ts | 6 ++- packages/extension/src/testTree.ts | 6 ++- .../multiple-configs/vitest.config.base.ts | 7 +++ .../z-leaf/test/selected-config.test.ts | 5 +++ .../multiple-configs/z-leaf/vitest.config.ts | 12 +++++ test/e2e/runner.test.ts | 19 ++++++++ test/unit/testTree.test.ts | 44 +++++++++++++++++++ 8 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 samples/multiple-configs/vitest.config.base.ts create mode 100644 samples/multiple-configs/z-leaf/test/selected-config.test.ts create mode 100644 samples/multiple-configs/z-leaf/vitest.config.ts create mode 100644 test/unit/testTree.test.ts diff --git a/packages/extension/src/api.ts b/packages/extension/src/api.ts index 60ce8cd3..67f6a20e 100644 --- a/packages/extension/src/api.ts +++ b/packages/extension/src/api.ts @@ -91,10 +91,11 @@ export async function resolveVitestAPI( const resolvedApisPromises = await Promise.allSettled(workspacePromises) const errors: unknown[] = [] const apis: VitestProcessAPI[] = [] + const resolvedResults: DiscoveryResult[] = [] for (const result of resolvedApisPromises) { if (result.status === 'fulfilled') { apis.push(result.value.api) - onResolved?.(result.value) + resolvedResults.push(result.value) } else { errors.push(result.reason) } @@ -144,7 +145,7 @@ export async function resolveVitestAPI( try { const result = await createVitestProcessAPI(usedConfigs, pkg) apis.push(result.api) - onResolved?.(result) + resolvedResults.push(result) if (result.api.workspaceSource) { workspaceRoots.push(dirname(result.api.workspaceSource)) } @@ -167,6 +168,10 @@ export async function resolveVitestAPI( showVitestError('The extension could not load some configs') } + resolvedResults + .sort((a, b) => b.api.package.cwd.split('/').length - a.api.package.cwd.split('/').length) + .forEach((result) => onResolved?.(result)) + return new VitestAPI(apis) } diff --git a/packages/extension/src/extension.ts b/packages/extension/src/extension.ts index 0de203e1..6e75e496 100644 --- a/packages/extension/src/extension.ts +++ b/packages/extension/src/extension.ts @@ -199,7 +199,11 @@ class VitestExtension { this.testTree.collectFile(vitest, file) }) - const prefix = vitest.prefix + // VS Code sorts default profiles by label, so indent nested configs first. + const depth = relative(vitest.workspaceFolder.uri.fsPath, vitest.package.cwd) + .split('/') + .filter(Boolean).length + const prefix = `${' '.repeat(depth)}${vitest.prefix}` let runProfile = this.runProfiles.get(`${vitest.id}:run`) if (!runProfile) { diff --git a/packages/extension/src/testTree.ts b/packages/extension/src/testTree.ts index 8a85b84d..2ccc4389 100644 --- a/packages/extension/src/testTree.ts +++ b/packages/extension/src/testTree.ts @@ -146,7 +146,11 @@ export class TestTree extends vscode.Disposable { const normalizedFile = normalize(file) const fileId = `${normalizedFile}${project}` const cached = this.fileItems.get(fileId) - if (cached) return cached + if (cached) { + // A file can be included in several configs (#799). + if (!cached.tags.includes(api.tag)) cached.tags = [...cached.tags, api.tag] + return cached + } const fileUri = vscode.Uri.file(resolve(file)) const parentItem = this.getOrCreateFolderTestItem(api, dirname(file)) diff --git a/samples/multiple-configs/vitest.config.base.ts b/samples/multiple-configs/vitest.config.base.ts new file mode 100644 index 00000000..45477a73 --- /dev/null +++ b/samples/multiple-configs/vitest.config.base.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['z-leaf/**/*.test.ts'], + }, +}) diff --git a/samples/multiple-configs/z-leaf/test/selected-config.test.ts b/samples/multiple-configs/z-leaf/test/selected-config.test.ts new file mode 100644 index 00000000..b2a6479b --- /dev/null +++ b/samples/multiple-configs/z-leaf/test/selected-config.test.ts @@ -0,0 +1,5 @@ +import { expect, it } from 'vitest' + +it('uses the leaf config', { tags: ['leaf-only'] }, () => { + expect(1 + 1).toBe(2) +}) diff --git a/samples/multiple-configs/z-leaf/vitest.config.ts b/samples/multiple-configs/z-leaf/vitest.config.ts new file mode 100644 index 00000000..94516183 --- /dev/null +++ b/samples/multiple-configs/z-leaf/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import baseConfig from '../vitest.config.base' + +export default mergeConfig( + baseConfig, + defineConfig({ + test: { + include: ['**/*.test.ts'], + tags: [{ name: 'leaf-only' }], + }, + }), +) diff --git a/test/e2e/runner.test.ts b/test/e2e/runner.test.ts index 9308695f..3a54920a 100644 --- a/test/e2e/runner.test.ts +++ b/test/e2e/runner.test.ts @@ -88,6 +88,25 @@ test('workspaces', async ({ launch }) => { await expect(tester.tree.getResultsLocator()).toHaveText('4/4') }) +test('gutter uses the nested config for shared test files', async ({ launch }) => { + const { page, tester } = await launch({ + workspacePath: './samples/multiple-configs', + }) + + await tester.tree.expand('z-leaf/test/selected-config.test.ts') + const nestedTest = tester.tree.getFileItem('selected-config.test.ts') + + await expect(nestedTest).toHaveTests({ + 'uses the leaf config': 'waiting', + }) + + await nestedTest.navigate() + await page.locator('.testing-run-glyph').first().click() + + await expect(tester.tree.getResultsLocator()).toHaveText('1/1') + await expect(nestedTest).toHaveState('passed') +}) + test('running a project does not update other projects', async ({ launch }) => { const { tester } = await launch({ workspacePath: './samples/projects', diff --git a/test/unit/testTree.test.ts b/test/unit/testTree.test.ts new file mode 100644 index 00000000..e2a44ce1 --- /dev/null +++ b/test/unit/testTree.test.ts @@ -0,0 +1,44 @@ +import { resolve } from 'node:path' +import * as vscode from 'vscode' +import { expect } from 'chai' +import type { VitestProcessAPI } from '../../packages/extension/src/apiProcess' +import { TransformSchemaProvider } from '../../packages/extension/src/schemaProvider' +import { TagsManager } from '../../packages/extension/src/tagsManager' +import { TestTree } from '../../packages/extension/src/testTree' + +describe('TestTree', () => { + it('adds every config tag to a shared test file', () => { + const controller = vscode.tests.createTestController('test-tree', 'Vitest') + const loader = controller.createTestItem('loader', 'Loading') + const schemaProvider = new TransformSchemaProvider(async () => null) + const tree = new TestTree(controller, loader, new TagsManager(), schemaProvider) + const root = resolve(__dirname, '../..') + const workspaceFolder = { + uri: vscode.Uri.file(root), + name: 'vscode', + index: 0, + } + const file = resolve(__dirname, 'testTree.test.ts') + const metadata = { project: '', pool: 'threads' } + const baseApi = { + tag: new vscode.TestTag('root:vitest.config.base.ts'), + } as VitestProcessAPI + const packageApi = { + tag: new vscode.TestTag('foo:vitest.config.ts'), + } as VitestProcessAPI + + tree.reset([workspaceFolder]) + const item = tree.getOrCreateFileTestItem(baseApi, metadata, file) + const cachedItem = tree.getOrCreateFileTestItem(packageApi, metadata, file) + + expect(cachedItem).to.equal(item) + expect(cachedItem.tags.map((tag) => tag.id)).to.deep.equal([ + 'root:vitest.config.base.ts', + 'foo:vitest.config.ts', + ]) + + tree.dispose() + schemaProvider.dispose() + controller.dispose() + }) +})