From 538eff22fd9a0c10baf599c767a37c3dedf4e309 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Sun, 12 Jun 2022 14:00:40 +0300 Subject: [PATCH 1/4] feat: bind expect state to context This fixes calling expect.assertions inside concurrent --- .../vitest/src/integrations/chai/constants.ts | 2 + .../vitest/src/integrations/chai/index.ts | 56 ++++++++++++++++--- .../chai/jest-asymmetric-matchers.ts | 4 +- .../src/integrations/chai/jest-expect.ts | 56 ++++--------------- .../src/integrations/chai/jest-extend.ts | 10 ++-- packages/vitest/src/runtime/context.ts | 5 ++ packages/vitest/src/runtime/run.ts | 16 +++++- test/core/test/concurrent.spec.ts | 19 +++++++ 8 files changed, 105 insertions(+), 63 deletions(-) create mode 100644 packages/vitest/src/integrations/chai/constants.ts create mode 100644 test/core/test/concurrent.spec.ts diff --git a/packages/vitest/src/integrations/chai/constants.ts b/packages/vitest/src/integrations/chai/constants.ts new file mode 100644 index 000000000000..4b7e029f0aba --- /dev/null +++ b/packages/vitest/src/integrations/chai/constants.ts @@ -0,0 +1,2 @@ +export const GLOBAL_EXPECT = Symbol.for('expect-global') +export const MATCHERS_OBJECT = Symbol.for('matchers-object') diff --git a/packages/vitest/src/integrations/chai/index.ts b/packages/vitest/src/integrations/chai/index.ts index a99ddeac034e..0aa034268147 100644 --- a/packages/vitest/src/integrations/chai/index.ts +++ b/packages/vitest/src/integrations/chai/index.ts @@ -1,12 +1,14 @@ -import chai from 'chai' +import chai, { util } from 'chai' import './setup' import type { Test } from '../../types' +import { getFullName } from '../../utils' import { getState, setState } from './jest-expect' +import { GLOBAL_EXPECT } from './constants' export function createExpect(test?: Test) { const expect = ((value: any, message?: string): Vi.Assertion => { - const { assertionCalls } = getState() - setState({ assertionCalls: assertionCalls + 1 }) + const { assertionCalls } = getState(expect) + setState({ assertionCalls: assertionCalls + 1 }, expect) const assert = chai.expect(value, message) as unknown as Vi.Assertion if (test) // @ts-expect-error internal @@ -16,16 +18,56 @@ export function createExpect(test?: Test) { }) as Vi.ExpectStatic Object.assign(expect, chai.expect) - expect.getState = getState - expect.setState = setState + expect.getState = () => getState(expect) + // @ts-expect-error type confusion + expect.setState = state => setState(state, expect) + + setState({ + assertionCalls: 0, + isExpectingAssertions: false, + isExpectingAssertionsError: null, + expectedAssertionsNumber: null, + expectedAssertionsNumberErrorGen: null, + testPath: test?.suite.file?.filepath, + currentTestName: test ? getFullName(test) : undefined, + }, expect) // @ts-expect-error untyped expect.extend = matchers => chai.expect.extend(expect, matchers) + function assertions(expected: number) { + const errorGen = () => new Error(`expected number of assertions to be ${expected}, but got ${getState(expect).assertionCalls}`) + if (Error.captureStackTrace) + Error.captureStackTrace(errorGen(), assertions) + + expect.setState({ + expectedAssertionsNumber: expected, + expectedAssertionsNumberErrorGen: errorGen, + }) + } + + function hasAssertions() { + const error = new Error('expected any number of assertion, but got none') + if (Error.captureStackTrace) + Error.captureStackTrace(error, hasAssertions) + + expect.setState({ + isExpectingAssertions: true, + isExpectingAssertionsError: error, + }) + } + + util.addMethod(expect, 'assertions', assertions) + util.addMethod(expect, 'hasAssertions', hasAssertions) + return expect } -const expect = createExpect() +const globalExpect = createExpect() + +Object.defineProperty(globalThis, GLOBAL_EXPECT, { + value: globalExpect, +}) export { assert, should } from 'chai' -export { chai, expect } +export { chai, globalExpect as expect } diff --git a/packages/vitest/src/integrations/chai/jest-asymmetric-matchers.ts b/packages/vitest/src/integrations/chai/jest-asymmetric-matchers.ts index 802feed8af89..64c21efe7dc9 100644 --- a/packages/vitest/src/integrations/chai/jest-asymmetric-matchers.ts +++ b/packages/vitest/src/integrations/chai/jest-asymmetric-matchers.ts @@ -19,9 +19,9 @@ export abstract class AsymmetricMatcher< constructor(protected sample: T, protected inverse = false) {} - protected getMatcherContext(): State { + protected getMatcherContext(expect: Vi.ExpectStatic): State { return { - ...getState(), + ...getState(expect), equals, isNot: this.inverse, utils: matcherUtils, diff --git a/packages/vitest/src/integrations/chai/jest-expect.ts b/packages/vitest/src/integrations/chai/jest-expect.ts index 41f62ae29a33..3059033296f1 100644 --- a/packages/vitest/src/integrations/chai/jest-expect.ts +++ b/packages/vitest/src/integrations/chai/jest-expect.ts @@ -10,31 +10,25 @@ import type { ChaiPlugin, MatcherState } from '../../types/chai' import { arrayBufferEquality, generateToBeMessage, iterableEquality, equals as jestEquals, sparseArrayEquality, subsetEquality, typeEquality } from './jest-utils' import type { AsymmetricMatcher } from './jest-asymmetric-matchers' import { stringify } from './jest-matcher-utils' +import { MATCHERS_OBJECT } from './constants' -const MATCHERS_OBJECT = Symbol.for('matchers-object') - -if (!Object.prototype.hasOwnProperty.call(global, MATCHERS_OBJECT)) { - const defaultState: Partial = { - assertionCalls: 0, - isExpectingAssertions: false, - isExpectingAssertionsError: null, - expectedAssertionsNumber: null, - expectedAssertionsNumberErrorGen: null, - } +if (!Object.prototype.hasOwnProperty.call(globalThis, MATCHERS_OBJECT)) { Object.defineProperty(globalThis, MATCHERS_OBJECT, { - value: { - state: defaultState, - }, + value: new WeakMap(), }) } -export const getState = (): State => - (globalThis as any)[MATCHERS_OBJECT].state +export const getState = (expect: Vi.ExpectStatic): State => + (globalThis as any)[MATCHERS_OBJECT].get(expect) export const setState = ( state: Partial, + expect: Vi.ExpectStatic, ): void => { - Object.assign((globalThis as any)[MATCHERS_OBJECT].state, state) + const map = (globalThis as any)[MATCHERS_OBJECT] + const current = map.get(expect) || {} + Object.assign(current, state) + map.set(expect, current) } // Jest Expect Compact @@ -676,36 +670,6 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { return proxy }) - utils.addMethod( - chai.expect, - 'assertions', - function assertions(expected: number) { - const errorGen = () => new Error(`expected number of assertions to be ${expected}, but got ${getState().assertionCalls}`) - if (Error.captureStackTrace) - Error.captureStackTrace(errorGen(), assertions) - - setState({ - expectedAssertionsNumber: expected, - expectedAssertionsNumberErrorGen: errorGen, - }) - }, - ) - - utils.addMethod( - chai.expect, - 'hasAssertions', - function hasAssertions() { - const error = new Error('expected any number of assertion, but got none') - if (Error.captureStackTrace) - Error.captureStackTrace(error, hasAssertions) - - setState({ - isExpectingAssertions: true, - isExpectingAssertionsError: error, - }) - }, - ) - utils.addMethod( chai.expect, 'addSnapshotSerializer', diff --git a/packages/vitest/src/integrations/chai/jest-extend.ts b/packages/vitest/src/integrations/chai/jest-extend.ts index a0ce4dbd64f7..ce6bf870d358 100644 --- a/packages/vitest/src/integrations/chai/jest-extend.ts +++ b/packages/vitest/src/integrations/chai/jest-extend.ts @@ -20,7 +20,7 @@ import { const isAsyncFunction = (fn: unknown) => typeof fn === 'function' && (fn as any)[Symbol.toStringTag] === 'AsyncFunction' -const getMatcherState = (assertion: Chai.AssertionStatic & Chai.Assertion) => { +const getMatcherState = (assertion: Chai.AssertionStatic & Chai.Assertion, expect: Vi.ExpectStatic) => { const obj = assertion._obj const isNot = util.flag(assertion, 'negate') as boolean const promise = util.flag(assertion, 'promise') || '' @@ -31,7 +31,7 @@ const getMatcherState = (assertion: Chai.AssertionStatic & Chai.Assertion) => { } const matcherState: MatcherState = { - ...getState(), + ...getState(expect), isNot, utils: jestUtils, promise, @@ -58,7 +58,7 @@ function JestExtendPlugin(expect: Vi.ExpectStatic, matchers: MatchersObject): Ch return (c, utils) => { Object.entries(matchers).forEach(([expectAssertionName, expectAssertion]) => { function expectSyncWrapper(this: Chai.AssertionStatic & Chai.Assertion, ...args: any[]) { - const { state, isNot, obj } = getMatcherState(this) + const { state, isNot, obj } = getMatcherState(this, expect) // @ts-expect-error args wanting tuple const { pass, message, actual, expected } = expectAssertion.call(state, obj, ...args) as SyncExpectationResult @@ -68,7 +68,7 @@ function JestExtendPlugin(expect: Vi.ExpectStatic, matchers: MatchersObject): Ch } async function expectAsyncWrapper(this: Chai.AssertionStatic & Chai.Assertion, ...args: any[]) { - const { state, isNot, obj } = getMatcherState(this) + const { state, isNot, obj } = getMatcherState(this, expect) // @ts-expect-error args wanting tuple const { pass, message, actual, expected } = await expectAssertion.call(state, obj, ...args) as SyncExpectationResult @@ -88,7 +88,7 @@ function JestExtendPlugin(expect: Vi.ExpectStatic, matchers: MatchersObject): Ch asymmetricMatch(other: unknown) { const { pass } = expectAssertion.call( - this.getMatcherContext(), + this.getMatcherContext(expect), other, ...this.sample, ) as SyncExpectationResult diff --git a/packages/vitest/src/runtime/context.ts b/packages/vitest/src/runtime/context.ts index c8edc9085fcd..9a8beee3348e 100644 --- a/packages/vitest/src/runtime/context.ts +++ b/packages/vitest/src/runtime/context.ts @@ -61,6 +61,11 @@ export function createTestContext(test: Test): TestContext { return _expect }, }) + Object.defineProperty(context, '_local', { + get() { + return _expect != null + }, + }) return context } diff --git a/packages/vitest/src/runtime/run.ts b/packages/vitest/src/runtime/run.ts index 2fd899016846..875f49837d3c 100644 --- a/packages/vitest/src/runtime/run.ts +++ b/packages/vitest/src/runtime/run.ts @@ -2,8 +2,9 @@ import type { File, HookCleanupCallback, HookListener, ResolvedConfig, Suite, Su import { vi } from '../integrations/vi' import { getSnapshotClient } from '../integrations/snapshot/chai' import { clearTimeout, getFullName, getWorkerState, hasFailed, hasTests, partitionSuiteChildren, setTimeout } from '../utils' -import { getState, setState } from '../integrations/chai/jest-expect' import { takeCoverage } from '../integrations/coverage' +import { getState, setState } from '../integrations/chai/jest-expect' +import { GLOBAL_EXPECT } from '../integrations/chai/constants' import { getFn, getHooks } from './map' import { rpc } from './rpc' import { collectTests } from './collect' @@ -111,9 +112,18 @@ export async function runTest(test: Test) { expectedAssertionsNumberErrorGen: null, testPath: test.suite.file?.filepath, currentTestName: getFullName(test), - }) + }, (globalThis as any)[GLOBAL_EXPECT]) await getFn(test)() - const { assertionCalls, expectedAssertionsNumber, expectedAssertionsNumberErrorGen, isExpectingAssertions, isExpectingAssertionsError } = getState() + const { + assertionCalls, + expectedAssertionsNumber, + expectedAssertionsNumberErrorGen, + isExpectingAssertions, + isExpectingAssertionsError, + // @ts-expect-error local is private + } = test.context._local + ? test.context.expect.getState() + : getState((globalThis as any)[GLOBAL_EXPECT]) if (expectedAssertionsNumber !== null && assertionCalls !== expectedAssertionsNumber) throw expectedAssertionsNumberErrorGen!() if (isExpectingAssertions === true && assertionCalls === 0) diff --git a/test/core/test/concurrent.spec.ts b/test/core/test/concurrent.spec.ts new file mode 100644 index 000000000000..f5aed509d5ec --- /dev/null +++ b/test/core/test/concurrent.spec.ts @@ -0,0 +1,19 @@ +import { test } from 'vitest' + +function delay(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +test.concurrent('test1', async ({ expect }) => { + expect.assertions(1) + await delay(10).then(() => { + expect(1).eq(1) + }) +}) + +test.concurrent('test2', async ({ expect }) => { + expect.assertions(1) + await delay(100).then(() => { + expect(2).eq(2) + }) +}) From b7c1bdff13acfe21ad5f6aa0a230672eb35e22c7 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Sun, 12 Jun 2022 14:03:19 +0300 Subject: [PATCH 2/4] refactor: cleanup --- packages/vitest/src/integrations/chai/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vitest/src/integrations/chai/index.ts b/packages/vitest/src/integrations/chai/index.ts index 0aa034268147..dabe4b9c02fa 100644 --- a/packages/vitest/src/integrations/chai/index.ts +++ b/packages/vitest/src/integrations/chai/index.ts @@ -36,7 +36,7 @@ export function createExpect(test?: Test) { expect.extend = matchers => chai.expect.extend(expect, matchers) function assertions(expected: number) { - const errorGen = () => new Error(`expected number of assertions to be ${expected}, but got ${getState(expect).assertionCalls}`) + const errorGen = () => new Error(`expected number of assertions to be ${expected}, but got ${expect.getState().assertionCalls}`) if (Error.captureStackTrace) Error.captureStackTrace(errorGen(), assertions) From 41b32f9a744a38fe4846a57a67770de97a225b3a Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Sun, 12 Jun 2022 14:06:57 +0300 Subject: [PATCH 3/4] chore: fix types --- packages/vitest/src/integrations/chai/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vitest/src/integrations/chai/index.ts b/packages/vitest/src/integrations/chai/index.ts index dabe4b9c02fa..0bc1f1f498b1 100644 --- a/packages/vitest/src/integrations/chai/index.ts +++ b/packages/vitest/src/integrations/chai/index.ts @@ -2,6 +2,7 @@ import chai, { util } from 'chai' import './setup' import type { Test } from '../../types' import { getFullName } from '../../utils' +import type { MatcherState } from '../../types/chai' import { getState, setState } from './jest-expect' import { GLOBAL_EXPECT } from './constants' @@ -19,8 +20,7 @@ export function createExpect(test?: Test) { Object.assign(expect, chai.expect) expect.getState = () => getState(expect) - // @ts-expect-error type confusion - expect.setState = state => setState(state, expect) + expect.setState = state => setState(state as Partial, expect) setState({ assertionCalls: 0, From ed34401563d142b0bbdb93c8f19d2a39b5a4b3af Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Sun, 12 Jun 2022 14:18:55 +0300 Subject: [PATCH 4/4] chore: allow not passing expect to getMatcherContext --- .../vitest/src/integrations/chai/jest-asymmetric-matchers.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/vitest/src/integrations/chai/jest-asymmetric-matchers.ts b/packages/vitest/src/integrations/chai/jest-asymmetric-matchers.ts index 64c21efe7dc9..5dfa8620ff18 100644 --- a/packages/vitest/src/integrations/chai/jest-asymmetric-matchers.ts +++ b/packages/vitest/src/integrations/chai/jest-asymmetric-matchers.ts @@ -1,4 +1,5 @@ import type { ChaiPlugin, MatcherState } from '../../types/chai' +import { GLOBAL_EXPECT } from './constants' import { getState } from './jest-expect' import * as matcherUtils from './jest-matcher-utils' @@ -19,9 +20,9 @@ export abstract class AsymmetricMatcher< constructor(protected sample: T, protected inverse = false) {} - protected getMatcherContext(expect: Vi.ExpectStatic): State { + protected getMatcherContext(expect?: Vi.ExpectStatic): State { return { - ...getState(expect), + ...getState(expect || (globalThis as any)[GLOBAL_EXPECT]), equals, isNot: this.inverse, utils: matcherUtils,