Skip to content

Commit

Permalink
Implement jest.fn(), expect().toHaveBeenCalled() and expect().toHaveB…
Browse files Browse the repository at this point in the history
…eenCalledTimes() (facebook#47699)

Summary:

Changelog: [internal]

Implements a `jest.fn()` and a subset of methods in `expect` using them (`.toHaveBeenCalled()` and `.toHaveBeenCalledTimes()`).

Differential Revision: D66118002
  • Loading branch information
rubennorte authored and facebook-github-bot committed Nov 19, 2024
1 parent 5cc97c5 commit 7f9de77
Showing 1 changed file with 92 additions and 2 deletions.
94 changes: 92 additions & 2 deletions jest/integration/runtime/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,64 @@ global.it.skip =
globalModifiers.pop();
};

global.jest = {
fn: createMockFunction,
};

const MOCK_FN_TAG = Symbol('mock function');

function createMockFunction<TArgs: $ReadOnlyArray<mixed>, TReturn>(
initialImplementation?: (...TArgs) => TReturn,
): JestMockFn<TArgs, TReturn> {
let implementation: ?(...TArgs) => TReturn = initialImplementation;

const mock: JestMockFn<TArgs, TReturn>['mock'] = {
calls: [],
// $FlowExpectedError[incompatible-type]
lastCall: undefined,
instances: [],
contexts: [],
results: [],
};

const mockFunction = function (this: mixed, ...args: TArgs): TReturn {
let result: JestMockFn<TArgs, TReturn>['mock']['results'][number] = {
isThrow: false,
// $FlowExpectedError[incompatible-type]
value: undefined,
};
if (implementation != null) {
try {
result.value = implementation.apply(this, args);
} catch (error) {
result.isThrow = true;
result.value = error;
}
}
mock.calls.push(args);
mock.lastCall = args;
// $FlowExpectedError[incompatible-call]
mock.instances.push(new.target ? this : undefined);
mock.contexts.push(this);
mock.results.push(result);
if (result.isThrow) {
throw result.value;
}
return result.value;
};
mockFunction.mock = mock;
// $FlowExpectedError[invalid-computed-prop]
mockFunction[MOCK_FN_TAG] = true;
// $FlowExpectedError[prop-missing]
return mockFunction;
}
// flowlint unsafe-getters-setters:off
class Expect {
Expand Down Expand Up @@ -152,7 +210,7 @@ class Expect {
Math.abs(expected - Number(this.#received)) < Math.pow(10, -precision);
if (!this.#isExpectedResult(pass)) {
throw new Error(
`expected ${String(this.#received)}${this.#maybeNotLabel()} to be close to ${expected}`,
`Expected ${String(this.#received)}${this.#maybeNotLabel()} to be close to ${expected}`,
);
}
}
Expand All @@ -171,7 +229,27 @@ class Expect {
}
if (!this.#isExpectedResult(pass)) {
throw new Error(
`expected ${String(this.#received)}${this.#maybeNotLabel()} to throw`,
`Expected ${String(this.#received)}${this.#maybeNotLabel()} to throw`,
);
}
}
toHaveBeenCalled(): void {
const mock = this.#requireMock();
const pass = mock.calls.length > 0;
if (!this.#isExpectedResult(pass)) {
throw new Error(
`Expected ${String(this.#received)}${this.#maybeNotLabel()} to have been called, but it was${this.#isNot ? '' : "n't"}`,
);
}
}

toHaveBeenCalledTimes(times: number): void {
const mock = this.#requireMock();
const pass = mock.calls.length === times;
if (!this.#isExpectedResult(pass)) {
throw new Error(
`Expected ${String(this.#received)}${this.#maybeNotLabel()} to have been called ${times} times, but it was called ${mock.calls.length} times`,
);
}
}
Expand All @@ -183,6 +261,18 @@ class Expect {
#maybeNotLabel(): string {
return this.#isNot ? ' not' : '';
}

#requireMock(): JestMockFn<$ReadOnlyArray<mixed>, mixed>['mock'] {
// $FlowExpectedError[incompatible-use]
if (!this.#received?.[MOCK_FN_TAG]) {
throw new Error(
`Expected ${String(this.#received)} to be a mock function, but it wasn't`,
);
}

// $FlowExpectedError[incompatible-use]
return this.#received.mock;
}
}

global.expect = (received: mixed) => new Expect(received);
Expand Down

0 comments on commit 7f9de77

Please sign in to comment.