-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(runner): add mergeTests utility to compose TestAPI fixtures #9662
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d189936
0277013
6446b39
7652696
adf5044
2f95529
fa60ae8
c6e3c77
02a34e9
0fbe859
2672a0e
a131cf9
58c9f5b
8ecefd9
d71197a
ddf97bf
c39a69e
a2f1534
bb33ec0
000fec2
1ad69af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ export { | |
| describe, | ||
| getCurrentSuite, | ||
| it, | ||
| mergeTests, | ||
| suite, | ||
| test, | ||
| } from './suite' | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -220,7 +220,7 @@ function createDefaultSuite(runner: VitestRunner) { | |
| if (config.concurrent != null) { | ||
| options.concurrent = config.concurrent | ||
| } | ||
| const collector = suite('', options, () => {}) | ||
| const collector = suite('', options, () => { }) | ||
| // no parent suite for top-level tests | ||
| delete collector.suite | ||
| return collector | ||
|
|
@@ -302,7 +302,7 @@ function parseArguments<T extends (...args: any[]) => any>( | |
| // implementations | ||
| function createSuiteCollector( | ||
| name: string, | ||
| factory: SuiteFactory = () => {}, | ||
| factory: SuiteFactory = () => { }, | ||
| mode: RunMode, | ||
| each?: boolean, | ||
| suiteOptions?: SuiteOptions, | ||
|
|
@@ -1092,3 +1092,46 @@ function formatTemplateString(cases: any[], args: any[]): any[] { | |
| } | ||
| return res | ||
| } | ||
|
|
||
| /** | ||
| * Merges multiple test instances into a single test instance. | ||
| * | ||
| * This is equivalent to calling `.extend()` repeatedly with the fixtures from each test. | ||
| * All fixtures from the provided tests will be available in the returned test. | ||
| * If multiple tests define the same fixture name, the one from the later test wins. | ||
| * | ||
| * @example | ||
| * const test = mergeTests(dbTest, serverTest, uiTest) | ||
| */ | ||
| export function mergeTests<A>(a: TestAPI<A>): TestAPI<A> | ||
| export function mergeTests<A, B>(a: TestAPI<A>, b: TestAPI<B>): TestAPI<A & B> | ||
| export function mergeTests<A, B, C>(a: TestAPI<A>, b: TestAPI<B>, c: TestAPI<C>): TestAPI<A & B & C> | ||
| export function mergeTests<A, B, C, D>(a: TestAPI<A>, b: TestAPI<B>, c: TestAPI<C>, d: TestAPI<D>): TestAPI<A & B & C & D> | ||
| export function mergeTests<A, B, C, D, E>(a: TestAPI<A>, b: TestAPI<B>, c: TestAPI<C>, d: TestAPI<D>, e: TestAPI<E>): TestAPI<A & B & C & D & E> | ||
| export function mergeTests<A, B, C, D, E, F>(a: TestAPI<A>, b: TestAPI<B>, c: TestAPI<C>, d: TestAPI<D>, e: TestAPI<E>, f: TestAPI<F>): TestAPI<A & B & C & D & E & F> | ||
| export function mergeTests(...tests: TestAPI<any>[]): TestAPI<any> { | ||
| if (tests.length === 0) { | ||
| throw new TypeError('mergeTests requires at least one test') | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not tested |
||
| } | ||
|
|
||
| // Use the first test as the base | ||
| let [currentTest, ...rest] = tests | ||
|
|
||
| for (const nextTest of rest) { | ||
| const nextContext = getChainableContext(nextTest) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not tested |
||
| if (!nextContext || typeof nextContext.getFixtures !== 'function') { | ||
| throw new TypeError('mergeTests requires extended test instances created via test.extend()') | ||
| } | ||
|
|
||
| // Extract fixtures from the next test and extend the current test | ||
| // This behaves exactly like currentTest.extend(nextFixtures) | ||
| const currentContext = getChainableContext(currentTest) | ||
| if (!currentContext) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not tested |
||
| throw new TypeError('Cannot merge tests: base test is not a valid test instance') | ||
| } | ||
| const fixtures = nextContext.getFixtures() | ||
| currentTest = currentTest.extend(fixtures as any) | ||
| } | ||
|
|
||
| return currentTest | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import { expectTypeOf, mergeTests, test } from 'vitest' | ||
|
|
||
| const testA = test.extend({ | ||
| a: 1, | ||
| }) | ||
|
|
||
| const testB = test.extend({ | ||
| b: 2, | ||
| }) | ||
|
|
||
| const merged = mergeTests(testA, testB) | ||
|
|
||
| merged('types', ({ a, b }) => { | ||
| expectTypeOf(a).not.toBeAny() | ||
| expectTypeOf(b).not.toBeAny() | ||
| expectTypeOf(a).toEqualTypeOf<number>() | ||
| expectTypeOf(b).toEqualTypeOf<number>() | ||
| }) | ||
|
|
||
| const testC = test.extend({ | ||
| c: 'string', | ||
| }) | ||
|
|
||
| const merged3 = mergeTests(merged, testC) | ||
|
|
||
| merged3('chained merge types', ({ a, b, c }) => { | ||
| expectTypeOf(a).toBeNumber() | ||
| expectTypeOf(b).toBeNumber() | ||
| expectTypeOf(c).toBeString() | ||
| }) | ||
|
|
||
| const testBool = test.extend({ | ||
| d: true, | ||
| }) | ||
|
|
||
| const mergedVariadic = mergeTests(testA, testB, testBool) | ||
|
|
||
| mergedVariadic('variadic types', ({ a, b, d }) => { | ||
| expectTypeOf(a).toBeNumber() | ||
| expectTypeOf(b).toBeNumber() | ||
| expectTypeOf(d).toBeBoolean() | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| import { describe, expect, mergeTests, test } from 'vitest' | ||
|
|
||
| describe('mergeTests', () => { | ||
| const testA = test.extend({ | ||
| a: 1, | ||
| }) | ||
| const testB = test.extend({ | ||
| b: 2, | ||
| }) | ||
| const merged = mergeTests(testA, testB) | ||
|
|
||
| merged('merges fixtures from two tests', ({ a, b }) => { | ||
| expect(a).toBe(1) | ||
| expect(b).toBe(2) | ||
| }) | ||
|
|
||
| describe('nested describe', () => { | ||
| const testC = test.extend({ | ||
| a: 2, | ||
| }) | ||
| const mergedOverride = mergeTests(testA, testC) | ||
|
|
||
| mergedOverride('overrides fixtures', ({ a }) => { | ||
| expect(a).toBe(2) | ||
| }) | ||
| }) | ||
|
|
||
| const mergedReverse = mergeTests(testB, testA) | ||
|
|
||
| mergedReverse('overrides fixtures (reverse)', ({ a }) => { | ||
| expect(a).toBe(1) | ||
| }) | ||
|
|
||
| const testD = test.extend({ | ||
| c: 3, | ||
| }) | ||
|
|
||
| const mergedChained = mergeTests(mergeTests(testA, testB), testD) | ||
|
|
||
| mergedChained('chained merge', ({ a, b, c }) => { | ||
| expect(a).toBe(1) | ||
| expect(b).toBe(2) | ||
| expect(c).toBe(3) | ||
| }) | ||
|
|
||
| describe('shared base', () => { | ||
| const base = test.extend({ | ||
| base: 'base', | ||
| }) | ||
| const derivedA = base.extend({ | ||
| a: 'a', | ||
| }) | ||
| const derivedB = base.extend({ | ||
| b: 'b', | ||
| }) | ||
| const mergedDerived = mergeTests(derivedA, derivedB) | ||
|
|
||
| mergedDerived('shared base fixtures', ({ base, a, b }) => { | ||
| expect(base).toBe('base') | ||
| expect(a).toBe('a') | ||
| expect(b).toBe('b') | ||
| }) | ||
| }) | ||
|
|
||
| describe('variadic merge (A, B, C) overrides correctly', () => { | ||
| const testA = test.extend({ | ||
| a: 1, | ||
| shared: 'a', | ||
| }) | ||
| const testB = test.extend({ | ||
| b: 2, | ||
| shared: 'b', | ||
| }) | ||
| const testC = test.extend({ | ||
| c: 3, | ||
| shared: 'c', | ||
| }) | ||
|
|
||
| const merged = mergeTests(testA, testB, testC) | ||
|
|
||
| merged('inherits all fixtures and overrides from last', ({ a, b, c, shared }) => { | ||
| expect(a).toBe(1) | ||
| expect(b).toBe(2) | ||
| expect(c).toBe(3) | ||
| expect(shared).toBe('c') | ||
| }) | ||
| }) | ||
|
|
||
| describe('overrides', () => { | ||
| describe('top-level', () => { | ||
| const base = test.extend({ a: 1 }) | ||
| base.override({ a: 2 }) | ||
| const merged = mergeTests(base) | ||
|
|
||
| merged('confirms top-level overrides are respected', ({ a }) => { | ||
| expect(a).toBe(2) | ||
| }) | ||
| }) | ||
|
|
||
| describe('overrides are dropped during merge (extend semantics)', () => { | ||
| const base = test.extend({ a: 1 }) | ||
| base.override({ a: 2 }) | ||
| const other = test.extend({ b: 3 }) | ||
| const merged = mergeTests(base, other) | ||
|
|
||
| merged('overrides are reset like extend()', ({ a, b }) => { | ||
| expect(a).toBe(1) | ||
| expect(b).toBe(3) | ||
| }) | ||
| }) | ||
|
|
||
| describe('scoped (identity)', () => { | ||
| const base = test.extend({ a: 1 }) | ||
| base.override({ a: 2 }) | ||
| const merged = mergeTests(base) | ||
|
|
||
| merged('confirms scoped overrides are preserved', ({ a }) => { | ||
| expect(a).toBe(2) | ||
| }) | ||
| }) | ||
| }) | ||
| }) |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this? Why do you need this if you already have access to the instance?