diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index da61fcfb2838..010dba90732f 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -1,6 +1,6 @@ import { existsSync, promises as fs } from 'node:fs' import type { ViteDevServer } from 'vite' -import { basename, dirname, normalize, relative } from 'pathe' +import { basename, dirname, join, normalize, relative } from 'pathe' import fg from 'fast-glob' import mm from 'micromatch' import c from 'picocolors' @@ -151,20 +151,23 @@ export class Vitest { } private async resolveWorkspaces(options: UserConfig) { - const rootFiles = await fs.readdir(this.config.root) - const workspacesConfigPath = workspacesFiles.find((configFile) => { + const configDir = dirname(this.server.config.configFile || this.config.root) + const rootFiles = await fs.readdir(configDir) + const workspacesConfigName = workspacesFiles.find((configFile) => { return rootFiles.includes(configFile) }) - if (!workspacesConfigPath) + if (!workspacesConfigName) return [await this.createCoreWorkspace(options)] - const workspacesModule = await this.runner.executeFile(workspacesConfigPath) as { + const workspacesConcigPath = join(configDir, workspacesConfigName) + + const workspacesModule = await this.runner.executeFile(workspacesConcigPath) as { default: string[] } if (!workspacesModule.default || !Array.isArray(workspacesModule.default)) - throw new Error(`Workspaces config file ${workspacesConfigPath} must export a default array of workspace paths`) + throw new Error(`Workspaces config file ${workspacesConcigPath} must export a default array of workspace paths`) const workspacesGlobMatches = workspacesModule.default.map((workspacePath) => { return workspacePath.replace('', this.config.root) @@ -453,7 +456,7 @@ export class Vitest { } private _rerunTimer: any - private async scheduleRerun(triggerId: string) { + private async scheduleRerun(triggerId: string[]) { const currentCount = this.restartsCount clearTimeout(this._rerunTimer) await this.runningPromise @@ -492,7 +495,9 @@ export class Vitest { if (this.coverageProvider && this.config.coverage.cleanOnRerun) await this.coverageProvider.clean() - await this.report('onWatcherRerun', files, triggerId) + const triggerIds = new Set(triggerId.map(id => relative(this.config.root, id))) + const triggerLabel = Array.from(triggerIds).join(', ') + await this.report('onWatcherRerun', files, triggerLabel) await this.runFiles(files.flatMap(file => this.getWorkspacesByTestFile(file))) @@ -525,8 +530,8 @@ export class Vitest { id = slash(id) updateLastChanged(id) const needsRerun = this.handleFileChanged(id) - if (needsRerun) - this.scheduleRerun(id) + if (needsRerun.length) + this.scheduleRerun(needsRerun) } const onUnlink = (id: string) => { id = slash(id) @@ -546,7 +551,7 @@ export class Vitest { if (await this.isTargetFile(id)) { this.changedTests.add(id) await this.cache.stats.updateStats(id) - this.scheduleRerun(id) + this.scheduleRerun([id]) } } const watcher = this.server.watcher @@ -571,18 +576,20 @@ export class Vitest { /** * @returns A value indicating whether rerun is needed (changedTests was mutated) */ - private handleFileChanged(id: string): boolean { + private handleFileChanged(id: string): string[] { if (this.changedTests.has(id) || this.invalidates.has(id)) - return false + return [] if (mm.isMatch(id, this.config.forceRerunTriggers)) { this.state.getFilepaths().forEach(file => this.changedTests.add(file)) - return true + return [] } const workspaces = this.getModuleWorkspaces(id) if (!workspaces.length) - return false + return [] + + const files: string[] = [] for (const { server, browser } of workspaces) { const mod = server.moduleGraph.getModuleById(id) || browser?.moduleGraph.getModuleById(id) @@ -590,13 +597,15 @@ export class Vitest { // files with `?v=` query from the browser const mods = browser?.moduleGraph.getModulesByFile(id) if (!mods?.size) - return false + return [] let rerun = false mods.forEach((m) => { if (m.id && this.handleFileChanged(m.id)) rerun = true }) - return rerun + if (rerun) + files.push(id) + continue } // remove queries from id @@ -606,7 +615,8 @@ export class Vitest { if (this.state.filesMap.has(id)) { this.changedTests.add(id) - return true + files.push(id) + continue } let rerun = false @@ -620,10 +630,10 @@ export class Vitest { }) if (rerun) - return rerun + files.push(id) } - return false + return files } private async reportCoverage(allTestsRun: boolean) { diff --git a/packages/vitest/src/node/reporters/base.ts b/packages/vitest/src/node/reporters/base.ts index 7ffb28d24ca7..7478d3977c29 100644 --- a/packages/vitest/src/node/reporters/base.ts +++ b/packages/vitest/src/node/reporters/base.ts @@ -80,7 +80,11 @@ export abstract class BaseReporter implements Reporter { if (this.ctx.config.logHeapUsage && task.result.heap != null) suffix += c.magenta(` ${Math.floor(task.result.heap / 1024 / 1024)} MB heap used`) - logger.log(` ${getStateSymbol(task)} ${task.name} ${suffix}`) + let title = ` ${getStateSymbol(task)} ` + if (task.projectName) + title += formatProjectName(task.projectName) + title += `${task.name} ${suffix}` + logger.log(title) // print short errors, full errors will be at the end in summary for (const test of failed) { diff --git a/packages/vitest/src/node/reporters/verbose.ts b/packages/vitest/src/node/reporters/verbose.ts index 9791cbc0175b..dd17b8d4b0f2 100644 --- a/packages/vitest/src/node/reporters/verbose.ts +++ b/packages/vitest/src/node/reporters/verbose.ts @@ -3,7 +3,7 @@ import type { TaskResultPack } from '../../types' import { getFullName } from '../../utils' import { F_RIGHT } from '../../utils/figures' import { DefaultReporter } from './default' -import { getStateSymbol } from './renderers/utils' +import { formatProjectName, getStateSymbol } from './renderers/utils' export class VerboseReporter extends DefaultReporter { constructor() { @@ -17,7 +17,10 @@ export class VerboseReporter extends DefaultReporter { for (const pack of packs) { const task = this.ctx.state.idMap.get(pack[0]) if (task && task.type === 'test' && task.result?.state && task.result?.state !== 'run') { - let title = ` ${getStateSymbol(task)} ${getFullName(task, c.dim(' > '))}` + let title = ` ${getStateSymbol(task)} ` + if (task.suite?.projectName) + title += formatProjectName(task.suite.projectName) + title += getFullName(task, c.dim(' > ')) if (this.ctx.config.logHeapUsage && task.result.heap != null) title += c.magenta(` ${Math.floor(task.result.heap / 1024 / 1024)} MB heap used`) this.ctx.logger.log(title) diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index 73e0ceb17ef4..6a60a44a7388 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -66,8 +66,8 @@ export class VitestWorkspace { public ctx: Vitest, ) { } - getName() { - return this.config.name + getName(): string { + return this.config.name || dirname(this.path).split('/').pop() || '' } isCore() { @@ -228,7 +228,7 @@ export class VitestWorkspace { }, }, snapshotOptions: { - ...this.ctx.config.snapshotOptions, + ...this.config.snapshotOptions, resolveSnapshotPath: undefined, }, onConsoleLog: undefined!, diff --git a/test/watch/test/file-watching.test.ts b/test/watch/test/file-watching.test.ts index 5c173524d2e6..1d01e0ac2330 100644 --- a/test/watch/test/file-watching.test.ts +++ b/test/watch/test/file-watching.test.ts @@ -31,7 +31,7 @@ test('editing source file triggers re-run', async () => { writeFileSync(sourceFile, editFile(sourceFileContent), 'utf8') await vitest.waitForOutput('New code running') - await vitest.waitForOutput('RERUN math.ts') + await vitest.waitForOutput('RERUN ../math.ts') await vitest.waitForOutput('1 passed') }) @@ -41,7 +41,7 @@ test('editing test file triggers re-run', async () => { writeFileSync(testFile, editFile(testFileContent), 'utf8') await vitest.waitForOutput('New code running') - await vitest.waitForOutput('RERUN math.test.ts') + await vitest.waitForOutput('RERUN ../math.test.ts') await vitest.waitForOutput('1 passed') }) @@ -71,7 +71,7 @@ describe('browser', () => { writeFileSync(sourceFile, editFile(sourceFileContent), 'utf8') await vitest.waitForOutput('New code running') - await vitest.waitForOutput('RERUN math.ts') + await vitest.waitForOutput('RERUN ../math.ts') await vitest.waitForOutput('1 passed') vitest.write('q') diff --git a/test/watch/test/utils.ts b/test/watch/test/utils.ts index 1125742e78a5..4f3b12954daf 100644 --- a/test/watch/test/utils.ts +++ b/test/watch/test/utils.ts @@ -1,9 +1,12 @@ import { afterEach } from 'vitest' -import { execa } from 'execa' +import { type Options, execa } from 'execa' import stripAnsi from 'strip-ansi' -export async function startWatchMode(...args: string[]) { - const subprocess = execa('vitest', ['--root', 'fixtures', ...args]) +export async function startWatchMode(options?: Options | string, ...args: string[]) { + if (typeof options === 'string') + args.unshift(options) + const argsWithRoot = args.includes('--root') ? args : ['--root', 'fixtures', ...args] + const subprocess = execa('vitest', argsWithRoot, typeof options === 'string' ? undefined : options) let setDone: (value?: unknown) => void const isDone = new Promise(resolve => (setDone = resolve)) diff --git a/test/watch/test/workspaces.test.ts b/test/watch/test/workspaces.test.ts new file mode 100644 index 000000000000..6870cdcce395 --- /dev/null +++ b/test/watch/test/workspaces.test.ts @@ -0,0 +1,66 @@ +import { fileURLToPath } from 'node:url' +import { readFileSync, writeFileSync } from 'node:fs' +import { afterAll, it } from 'vitest' +import { dirname, resolve } from 'pathe' +import { startWatchMode } from './utils' + +const file = fileURLToPath(import.meta.url) +const dir = dirname(file) +const root = resolve(dir, '..', '..', 'workspaces') +const config = resolve(root, 'vitest.config.ts') + +const srcMathFile = resolve(root, 'src', 'math.ts') +const specSpace2File = resolve(root, 'space_2', 'test', 'node.spec.ts') + +const srcMathContent = readFileSync(srcMathFile, 'utf-8') +const specSpace2Content = readFileSync(specSpace2File, 'utf-8') + +function startVitest() { + return startWatchMode( + { cwd: root, env: { TEST_WATCH: 'true' } }, + '--root', + root, + '--config', + config, + '--no-coverage', + ) +} + +afterAll(() => { + writeFileSync(srcMathFile, srcMathContent, 'utf8') + writeFileSync(specSpace2File, specSpace2Content, 'utf8') +}) + +it('editing a test file in a suite with workspaces reruns test', async () => { + const vitest = await startVitest() + + writeFileSync(specSpace2File, `${specSpace2Content}\n`, 'utf8') + + await vitest.waitForOutput('RERUN space_2/test/node.spec.ts x1') + await vitest.waitForOutput('|space_2| test/node.spec.ts') + await vitest.waitForOutput('Test Files 1 passed') +}) + +it('editing a file that is imported in different workspaces reruns both files', async () => { + const vitest = await startVitest() + + writeFileSync(srcMathFile, `${srcMathContent}\n`, 'utf8') + + await vitest.waitForOutput('RERUN src/math.ts') + await vitest.waitForOutput('|space_3| math.space-test.ts') + await vitest.waitForOutput('|space_1| test/math.spec.ts') + await vitest.waitForOutput('Test Files 2 passed') +}) + +it('filters by test name inside a workspace', async () => { + const vitest = await startVitest() + + vitest.write('t') + + await vitest.waitForOutput('Input test name pattern') + + vitest.write('2 x 2 = 4\n') + + await vitest.waitForOutput('Test name pattern: /2 x 2 = 4/') + await vitest.waitForOutput('Test Files 1 passed') +}) diff --git a/test/watch/vitest.config.ts b/test/watch/vitest.config.ts index c689a4b3316c..48120e0f368d 100644 --- a/test/watch/vitest.config.ts +++ b/test/watch/vitest.config.ts @@ -6,7 +6,7 @@ export default defineConfig({ include: ['test/**/*.test.*'], // For Windows CI mostly - testTimeout: process.env.CI ? 30_000 : 5_000, + testTimeout: process.env.CI ? 30_000 : 10_000, // Test cases may have side effects, e.g. files under fixtures/ are modified on the fly to trigger file watchers singleThread: true, diff --git a/test/workspaces/space_1/test/math.spec.ts b/test/workspaces/space_1/test/math.spec.ts new file mode 100644 index 000000000000..89170fcbbddd --- /dev/null +++ b/test/workspaces/space_1/test/math.spec.ts @@ -0,0 +1,6 @@ +import { expect, test } from 'vitest' +import { sum } from '../../src/math' + +test('3 + 3 = 6', () => { + expect(sum(3, 3)).toBe(6) +}) diff --git a/test/workspaces/space_3/math.space-test.ts b/test/workspaces/space_3/math.space-test.ts index 857b5a85ce4f..0036634657d8 100644 --- a/test/workspaces/space_3/math.space-test.ts +++ b/test/workspaces/space_3/math.space-test.ts @@ -1,6 +1,11 @@ import { expect, test } from 'vitest' +import { sum } from '../src/math' import { multiple } from './src/multiply' test('2 x 2 = 4', () => { expect(multiple(2, 2)).toBe(4) }) + +test('2 + 2 = 4', () => { + expect(sum(2, 2)).toBe(4) +}) diff --git a/test/workspaces/space_shared/setup.jsdom.ts b/test/workspaces/space_shared/setup.jsdom.ts new file mode 100644 index 000000000000..c3ac6eeb2dc9 --- /dev/null +++ b/test/workspaces/space_shared/setup.jsdom.ts @@ -0,0 +1 @@ +Object.defineProperty(global, 'testValue', { value: 'jsdom' }) diff --git a/test/workspaces/space_shared/setup.node.ts b/test/workspaces/space_shared/setup.node.ts new file mode 100644 index 000000000000..5250360056cb --- /dev/null +++ b/test/workspaces/space_shared/setup.node.ts @@ -0,0 +1 @@ +Object.defineProperty(global, 'testValue', { value: 'node' }) diff --git a/test/workspaces/space_shared/test.spec.ts b/test/workspaces/space_shared/test.spec.ts new file mode 100644 index 000000000000..aa826563be48 --- /dev/null +++ b/test/workspaces/space_shared/test.spec.ts @@ -0,0 +1,9 @@ +import { expect, it } from 'vitest' + +declare global { + const testValue: string +} + +it('the same file works with different workspaces', () => { + expect(testValue).toBe(expect.getState().environment === 'node' ? 'node' : 'jsdom') +}) diff --git a/test/workspaces/src/math.ts b/test/workspaces/src/math.ts new file mode 100644 index 000000000000..5d8550bb9d4d --- /dev/null +++ b/test/workspaces/src/math.ts @@ -0,0 +1,3 @@ +export function sum(a: number, b: number) { + return a + b +} diff --git a/test/workspaces/vitest.config.ts b/test/workspaces/vitest.config.ts index 490da38490a0..057b449b3a3a 100644 --- a/test/workspaces/vitest.config.ts +++ b/test/workspaces/vitest.config.ts @@ -1,5 +1,11 @@ import { defineConfig } from 'vitest/config' +if (process.env.TEST_WATCH) { + // Patch stdin on the process so that we can fake it to seem like a real interactive terminal and pass the TTY checks + process.stdin.isTTY = true + process.stdin.setRawMode = () => process.stdin +} + export default defineConfig({ test: { coverage: {