···2424 expect(true).toBe(true)
2525})
2626```
2727+2828+The pattern is matched against the test's full name: the enclosing suite names and the test name joined with `' > '` (the same string shown in the reporter output). For example, the test below has the full name `math > adds`, so it is matched by `-t 'math > adds'` or `-t adds`:
2929+3030+```js
3131+import { describe, expect, test } from 'vitest'
3232+3333+describe('math', () => {
3434+ test('adds', () => {
3535+ expect(1 + 1).toBe(2)
3636+ })
3737+})
3838+```
3939+4040+::: warning
4141+Before Vitest 5, the segments were joined with a single space (`math adds`) to mirror Jest. See the [migration guide](/guide/migration#vitest-5) for details.
4242+:::
+1-1
docs/guide/filtering.md
···44444545## Filtering by Test Name
46464747-Sometimes the test you care about is buried in a file with many other tests. The `-t` (or `--testNamePattern`) option filters by the test's name rather than the filename. It accepts a regex pattern and matches against the full test name, which includes any `describe` block names:
4747+Sometimes the test you care about is buried in a file with many other tests. The `-t` (or `--testNamePattern`) option filters by the test's name rather than the filename. It accepts a regex pattern and matches against the full test name, which is the enclosing `describe` block names and the test name joined with `' > '` (for example `math > adds`):
48484949```bash
5050vitest -t "handles empty input"
+26
docs/guide/migration.md
···5252})
5353```
54545555+### `testNamePattern` Matches the `>`-Joined Full Name
5656+5757+[`testNamePattern`](/config/testnamepattern) (the `-t` CLI flag) now matches against the test's full name with the suite chain and test name joined by `' > '`, the same string shown in the reporter output. Previously the segments were joined with a single space, mirroring Jest.
5858+5959+This only affects patterns that span the boundary between a suite and a test (or between nested suites). Patterns that match within a single name segment, and patterns that use `.`/`.*` between segments, are unaffected.
6060+6161+```ts
6262+describe('math', () => {
6363+ test('adds', () => {})
6464+})
6565+```
6666+6767+```bash
6868+vitest -t 'math adds' # [!code --]
6969+vitest -t 'math > adds' # [!code ++]
7070+```
7171+7272+To keep a pattern working regardless of the separator, match a single segment (`-t adds`) or use a wildcard between segments (`-t 'math.*adds'`).
7373+5574### Benchmarking API Rewrite
56755776The benchmarking API has been rewritten. `bench` is no longer a top-level import from `vitest`; it is a [test-context fixture](/guide/test-context#bench) accessed from inside a regular `test()`. See the [Benchmarking guide](/guide/benchmarking) for the new API.
···836855```diff
837856- `${describeTitle} ${testTitle}`
838857+ `${describeTitle} > ${testTitle}`
858858+```
859859+860860+The same applies to [`testNamePattern`](/config/testnamepattern) (the `-t` flag): Vitest matches against the `>`-joined full name, while Jest matches the space-joined name. Update patterns that span a suite and a test accordingly, or match a single segment (`-t adds`) or use a wildcard between segments (`-t 'math.*adds'`).
861861+862862+```diff
863863+- vitest -t 'math adds'
864864++ vitest -t 'math > adds'
839865```
840866841867### Envs
+17
packages/vitest/src/node/ast-collect.ts
···459459 }
460460 definition.task = task
461461 latestSuite.tasks.push(task)
462462+ if (mode === 'only') {
463463+ markAncestorsContainOnly(latestSuite)
464464+ }
462465 lastSuite = task
463466 return
464467 }
···488491 }
489492 definition.task = task
490493 latestSuite.tasks.push(task)
494494+ if (mode === 'only') {
495495+ markAncestorsContainOnly(latestSuite)
496496+ }
491497 })
492498 calculateSuiteHash(file)
493499 markDynamicTests(file.tasks)
···567573 const transformResult = await env.transformRequest(filepath)
568574569575 return transformResult ? { ...transformResult, fileTags } : null
576576+}
577577+578578+// Walk up from the suite a task was added to, marking each ancestor as
579579+// containing an `only` task. Stops at the first already-marked ancestor (its
580580+// own ancestors are already marked), keeping the total work linear.
581581+function markAncestorsContainOnly(suite: Suite) {
582582+ let current: Suite | undefined = suite
583583+ while (current && !current.containsOnly) {
584584+ current.containsOnly = true
585585+ current = current.suite
586586+ }
570587}
571588572589function markDynamicTests(tasks: Task[]) {
···289289 * An array of tasks that are part of the suite.
290290 */
291291 tasks: Task[]
292292+ /**
293293+ * Whether this suite's subtree contains a task in `only` mode. Computed during
294294+ * collection and consumed when resolving test modes (`interpretTaskModes`).
295295+ * @internal
296296+ */
297297+ containsOnly?: boolean
298298+ /**
299299+ * Whether this suite's subtree contains at least one test. Computed during
300300+ * collection and consumed to detect empty suites at run time.
301301+ * @internal
302302+ */
303303+ containsTest?: boolean
292304}
293305294306export interface File extends Suite {
+6-32
packages/vitest/src/utils/tasks.ts
···5252}
53535454/* @__NO_SIDE_EFFECTS__ */
5555-export function hasTests(suite: Arrayable<Suite>): boolean {
5656- return toArray(suite).some(s =>
5757- s.tasks.some(c => isTestCase(c) || hasTests(c)),
5858- )
5959-}
6060-6161-/* @__NO_SIDE_EFFECTS__ */
6255export function hasFailed(suite: Arrayable<Task>): boolean {
6356 return toArray(suite).some(
6457 s =>
···9891/* @__NO_SIDE_EFFECTS__ */
9992export function createTaskName(names: readonly (string | undefined)[], separator = ' > '): string {
10093 return names.filter(name => name !== undefined).join(separator)
101101-}
102102-103103-/* @__NO_SIDE_EFFECTS__ */
104104-export function hasBenchmark(suite: Arrayable<Suite>): boolean {
105105- return toArray(suite).some(s =>
106106- s?.tasks?.some(c => c.meta?.benchmark || hasBenchmark(c as Suite)),
107107- )
10894}
1099511096/* @__NO_SIDE_EFFECTS__ */
···261247 const traverseSuite = (suite: Suite, parentIsOnly?: boolean, parentMatchedWithLocation?: boolean) => {
262248 const suiteIsOnly = parentIsOnly || suite.mode === 'only'
263249264264- // Check if any tasks in this suite have `.only` - if so, only those should run
265265- const hasSomeTasksOnly = onlyMode && suite.tasks.some(
266266- t => t.mode === 'only' || (t.type === 'suite' && someTasksAreOnly(t)),
267267- )
250250+ // Check if any tasks in this suite have `.only` - if so, only those should run.
251251+ // `containsOnly` is computed during collection (in the runtime/AST collectors).
252252+ const hasSomeTasksOnly = !!(onlyMode && suite.containsOnly)
268253269254 suite.tasks.forEach((t) => {
270255 // Check if either the parent suite or the task itself are marked as included
271256 // If there are tasks with `.only` in this suite, only include those (not all tasks from describe.only)
272257 const includeTask = hasSomeTasksOnly
273273- ? (t.mode === 'only' || (t.type === 'suite' && someTasksAreOnly(t)))
258258+ ? (t.mode === 'only' || (t.type === 'suite' && !!t.containsOnly))
274259 : (suiteIsOnly || t.mode === 'only')
275260 if (onlyMode) {
276276- if (t.type === 'suite' && (includeTask || someTasksAreOnly(t))) {
261261+ if (t.type === 'suite' && (includeTask || t.containsOnly)) {
277262 // Don't skip this suite
278263 if (t.mode === 'only') {
279264 checkAllowOnly(t, allowOnly)
···308293 }
309294310295 if (t.type === 'test') {
311311- if (namePattern && !getTaskFullName(t).match(namePattern)) {
296296+ if (namePattern && !t.fullTestName.match(namePattern)) {
312297 t.mode = 'skip'
313298 }
314299 if (testIdsSet && !testIdsSet.has(t.id)) {
···364349 stack: error.stack,
365350 })
366351 }
367367-}
368368-369369-function getTaskFullName(task: TaskBase): string {
370370- return `${task.suite ? `${getTaskFullName(task.suite)} ` : ''}${task.name}`
371371-}
372372-373373-/* @__NO_SIDE_EFFECTS__ */
374374-export function someTasksAreOnly(suite: Suite): boolean {
375375- return suite.tasks.some(
376376- t => t.mode === 'only' || (t.type === 'suite' && someTasksAreOnly(t)),
377377- )
378352}
379353380354function skipAllTasks(suite: Suite) {
+51
test/e2e/test/test-name-pattern.test.ts
···11+import { expect, test } from 'vitest'
22+import { runInlineTests } from '../../test-utils'
33+44+// `testNamePattern` matches against the task's `fullTestName`, i.e. the suite
55+// chain and the test name joined with ` > ` (the same string shown in reporters).
66+// Before Vitest 4 the segments were joined with a single space, mirroring Jest.
77+const files = {
88+ 'a.test.js': `
99+ import { describe, test } from 'vitest'
1010+ describe('group', () => {
1111+ test('matches', () => {})
1212+ })
1313+ test('group matches', () => {})
1414+ `,
1515+}
1616+1717+test('testNamePattern matches the " > "-joined full name', async () => {
1818+ const { buildTree, stderr } = await runInlineTests(files, {
1919+ testNamePattern: 'group > matches',
2020+ })
2121+2222+ expect(stderr).toBe('')
2323+ expect(buildTree(t => t.result().state)).toMatchInlineSnapshot(`
2424+ {
2525+ "a.test.js": {
2626+ "group": {
2727+ "matches": "passed",
2828+ },
2929+ "group matches": "skipped",
3030+ },
3131+ }
3232+ `)
3333+})
3434+3535+test('testNamePattern no longer matches the space-joined chain across suites', async () => {
3636+ const { buildTree, stderr } = await runInlineTests(files, {
3737+ testNamePattern: 'group matches',
3838+ })
3939+4040+ expect(stderr).toBe('')
4141+ expect(buildTree(t => t.result().state)).toMatchInlineSnapshot(`
4242+ {
4343+ "a.test.js": {
4444+ "group": {
4545+ "matches": "skipped",
4646+ },
4747+ "group matches": "passed",
4848+ },
4949+ }
5050+ `)
5151+})