Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
"src/jest.config.js",
"src/jest.config.fs.js",
"src/jest.environment.js",
"src/jest.fn.context.cjs",
"src/jest.fn.js",
"src/jest.mock.js",
"src/jest.setup.js",
Expand Down
11 changes: 9 additions & 2 deletions src/expect.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ let expect
let assertionsDelta = 0
const extend = []
const set = []
const mockContexts = require('./jest.fn.context.cjs')

function fixupAssertions() {
if (assertionsDelta === 0) return
Expand Down Expand Up @@ -34,6 +35,12 @@ function loadExpect(loadReason) {

const areNumeric = (...args) => args.every((a) => typeof a === 'number' || typeof a === 'bigint')

const mockCallCount = (x) => {
const state = x.mock
const context = mockContexts.get(state)
return context ? context.callCount() : state?.calls?.length
}

const matchers = {
__proto__: null,
toBe: (x, y) => Object.is(x, y),
Expand All @@ -54,8 +61,8 @@ const matchers = {
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,
toHaveBeenCalled: (x) => x?._isMockFunction && mockCallCount(x) > 0,
toHaveBeenCalledTimes: (x, c) => x?._isMockFunction && mockCallCount(x) === c,
toBeCalled: (...a) => matchers.toHaveBeenCalled(...a),
toBeCalledTimes: (...a) => matchers.toHaveBeenCalledTimes(...a),
toHaveBeenCalledOnce: (x) => matchers.toHaveBeenCalledTimes(x, 1),
Expand Down
1 change: 1 addition & 0 deletions src/jest.fn.context.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = new WeakMap()
2 changes: 2 additions & 0 deletions src/jest.fn.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { mock, assert } from './engine.js'
import mockContexts from './jest.fn.context.cjs'

const registry = new Set()
let callId = 0
Expand Down Expand Up @@ -111,6 +112,7 @@ export const jestfn = (baseimpl, parent, property) => {
return fnmock.calls.at(-1)?.arguments
},
}
mockContexts.set(jestfnmock, fnmock)

const fnProxyGet = (obj, key) => {
const wrap =
Expand Down
79 changes: 79 additions & 0 deletions tests/jest-superset/expect.mock-count.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const testOptimized = jest.exodus.platform === 'nova' ? test.skip : test

testOptimized('count matchers do not materialize call history', () => {
const fn = jest.fn()
const { get } = Object.getOwnPropertyDescriptor(fn.mock, 'calls')
let reads = 0
Object.defineProperty(fn.mock, 'calls', {
get() {
reads++
return get.call(this)
},
})

expect(fn).not.toHaveBeenCalled()
expect(fn).not.toBeCalled()
expect(fn).toHaveBeenCalledTimes(0)
fn('first')
expect(fn).toHaveBeenCalledOnce()
const bound = fn.bind(null, 'second')
bound()
expect(fn).toHaveBeenCalled()
expect(fn).toBeCalled()
expect(fn).toHaveBeenCalledTimes(2)
expect(bound).toBeCalledTimes(2)
expect(fn).not.toHaveBeenCalledTimes(1)
expect(fn).not.toBeCalledTimes(1)
expect(reads).toBe(0)
expect(fn.mock.calls).toEqual([['first'], ['second']])

fn.mockClear()
expect(fn).toHaveBeenCalledTimes(0)
expect(bound).not.toHaveBeenCalled()
fn()
expect(fn).toHaveBeenCalledTimes(1)
fn.mockReset()
expect(fn).toHaveBeenCalledTimes(0)
expect(reads).toBe(1)
})

test('count matchers use call history for unregistered mocks', () => {
const calls = []
const fn = (...args) => calls.push(args)
fn._isMockFunction = true
fn.mock = { calls }

expect(fn).not.toHaveBeenCalled()
expect(fn).toHaveBeenCalledTimes(0)
fn('first')
expect(fn).toHaveBeenCalled()
expect(fn).toHaveBeenCalledTimes(1)
calls.length = 0
expect(fn).not.toHaveBeenCalled()
expect(fn).toHaveBeenCalledTimes(0)
})

test('count matchers follow completed calls, including thrown calls', () => {
let completed = 0
const fn = jest.fn(() => {
expect(fn).toHaveBeenCalledTimes(completed)
throw new Error('mock failure')
})

for (let i = 0; i < 2; i++) {
expect(fn).toThrow('mock failure')
completed++
expect(fn).toHaveBeenCalledTimes(completed)
}
})

test('count matchers still report failures and reject non-mocks', () => {
const fn = jest.fn()
expect(() => expect(fn).toHaveBeenCalled()).toThrow()
expect(() => expect(fn).toHaveBeenCalledTimes(1)).toThrow()
expect(() => expect(fn).toHaveBeenCalledTimes()).toThrow()
fn()
expect(() => expect(fn).not.toHaveBeenCalled()).toThrow()
expect(() => expect(fn).not.toHaveBeenCalledTimes(1)).toThrow()
expect(() => expect(() => {}).toHaveBeenCalledTimes(0)).toThrow()
})