From 7fb35fa04fc990fed0e0074c3f1e554b7d470737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Tue, 19 Nov 2024 06:24:29 -0800 Subject: [PATCH 1/2] [skip ci] Implement jest.fn(), expect().toHaveBeenCalled() and expect().toHaveBeenCalledTimes() (#47699) Summary: Changelog: [internal] Implements a `jest.fn()` and a subset of methods in `expect` using them (`.toHaveBeenCalled()` and `.toHaveBeenCalledTimes()`). Reviewed By: sammy-SC Differential Revision: D66118002 --- jest/integration/runtime/setup.js | 94 ++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 2 deletions(-) diff --git a/jest/integration/runtime/setup.js b/jest/integration/runtime/setup.js index 9dda572b621c40..9cd0e19dbbc4ec 100644 --- a/jest/integration/runtime/setup.js +++ b/jest/integration/runtime/setup.js @@ -114,6 +114,64 @@ global.it.skip = globalModifiers.pop(); }; +global.jest = { + fn: createMockFunction, +}; + +const MOCK_FN_TAG = Symbol('mock function'); + +function createMockFunction, TReturn>( + initialImplementation?: (...TArgs) => TReturn, +): JestMockFn { + let implementation: ?(...TArgs) => TReturn = initialImplementation; + + const mock: JestMockFn['mock'] = { + calls: [], + // $FlowExpectedError[incompatible-type] + lastCall: undefined, + instances: [], + contexts: [], + results: [], + }; + + const mockFunction = function (this: mixed, ...args: TArgs): TReturn { + let result: JestMockFn['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 { @@ -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}`, ); } } @@ -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`, ); } } @@ -183,6 +261,18 @@ class Expect { #maybeNotLabel(): string { return this.#isNot ? ' not' : ''; } + + #requireMock(): JestMockFn<$ReadOnlyArray, 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); From cc6d24b90564002d14cd21e6944ea1f30aa11483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Tue, 19 Nov 2024 06:24:29 -0800 Subject: [PATCH 2/2] Implement toThrow(message) with a specific error message string (#47700) Summary: Changelog: [internal] Add support for the string parameter for `toThrow` to assert for specific error messages. Reviewed By: sammy-SC Differential Revision: D66118001 --- jest/integration/runtime/setup.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/jest/integration/runtime/setup.js b/jest/integration/runtime/setup.js index 9cd0e19dbbc4ec..217d4662b9317f 100644 --- a/jest/integration/runtime/setup.js +++ b/jest/integration/runtime/setup.js @@ -215,17 +215,19 @@ class Expect { } } - toThrow(error: mixed): void { - if (error != null) { - throw new Error('toThrow() implementation does not accept arguments.'); + toThrow(expected?: string): void { + if (expected != null && typeof expected !== 'string') { + throw new Error( + 'toThrow() implementation only accepts strings as arguments.', + ); } let pass = false; try { // $FlowExpectedError[not-a-function] this.#received(); - } catch { - pass = true; + } catch (error) { + pass = expected != null ? error.message === expected : true; } if (!this.#isExpectedResult(pass)) { throw new Error(