Skip to content
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

feat(expect): support expect.soft #3507

Merged
merged 41 commits into from Jun 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
1474990
feat(expect): support `expect.soft`
Dunqing Jun 4, 2023
54116a7
refactor: remove softFail state
Dunqing Jun 4, 2023
9f2bae1
refactor: ensure use same softWrapper
Dunqing Jun 4, 2023
f98adb0
fix: retry with expect.soft test is incorrect
Dunqing Jun 4, 2023
dd16910
fix: repeats failed
Dunqing Jun 4, 2023
4785e33
test: fix ts error
Dunqing Jun 4, 2023
6f2e077
test: update
Dunqing Jun 4, 2023
3ea9a39
fix: should use same softWrapper
Dunqing Jun 4, 2023
2446944
fix: errors repeat handling
Dunqing Jun 4, 2023
2d4e6e5
test: update snapshot
Dunqing Jun 4, 2023
18cec9d
test: update snapshot
Dunqing Jun 4, 2023
2405231
test: update test name and snapshot
Dunqing Jun 4, 2023
49c892d
test: remove snap
Dunqing Jun 4, 2023
8a2b0d4
test: update snapshot
Dunqing Jun 4, 2023
cb64a2b
chore: revert lock file
Dunqing Jun 4, 2023
a90e3ea
test: remove snapshot
Dunqing Jun 4, 2023
3cb68fa
test: update timeout
Dunqing Jun 4, 2023
6a98eaf
test: update timeout
Dunqing Jun 5, 2023
9ab8bc4
test: fix timeout
Dunqing Jun 5, 2023
b848021
test: remove any, add type for it
Dunqing Jun 5, 2023
8ab4dcb
refactor: same syntax
Dunqing Jun 5, 2023
dccdf03
refactor: move errros function to @vitest/utils
Dunqing Jun 5, 2023
79057a4
test: add types
Dunqing Jun 5, 2023
64e74dd
test: using expect.soft for test
Dunqing Jun 5, 2023
9c0cedd
chore: missing json plugin
Dunqing Jun 5, 2023
aebb44c
feat: add entry
Dunqing Jun 5, 2023
093917b
test: fix ci
Dunqing Jun 5, 2023
beaea42
test: should better
Dunqing Jun 5, 2023
e4652f6
chore: update lock
Dunqing Jun 5, 2023
770d11f
chore: update lock
Dunqing Jun 5, 2023
d60dbb6
Merge remote-tracking branch 'upstream/main' into feat/expect-soft
Dunqing Jun 5, 2023
e9ee91b
test: up
Dunqing Jun 5, 2023
c67e3bf
fix: getting error when run outside of the test
Dunqing Jun 6, 2023
95fbde7
feat: cast types
Dunqing Jun 6, 2023
253ae36
chore: should be runner
Dunqing Jun 6, 2023
b5dad65
feat: should throw an error when soft run outside of the test
Dunqing Jun 6, 2023
b2d8cdf
chore: update lock
Dunqing Jun 6, 2023
7b2c8ce
feat: better handle
Dunqing Jun 6, 2023
d90dd3f
Merge branch 'main' into feat/expect-soft
Dunqing Jun 6, 2023
36a6a43
test: add failing test
Dunqing Jun 6, 2023
5ea3b91
test: update
Dunqing Jun 6, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/expect/package.json
Expand Up @@ -39,6 +39,7 @@
"chai": "^4.3.7"
},
"devDependencies": {
"@vitest/runner": "workspace:*",
"picocolors": "^1.0.0"
}
}
1 change: 1 addition & 0 deletions packages/expect/rollup.config.js
Expand Up @@ -9,6 +9,7 @@ const external = [
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
'@vitest/utils/diff',
'@vitest/utils/error',
]

const plugins = [
Expand Down
12 changes: 7 additions & 5 deletions packages/expect/src/jest-expect.ts
Expand Up @@ -3,21 +3,23 @@ import { assertTypes, getColors } from '@vitest/utils'
import type { Constructable } from '@vitest/utils'
import type { EnhancedSpy } from '@vitest/spy'
import { isMockFunction } from '@vitest/spy'
import type { Test } from '@vitest/runner'
import type { Assertion, ChaiPlugin } from './types'
import { arrayBufferEquality, generateToBeMessage, iterableEquality, equals as jestEquals, sparseArrayEquality, subsetEquality, typeEquality } from './jest-utils'
import type { AsymmetricMatcher } from './jest-asymmetric-matchers'
import { diff, stringify } from './jest-matcher-utils'
import { JEST_MATCHERS_OBJECT } from './constants'
import { recordAsyncExpect } from './utils'
import { recordAsyncExpect, wrapSoft } from './utils'

// Jest Expect Compact
export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
const c = () => getColors()

function def(name: keyof Assertion | (keyof Assertion)[], fn: ((this: Chai.AssertionStatic & Assertion, ...args: any[]) => any)) {
const addMethod = (n: keyof Assertion) => {
utils.addMethod(chai.Assertion.prototype, n, fn)
utils.addMethod((globalThis as any)[JEST_MATCHERS_OBJECT].matchers, n, fn)
const softWrapper = wrapSoft(utils, fn)
utils.addMethod(chai.Assertion.prototype, n, softWrapper)
utils.addMethod((globalThis as any)[JEST_MATCHERS_OBJECT].matchers, n, softWrapper)
}

if (Array.isArray(name))
Expand Down Expand Up @@ -636,7 +638,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
utils.addProperty(chai.Assertion.prototype, 'resolves', function __VITEST_RESOLVES__(this: any) {
utils.flag(this, 'promise', 'resolves')
utils.flag(this, 'error', new Error('resolves'))
const test = utils.flag(this, 'vitest-test')
const test: Test = utils.flag(this, 'vitest-test')
const obj = utils.flag(this, 'object')

if (typeof obj?.then !== 'function')
Expand Down Expand Up @@ -671,7 +673,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
utils.addProperty(chai.Assertion.prototype, 'rejects', function __VITEST_REJECTS__(this: any) {
utils.flag(this, 'promise', 'rejects')
utils.flag(this, 'error', new Error('rejects'))
const test = utils.flag(this, 'vitest-test')
const test: Test = utils.flag(this, 'vitest-test')
const obj = utils.flag(this, 'object')
const wrapper = typeof obj === 'function' ? obj() : obj // for jest compat

Expand Down
6 changes: 4 additions & 2 deletions packages/expect/src/jest-extend.ts
Expand Up @@ -17,6 +17,7 @@ import {
iterableEquality,
subsetEquality,
} from './jest-utils'
import { wrapSoft } from './utils'

function getMatcherState(assertion: Chai.AssertionStatic & Chai.Assertion, expect: ExpectStatic) {
const obj = assertion._obj
Expand Down Expand Up @@ -75,8 +76,9 @@ function JestExtendPlugin(expect: ExpectStatic, matchers: MatchersObject): ChaiP
throw new JestExtendError(message(), actual, expected)
}

utils.addMethod((globalThis as any)[JEST_MATCHERS_OBJECT].matchers, expectAssertionName, expectWrapper)
utils.addMethod(c.Assertion.prototype, expectAssertionName, expectWrapper)
const softWrapper = wrapSoft(utils, expectWrapper)
utils.addMethod((globalThis as any)[JEST_MATCHERS_OBJECT].matchers, expectAssertionName, softWrapper)
utils.addMethod(c.Assertion.prototype, expectAssertionName, softWrapper)

class CustomMatcher extends AsymmetricMatcher<[unknown, ...unknown[]]> {
constructor(inverse = false, ...sample: [unknown, ...unknown[]]) {
Expand Down
3 changes: 2 additions & 1 deletion packages/expect/src/types.ts
Expand Up @@ -79,6 +79,7 @@ export interface MatcherState {
iterableEquality: Tester
subsetEquality: Tester
}
soft?: boolean
}

export interface SyncExpectationResult {
Expand All @@ -100,7 +101,7 @@ export type MatchersObject<T extends MatcherState = MatcherState> = Record<strin

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

soft<T>(actual: T, message?: string): Assertion<T>
extend(expects: MatchersObject): void
assertions(expected: number): void
hasAssertions(): void
Expand Down
33 changes: 33 additions & 0 deletions packages/expect/src/utils.ts
@@ -1,3 +1,9 @@
import { processError } from '@vitest/utils/error'
import type { Test } from '@vitest/runner/types'
import { GLOBAL_EXPECT } from './constants'
import { getState } from './state'
import type { Assertion, MatcherState } from './types'

export function recordAsyncExpect(test: any, promise: Promise<any> | PromiseLike<any>) {
// record promise for test, that resolves before test ends
if (test && promise instanceof Promise) {
Expand All @@ -16,3 +22,30 @@ export function recordAsyncExpect(test: any, promise: Promise<any> | PromiseLike

return promise
}

export function wrapSoft(utils: Chai.ChaiUtils, fn: (this: Chai.AssertionStatic & Assertion, ...args: any[]) => void) {
return function (this: Chai.AssertionStatic & Assertion, ...args: any[]) {
const test: Test = utils.flag(this, 'vitest-test')

// @ts-expect-error local is untyped
const state: MatcherState = test?.context._local
? test.context.expect.getState()
: getState((globalThis as any)[GLOBAL_EXPECT])

if (!state.soft)
return fn.apply(this, args)

if (!test)
throw new Error('expect.soft() can only be used inside a test')

try {
return fn.apply(this, args)
}
catch (err) {
test.result ||= { state: 'fail' }
test.result.state = 'fail'
Dunqing marked this conversation as resolved.
Show resolved Hide resolved
test.result.errors ||= []
test.result.errors.push(processError(err))
}
}
}
2 changes: 1 addition & 1 deletion packages/runner/rollup.config.js
Expand Up @@ -9,7 +9,7 @@ const external = [
...builtinModules,
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
'@vitest/utils/diff',
'@vitest/utils/error',
]

const entries = {
Expand Down
2 changes: 1 addition & 1 deletion packages/runner/src/collect.ts
@@ -1,10 +1,10 @@
import { relative } from 'pathe'
import { processError } from '@vitest/utils/error'
import type { File } from './types'
import type { VitestRunner } from './types/runner'
import { calculateSuiteHash, generateHash, interpretTaskModes, someTasksAreOnly } from './utils/collect'
import { clearCollectorContext, getDefaultSuite } from './suite'
import { getHooks, setHooks } from './map'
import { processError } from './utils/error'
import { collectorContext } from './context'
import { runSetupFiles } from './setup'

Expand Down
19 changes: 13 additions & 6 deletions packages/runner/src/run.ts
@@ -1,11 +1,11 @@
import limit from 'p-limit'
import { getSafeTimers, shuffle } from '@vitest/utils'
import { processError } from '@vitest/utils/error'
import type { VitestRunner } from './types/runner'
import type { File, HookCleanupCallback, HookListener, SequenceHooks, Suite, SuiteHooks, Task, TaskMeta, TaskResult, TaskResultPack, TaskState, Test } from './types'
import { partitionSuiteChildren } from './utils/suite'
import { getFn, getHooks } from './map'
import { collectTests } from './collect'
import { processError } from './utils/error'
import { setCurrentTest } from './test-state'
import { hasFailed, hasTests } from './utils/tasks'

Expand Down Expand Up @@ -156,7 +156,6 @@ export async function runTest(test: Test, runner: VitestRunner) {
throw new Error('Test function is not found. Did you add it using `setFn`?')
await fn()
}

// some async expect will be added to this array, in case user forget to await theme
if (test.promises) {
const result = await Promise.allSettled(test.promises)
Expand All @@ -167,10 +166,12 @@ export async function runTest(test: Test, runner: VitestRunner) {

await runner.onAfterTryTest?.(test, { retry: retryCount, repeats: repeatCount })

if (!test.repeats)
test.result.state = 'pass'
else if (test.repeats && retry === retryCount)
test.result.state = 'pass'
if (test.result.state !== 'fail') {
if (!test.repeats)
test.result.state = 'pass'
else if (test.repeats && retry === retryCount)
test.result.state = 'pass'
}
}
catch (e) {
failTask(test.result, e)
Expand All @@ -186,6 +187,12 @@ export async function runTest(test: Test, runner: VitestRunner) {

if (test.result.state === 'pass')
break

if (retryCount < retry - 1) {
// reset state when retry test
test.result.state = 'run'
}

// update retry info
updateTask(test, runner)
}
Expand Down
3 changes: 1 addition & 2 deletions packages/runner/src/types/tasks.ts
@@ -1,6 +1,5 @@
import type { Awaitable } from '@vitest/utils'
import type { Awaitable, ErrorWithDiff } from '@vitest/utils'
import type { ChainableFunction } from '../utils/chain'
import type { ErrorWithDiff } from '../utils/error'

export type RunMode = 'run' | 'skip' | 'only' | 'todo'
export type TaskState = RunMode | 'pass' | 'fail'
Expand Down
2 changes: 1 addition & 1 deletion packages/runner/src/utils/collect.ts
@@ -1,5 +1,5 @@
import { processError } from '@vitest/utils/error'
import type { Suite, TaskBase } from '../types'
import { processError } from './error'

/**
* If any tasks been marked as `only`, mark all other tasks as `skip`.
Expand Down