Skip to content

fix(jest): keep mocked hooks working after jest.resetAllMocks() - #749

Open
giaBaoJS wants to merge 1 commit into
appandflow:mainfrom
giaBaoJS:fix/jest-mock-survives-reset-all-mocks
Open

fix(jest): keep mocked hooks working after jest.resetAllMocks()#749
giaBaoJS wants to merge 1 commit into
appandflow:mainfrom
giaBaoJS:fix/jest-mock-survives-reset-all-mocks

Conversation

@giaBaoJS

Copy link
Copy Markdown

Fixes #551

@jacobp100 said in the issue:

A PR would be helpful if anyone wants to

Nobody has picked it up since, so here it is.

The problem

src/jest/mock.tsx wraps the two hooks in jest.fn(...) (added in #449) so tests can override them:

useSafeAreaInsets: jest.fn(() => { ... }),
useSafeAreaFrame: jest.fn(() => { ... }),

jest.resetAllMocks() removes the implementation from every mock function. Apps commonly call it from a global beforeEach/afterEach, and after that both hooks return undefined, so any component reading insets.top or frame.width throws:

TypeError: Cannot read properties of undefined (reading 'top')

This matches what @adamgeorgsson and @eppisapiafsl reported: the first test in a suite passes and every one after it fails. The current workarounds are patching the package to revert #449, or switching to jest.clearAllMocks() — which is not equivalent, and consumers should not have to know this.

The fix

Wrap the hooks in a Proxy that reinstalls the default implementation when getMockImplementation() reports it was stripped:

function mockHook<T extends (...args: never[]) => unknown>(implementation: T) {
  const hook = jest.fn(implementation);
  return new Proxy(hook, {
    apply(target, thisArg, args) {
      if (target.getMockImplementation() == null) {
        target.mockImplementation(implementation);
      }
      return Reflect.apply(target, thisArg, args);
    },
  });
}

mockReturnValue/mockImplementation set an implementation, so getMockImplementation() is non-null and the default is not reinstalled — overrides still win. Call tracking is untouched, since the proxy forwards to the same mock function.

This keeps the #449 behaviour (the hooks remain overridable jest.fns) rather than reverting it.

What I verified

Everything below was run locally on this branch, not inferred.

The defect reproduces on current main (f65d4d9, v5.9.0). The new test renders a component through jest.mock('react-native-safe-area-context', () => mockSafeAreaContext) — the exact setup the README prescribes — with jest.resetAllMocks() in a beforeEach. On unmodified main all three cases fail with TypeError: Cannot read properties of undefined (reading 'top' / 'width'). Removing only the jest.resetAllMocks() line makes them pass on unmodified main, which isolates the cause to the reset.

Counterfactual. With the test in place and src/jest/mock.tsx reverted to main: 3 failed. With the fix applied: 3 passed.

The two things people rely on still work, asserted in the test rather than argued:

  • jest.mocked(useSafeAreaInsets).mockReturnValue(...) after a reset still wins over the reinstated default (useSafeAreaFrame in the same render falls back to the default, so one assertion covers both paths).
  • toHaveBeenCalledTimes(1) on both hooks still holds after a reset.

No public type change. I built lib/typescript before and after the fix and diffed the generated jest/mock.d.ts — byte identical. Both hooks are still Mock<() => EdgeInsets> / Mock<() => Rect>.

Suite. yarn validate:jest goes from 4 suites / 21 tests to 5 suites / 24 tests, all passing, 11 snapshots unchanged. yarn format:prettier:check, yarn validate:eslint (still the same 3 pre-existing no-deep-imports warnings, none new) and yarn validate:typescript all pass.

Jest 30. I checked the fix against jest@30.4.2 in a scratch project. jest.fn(impl) still returns undefined after resetAllMocks() there, so the bug is not fixed upstream, and the proxied version returns the default, honours mockReturnValue, and stays jest.isMockFunction-true.

Packaging. npm pack --dry-run confirms the new src/jest/__tests__/ directory is excluded from the tarball by the existing !**/__tests__ entry in files.

What I could not verify

yarn test also runs format:clang:check and format:spotless:check. clang-format could not run on my machine — the bundled node_modules/clang-format/bin/darwin_x64/clang-format is an x86_64 binary and fails with spawn Unknown system error -86 on Apple Silicon. It fails identically on a clean checkout with zero changes, and this PR touches no .h/.cpp/.m/.mm/.java/.kt files, so neither native formatter is affected. CI runs on ubuntu-latest, where both should run normally.

The jest mock wraps useSafeAreaInsets and useSafeAreaFrame in jest.fn()
so tests can override them. jest.resetAllMocks(), which apps commonly
call from a global beforeEach/afterEach, strips the implementation off
every mock function. The hooks then return undefined and any component
reading insets.top or frame.width throws.

Wrap the two hooks in a Proxy that reinstalls the default implementation
when getMockImplementation() reports it has been stripped. Overrides made
with mockReturnValue/mockImplementation set an implementation, so they
still take precedence, and call tracking is untouched.

The generated jest/mock.d.ts is byte identical, so this is not a type
change.

Fixes appandflow#551
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Jest mock "TypeError: Cannot read properties of undefined (reading 'top')"

1 participant