Skip to content

Commit d523a15

Browse files
authored
fix: set skip in skipIf, ignore undefined options (#11591)
1 parent 3a8289a commit d523a15

6 files changed

Lines changed: 140 additions & 15 deletions

File tree

‎docs/api/test.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ test('skipped concurrent test', { skip: true, concurrent: true }, () => {
7575

7676
They both work in exactly the same way. To use either one is purely a stylistic choice.
7777

78+
Options set to `undefined` are ignored. They don't override values from the chain, the parent suite, or [tags](/guide/test-tags).
79+
7880
### timeout
7981

8082
- **Type:** `number`
@@ -372,11 +374,13 @@ test.skipIf(isDev)('prod only test', () => {
372374
})
373375
```
374376

377+
Unlike `test.skip`, `test.skipIf` always sets the [`skip`](#skip) option: `true` if the condition is truthy, `false` otherwise. A falsy condition overrides `skip` inherited from [tags](/guide/test-tags), the same as `test(name, { skip: false })`.
378+
375379
## test.runIf
376380

377381
- **Alias:** `it.runIf`
378382

379-
Opposite of [test.skipIf](#test-skipif).
383+
Opposite of [test.skipIf](#test-skipif). It always sets the [`skip`](#skip) option: `false` if the condition is truthy, `true` otherwise.
380384

381385
```ts
382386
import { assert, test } from 'vitest'

‎docs/guide/test-tags.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,13 @@ test('flaky database test', { tags: ['flaky', 'db'], timeout: 120_000 })
138138
// { timeout: 120_000, retry: 3 }
139139
```
140140

141+
Options set to `undefined` are ignored, so they don't override the values from tags:
142+
143+
```ts
144+
test('flaky database test', { tags: ['flaky', 'db'], timeout: undefined })
145+
// { timeout: 30_000, retry: 3 }
146+
```
147+
141148
## Using Tags in Tests
142149

143150
You can apply tags to individual tests or entire suites using the `tags` option:

‎packages/vitest/src/runtime/runner/suite.ts‎

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,15 @@ export function createSuiteHooks(): SuiteHooks {
250250

251251
const POSITIVE_INFINITY = Number.POSITIVE_INFINITY
252252

253+
function assignDefined<T extends object>(target: T, source: T): T {
254+
for (const key in source) {
255+
if (source[key] !== undefined) {
256+
target[key] = source[key]
257+
}
258+
}
259+
return target
260+
}
261+
253262
function parseArguments<T extends (...args: any[]) => any>(
254263
optionsOrFn: T | object | undefined,
255264
timeoutOrTest: T | number | undefined,
@@ -322,7 +331,7 @@ function createSuiteCollector(
322331
)
323332
.reduce((acc, tag) => {
324333
const { name, description, priority, meta, ...options } = tag
325-
Object.assign(acc, options)
334+
assignDefined(acc, options)
326335
if (meta) {
327336
acc.meta = Object.assign(acc.meta ?? Object.create(null), meta)
328337
}
@@ -331,11 +340,7 @@ function createSuiteCollector(
331340

332341
const testOwnMeta = options.meta
333342
const parentOptions = collectorContext.currentSuite?.options
334-
options = {
335-
...parentOptions,
336-
...tagsOptions,
337-
...options,
338-
}
343+
options = assignDefined({ ...parentOptions, ...tagsOptions }, options)
339344
const timeout = options.timeout ?? runner.config.testTimeout
340345
// TODO: should this be `parentTask.meta`?
341346
// currently we don't inherit
@@ -449,8 +454,7 @@ function createSuiteCollector(
449454
}
450455

451456
const test = task(formatName(name), {
452-
...this,
453-
...options,
457+
...assignDefined({ ...this }, options),
454458
handler,
455459
}) as unknown as Test
456460

@@ -605,10 +609,7 @@ function createSuite() {
605609

606610
const { meta: parentMeta, ...parentOptions } = currentSuite?.options || {}
607611
// inherit options from current suite
608-
options = {
609-
...parentOptions,
610-
...options,
611-
}
612+
options = assignDefined({ ...parentOptions }, options)
612613

613614
const shuffle =
614615
this.shuffle ??
@@ -818,10 +819,10 @@ export function createTaskCollector(fn: (...args: any[]) => any): TestAPI {
818819
}
819820

820821
taskFn.skipIf = function (this: TestAPI, condition: any) {
821-
return condition ? this.skip : this
822+
return getChainableContext(this).extendContext({ skip: !!condition })
822823
}
823824
taskFn.runIf = function (this: TestAPI, condition: any) {
824-
return condition ? this : this.skip
825+
return getChainableContext(this).extendContext({ skip: !condition })
825826
}
826827

827828
/**

‎packages/vitest/src/runtime/runner/types.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,8 @@ export interface InternalChainableContext<API = TestAPI> {
526526
/** @internal */
527527
withContext: () => API
528528
/** @internal */
529+
extendContext: (ctx: Partial<InternalTestContext>) => API
530+
/** @internal */
529531
getFixtures: () => TestFixtures
530532
}
531533

‎packages/vitest/src/runtime/runner/utils/chain.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export function createChainable<T extends string, Args extends any[], R = any>(
3535
Object.defineProperty(chain, kChainableContext, {
3636
value: {
3737
withContext: () => chain.bind(context) as any,
38+
extendContext: (ctx) => create({ ...context, ...ctx }) as any,
3839
getFixtures: () => (context as any).fixtures,
3940
setContext: (key, value) => {
4041
context[key as T] = value

‎test/e2e/test/test-tags.test.ts‎

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,116 @@ test('custom options override tag options', async () => {
467467
`)
468468
})
469469

470+
test('skipIf and runIf override skip from tags like the skip option', async () => {
471+
const { stderr, testTree } = await runInlineTests({
472+
'basic.test.js': `
473+
describe('skipped by tag', { tags: ['skipped'] }, () => {
474+
test('no options', () => {})
475+
test('skip: false', { skip: false }, () => {})
476+
test('skip: undefined', { skip: undefined }, () => {})
477+
test.skipIf(false)('skipIf(false)', () => {})
478+
test.skipIf(undefined)('skipIf(undefined)', () => {})
479+
test.skipIf(true)('skipIf(true)', () => {})
480+
test.runIf(true)('runIf(true)', () => {})
481+
test.runIf(false)('runIf(false)', () => {})
482+
})
483+
`,
484+
'vitest.config.js': {
485+
test: {
486+
globals: true,
487+
tags: [{ name: 'skipped', skip: true }],
488+
},
489+
},
490+
})
491+
expect(stderr).toBe('')
492+
expect(testTree()).toMatchInlineSnapshot(`
493+
{
494+
"basic.test.js": {
495+
"skipped by tag": {
496+
"no options": "skipped",
497+
"runIf(false)": "skipped",
498+
"runIf(true)": "passed",
499+
"skip: false": "passed",
500+
"skip: undefined": "skipped",
501+
"skipIf(false)": "passed",
502+
"skipIf(true)": "skipped",
503+
"skipIf(undefined)": "passed",
504+
},
505+
},
506+
}
507+
`)
508+
})
509+
510+
test('undefined options do not override inherited options', async () => {
511+
const { stderr, buildTree } = await runInlineTests({
512+
'basic.test.js': `
513+
test.skip('test.skip', { skip: undefined }, () => {})
514+
test('tag timeout', { tags: ['slow'], timeout: undefined }, () => {})
515+
test('tag with undefined timeout', { tags: ['slow', 'ci'] }, () => {})
516+
describe('suite', { timeout: 30_000, concurrent: true }, () => {
517+
test('test', { timeout: undefined, concurrent: undefined }, () => {})
518+
describe('nested suite', { timeout: undefined }, () => {
519+
test('test', () => {})
520+
})
521+
})
522+
`,
523+
'vitest.config.js': /* js */ `
524+
export default {
525+
test: {
526+
globals: true,
527+
tags: [
528+
{ name: 'slow', timeout: 60_000 },
529+
{ name: 'ci', timeout: undefined, priority: 1 },
530+
],
531+
},
532+
}
533+
`,
534+
})
535+
expect(stderr).toBe('')
536+
expect(buildOptionsTree(buildTree)).toMatchInlineSnapshot(`
537+
{
538+
"basic.test.js": {
539+
"suite": {
540+
"nested suite": {
541+
"test": {
542+
"concurrent": true,
543+
"mode": "run",
544+
"tags": [],
545+
"timeout": 30000,
546+
},
547+
},
548+
"test": {
549+
"concurrent": true,
550+
"mode": "run",
551+
"tags": [],
552+
"timeout": 30000,
553+
},
554+
},
555+
"tag timeout": {
556+
"mode": "run",
557+
"tags": [
558+
"slow",
559+
],
560+
"timeout": 60000,
561+
},
562+
"tag with undefined timeout": {
563+
"mode": "run",
564+
"tags": [
565+
"slow",
566+
"ci",
567+
],
568+
"timeout": 60000,
569+
},
570+
"test.skip": {
571+
"mode": "skip",
572+
"tags": [],
573+
"timeout": 5000,
574+
},
575+
},
576+
}
577+
`)
578+
})
579+
470580
test('strictFlag: false does not throw an error if test has an undefined tag', async () => {
471581
const { stderr } = await runInlineTests({
472582
'basic.test.js': `

0 commit comments

Comments
 (0)