diff --git a/src/api.ts b/src/api.ts index 225ef9d..d4d0e80 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,7 +1,7 @@ import { normalize, relative } from 'pathe' import * as vscode from 'vscode' import { log } from './log' -import type { VitestEvents, VitestRPC } from './api/rpc' +import type { SerializedTestSpecification, VitestEvents, VitestRPC } from './api/rpc' import type { VitestPackage } from './api/pkg' import { showVitestError } from './utils' import type { VitestProcess } from './api/types' @@ -105,12 +105,12 @@ export class VitestFolderAPI { return this.pkg } - async runFiles(files?: string[], testNamePatern?: string) { - await this.meta.rpc.runTests(files?.map(normalize), testNamePatern) + async runFiles(specs?: SerializedTestSpecification[] | string[], testNamePatern?: string) { + await this.meta.rpc.runTests(normalizeSpecs(specs), testNamePatern) } - async updateSnapshots(files?: string[], testNamePatern?: string) { - await this.meta.rpc.updateSnapshots(files?.map(normalize), testNamePatern) + async updateSnapshots(specs?: SerializedTestSpecification[] | string[], testNamePatern?: string) { + await this.meta.rpc.updateSnapshots(normalizeSpecs(specs), testNamePatern) } getFiles() { @@ -194,8 +194,8 @@ export class VitestFolderAPI { await this.meta.rpc.disableCoverage() } - async watchTests(files?: string[], testNamePattern?: string) { - await this.meta.rpc.watchTests(files?.map(normalize), testNamePattern) + async watchTests(files?: SerializedTestSpecification[] | string[], testNamePattern?: string) { + await this.meta.rpc.watchTests(normalizeSpecs(files), testNamePattern) } async unwatchTests() { @@ -272,3 +272,15 @@ export interface ResolvedMeta { removeListener: (name: string, listener: any) => void } } + +function normalizeSpecs(specs?: string[] | SerializedTestSpecification[]) { + if (!specs) { + return specs + } + return specs.map((spec) => { + if (typeof spec === 'string') { + return normalize(spec) + } + return [spec[0], normalize(spec[1])] as SerializedTestSpecification + }) as string[] | SerializedTestSpecification[] +} diff --git a/src/api/rpc.ts b/src/api/rpc.ts index 73598dd..194ad1b 100644 --- a/src/api/rpc.ts +++ b/src/api/rpc.ts @@ -2,14 +2,20 @@ import v8 from 'node:v8' import { type BirpcReturn, createBirpc } from 'birpc' import type { RunnerTestFile, TaskResultPack, UserConsoleLog } from 'vitest' +export type SerializedTestSpecification = [ + project: { name: string | undefined }, + file: string, +] + export interface VitestMethods { getFiles: () => Promise<[project: string, file: string][]> collectTests: (testFile: [project: string, filepath: string][]) => Promise cancelRun: () => Promise - runTests: (files?: string[], testNamePattern?: string) => Promise - updateSnapshots: (files?: string[], testNamePattern?: string) => Promise + // accepts files with the project or folders (project doesn't matter for them) + runTests: (files?: SerializedTestSpecification[] | string[], testNamePattern?: string) => Promise + updateSnapshots: (files?: SerializedTestSpecification[] | string[], testNamePattern?: string) => Promise - watchTests: (files?: string[], testNamePattern?: string) => void + watchTests: (files?: SerializedTestSpecification[] | string[], testNamePattern?: string) => void unwatchTests: () => void enableCoverage: () => void diff --git a/src/runner/runner.ts b/src/runner/runner.ts index 74be4a5..a58e56a 100644 --- a/src/runner/runner.ts +++ b/src/runner/runner.ts @@ -12,6 +12,7 @@ import { log } from '../log' import { showVitestError } from '../utils' import { coverageContext, readCoverageReport } from '../coverage' import { normalizeDriveLetter } from '../worker/utils' +import type { SerializedTestSpecification } from '../api/rpc' export class TestRunner extends vscode.Disposable { private continuousRequests = new Set() @@ -257,7 +258,7 @@ export class TestRunner extends vscode.Disposable { log.verbose?.('Initiating deferred test run') this.testRunDefer = Promise.withResolvers() - const runTests = (files?: string[], testNamePatern?: string) => + const runTests = (files?: SerializedTestSpecification[] | string[], testNamePatern?: string) => 'updateSnapshots' in request ? this.api.updateSnapshots(files, testNamePatern) : this.api.runFiles(files, testNamePatern) @@ -471,8 +472,8 @@ export class TestRunner extends vscode.Disposable { this.markTestCase(testRun, test, result) } - private relative(file: string) { - return relative(this.api.workspaceFolder.uri.fsPath, file) + private relative(file: string | SerializedTestSpecification) { + return relative(this.api.workspaceFolder.uri.fsPath, typeof file === 'string' ? file : file[1]) } } @@ -529,16 +530,38 @@ function parseLocationFromStacks(testItem: vscode.TestItem, stacks: ParsedStack[ log.verbose?.('Could not find a valid stack for', testItem.label, JSON.stringify(stacks, null, 2)) } -function getTestFiles(tests: readonly vscode.TestItem[]) { - return Array.from( - new Set(tests.map((test) => { - const data = getTestData(test) - const fsPath = normalize(test.uri!.fsPath) - if (data instanceof TestFolder) - return `${fsPath}/` - return fsPath - }).filter(Boolean) as string[]), - ) +function getTestFiles(tests: readonly vscode.TestItem[]): string[] | SerializedTestSpecification[] { + // if there is a folder, we can't limit the tests to a specific project + const hasFolder = tests.some(test => getTestData(test) instanceof TestFolder) + if (hasFolder) { + return Array.from( + new Set(tests.map((test) => { + const data = getTestData(test) + const fsPath = normalize(test.uri!.fsPath) + if (data instanceof TestFolder) + return `${fsPath}/` + return fsPath + }).filter(Boolean) as string[]), + ) + } + const testSpecs: SerializedTestSpecification[] = [] + const testFiles = new Set() + for (const test of tests) { + const fsPath = test.uri!.fsPath + const data = getTestData(test) + // just to type guard, actually not possible to have + if (data instanceof TestFolder) { + continue + } + const project = data instanceof TestFile ? data.project : data.file.project + const key = `${project}\0${fsPath}` + if (testFiles.has(key)) { + continue + } + testFiles.add(key) + testSpecs.push([{ name: project }, fsPath]) + } + return testSpecs } function formatTestPattern(tests: readonly vscode.TestItem[]) { diff --git a/src/worker/vitest.ts b/src/worker/vitest.ts index 77bf0aa..52f7415 100644 --- a/src/worker/vitest.ts +++ b/src/worker/vitest.ts @@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs' import type { Vitest as VitestCore, WorkspaceProject } from 'vitest/node' import { relative } from 'pathe' import mm from 'micromatch' -import type { VitestMethods } from '../api/rpc' +import type { SerializedTestSpecification, VitestMethods } from '../api/rpc' import { VitestWatcher } from './watcher' import { VitestCoverage } from './coverage' import { assert, limitConcurrency } from './utils' @@ -57,7 +57,9 @@ export class Vitest implements VitestMethods { })(), (async () => { if (otherTests.length) { - const files = otherTests.map(([_, filepath]) => filepath) + const files = otherTests.map( + ([project, filepath]) => [{ name: project.getName() }, filepath], + ) try { await this.runTestFiles(files, Vitest.COLLECT_NAME_PATTERN) @@ -87,7 +89,7 @@ export class Vitest implements VitestMethods { this.setTestNamePattern(undefined) } - public async updateSnapshots(files?: string[] | undefined, testNamePattern?: string | undefined) { + public async updateSnapshots(files?: SerializedTestSpecification[] | string[] | undefined, testNamePattern?: string | undefined) { this.ctx.configOverride.snapshotOptions = { updateSnapshot: 'all', // environment is resolved inside a worker thread @@ -101,17 +103,23 @@ export class Vitest implements VitestMethods { } } - public async runTests(files: string[] | undefined, testNamePattern?: string) { + async resolveTestSpecs(specs: string[] | SerializedTestSpecification[] | undefined): Promise { + if (!specs || typeof specs[0] === 'string') { + const files = await this.globTestFiles(specs as string[] | undefined) + return files.map(([project, file]) => { + return [{ name: project.getName() }, file] + }) + } + return (specs || []) as SerializedTestSpecification[] + } + + public async runTests(specsOrPaths: SerializedTestSpecification[] | string[] | undefined, testNamePattern?: string) { // @ts-expect-error private method await this.ctx.initBrowserProviders() - if (testNamePattern) { - await this.runTestFiles(files || this.ctx.state.getFilepaths(), testNamePattern) - } - else { - const specs = await this.globTestFiles(files) - await this.runTestFiles(specs.map(([_, spec]) => spec), undefined, !files) - } + const specs = await this.resolveTestSpecs(specsOrPaths) + + await this.runTestFiles(specs, testNamePattern, !specs) } public cancelRun() { @@ -145,7 +153,7 @@ export class Vitest implements VitestMethods { }) } - private async runTestFiles(files: string[], testNamePattern?: string | undefined, runAllFiles = false) { + private async runTestFiles(specs: SerializedTestSpecification[], testNamePattern?: string | undefined, runAllFiles = false) { await this.ctx.runningPromise this.watcher.markRerun(false) @@ -153,20 +161,34 @@ export class Vitest implements VitestMethods { // populate cache so it can find test files if (this.debug) - await this.globTestFiles(files) + await this.globTestFiles(specs.map(f => f[1])) - await this.rerunTests(files, runAllFiles) + await this.rerunTests(specs, runAllFiles) } private setTestNamePattern(pattern: string | undefined) { this.ctx.configOverride.testNamePattern = pattern ? new RegExp(pattern) : undefined } - private async rerunTests(files: string[], runAllFiles = false) { - await this.ctx.report('onWatcherRerun', files) - await this.ctx.runFiles(files.flatMap(file => this.ctx.getProjectsByTestFile(file)), runAllFiles) + private async rerunTests(specs: SerializedTestSpecification[], runAllFiles = false) { + const paths = specs.map(spec => spec[1]) + await this.ctx.report('onWatcherRerun', paths) + + const specsToRun = specs.flatMap((spec) => { + const file = typeof spec === 'string' ? spec : spec[1] + const fileSpecs = this.ctx.getFileWorkspaceSpecs + ? this.ctx.getFileWorkspaceSpecs(file) + // supported by the older version + : this.ctx.getProjectsByTestFile(file) + if (!fileSpecs.length) { + return [] + } + return fileSpecs.filter(([project]) => project.getName() === spec[0].name) + }) + + await this.ctx.runFiles(specsToRun, runAllFiles) - await this.ctx.report('onWatcherStart', this.ctx.state.getFiles(files)) + await this.ctx.report('onWatcherStart', this.ctx.state.getFiles(paths)) } private handleFileChanged(file: string): string[] { @@ -262,9 +284,9 @@ export class Vitest implements VitestMethods { return this.watcher.stopTracking() } - watchTests(files?: string[], testNamePatern?: string) { + watchTests(files?: SerializedTestSpecification[] | string[] | undefined, testNamePatern?: string) { if (files) - this.watcher.trackTests(files, testNamePatern) + this.watcher.trackTests(files.map(f => typeof f === 'string' ? f : f[1]), testNamePatern) else this.watcher.trackEveryFile() } diff --git a/test-e2e/tester.ts b/test-e2e/tester.ts index 290330c..0cb2120 100644 --- a/test-e2e/tester.ts +++ b/test-e2e/tester.ts @@ -22,7 +22,11 @@ export class VSCodeTester { } async runAllTests() { - await this.page.getByRole('button', { name: /^Run Tests$/ }).click() + await this.page + .getByRole('toolbar', { name: 'Testing actions', exact: true }) + .getByRole('button', { name: /^Run Tests$/ }) + .first() + .click() } }