diff --git a/packages/vitest/src/runtime/collect.ts b/packages/vitest/src/runtime/collect.ts index e10caea096fd..b198758ea5f8 100644 --- a/packages/vitest/src/runtime/collect.ts +++ b/packages/vitest/src/runtime/collect.ts @@ -1,7 +1,7 @@ import { performance } from 'perf_hooks' import { createHash } from 'crypto' import { relative } from 'pathe' -import type { File, ResolvedConfig, Suite, Task } from '../types' +import type { File, ResolvedConfig, Suite } from '../types' import { clearContext, defaultSuite } from './suite' import { getHooks, setHooks } from './map' import { processError } from './error' @@ -64,37 +64,40 @@ export async function collectTests(paths: string[], config: ResolvedConfig) { } calculateHash(file) + + interpretTaskModes(file, config.testNamePattern) + files.push(file) } - interpretTaskModes(files, config.testNamePattern) - return files } /** * If any tasks been marked as `only`, mark all other tasks as `skip`. */ -function interpretTaskModes(tasks: Task[], namePattern?: string | RegExp) { - if (tasks.some(t => t.mode === 'only')) { - tasks.forEach((t) => { - if (t.mode === 'run') - t.mode = 'skip' - else if (t.mode === 'only') - t.mode = 'run' - }) - } +function interpretTaskModes(suite: Suite, namePattern?: string | RegExp, onlyMode?: boolean) { + if (onlyMode === undefined) + onlyMode = someTasksAreOnly(suite) - tasks.forEach((t) => { + suite.tasks.forEach((t) => { + if (onlyMode) { + if (t.type === 'suite' && someTasksAreOnly(t)) { + // Don't skip this suite + if (t.mode === 'only') + t.mode = 'run' + interpretTaskModes(t, namePattern, onlyMode) + } + else if (t.mode === 'run') { t.mode = 'skip' } + else if (t.mode === 'only') { t.mode = 'run' } + } if (t.type === 'test') { if (namePattern && !t.name.match(namePattern)) t.mode = 'skip' } else if (t.type === 'suite') { if (t.mode === 'skip') - t.tasks.forEach(c => c.mode === 'run' && (c.mode = 'skip')) - - interpretTaskModes(t.tasks, namePattern) + skipAllTasks(t) // if all subtasks are skipped, marked as skip if (t.mode === 'run') { @@ -105,6 +108,20 @@ function interpretTaskModes(tasks: Task[], namePattern?: string | RegExp) { }) } +function someTasksAreOnly(suite: Suite): boolean { + return suite.tasks.some(t => t.mode === 'only' || (t.type === 'suite' && someTasksAreOnly(t))) +} + +function skipAllTasks(suite: Suite) { + suite.tasks.forEach((t) => { + if (t.mode === 'run') { + t.mode = 'skip' + if (t.type === 'suite') + skipAllTasks(t) + } + }) +} + function calculateHash(parent: Suite) { parent.tasks.forEach((t, idx) => { t.id = `${parent.id}_${idx}` diff --git a/test/core/test/only.test.ts b/test/core/test/only.test.ts new file mode 100644 index 000000000000..7f702a050163 --- /dev/null +++ b/test/core/test/only.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from 'vitest' + +const run = [false, false, false, false] + +it.only('visited before', () => { + expect(run.some(Boolean)).toBe(false) +}) + +describe('a0', () => { + it.only('0', () => { + run[0] = true + }) + it('s0', () => { + expect(true).toBe(false) + }) +}) + +describe('a1', () => { + describe('b1', () => { + describe('c1', () => { + it.only('1', () => { + run[1] = true + }) + }) + it('s1', () => { + expect(true).toBe(false) + }) + }) +}) + +describe.only('a2', () => { + it('2', () => { + run[2] = true + }) +}) + +it('s2', () => { + expect(true).toBe(false) +}) + +describe.only('a3', () => { + describe('b3', () => { + it('3', () => { + run[3] = true + }) + }) + it.skip('s3', () => { + expect(true).toBe(false) + }) +}) + +describe('a4', () => { + describe.only('b4', () => { + it('4', () => { + run[4] = true + }) + }) + describe('sb4', () => { + it('s4', () => { + expect(true).toBe(false) + }) + }) +}) + +it.only('visited', () => { + expect(run.every(Boolean)).toBe(true) +})