From b28d780c9b57f7af26e2ab729da52ec989d96703 Mon Sep 17 00:00:00 2001 From: Nikita Skovoroda Date: Tue, 10 Sep 2024 23:35:25 +0400 Subject: [PATCH] perf: lazy-load expect when needed --- package.json | 2 + src/expect.cjs | 125 +++++++++++++++++++++++++++++++++++++++++++++++ src/jest.js | 6 +-- src/jest.mock.js | 2 + 4 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 src/expect.cjs diff --git a/package.json b/package.json index 5185ba2..dfe0730 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ }, "exports": { "./node-test-reporter": "./bin/reporter.js", + "./expect": "./src/expect.cjs", "./jest": "./src/jest.js", "./node": "./src/node.js", "./tape": { @@ -67,6 +68,7 @@ "src/engine.pure.cjs", "src/engine.pure.snapshot.cjs", "src/engine.select.cjs", + "src/expect.cjs", "src/jest.js", "src/jest.config.js", "src/jest.config.fs.js", diff --git a/src/expect.cjs b/src/expect.cjs new file mode 100644 index 0000000..01bebb4 --- /dev/null +++ b/src/expect.cjs @@ -0,0 +1,125 @@ +let expect +let assertionsDelta = 0 +const extend = [] +const set = [] + +function fixupAssertions() { + if (assertionsDelta === 0) return + const state = expect.getState() + state.assertionCalls += assertionsDelta + state.numPassingAsserts += assertionsDelta + assertionsDelta = 0 +} + +function loadExpect() { + if (expect) return expect + expect = require('expect').expect + const matchers = require('jest-extended') + expect.extend(matchers) + for (const x of extend) expect.extend(...x) + for (const [key, value] of set) expect[key] = value + fixupAssertions() + return expect +} + +const areNumeric = (...args) => args.every((a) => typeof a === 'number' || typeof a === 'bigint') + +const matchers = { + __proto__: null, + toBe: (x, y) => x === y, + toBeNull: (x) => x === null, + toBeTruthy: (x) => x, + toBeFalsy: (x) => !x, + toBeTrue: (x) => x === true, + toBeFalse: (x) => x === false, + toBeDefined: (x) => x !== undefined, + toBeUndefined: (x) => x === undefined, + toBeInstanceOf: (x, y) => y && x instanceof y, + toBeString: (x) => typeof x === 'string' || x instanceof String, + toBeNumber: (x) => typeof x === 'number', // yes, mismatches toBeString logic. yes, no bigints + toBeArray: (x) => Array.isArray(x), + toBeArrayOfSize: (x, l) => Array.isArray(x) && x.length === l, + toHaveLength: (x, l) => x && x.length === l, + toBeGreaterThan: (x, c) => areNumeric(x, c) && x > c, + toBeGreaterThanOrEqual: (x, c) => areNumeric(x, c) && x >= c, + toBeLessThan: (x, c) => areNumeric(x, c) && x < c, + toBeLessThanOrEqual: (x, c) => areNumeric(x, c) && x <= c, + toHaveBeenCalled: (x) => x?._isMockFunction && x?.mock?.calls?.length > 0, + toHaveBeenCalledTimes: (x, c) => x?._isMockFunction && x?.mock?.calls?.length === c, + toBeCalled: (...a) => matchers.toHaveBeenCalled(...a), + toBeCalledTimes: (...a) => matchers.toHaveBeenCalledTimes(...a), + toHaveBeenCalledOnce: (x) => matchers.toHaveBeenCalledTimes(x, 1), +} + +const matchersFalseNegative = { + __proto__: null, + toEqual: (x, y) => x === y, + toStrictEqual: (x, y) => x === y, + toContain: (x, c) => Array.isArray(x) && [...x].includes(c), + toBeEven: (x) => Number.isSafeInteger(x) && x % 2 === 0, + toBeOdd: (x) => Number.isSafeInteger(x) && x % 2 === 1, +} + +function createExpect() { + return new Proxy(() => {}, { + apply: (target, that, [x, ...rest]) => { + if (rest.length > 0) return loadExpect()(x, ...rest) + return new Proxy(Object.create(null), { + get: (_, name) => { + const matcher = matchers[name] || matchersFalseNegative[name] + if (matcher) + return (...args) => { + if (!matcher(x, ...args)) return loadExpect()(x)[name](...args) + assertionsDelta++ + } + + if (name === 'not') + return new Proxy(Object.create(null), { + get: (_, not) => { + if (matchers[not]) + return (...args) => { + if (matchers[not](x, ...args)) return loadExpect()(x).not[not](...args) + assertionsDelta++ + } + + // console.log ({ loadReason: 'not', name: not }) + return loadExpect()(x).not[not] + }, + }) + + // console.log ({ loadReason: 'expect', name }) + return loadExpect()(x)[name] + }, + }) + }, + get: (_, name) => { + if (name === 'extend' && !expect) return (...args) => extend.push(args) + if (name === 'extractExpectedAssertionsErrors') { + return expect + ? (...args) => { + fixupAssertions() + return expect[name](...args) + } + : () => { + assertionsDelta = 0 + return [] // no .assertions call were made, those cause loading + } + } + + // console.log({ loadReason: 'get', name }) + return loadExpect()[name] + }, + set: (_, name, value) => { + if (expect) { + expect[name] = value + } else { + set.push([name, value]) + } + + return true + }, + }) +} + +exports.expect = createExpect() +exports.loadExpect = loadExpect diff --git a/src/jest.js b/src/jest.js index c2935de..37576e4 100644 --- a/src/jest.js +++ b/src/jest.js @@ -8,8 +8,7 @@ import { setupSnapshots } from './jest.snapshot.js' import { fetchReplay, fetchRecord, websocketRecord, websocketReplay } from './replay.js' import { createCallerLocationHook, insideEsbuild } from './dark.cjs' import { haveValidTimers } from './version.js' -import { expect } from 'expect' -import matchers from 'jest-extended' +import { expect } from './expect.cjs' import { format as prettyFormat } from 'pretty-format' const { getCallerLocation, installLocationInNextTest } = createCallerLocationHook() @@ -21,7 +20,6 @@ if (process.env.EXODUS_TEST_ENVIRONMENT !== 'bundle') { if (files.length === 1 && files[0].endsWith('/inband.js')) addStatefulApis = false } -expect.extend(matchers) if (addStatefulApis) setupSnapshots(expect) let defaultTimeout = jestConfig().testTimeout // overridable via jest.setTimeout() @@ -262,4 +260,4 @@ export const beforeAll = (fn) => node.before(wrapCallback(fn)) export const afterAll = (fn) => node.after(wrapCallback(fn)) export { describe, test, test as it } -export { expect } from 'expect' +export { expect } from './expect.cjs' diff --git a/src/jest.mock.js b/src/jest.mock.js index a6b88c4..4058f1c 100644 --- a/src/jest.mock.js +++ b/src/jest.mock.js @@ -8,6 +8,7 @@ import { syncBuiltinESMExports, } from './engine.js' import { jestfn } from './jest.fn.js' +import { loadExpect } from './expect.cjs' import { makeEsbuildMockable, insideEsbuild } from './dark.cjs' const mapMocks = new Map() @@ -212,6 +213,7 @@ function jestmock(name, mocker, { override = false } = {}) { const value = mocker ? expand(mocker()) : mockClone(mapActual.get(resolved)) mapMocks.set(resolved, value) + loadExpect() // we need to do this as we don't want mocks affecting expect const topLevelESM = isTopLevelESM() let likelyESM = topLevelESM && !insideEsbuild && ![null, resolved].includes(resolveImport(name)) let okFromESM = false