Skip to content

Commit

Permalink
feat: support suite.each, test.each, close #120 (#640)
Browse files Browse the repository at this point in the history
* feat: support `suite.each`, `test.each`, close #120

* chore: fix types

* chore: update
  • Loading branch information
antfu committed Jan 27, 2022
1 parent 65aad85 commit 9abb557
Show file tree
Hide file tree
Showing 18 changed files with 177 additions and 49 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.1.3",
"@types/lodash": "^4.14.178",
"@types/node": "^17.0.12",
"bumpp": "^7.1.1",
"c8": "^7.11.0",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/client/auto-imports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ declare global {
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
Expand Down
29 changes: 29 additions & 0 deletions packages/vitest/LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1231,6 +1231,35 @@ Repository: unjs/mlly
---------------------------------------

## mockdate
License: MIT
By: Bob Lauer
Repository: https://github.com/boblauer/MockDate.git

> The MIT License (MIT)
>
> Copyright (c) 2014 Bob Lauer
>
> Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the "Software"), to deal
> in the Software without restriction, including without limitation the rights
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in all
> copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
> SOFTWARE.
---------------------------------------

## natural-compare
License: MIT
By: Lauri Rooden
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const external = [
...Object.keys(pkg.dependencies),
...Object.keys(pkg.peerDependencies),
'worker_threads',
'inspector',
]

export default ({ watch }) => [
Expand Down
9 changes: 5 additions & 4 deletions packages/vitest/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ type Promisify<O> = {

declare global {
namespace Vi {

interface ExpectStatic extends Chai.ExpectStatic, AsymmetricMatchersContaining {
<T>(actual: T, message?: string): Vi.Assertion<T>

Expand Down Expand Up @@ -115,15 +114,17 @@ declare global {
nthReturnedWith<E>(nthCall: number, value: E): void
}

type VitestifyAssertion<A> = {
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore build namspace conflict
type VitestAssertion<A> = {
[K in keyof A]: A[K] extends Chai.Assertion
? Assertion<any>
: A[K] extends (...args: any[]) => any
? A[K] // not converting function since they may contain overload
: VitestifyAssertion<A[K]>
: VitestAssertion<A[K]>
}

interface Assertion<T = any> extends VitestifyAssertion<Chai.Assertion>, JestAssertion<T> {
interface Assertion<T = any> extends VitestAssertion<Chai.Assertion>, JestAssertion<T> {
resolves: Promisify<Assertion<T>>
rejects: Promisify<Assertion<T>>
}
Expand Down
91 changes: 64 additions & 27 deletions packages/vitest/src/runtime/suite.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
import type { File, RunMode, Suite, SuiteCollector, SuiteHooks, Test, TestCollector, TestFactory, TestFunction } from '../types'
import { noop } from '../utils'
import { format } from 'util'
import type { File, RunMode, Suite, SuiteAPI, SuiteCollector, SuiteFactory, SuiteHooks, Test, TestAPI, TestFunction } from '../types'
import { isObject, noop, toArray } from '../utils'
import { createChainable } from './chain'
import { collectTask, context, normalizeTest, runWithSuite } from './context'
import { getHooks, setFn, setHooks } from './map'

// apis
export const suite = createSuite()
export const test: TestCollector = createChainable(
['concurrent', 'skip', 'only', 'todo', 'fails'],
export const test = createTest(
function(name: string, fn?: TestFunction, timeout?: number) {
// @ts-expect-error untyped internal prop
getCurrentSuite().test.fn.call(this, name, fn, timeout)
},
)

function formatTitle(template: string, items: any[]) {
const count = template.split('%').length - 1
let formatted = format(template, ...items.slice(0, count))
if (isObject(items[0])) {
formatted = formatted.replace(/\$([$\w_]+)/g, (_, key) => {
return items[0][key]
})
}
return formatted
}

// alias
export const describe = suite
export const it = test
Expand All @@ -40,34 +51,31 @@ export function createSuiteHooks() {
}
}

function createSuiteCollector(name: string, factory: TestFactory = () => { }, mode: RunMode, concurrent?: boolean) {
function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, mode: RunMode, concurrent?: boolean) {
const tasks: (Test | Suite | SuiteCollector)[] = []
const factoryQueue: (Test | Suite | SuiteCollector)[] = []

let suite: Suite

initSuite()

const test = createChainable(
['concurrent', 'skip', 'only', 'todo', 'fails'],
function(name: string, fn?: TestFunction, timeout?: number) {
const mode = this.only ? 'only' : this.skip ? 'skip' : this.todo ? 'todo' : 'run'
const test = createTest(function(name: string, fn?: TestFunction, timeout?: number) {
const mode = this.only ? 'only' : this.skip ? 'skip' : this.todo ? 'todo' : 'run'

const test: Test = {
id: '',
type: 'test',
name,
mode,
suite: undefined!,
fails: this.fails,
}
if (this.concurrent || concurrent)
test.concurrent = true

setFn(test, normalizeTest(fn || noop, timeout))
tasks.push(test)
},
)
const test: Test = {
id: '',
type: 'test',
name,
mode,
suite: undefined!,
fails: this.fails,
}
if (this.concurrent || concurrent)
test.concurrent = true

setFn(test, normalizeTest(fn || noop, timeout))
tasks.push(test)
})

const collector: SuiteCollector = {
type: 'collector',
Expand Down Expand Up @@ -129,11 +137,40 @@ function createSuiteCollector(name: string, factory: TestFactory = () => { }, mo
}

function createSuite() {
return createChainable(
const suite = createChainable(
['concurrent', 'skip', 'only', 'todo'],
function(name: string, factory?: TestFactory) {
function(name: string, factory?: SuiteFactory) {
const mode = this.only ? 'only' : this.skip ? 'skip' : this.todo ? 'todo' : 'run'
return createSuiteCollector(name, factory, mode, this.concurrent)
},
)
) as SuiteAPI

suite.each = <T>(cases: T[]) => {
return (name: string, fn: (...args: T extends any[] ? T : [T]) => void) => {
cases.forEach((i) => {
const items = toArray(i) as any
suite(formatTitle(name, items), () => fn(...items))
})
}
}

return suite as SuiteAPI
}

function createTest(fn: ((this: Record<'concurrent'| 'skip'| 'only'| 'todo'| 'fails', boolean | undefined>, title: string, fn?: TestFunction, timeout?: number) => void)) {
const test = createChainable(
['concurrent', 'skip', 'only', 'todo', 'fails'],
fn,
) as TestAPI

test.each = <T>(cases: T[]) => {
return (name: string, fn: (...args: T extends any[] ? T : [T]) => void) => {
cases.forEach((i) => {
const items = toArray(i) as any
test(formatTitle(name, items), () => fn(...items))
})
}
}

return test as TestAPI
}
15 changes: 11 additions & 4 deletions packages/vitest/src/types/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,19 @@ export type Task = Test | Suite | File

export type DoneCallback = (error?: any) => void
export type TestFunction = (done: DoneCallback) => Awaitable<void>
export type EachFunction = <T>(cases: T[]) => (name: string, fn: (...args: T extends any[] ? T : [T]) => void) => void

export type TestCollector = ChainableFunction<
export type TestAPI = ChainableFunction<
'concurrent' | 'only' | 'skip' | 'todo' | 'fails',
[name: string, fn?: TestFunction, timeout?: number],
void
>
> & { each: EachFunction }

export type SuiteAPI = ChainableFunction<
'concurrent' | 'only' | 'skip' | 'todo',
[name: string, factory?: SuiteFactory],
SuiteCollector
> & { each: EachFunction }

export type HookListener<T extends any[]> = (...args: T) => Awaitable<void>

Expand All @@ -64,14 +71,14 @@ export interface SuiteCollector {
readonly name: string
readonly mode: RunMode
type: 'collector'
test: TestCollector
test: TestAPI
tasks: (Suite | Test | SuiteCollector)[]
collect: (file?: File) => Promise<Suite>
clear: () => void
on: <T extends keyof SuiteHooks>(name: T, ...fn: SuiteHooks[T]) => void
}

export type TestFactory = (test: (name: string, fn: TestFunction) => void) => Awaitable<void>
export type SuiteFactory = (test: (name: string, fn: TestFunction) => void) => Awaitable<void>

export interface RuntimeContext {
tasks: (SuiteCollector | Test)[]
Expand Down
1 change: 1 addition & 0 deletions packages/ws-client/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const external = [
'birpc',
'worker_threads',
'vitest',
'inspector',
]

export default () => [
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions test/core/test/each.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, expect, test } from 'vitest'

test.each([
[1, 1, 2],
[1, 2, 3],
[2, 1, 3],
])('add(%i, %i) -> %i', (a, b, expected) => {
expect(a + b).toBe(expected)
})

describe.each([
[1, 1, 2],
[1, 2, 3],
[2, 1, 3],
])('describe add(%i, %i)', (a, b, expected) => {
test(`returns ${expected}`, () => {
expect(a + b).toBe(expected)
})

test(`returned value not be greater than ${expected}`, () => {
expect(a + b).not.toBeGreaterThan(expected)
})

test(`returned value not be less than ${expected}`, () => {
expect(a + b).not.toBeLessThan(expected)
})
})

describe.each([
{ a: 1, b: 1, expected: 2 },
{ a: 1, b: 2, expected: 3 },
{ a: 2, b: 1, expected: 3 },
])('describe object add($a, $b)', ({ a, b, expected }) => {
test(`returns ${expected}`, () => {
expect(a + b).toBe(expected)
})

test(`returned value not be greater than ${expected}`, () => {
expect(a + b).not.toBeGreaterThan(expected)
})

test(`returned value not be less than ${expected}`, () => {
expect(a + b).not.toBeLessThan(expected)
})
})
1 change: 1 addition & 0 deletions test/core/test/hoist-import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ vi.mock('../src/timeout.ts', () => {
})

test('"vi" can be used inside factory with empty lines', () => {
// @ts-expect-error no types
expect(globalThis.vi).toBeUndefined()
expect(timeout).toBe(10_000)
expect(vi.isMockFunction(fn)).toBe(true)
Expand Down
12 changes: 6 additions & 6 deletions test/core/test/jest-expect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ describe('jest-expect', () => {
// https://jestjs.io/docs/expect#tostrictequalvalue

class LaCroix {
constructor(public flavor) {}
constructor(public flavor: any) {}
}

describe('the La Croix cans on my desk', () => {
Expand All @@ -173,7 +173,7 @@ describe('jest-expect', () => {
expect([]).not.toBe([])
expect([]).toStrictEqual([])

const foo = []
const foo: any[] = []

expect(foo).toBe(foo)
expect(foo).toStrictEqual(foo)
Expand All @@ -195,21 +195,21 @@ describe('jest-expect', () => {

describe('.toStrictEqual()', () => {
class TestClassA {
constructor(public a, public b) {}
constructor(public a: any, public b: any) {}
}

class TestClassB {
constructor(public a, public b) {}
constructor(public a: any, public b: any) {}
}

const TestClassC = class Child extends TestClassA {
constructor(a, b) {
constructor(a: any, b: any) {
super(a, b)
}
}

const TestClassD = class Child extends TestClassB {
constructor(a, b) {
constructor(a: any, b: any) {
super(a, b)
}
}
Expand Down
1 change: 1 addition & 0 deletions test/core/test/jest-mock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ describe('jest mock compat layer', () => {
const Spy = vi.fn(() => {})

expect(Spy.mock.instances).toHaveLength(0)
// @ts-expect-error ignore
// eslint-disable-next-line no-new
new Spy()
expect(Spy.mock.instances).toHaveLength(1)
Expand Down
Loading

0 comments on commit 9abb557

Please sign in to comment.