From 21cdbbe4143342bec73871a5832a0c3d52c72307 Mon Sep 17 00:00:00 2001 From: Rob Hogan <2590098+robhogan@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:14:00 +0100 Subject: [PATCH] Don't rely on inline requires to apply mocks in JS tests React Native's Jest transform doesn't hoist `jest.mock` calls, and several tests only saw their mocks because inline requires deferred `require`s until first use. That holds for destructured requires only while the Babel preset lowers destructuring, which it does in this config as a side effect of lowering generators with regenerator. These tests now install mocks before the modules under test load, or load those modules after mocking, so they pass whether or not destructuring is lowered: - Require modules under test after the top-level `jest.mock` calls that affect them. - Use mock factories instead of spies or automocks where the code under test binds a function, or loads an ESM-only package, when it first loads. - Reload modules on each call or after `jest.resetModules()` where tests reset the registry. Changelog: [Internal] --- .../__tests__/publishTemplate-test.js | 10 ++-- .../verifyArtifactsAreOnMaven-test.js | 4 +- .../__tests__/verifyPublishedPackage-test.js | 4 +- .../__tests__/verifyReleaseOnNpm-test.js | 4 +- .../parsers/__tests__/parsers-commons-test.js | 47 +++++++++++-------- .../Animated/__tests__/Animated-test.js | 16 +++++-- .../LogBoxInspectorContainer-test.js | 18 +++---- .../LogBoxNotificationContainer-test.js | 14 +++--- .../__tests__/headers-xcframework-test.js | 15 ++++-- .../swiftpm/__tests__/headers-utils-test.js | 12 ++--- .../__tests__/prepare-app-utils-test.js | 29 +++++++----- .../releases-ci/__tests__/publish-npm-test.js | 8 +++- .../publish-updated-packages-test.js | 10 ++-- ...upload-release-assets-for-dotslash-test.js | 4 ++ .../utils/__tests__/npm-utils-test.js | 4 +- .../utils/__tests__/scm-utils-test.js | 4 +- 16 files changed, 120 insertions(+), 83 deletions(-) diff --git a/.github/workflow-scripts/__tests__/publishTemplate-test.js b/.github/workflow-scripts/__tests__/publishTemplate-test.js index 9f735a633977..25734752cf4c 100644 --- a/.github/workflow-scripts/__tests__/publishTemplate-test.js +++ b/.github/workflow-scripts/__tests__/publishTemplate-test.js @@ -7,11 +7,6 @@ * @format */ -const { - publishTemplate, - verifyPublishedTemplate, -} = require('../publishTemplate'); - const mockRun = jest.fn(); const mockSleep = jest.fn(); const mockGetNpmPackageInfo = jest.fn(); @@ -29,6 +24,11 @@ jest.mock('../verifyPublishedPackage.js', () => ({ verifyPublishedPackage: mockVerifyPublishedPackage, })); +const { + publishTemplate, + verifyPublishedTemplate, +} = require('../publishTemplate'); + const getMockGithub = () => ({ rest: { actions: { diff --git a/.github/workflow-scripts/__tests__/verifyArtifactsAreOnMaven-test.js b/.github/workflow-scripts/__tests__/verifyArtifactsAreOnMaven-test.js index 8e9758f28609..2216b94d8543 100644 --- a/.github/workflow-scripts/__tests__/verifyArtifactsAreOnMaven-test.js +++ b/.github/workflow-scripts/__tests__/verifyArtifactsAreOnMaven-test.js @@ -7,8 +7,6 @@ * @format */ -const {verifyArtifactsAreOnMaven} = require('../verifyArtifactsAreOnMaven'); - const mockSleep = jest.fn(); const silence = () => {}; const mockFetch = jest.fn(); @@ -19,6 +17,8 @@ jest.mock('../utils.js', () => ({ sleep: mockSleep, })); +const {verifyArtifactsAreOnMaven} = require('../verifyArtifactsAreOnMaven'); + process.exit = mockExit; global.fetch = mockFetch; diff --git a/.github/workflow-scripts/__tests__/verifyPublishedPackage-test.js b/.github/workflow-scripts/__tests__/verifyPublishedPackage-test.js index 95e22baa854e..78c19637e52e 100644 --- a/.github/workflow-scripts/__tests__/verifyPublishedPackage-test.js +++ b/.github/workflow-scripts/__tests__/verifyPublishedPackage-test.js @@ -7,8 +7,6 @@ * @format */ -const {verifyPublishedPackage} = require('../verifyPublishedPackage'); - const mockRun = jest.fn(); const mockSleep = jest.fn(); const mockGetNpmPackageInfo = jest.fn(); @@ -23,6 +21,8 @@ jest.mock('../utils.js', () => ({ getNpmPackageInfo: mockGetNpmPackageInfo, })); +const {verifyPublishedPackage} = require('../verifyPublishedPackage'); + describe('#verifyPublishedPackage', () => { beforeEach(jest.clearAllMocks); diff --git a/.github/workflow-scripts/__tests__/verifyReleaseOnNpm-test.js b/.github/workflow-scripts/__tests__/verifyReleaseOnNpm-test.js index 761c4352b408..e55f126183b5 100644 --- a/.github/workflow-scripts/__tests__/verifyReleaseOnNpm-test.js +++ b/.github/workflow-scripts/__tests__/verifyReleaseOnNpm-test.js @@ -7,8 +7,6 @@ * @format */ -const {verifyReleaseOnNpm} = require('../verifyReleaseOnNpm'); - const mockVerifyPublishedPackage = jest.fn(); const silence = () => {}; @@ -21,6 +19,8 @@ jest.mock('../verifyPublishedPackage.js', () => ({ verifyPublishedPackage: mockVerifyPublishedPackage, })); +const {verifyReleaseOnNpm} = require('../verifyReleaseOnNpm'); + describe('#verifyReleaseOnNPM', () => { beforeEach(jest.clearAllMocks); diff --git a/packages/react-native-codegen/src/parsers/__tests__/parsers-commons-test.js b/packages/react-native-codegen/src/parsers/__tests__/parsers-commons-test.js index dad048f3be18..d85c5581c78a 100644 --- a/packages/react-native-codegen/src/parsers/__tests__/parsers-commons-test.js +++ b/packages/react-native-codegen/src/parsers/__tests__/parsers-commons-test.js @@ -10,11 +10,33 @@ 'use-strict'; +// The buildSchema tests spy on `getConfigType`, which parsers-commons reads when +// it loads, so it's wrapped before any module that uses it is required. +jest.mock('../utils', () => { + const actual = jest.requireActual('../utils'); + return {...actual, getConfigType: jest.fn(actual.getConfigType)}; +}); + import type {ParserType} from '../errors'; +import typeof * as Utils from '../utils'; -import {FlowParser} from '../flow/parser'; -import {MockedParser} from '../parserMock'; -import { +const { + IncorrectModuleRegistryCallArgumentTypeParserError, + IncorrectModuleRegistryCallArityParserError, + MisnamedModuleInterfaceParserError, + ModuleInterfaceNotFoundParserError, + MoreThanOneModuleInterfaceParserError, + MoreThanOneModuleRegistryCallsParserError, + ParserError, + UnsupportedObjectPropertyTypeAnnotationParserError, + UntypedModuleRegistryCallParserError, + UnusedModuleInterfaceParserError, +} = require('../errors'); +const {buildComponentSchema} = require('../flow/components'); +const {flowTranslateTypeAnnotation} = require('../flow/modules/index'); +const {FlowParser} = require('../flow/parser'); +const {MockedParser} = require('../parserMock'); +const { assertGenericTypeAnnotationHasExactlyOneTypeParameter, buildSchema, buildSchemaFromConfigType, @@ -30,22 +52,7 @@ import { propertyNames, unwrapNullable, wrapNullable, -} from '../parsers-commons'; - -const { - IncorrectModuleRegistryCallArgumentTypeParserError, - IncorrectModuleRegistryCallArityParserError, - MisnamedModuleInterfaceParserError, - ModuleInterfaceNotFoundParserError, - MoreThanOneModuleInterfaceParserError, - MoreThanOneModuleRegistryCallsParserError, - ParserError, - UnsupportedObjectPropertyTypeAnnotationParserError, - UntypedModuleRegistryCallParserError, - UnusedModuleInterfaceParserError, -} = require('../errors'); -const {buildComponentSchema} = require('../flow/components'); -const {flowTranslateTypeAnnotation} = require('../flow/modules/index'); +} = require('../parsers-commons'); const {buildModuleSchema} = require('../parsers-commons.js'); const {Visitor} = require('../parsers-primitives'); const {wrapComponentSchema} = require('../schema.js'); @@ -665,7 +672,7 @@ describe('buildSchemaFromConfigType', () => { }); describe('buildSchema', () => { - const getConfigTypeSpy = jest.spyOn(require('../utils'), 'getConfigType'); + const getConfigTypeSpy = require('../utils').getConfigType; describe('when there is no codegenNativeComponent and no TurboModule', () => { const contents = ''; diff --git a/packages/react-native/Libraries/Animated/__tests__/Animated-test.js b/packages/react-native/Libraries/Animated/__tests__/Animated-test.js index 3d4b8c70fdc7..8e7aa3f02c5a 100644 --- a/packages/react-native/Libraries/Animated/__tests__/Animated-test.js +++ b/packages/react-native/Libraries/Animated/__tests__/Animated-test.js @@ -12,13 +12,13 @@ import * as React from 'react'; const {PlatformColor} = require('../../StyleSheet/PlatformColorValueTypes'); let Animated = require('../Animated').default; -const AnimatedProps = require('../nodes/AnimatedProps').default; -const { +let AnimatedProps = require('../nodes/AnimatedProps').default; +let { create, unmount, update, } = require('@react-native/jest-preset/jest/renderer'); -const TestRenderer = require('react-test-renderer'); +let TestRenderer = require('react-test-renderer'); // WORKAROUND: `jest.runAllTicks` skips tasks scheduled w/ `queueMicrotask`. function mockQueueMicrotask() { @@ -37,6 +37,16 @@ function mockQueueMicrotask() { describe('Animated', () => { beforeEach(() => { jest.resetModules(); + // Reload after the reset, so that Animated and the renderer share a copy + // of React. + Animated = require('../Animated').default; + AnimatedProps = require('../nodes/AnimatedProps').default; + ({ + create, + unmount, + update, + } = require('@react-native/jest-preset/jest/renderer')); + TestRenderer = require('react-test-renderer'); }); mockQueueMicrotask(); diff --git a/packages/react-native/Libraries/LogBox/__tests__/LogBoxInspectorContainer-test.js b/packages/react-native/Libraries/LogBox/__tests__/LogBoxInspectorContainer-test.js index 89a9366e1b59..cd1601713a05 100644 --- a/packages/react-native/Libraries/LogBox/__tests__/LogBoxInspectorContainer-test.js +++ b/packages/react-native/Libraries/LogBox/__tests__/LogBoxInspectorContainer-test.js @@ -10,15 +10,6 @@ 'use strict'; -const BackHandler: $FlowFixMe = require('../../Utilities/BackHandler').default; -const LogBoxData = require('../Data/LogBoxData'); -const LogBoxLog = require('../Data/LogBoxLog').default; -const { - _LogBoxInspectorContainer: LogBoxInspectorContainer, -} = require('../LogBoxInspectorContainer'); -const render = require('@react-native/jest-preset/jest/renderer'); -const React = require('react'); - // Mock `LogBoxInspector` because we are interested in snapshotting the behavior // of `LogBoxInspectorContainer`, not `LogBoxInspector`. jest.mock('../UI/LogBoxInspector', () => ({ @@ -30,6 +21,15 @@ jest.mock('../../Utilities/BackHandler', () => require('../../Utilities/__mocks__/BackHandler'), ); +const BackHandler: $FlowFixMe = require('../../Utilities/BackHandler').default; +const LogBoxData = require('../Data/LogBoxData'); +const LogBoxLog = require('../Data/LogBoxLog').default; +const { + _LogBoxInspectorContainer: LogBoxInspectorContainer, +} = require('../LogBoxInspectorContainer'); +const render = require('@react-native/jest-preset/jest/renderer'); +const React = require('react'); + describe('LogBoxInspectorContainer', () => { let output; diff --git a/packages/react-native/Libraries/LogBox/__tests__/LogBoxNotificationContainer-test.js b/packages/react-native/Libraries/LogBox/__tests__/LogBoxNotificationContainer-test.js index 16d3292fa2bb..343331d4fb9e 100644 --- a/packages/react-native/Libraries/LogBox/__tests__/LogBoxNotificationContainer-test.js +++ b/packages/react-native/Libraries/LogBox/__tests__/LogBoxNotificationContainer-test.js @@ -10,13 +10,6 @@ 'use strict'; -const BackHandler: $FlowFixMe = require('../../Utilities/BackHandler').default; -const LogBoxData = require('../Data/LogBoxData'); -const LogBoxLog = require('../Data/LogBoxLog').default; -const {LogBoxNotificationContainer} = require('../LogBoxNotificationContainer'); -const render = require('@react-native/jest-preset/jest/renderer'); -const React = require('react'); - // Mock `LogBoxLogNotification` because we are interested in snapshotting the // behavior of `LogBoxNotificationContainer`, not `LogBoxLogNotification`. jest.mock('../UI/LogBoxNotification', () => ({ @@ -28,6 +21,13 @@ jest.mock('../../Utilities/BackHandler', () => require('../../Utilities/__mocks__/BackHandler'), ); +const BackHandler: $FlowFixMe = require('../../Utilities/BackHandler').default; +const LogBoxData = require('../Data/LogBoxData'); +const LogBoxLog = require('../Data/LogBoxLog').default; +const {LogBoxNotificationContainer} = require('../LogBoxNotificationContainer'); +const render = require('@react-native/jest-preset/jest/renderer'); +const React = require('react'); + describe('LogBoxNotificationContainer', () => { let output; diff --git a/packages/react-native/scripts/ios-prebuild/__tests__/headers-xcframework-test.js b/packages/react-native/scripts/ios-prebuild/__tests__/headers-xcframework-test.js index b9492192a081..7a902be56f2c 100644 --- a/packages/react-native/scripts/ios-prebuild/__tests__/headers-xcframework-test.js +++ b/packages/react-native/scripts/ios-prebuild/__tests__/headers-xcframework-test.js @@ -10,11 +10,18 @@ 'use strict'; +// headers-xcframework reads `execFileSync` when it loads, so it's mocked before +// the module is required. +const mockExecFileSync = jest.fn(); +jest.mock('node:child_process', () => ({ + ...jest.requireActual('node:child_process'), + execFileSync: mockExecFileSync, +})); + const { buildDepsHeadersXcframework, stubSlicesFromXcframework, } = require('../headers-xcframework'); -const childProcess = require('node:child_process'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); @@ -67,9 +74,9 @@ describe('stubSlicesFromXcframework', () => { // SupportedPlatform/Variant -> key mapping and the unknown-slice guard can be // tested without a real xcframework or macOS tooling. const mockPlist = (obj /*: unknown */) => - jest - .spyOn(childProcess, 'execFileSync') - .mockReturnValue(Buffer.from(JSON.stringify(obj) ?? '', 'utf8')); + mockExecFileSync.mockReturnValue( + Buffer.from(JSON.stringify(obj) ?? '', 'utf8'), + ); afterEach(() => { jest.restoreAllMocks(); diff --git a/packages/react-native/scripts/swiftpm/__tests__/headers-utils-test.js b/packages/react-native/scripts/swiftpm/__tests__/headers-utils-test.js index cd48b6bc063a..d9a4971ff4c2 100644 --- a/packages/react-native/scripts/swiftpm/__tests__/headers-utils-test.js +++ b/packages/react-native/scripts/swiftpm/__tests__/headers-utils-test.js @@ -10,6 +10,12 @@ 'use strict'; +// Mock all required modules +jest.mock('../utils'); +jest.mock('../headers-mappings'); +jest.mock('node:fs'); +jest.mock('node:path'); + const { symlinkCodegenHeaders, symlinkHeadersFromPath, @@ -18,12 +24,6 @@ const { symlinkThirdPartyDependenciesHeaders, } = require('../headers-utils'); -// Mock all required modules -jest.mock('../utils'); -jest.mock('../headers-mappings'); -jest.mock('node:fs'); -jest.mock('node:path'); - describe('symlinkHeadersFromPath', () => { let mockUtils; let mockFs; diff --git a/packages/react-native/scripts/swiftpm/__tests__/prepare-app-utils-test.js b/packages/react-native/scripts/swiftpm/__tests__/prepare-app-utils-test.js index c4cb0f884101..285695a9f7e0 100644 --- a/packages/react-native/scripts/swiftpm/__tests__/prepare-app-utils-test.js +++ b/packages/react-native/scripts/swiftpm/__tests__/prepare-app-utils-test.js @@ -10,17 +10,6 @@ 'use strict'; -const { - configureAppForSwift, - createHardlinks, - findXcodeProjectDirectory, - generateCodegenArtifacts, - prepareHeaders, - runIosPrebuild, - runPodDeintegrate, - setBuildFromSource, -} = require('../prepare-app-utils'); - // Mock child_process module jest.mock('node:child_process'); @@ -33,8 +22,11 @@ jest.mock('../headers-utils'); // Mock prepare-app-dependencies-headers module jest.mock('../prepare-app-dependencies-headers'); -// Mock codegen executor module -jest.mock('../../codegen/generate-artifacts-executor'); +// Mock codegen executor module. Automocking loads the real module, which reads +// package.json through the mocked `fs`. +jest.mock('../../codegen/generate-artifacts-executor', () => ({ + execute: jest.fn(), +})); // Mock path module for absolute paths jest.mock('node:path', () => { @@ -46,6 +38,17 @@ jest.mock('node:path', () => { }; }); +const { + configureAppForSwift, + createHardlinks, + findXcodeProjectDirectory, + generateCodegenArtifacts, + prepareHeaders, + runIosPrebuild, + runPodDeintegrate, + setBuildFromSource, +} = require('../prepare-app-utils'); + describe('createHardlinks', () => { let mockSymlinkReactNativeHeaders; let mockSymlinkThirdPartyDependenciesHeaders; diff --git a/scripts/releases-ci/__tests__/publish-npm-test.js b/scripts/releases-ci/__tests__/publish-npm-test.js index de37e4c92939..83d788099a24 100644 --- a/scripts/releases-ci/__tests__/publish-npm-test.js +++ b/scripts/releases-ci/__tests__/publish-npm-test.js @@ -23,10 +23,16 @@ const getPackagesMock = jest.fn(); const updateHermesVersionsToPrebuiltMock = jest.fn(); const getBranchName = jest.fn(); +import type {BuildType} from '../../releases/utils/version-utils'; + const {REPO_ROOT} = require('../../shared/consts'); -const {publishNpm} = require('../publish-npm'); const path = require('node:path'); +// Required on each call, so that publish-npm picks up the mocks and module +// registry that each test sets up after this file loads. +const publishNpm = (buildType: BuildType): Promise => + require('../publish-npm').publishNpm(buildType); + let consoleLog; describe('publish-npm', () => { diff --git a/scripts/releases-ci/__tests__/publish-updated-packages-test.js b/scripts/releases-ci/__tests__/publish-updated-packages-test.js index 76e721789881..78275ec5ef93 100644 --- a/scripts/releases-ci/__tests__/publish-updated-packages-test.js +++ b/scripts/releases-ci/__tests__/publish-updated-packages-test.js @@ -8,11 +8,6 @@ * @format */ -const { - getTagsFromCommitMessage, - publishUpdatedPackages, -} = require('../publish-updated-packages'); - const getPackagesMock = jest.fn(); const execSync = jest.fn(); const execMock = jest.fn(); @@ -23,6 +18,11 @@ jest.mock('shelljs', () => ({exec: execMock})); jest.mock('../../shared/monorepoUtils', () => ({ getPackages: getPackagesMock, })); + +const { + getTagsFromCommitMessage, + publishUpdatedPackages, +} = require('../publish-updated-packages'); // $FlowFixMe[cannot-write] global.fetch = fetchMock; diff --git a/scripts/releases/__tests__/upload-release-assets-for-dotslash-test.js b/scripts/releases/__tests__/upload-release-assets-for-dotslash-test.js index cf125a11de8e..6f9cf680e0d1 100644 --- a/scripts/releases/__tests__/upload-release-assets-for-dotslash-test.js +++ b/scripts/releases/__tests__/upload-release-assets-for-dotslash-test.js @@ -10,6 +10,10 @@ 'use strict'; +// @octokit/rest is ESM-only, which Jest can't load, and these tests pass their +// own client. +jest.mock('@octokit/rest', () => ({Octokit: jest.fn()})); + const { getReleaseAssetMap, uploadReleaseAssetsForDotSlashFile, diff --git a/scripts/releases/utils/__tests__/npm-utils-test.js b/scripts/releases/utils/__tests__/npm-utils-test.js index 2ece11afa4fd..b3e43d69fe94 100644 --- a/scripts/releases/utils/__tests__/npm-utils-test.js +++ b/scripts/releases/utils/__tests__/npm-utils-test.js @@ -8,8 +8,6 @@ * @format */ -const {getNpmInfo, publishPackage} = require('../npm-utils'); - const execMock = jest.fn(); const getCurrentCommitMock = jest.fn(); const exitIfNotOnGitMock = jest.fn(); @@ -23,6 +21,8 @@ jest.mock('./../scm-utils', () => ({ exitIfNotOnGit: exitIfNotOnGitMock, })); +const {getNpmInfo, publishPackage} = require('../npm-utils'); + describe('npm-utils', () => { beforeEach(() => { jest.resetModules(); diff --git a/scripts/releases/utils/__tests__/scm-utils-test.js b/scripts/releases/utils/__tests__/scm-utils-test.js index 250d8a84b36c..f17b40714fb1 100644 --- a/scripts/releases/utils/__tests__/scm-utils-test.js +++ b/scripts/releases/utils/__tests__/scm-utils-test.js @@ -8,8 +8,6 @@ * @format */ -const {isTaggedLatest} = require('../scm-utils'); - let execResult = null; jest @@ -37,6 +35,8 @@ jest ), })); +const {isTaggedLatest} = require('../scm-utils'); + describe('scm-utils', () => { beforeEach(() => { jest.resetModules();