From f267e33271bf9f29c0758afb2355b28c3ff8636e Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 4 Sep 2026 18:15:10 -0700 Subject: [PATCH 01/16] Run all Jest tests through the OXC transformer babel-jest was still transforming the regular suite so jest.mock stayed hoisted. Route every JS/TS file through OXC, then a small Babel pass for loose CJS, import() lowering, and mock hoisting. Keep babel-jest only as the node_modules/Flow fallback. --- .github/workflows/test.yml | 2 +- config/babel/oxcJestTransformer.js | 67 +++++++++++++++++++++------- jest.config.js | 9 ++-- tests/tooling/oxcTransformer.test.ts | 25 ++++++++--- 4 files changed, 76 insertions(+), 27 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a05f12967e03..88b987174efb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,7 +39,7 @@ jobs: # Per-shard key: each shard only instruments the ~1/8 of suites it runs, so a single # shared key freezes one shard's partial cache for all shards. The hash refreshes the # cache when dependencies or Babel config change; restore-keys warms it in between. - key: ${{ runner.os }}-jest-${{ matrix.chunk }}-${{ hashFiles('package-lock.json', 'patches/**', 'babel.config.js') }} + key: ${{ runner.os }}-jest-${{ matrix.chunk }}-${{ hashFiles('package-lock.json', 'patches/**', 'babel.config.js', 'jest.config.js', 'config/babel/oxcJestTransformer.js', 'config/babel/reactCompilerConfig.js') }} restore-keys: ${{ runner.os }}-jest-${{ matrix.chunk }}- - name: Jest tests diff --git a/config/babel/oxcJestTransformer.js b/config/babel/oxcJestTransformer.js index 33a1f167c35f..a0d9b3ef607c 100644 --- a/config/babel/oxcJestTransformer.js +++ b/config/babel/oxcJestTransformer.js @@ -1,10 +1,14 @@ const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); -const esbuild = require('esbuild'); +const babel = require('@babel/core'); const {transformSync} = require('oxc-transform-react'); const babelJest = require('babel-jest'); +const BABEL_CORE_VERSION = require('@babel/core/package.json').version; +const DYNAMIC_IMPORT_PLUGIN_VERSION = require('@babel/plugin-transform-dynamic-import/package.json').version; +const CJS_PLUGIN_VERSION = require('@babel/plugin-transform-modules-commonjs/package.json').version; +const JEST_HOIST_VERSION = require('babel-plugin-jest-hoist/package.json').version; const OXC_TRANSFORM_REACT_VERSION = require('oxc-transform-react/package.json').version; const BaseReactCompilerConfig = require('./reactCompilerConfig'); @@ -14,6 +18,7 @@ const NODE_MODULES_RE = /[/\\]node_modules[/\\]/; const TESTS_RE = /[/\\]tests[/\\]/; const JEST_SETUP_RE = /[/\\]jest[/\\]/; const MOCKS_RE = /[/\\]__mocks__[/\\]/; +const JEST_HOIST_RE = /\bjest\s*\.\s*(mock|unmock|deepUnmock|disableAutomock|enableAutomock)\b/; const TRANSFORMER_SOURCE = fs.readFileSync(__filename); const REACT_COMPILER_CONFIG_KEY = JSON.stringify(BaseReactCompilerConfig); @@ -24,6 +29,10 @@ const REACT_COMPILER_OPTIONS = { eslintSuppressionRules: [], }; +const CJS_PLUGIN_OPTIONS = {loose: true, strictMode: false}; +const CJS_PLUGINS = ['@babel/plugin-transform-dynamic-import', ['@babel/plugin-transform-modules-commonjs', CJS_PLUGIN_OPTIONS]]; +const CJS_AND_HOIST_PLUGINS = [...CJS_PLUGINS, 'babel-plugin-jest-hoist']; + function getLang(filename) { const ext = path.extname(filename).slice(1); if (ext === 'tsx') { @@ -36,7 +45,33 @@ function getLang(filename) { } function shouldUseOxc(filename) { - return !NODE_MODULES_RE.test(filename) && !TESTS_RE.test(filename) && !JEST_SETUP_RE.test(filename) && !MOCKS_RE.test(filename); + return !NODE_MODULES_RE.test(filename); +} + +function shouldRunReactCompiler(filename) { + return !TESTS_RE.test(filename) && !JEST_SETUP_RE.test(filename) && !MOCKS_RE.test(filename); +} + +function toCommonJS(code, sourcePath, inputSourceMap) { + const plugins = JEST_HOIST_RE.test(code) ? CJS_AND_HOIST_PLUGINS : CJS_PLUGINS; + const result = babel.transformSync(code, { + filename: sourcePath, + ast: false, + code: true, + babelrc: false, + configFile: false, + compact: false, + sourceType: 'module', + sourceMaps: true, + inputSourceMap: inputSourceMap ?? undefined, + plugins, + }); + + if (!result?.code) { + return {code, map: inputSourceMap}; + } + + return {code: result.code, map: result.map ?? inputSourceMap}; } function processWithOxc(sourceText, sourcePath) { @@ -44,22 +79,14 @@ function processWithOxc(sourceText, sourcePath) { lang: getLang(sourcePath), sourcemap: true, jsx: {runtime: 'automatic', development: true}, - reactCompiler: REACT_COMPILER_OPTIONS, + reactCompiler: shouldRunReactCompiler(sourcePath) ? REACT_COMPILER_OPTIONS : false, }); if (oxcResult.fatal || !oxcResult.code) { return null; } - const cjs = esbuild.transformSync(oxcResult.code, { - loader: 'js', - format: 'cjs', - supported: {'dynamic-import': false}, - sourcefile: sourcePath, - sourcemap: true, - }); - - return {code: cjs.code, map: cjs.map}; + return toCommonJS(oxcResult.code, sourcePath, oxcResult.map); } module.exports = { @@ -76,15 +103,23 @@ module.exports = { .update(sourcePath) .update(TRANSFORMER_SOURCE) .update(REACT_COMPILER_CONFIG_KEY) - .update(esbuild.version) + .update(JSON.stringify(CJS_PLUGIN_OPTIONS)) + .update(BABEL_CORE_VERSION) + .update(DYNAMIC_IMPORT_PLUGIN_VERSION) + .update(CJS_PLUGIN_VERSION) + .update(JEST_HOIST_VERSION) .update(OXC_TRANSFORM_REACT_VERSION) .digest('hex'); }, process(sourceText, sourcePath, transformOptions) { if (shouldUseOxc(sourcePath)) { - const result = processWithOxc(sourceText, sourcePath); - if (result) { - return result; + try { + const result = processWithOxc(sourceText, sourcePath); + if (result) { + return result; + } + } catch { + // Fall through to babel-jest for syntax OXC or the CJS pass cannot parse. } } diff --git a/jest.config.js b/jest.config.js index 14b6e257de98..f69f876a124b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -18,10 +18,11 @@ module.exports = { `/?(*.)+(spec|test).${testFileExtension}`, ], transform: { - // Reassure re-transforms ~7k files under `--max-opt=1` (V8 sparkplug only), which - // makes Babel ~half of each measure job. OXC + esbuild is native and stays fast - // without TurboFan. Test files stay on babel-jest so `jest.mock` is still hoisted. - '^.+\\.[jt]sx?$': isPerfTestRun ? '/config/babel/oxcJestTransformer.js' : 'babel-jest', + // OXC compiles TS/JSX (and React Compiler on app sources). A small Babel pass then + // emits loose CommonJS, lowers import(), and hoists jest.mock so test files no + // longer need a babel-jest split. Native OXC stays fast under Reassure's + // `--max-opt=1` (V8 sparkplug only). + '^.+\\.[jt]sx?$': '/config/babel/oxcJestTransformer.js', '^.+\\.svg?$': 'jest-transformer-svg', }, transformIgnorePatterns: [ diff --git a/tests/tooling/oxcTransformer.test.ts b/tests/tooling/oxcTransformer.test.ts index 85d92cc27dbb..2fbbcd6f20cf 100644 --- a/tests/tooling/oxcTransformer.test.ts +++ b/tests/tooling/oxcTransformer.test.ts @@ -3,7 +3,7 @@ import {describe, expect, it} from 'bun:test'; import {createRequire} from 'node:module'; import path from 'node:path'; -type TransformResult = {code: string}; +type TransformResult = {code: string; map?: {sources?: string[]}}; type OxcTransformer = { process: (sourceText: string, sourcePath: string, transformOptions: unknown) => TransformResult; @@ -31,10 +31,10 @@ describe('oxcTransformer', () => { } `; const result = oxcTransformer.process(source, path.resolve('src/libs/math.ts'), transformOptions); - expect(result.code).toContain('module.exports'); - expect(result.code).toContain('add: () => add'); + expect(result.code).toContain('exports.add = add'); expect(result.code).not.toMatch(/^export /m); expect(result.code).not.toContain(': number'); + expect(result.map?.sources?.some((source) => source.endsWith('math.ts'))).toBe(true); }); it('runs React Compiler on app components', () => { @@ -48,15 +48,28 @@ describe('oxcTransformer', () => { expect(result.code).toContain('jsxDEV'); }); - it('leaves test files on babel-jest so jest.mock is hoisted', () => { + it.each(['tests/unit/Hello.test.tsx', 'jest/setup.tsx', '__mocks__/Hello.tsx'])('skips React Compiler on %s', (relativePath) => { + const source = ` + export function Hello({name}: {name: string}) { + return
{name.toUpperCase()}
; + } + `; + const result = oxcTransformer.process(source, path.resolve(relativePath), transformOptions); + expect(result.code).not.toMatch(/compiler-runtime|_c\(/); + expect(result.code).toContain('jsxDEV'); + }); + + it('hoists jest.mock above require() after CJS conversion', () => { const source = ` import foo from './foo'; jest.mock('./foo'); - export const x = 1; + export const x = foo; `; const result = oxcTransformer.process(source, path.resolve('tests/perf-test/Hello.perf-test.tsx'), transformOptions); expect(result.code).toContain('_getJestObj().mock("./foo")'); - expect(result.code.indexOf('_getJestObj().mock')).toBeLessThan(result.code.indexOf('exports.x')); + expect(result.code).toMatch(/require\(['"]\.\/foo['"]\)/); + expect(result.code.indexOf('_getJestObj().mock')).toBeLessThan(result.code.search(/require\(['"]\.\/foo['"]\)/)); + expect(result.code).toContain('exports.x'); }); it('lowers dynamic import() so Jest still owns the module graph', () => { From 2c0cc384a6cf5209e507c885fa2bb8eefca840e9 Mon Sep 17 00:00:00 2001 From: rory Date: Sun, 6 Sep 2026 09:29:10 -0700 Subject: [PATCH 02/16] Lower const/let in the OXC Jest CJS pass CI failed with TDZ on circular imports and jest.mock factories that closed over const. Babel used to rewrite those to var; restore that with plugin-transform-block-scoping so existing tests keep working. --- config/babel/oxcJestTransformer.js | 8 +++++++- tests/tooling/oxcTransformer.test.ts | 15 ++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/config/babel/oxcJestTransformer.js b/config/babel/oxcJestTransformer.js index a0d9b3ef607c..bd6917a0a11b 100644 --- a/config/babel/oxcJestTransformer.js +++ b/config/babel/oxcJestTransformer.js @@ -6,6 +6,7 @@ const {transformSync} = require('oxc-transform-react'); const babelJest = require('babel-jest'); const BABEL_CORE_VERSION = require('@babel/core/package.json').version; +const BLOCK_SCOPING_PLUGIN_VERSION = require('@babel/plugin-transform-block-scoping/package.json').version; const DYNAMIC_IMPORT_PLUGIN_VERSION = require('@babel/plugin-transform-dynamic-import/package.json').version; const CJS_PLUGIN_VERSION = require('@babel/plugin-transform-modules-commonjs/package.json').version; const JEST_HOIST_VERSION = require('babel-plugin-jest-hoist/package.json').version; @@ -30,7 +31,11 @@ const REACT_COMPILER_OPTIONS = { }; const CJS_PLUGIN_OPTIONS = {loose: true, strictMode: false}; -const CJS_PLUGINS = ['@babel/plugin-transform-dynamic-import', ['@babel/plugin-transform-modules-commonjs', CJS_PLUGIN_OPTIONS]]; +const CJS_PLUGINS = [ + '@babel/plugin-transform-block-scoping', + '@babel/plugin-transform-dynamic-import', + ['@babel/plugin-transform-modules-commonjs', CJS_PLUGIN_OPTIONS], +]; const CJS_AND_HOIST_PLUGINS = [...CJS_PLUGINS, 'babel-plugin-jest-hoist']; function getLang(filename) { @@ -105,6 +110,7 @@ module.exports = { .update(REACT_COMPILER_CONFIG_KEY) .update(JSON.stringify(CJS_PLUGIN_OPTIONS)) .update(BABEL_CORE_VERSION) + .update(BLOCK_SCOPING_PLUGIN_VERSION) .update(DYNAMIC_IMPORT_PLUGIN_VERSION) .update(CJS_PLUGIN_VERSION) .update(JEST_HOIST_VERSION) diff --git a/tests/tooling/oxcTransformer.test.ts b/tests/tooling/oxcTransformer.test.ts index 2fbbcd6f20cf..6f212b14e8ba 100644 --- a/tests/tooling/oxcTransformer.test.ts +++ b/tests/tooling/oxcTransformer.test.ts @@ -34,7 +34,7 @@ describe('oxcTransformer', () => { expect(result.code).toContain('exports.add = add'); expect(result.code).not.toMatch(/^export /m); expect(result.code).not.toContain(': number'); - expect(result.map?.sources?.some((source) => source.endsWith('math.ts'))).toBe(true); + expect(result.map?.sources?.some((mapSource) => mapSource.endsWith('math.ts'))).toBe(true); }); it('runs React Compiler on app components', () => { @@ -59,6 +59,19 @@ describe('oxcTransformer', () => { expect(result.code).toContain('jsxDEV'); }); + it('lowers const in jest.mock factories so circular imports do not TDZ', () => { + const source = ` + const mockedReportID = '1'; + jest.mock('./foo', () => ({ + parseReportRouteParams: () => ({reportID: mockedReportID}), + })); + export const x = mockedReportID; + `; + const result = oxcTransformer.process(source, path.resolve('tests/unit/Hello.test.ts'), transformOptions); + expect(result.code).toMatch(/var mockedReportID/); + expect(result.code).not.toMatch(/\bconst mockedReportID\b/); + }); + it('hoists jest.mock above require() after CJS conversion', () => { const source = ` import foo from './foo'; From 08f306654789df74b9e9c5d81841d3087ed96561 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 7 Sep 2026 11:31:13 -0700 Subject: [PATCH 03/16] Fix oxfmt and knip on the OXC Jest transformer Collapse CJS_PLUGINS to satisfy oxfmt, and ignore the Babel plugins the transformer loads by string name so knip does not flag them as unlisted. --- config/babel/oxcJestTransformer.js | 6 +----- knip.json | 6 +++++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/config/babel/oxcJestTransformer.js b/config/babel/oxcJestTransformer.js index bd6917a0a11b..8d93fa0a547a 100644 --- a/config/babel/oxcJestTransformer.js +++ b/config/babel/oxcJestTransformer.js @@ -31,11 +31,7 @@ const REACT_COMPILER_OPTIONS = { }; const CJS_PLUGIN_OPTIONS = {loose: true, strictMode: false}; -const CJS_PLUGINS = [ - '@babel/plugin-transform-block-scoping', - '@babel/plugin-transform-dynamic-import', - ['@babel/plugin-transform-modules-commonjs', CJS_PLUGIN_OPTIONS], -]; +const CJS_PLUGINS = ['@babel/plugin-transform-block-scoping', '@babel/plugin-transform-dynamic-import', ['@babel/plugin-transform-modules-commonjs', CJS_PLUGIN_OPTIONS]]; const CJS_AND_HOIST_PLUGINS = [...CJS_PLUGINS, 'babel-plugin-jest-hoist']; function getLang(filename) { diff --git a/knip.json b/knip.json index 0096a0064917..04dc75c32cf8 100644 --- a/knip.json +++ b/knip.json @@ -86,7 +86,11 @@ "shellcheck", "patch-package", "diff-so-fancy", - "@babel/plugin-proposal-class-properties" + "@babel/plugin-proposal-class-properties", + "@babel/plugin-transform-block-scoping", + "@babel/plugin-transform-dynamic-import", + "@babel/plugin-transform-modules-commonjs", + "babel-plugin-jest-hoist" ], "ignoreBinaries": ["metro-symbolicate", "mkcert"] } From fa9328d5c6bb1ccdea648823c844a5ece7efaadb Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 7 Sep 2026 11:35:58 -0700 Subject: [PATCH 04/16] Use esbuild for CJS in the OXC Jest transformer Babel was only needed to hoist jest.mock. Emit CJS and lower import() with esbuild, then run a hoist-only Babel pass on files that call jest.mock. Drop the unused Babel CJS plugin knip ignores. --- __mocks__/react-native.ts | 1 - config/babel/oxcJestTransformer.js | 90 +++++++---- jest.config.js | 8 +- jest/setup.ts | 5 + knip.json | 6 +- tests/actions/AppTest.ts | 27 +++- tests/actions/CompanyCardsImportTest.ts | 17 ++- tests/actions/DomainTest.ts | 109 ++++++++----- .../actions/IOU/CreateDraftTransactionTest.ts | 14 +- tests/actions/IOU/MoneyRequestBuilderTest.ts | 12 +- tests/actions/IOU/MoneyRequestSettersTest.ts | 12 +- tests/actions/IOU/RequestMoneyTest.ts | 16 +- tests/actions/IOU/SearchUpdateTest.ts | 14 +- tests/actions/IOU/SplitReportTotalsTest.ts | 29 ++-- tests/actions/IOUTest/BulkEditTest.ts | 83 +++++++--- .../actions/IOUTest/DeleteMoneyRequestTest.ts | 4 +- tests/actions/IOUTest/DuplicateTest.ts | 71 ++++++--- tests/actions/IOUTest/HoldTest.ts | 4 +- tests/actions/IOUTest/PayMoneyRequestTest.ts | 17 +-- tests/actions/IOUTest/ReceiptTest.ts | 27 +++- .../actions/IOUTest/RejectMoneyRequestTest.ts | 28 +++- tests/actions/IOUTest/ReportWorkflowTest.ts | 75 ++++++--- tests/actions/IOUTest/SendInvoiceTest.ts | 17 ++- tests/actions/IOUTest/SendMoneyTest.ts | 11 +- tests/actions/IOUTest/SplitTest.ts | 17 ++- tests/actions/IOUTest/TrackExpenseTest.ts | 10 +- .../actions/IOUTest/UpdateMoneyRequestTest.ts | 23 ++- .../IOUTest/UpdateMoneyRequestVendorTest.ts | 11 +- tests/actions/MergeTransactionTest.ts | 10 +- tests/actions/OdometerTransactionUtilsTest.ts | 12 +- tests/actions/PlaidTest.ts | 16 +- tests/actions/PolicyTest.ts | 142 ++++++++++------- tests/actions/ReportTest.ts | 18 ++- tests/actions/SessionTest.ts | 123 ++++++++++----- tests/actions/TaskTest.ts | 39 +++-- tests/actions/TransactionTest.ts | 12 +- tests/actions/UserTest.ts | 10 +- tests/actions/connections/DualEntry.ts | 10 +- tests/actions/connections/NetSuite.ts | 10 +- .../actions/connections/QuickbooksDesktop.ts | 10 +- tests/actions/connections/QuickbooksOnline.ts | 10 +- tests/actions/connections/SageIntacct.ts | 10 +- tests/actions/connections/Xero.test.ts | 10 +- .../LinkedActionNotFoundGuardTest.tsx | 16 +- tests/tooling/oxcTransformer.test.ts | 22 +-- tests/ui/AddDomainPageTest.tsx | 11 +- tests/ui/BaseLoginFormTest.tsx | 12 +- tests/ui/BaseVerifyDomainPageTest.tsx | 10 +- tests/ui/ClearReportActionErrorsUITest.tsx | 9 +- tests/ui/ConciergePromptBoxTest.tsx | 10 +- tests/ui/DomainAlreadyExistsPageTest.tsx | 11 +- ...micPaymentCardCurrencySelectorPageTest.tsx | 60 ++++---- tests/ui/ForYouSectionTest.tsx | 8 +- tests/ui/GroupHeaderTest.tsx | 12 +- .../ui/IOURequestRedirectToStartPageTest.tsx | 10 +- tests/ui/IOURequestStepAmountDraftTest.tsx | 40 ++--- tests/ui/IOURequestStepDescriptionTest.tsx | 14 +- ...stStepDistanceOdometerDiscardGuardTest.tsx | 10 +- tests/ui/IOURequestStepHoursTest.tsx | 20 ++- tests/ui/IOURequestStepScanTest.tsx | 18 +-- tests/ui/IOURequestStepTimeRateTest.tsx | 18 ++- tests/ui/ImportedMembersPageTest.tsx | 10 +- ...equestReportActionsListRejectModalTest.tsx | 36 ++--- tests/ui/MoneyRequestReportPreview.test.tsx | 20 ++- tests/ui/MoneyRequestReportViewTest.tsx | 31 +++- ...ultifactorAuthenticationRevokePageTest.tsx | 65 ++++---- ...oardingAccountingAndInterestedFeatures.tsx | 10 +- tests/ui/ReportActionAvatarsTest.tsx | 8 +- tests/ui/ReportActionItemMessageEditTest.tsx | 4 +- tests/ui/ReportActionItemTest.tsx | 10 +- tests/ui/ReportActionsListTest.tsx | 12 +- tests/ui/ReportActionsTest.tsx | 12 +- tests/ui/ScanSkipConfirmationTest.tsx | 10 +- tests/ui/SearchPageTest.tsx | 22 +-- tests/ui/SessionTest.tsx | 35 ++++- ...rkspaceCompanyCardFeedSelectorPageTest.tsx | 8 +- ...WorkspaceCompanyCardPageEmptyStateTest.tsx | 10 +- tests/ui/WorkspaceMoreFeaturesPageTest.tsx | 14 +- tests/ui/WorkspacePageWithSectionsTest.tsx | 12 +- .../ui/components/ApproveActionButtonTest.tsx | 8 +- .../ui/components/ButtonKeyboardShortcut.tsx | 28 ++-- .../ExpenseReportListItemAvatarTest.tsx | 8 +- .../SpendRuleMerchantEditBaseTest.tsx | 26 ++-- .../ui/components/SubmitActionButtonTest.tsx | 12 +- .../components/SubmitPlanWelcomeModalTest.tsx | 10 +- tests/unit/APITest.ts | 10 +- tests/unit/BulletListRendererTest.tsx | 36 ++--- tests/unit/CloudflareSessionTest.ts | 6 +- tests/unit/ComposerLocalTimeTest.tsx | 10 +- tests/unit/CopyPolicySettingsConfirmTest.tsx | 14 +- .../unit/CopyPolicySettingsNavigationTest.tsx | 74 ++++----- .../CopyPolicySettingsProgressModalTest.tsx | 74 ++++----- tests/unit/CopyPolicySettingsUpgradeTest.tsx | 26 ++-- tests/unit/DefaultP2PMileageRateTest.ts | 16 +- .../DynamicContactMethodDetailsPageTest.tsx | 4 +- tests/unit/EmojiTest.ts | 12 +- tests/unit/FloatingMessageCounterTest.tsx | 4 +- tests/unit/FocusTrapForModalTest.tsx | 50 +++--- .../useRecentlyAddedDataTest.ts | 100 ++++++------ .../YourSpendSection/useYourSpendDataTest.ts | 50 +++--- tests/unit/IOUUtilsTest.ts | 18 ++- tests/unit/ImportFromFileStepTest.tsx | 14 +- tests/unit/ImportTransactions.test.ts | 11 +- tests/unit/ImportedMerchantRulesPageTest.tsx | 22 ++- tests/unit/ModifiedExpenseMessageTest.ts | 20 ++- .../MoneyRequestReportButtonUtils.test.ts | 4 +- tests/unit/NetworkStateReachabilityTest.ts | 16 +- tests/unit/NetworkTest.tsx | 10 +- tests/unit/OnyxUpdateManagerTest.ts | 12 +- tests/unit/PolicyUtilsTest.ts | 6 +- tests/unit/PopoverMenuFocusReturnTest.tsx | 14 +- tests/unit/PopoverMenuV2Test.tsx | 32 ++-- tests/unit/QuickActionUtilsTest.ts | 12 +- tests/unit/ReceiptObservabilityTest.ts | 13 +- tests/unit/ReconnectTest.ts | 10 +- .../unit/ReportActionsListPaddingViewTest.tsx | 6 +- tests/unit/ReportActionsListThresholdTest.tsx | 14 +- tests/unit/ReportNotFoundGuardTest.tsx | 14 +- tests/unit/ReportSecondaryActionUtilsTest.ts | 144 +++++++++++------- tests/unit/ReportUtilsTest.ts | 41 ++++- tests/unit/Search/SearchQueryUtilsTest.ts | 4 +- tests/unit/Search/SearchUIUtilsTest.ts | 12 +- .../Search/handleActionButtonPressTest.ts | 22 ++- tests/unit/SearchAutocompleteListTest.tsx | 10 +- tests/unit/SearchStaticListTest.tsx | 10 +- tests/unit/SequentialQueueReadGateTest.ts | 13 +- tests/unit/SequentialQueueTest.ts | 30 +++- tests/unit/SignUpWelcomeFormTest.tsx | 10 +- tests/unit/SuggestedFollowupTest.ts | 12 +- tests/unit/TransactionTest.ts | 51 +++++-- tests/unit/TravelBillingTest.ts | 10 +- tests/unit/ViolationUtilsTest.ts | 22 ++- .../unit/WhisperContentMentionContextTest.tsx | 26 ++-- .../Avatar/connected/GroupChatAvatarTest.tsx | 20 +-- ...VacationDelegateSelectionComponentTest.tsx | 8 +- .../Charts/useChartLabelLayout.test.ts | 12 +- tests/unit/components/SelectionButtonTest.tsx | 8 +- .../VacationDelegateMenuItemTest.tsx | 10 +- .../unit/hooks/useActiveAdminPolicies.test.ts | 4 +- .../useAutoCreateSubmitWorkspace.test.ts | 33 +++- .../hooks/useAutocompleteSuggestions.test.ts | 30 ++-- .../unit/hooks/useBlurOnKeyboardHide.test.ts | 2 +- .../hooks/useBulkDuplicateReportActionTest.ts | 24 +-- .../useConciergeAttachmentPicker.test.tsx | 10 +- .../unit/hooks/useDefaultParticipants.test.ts | 8 +- .../unit/hooks/useDeferNonEssentials.test.ts | 10 +- .../unit/hooks/useEditComposerToggle.test.ts | 4 +- ...eExpensifyCardFeedsForFeedSelector.test.ts | 16 +- tests/unit/hooks/useExportActionsTest.ts | 20 +-- tests/unit/hooks/useHoldRejectActionsTest.ts | 6 +- .../useIsAllowedToIssueCompanyCard.test.ts | 6 +- .../hooks/useLoadSearchCategoryData.test.ts | 16 +- .../hooks/useOutstandingBalanceGuard.test.tsx | 30 ++-- .../unit/hooks/useReportPrimaryActionTest.ts | 16 +- .../hooks/useReportRecipientLocalTime.test.ts | 6 +- .../hooks/useSearchBulkActionsDeleteTest.ts | 8 +- .../useSearchBulkActionsDownloadPDFTest.ts | 14 +- ...seSearchBulkActionsDownloadReceiptsTest.ts | 12 +- .../useSearchBulkActionsDuplicateTest.ts | 38 ++--- .../hooks/useSearchBulkActionsExportTest.ts | 14 +- .../unit/hooks/useSearchBulkActionsPayTest.ts | 8 +- tests/unit/hooks/useSearchBulkActionsTest.ts | 6 +- .../useSelectedTransactionsActions.test.ts | 53 ++++--- .../useSelectionModeReportActions.test.ts | 10 +- .../unit/libs/Accessibility/warmCacheTest.ts | 8 +- .../libs/prepareRequestPayloadNativeTest.ts | 22 ++- tests/unit/libs/receiptStorageTest.ts | 18 +-- .../ReportActionEditMessageContext.test.tsx | 11 +- .../CustomStatus/VacationDelegatePageTest.tsx | 8 +- tests/unit/useArrowKeyFocusManagerTest.ts | 12 +- tests/unit/useAutoUpdateTimezoneTest.tsx | 10 +- .../useCreateEmptyReportConfirmationTest.tsx | 26 ++-- tests/unit/useCreateReportTest.tsx | 6 +- tests/unit/useListItemHighlightTest.ts | 8 +- tests/unit/useListKeyboardNavTest.ts | 16 +- tests/unit/useParticipantSubmissionTest.ts | 14 +- tests/unit/usePreMountDestinationTest.ts | 10 +- tests/unit/useProfileAvatarFormTest.tsx | 26 ++-- .../useReportActionsNewActionLiveTailTest.ts | 16 +- tests/unit/useReportPreviewSenderIDTest.ts | 22 ++- tests/unit/useSearchHighlightAndScrollTest.ts | 13 +- tests/unit/useSearchSelectorTest.tsx | 8 +- tests/unit/useSearchTableItemHighlightTest.ts | 6 +- tests/unit/useUnreadMarkerTest.ts | 4 +- 184 files changed, 2410 insertions(+), 1399 deletions(-) diff --git a/__mocks__/react-native.ts b/__mocks__/react-native.ts index 0d53bb3e38b4..01ce99d2141a 100644 --- a/__mocks__/react-native.ts +++ b/__mocks__/react-native.ts @@ -39,7 +39,6 @@ jest.doMock('react-native', () => { const reactNativeMock = Object.setPrototypeOf( { NativeModules: { - ...ReactNative.NativeModules, BootSplash: { hide: jest.fn().mockResolvedValue(undefined), logoSizeRatio: 1, diff --git a/config/babel/oxcJestTransformer.js b/config/babel/oxcJestTransformer.js index 8d93fa0a547a..631a5945c2e5 100644 --- a/config/babel/oxcJestTransformer.js +++ b/config/babel/oxcJestTransformer.js @@ -2,14 +2,11 @@ const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const babel = require('@babel/core'); +const esbuild = require('esbuild'); const {transformSync} = require('oxc-transform-react'); const babelJest = require('babel-jest'); const BABEL_CORE_VERSION = require('@babel/core/package.json').version; -const BLOCK_SCOPING_PLUGIN_VERSION = require('@babel/plugin-transform-block-scoping/package.json').version; -const DYNAMIC_IMPORT_PLUGIN_VERSION = require('@babel/plugin-transform-dynamic-import/package.json').version; -const CJS_PLUGIN_VERSION = require('@babel/plugin-transform-modules-commonjs/package.json').version; -const JEST_HOIST_VERSION = require('babel-plugin-jest-hoist/package.json').version; const OXC_TRANSFORM_REACT_VERSION = require('oxc-transform-react/package.json').version; const BaseReactCompilerConfig = require('./reactCompilerConfig'); @@ -20,6 +17,7 @@ const TESTS_RE = /[/\\]tests[/\\]/; const JEST_SETUP_RE = /[/\\]jest[/\\]/; const MOCKS_RE = /[/\\]__mocks__[/\\]/; const JEST_HOIST_RE = /\bjest\s*\.\s*(mock|unmock|deepUnmock|disableAutomock|enableAutomock)\b/; +const HOISTABLE_JEST_FNS = new Set(['mock', 'unmock', 'deepUnmock', 'disableAutomock', 'enableAutomock']); const TRANSFORMER_SOURCE = fs.readFileSync(__filename); const REACT_COMPILER_CONFIG_KEY = JSON.stringify(BaseReactCompilerConfig); @@ -30,10 +28,6 @@ const REACT_COMPILER_OPTIONS = { eslintSuppressionRules: [], }; -const CJS_PLUGIN_OPTIONS = {loose: true, strictMode: false}; -const CJS_PLUGINS = ['@babel/plugin-transform-block-scoping', '@babel/plugin-transform-dynamic-import', ['@babel/plugin-transform-modules-commonjs', CJS_PLUGIN_OPTIONS]]; -const CJS_AND_HOIST_PLUGINS = [...CJS_PLUGINS, 'babel-plugin-jest-hoist']; - function getLang(filename) { const ext = path.extname(filename).slice(1); if (ext === 'tsx') { @@ -53,8 +47,54 @@ function shouldRunReactCompiler(filename) { return !TESTS_RE.test(filename) && !JEST_SETUP_RE.test(filename) && !MOCKS_RE.test(filename); } -function toCommonJS(code, sourcePath, inputSourceMap) { - const plugins = JEST_HOIST_RE.test(code) ? CJS_AND_HOIST_PLUGINS : CJS_PLUGINS; +function isJestIdentifier(expression) { + return expression.isIdentifier({name: 'jest'}) && !expression.scope.hasBinding('jest'); +} + +function isHoistableJestCall(expression) { + if (!expression.isCallExpression()) { + return false; + } + + const callee = expression.get('callee'); + if (!callee.isMemberExpression() || callee.node.computed) { + return false; + } + + const object = callee.get('object'); + const property = callee.get('property'); + if (!property.isIdentifier() || !HOISTABLE_JEST_FNS.has(property.node.name)) { + return false; + } + + return isJestIdentifier(object) || isHoistableJestCall(object); +} + +function hoistJestMocksPlugin() { + return { + name: 'oxc-jest-hoist-mocks', + visitor: { + Program(program) { + const mockStatements = []; + for (const statement of program.get('body')) { + if (statement.isExpressionStatement() && isHoistableJestCall(statement.get('expression'))) { + mockStatements.push(statement.node); + statement.remove(); + } + } + if (mockStatements.length > 0) { + program.unshiftContainer('body', mockStatements); + } + }, + }, + }; +} + +function hoistJestMocks(code, sourcePath) { + if (!JEST_HOIST_RE.test(code)) { + return code; + } + const result = babel.transformSync(code, { filename: sourcePath, ast: false, @@ -62,17 +102,11 @@ function toCommonJS(code, sourcePath, inputSourceMap) { babelrc: false, configFile: false, compact: false, - sourceType: 'module', - sourceMaps: true, - inputSourceMap: inputSourceMap ?? undefined, - plugins, + sourceType: 'script', + plugins: [hoistJestMocksPlugin], }); - if (!result?.code) { - return {code, map: inputSourceMap}; - } - - return {code: result.code, map: result.map ?? inputSourceMap}; + return result?.code ?? code; } function processWithOxc(sourceText, sourcePath) { @@ -87,7 +121,15 @@ function processWithOxc(sourceText, sourcePath) { return null; } - return toCommonJS(oxcResult.code, sourcePath, oxcResult.map); + const cjs = esbuild.transformSync(oxcResult.code, { + loader: 'js', + format: 'cjs', + supported: {'dynamic-import': false}, + sourcefile: sourcePath, + sourcemap: true, + }); + + return {code: hoistJestMocks(cjs.code, sourcePath), map: cjs.map}; } module.exports = { @@ -104,12 +146,8 @@ module.exports = { .update(sourcePath) .update(TRANSFORMER_SOURCE) .update(REACT_COMPILER_CONFIG_KEY) - .update(JSON.stringify(CJS_PLUGIN_OPTIONS)) + .update(esbuild.version) .update(BABEL_CORE_VERSION) - .update(BLOCK_SCOPING_PLUGIN_VERSION) - .update(DYNAMIC_IMPORT_PLUGIN_VERSION) - .update(CJS_PLUGIN_VERSION) - .update(JEST_HOIST_VERSION) .update(OXC_TRANSFORM_REACT_VERSION) .digest('hex'); }, @@ -121,7 +159,7 @@ module.exports = { return result; } } catch { - // Fall through to babel-jest for syntax OXC or the CJS pass cannot parse. + // Fall through to babel-jest for syntax OXC, esbuild, or the hoist pass cannot parse. } } diff --git a/jest.config.js b/jest.config.js index f69f876a124b..a5aef81e20e5 100644 --- a/jest.config.js +++ b/jest.config.js @@ -18,10 +18,10 @@ module.exports = { `/?(*.)+(spec|test).${testFileExtension}`, ], transform: { - // OXC compiles TS/JSX (and React Compiler on app sources). A small Babel pass then - // emits loose CommonJS, lowers import(), and hoists jest.mock so test files no - // longer need a babel-jest split. Native OXC stays fast under Reassure's - // `--max-opt=1` (V8 sparkplug only). + // OXC compiles TS/JSX (and React Compiler on app sources). esbuild then emits CJS + // and lowers import(). Files that call jest.mock get a tiny Babel pass that only + // hoists those calls above require() — not a full Babel CJS transform. Native + // OXC + esbuild stays fast under Reassure's `--max-opt=1` (V8 sparkplug only). '^.+\\.[jt]sx?$': '/config/babel/oxcJestTransformer.js', '^.+\\.svg?$': 'jest-transformer-svg', }, diff --git a/jest/setup.ts b/jest/setup.ts index 3a6464839967..903784bed92d 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -49,6 +49,11 @@ jest.mock('expo-task-manager', () => ({ // Add other methods here if you use them })); +jest.mock('expo-web-browser', () => ({ + openAuthSessionAsync: jest.fn(() => Promise.resolve({type: 'dismiss'})), + maybeCompleteAuthSession: jest.fn(), +})); + // Mock expo-location — the jest-expo preset replaces all native module methods with jest.fn(async () => {}), // which returns undefined instead of a proper PermissionResponse. This causes crashes when code reads .status // from the result of requestForegroundPermissionsAsync(). diff --git a/knip.json b/knip.json index 04dc75c32cf8..0096a0064917 100644 --- a/knip.json +++ b/knip.json @@ -86,11 +86,7 @@ "shellcheck", "patch-package", "diff-so-fancy", - "@babel/plugin-proposal-class-properties", - "@babel/plugin-transform-block-scoping", - "@babel/plugin-transform-dynamic-import", - "@babel/plugin-transform-modules-commonjs", - "babel-plugin-jest-hoist" + "@babel/plugin-proposal-class-properties" ], "ignoreBinaries": ["metro-symbolicate", "mkcert"] } diff --git a/tests/actions/AppTest.ts b/tests/actions/AppTest.ts index 37a675885c27..766a4fd91a05 100644 --- a/tests/actions/AppTest.ts +++ b/tests/actions/AppTest.ts @@ -27,9 +27,27 @@ import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; jest.mock('@src/components/ConfirmedRoute.tsx'); +jest.mock('@libs/API', () => { + const actual = jest.requireActual('@libs/API'); + return { + ...actual, + read: jest.fn(actual.read), + writeWithNoDuplicatesOpenAppConflictAction: jest.fn(actual.writeWithNoDuplicatesOpenAppConflictAction), + }; +}); + +jest.mock('../../src/libs/actions/App', () => { + const actual = jest.requireActual('../../src/libs/actions/App'); + return { + ...actual, + triggerFullReconnect: jest.fn(actual.triggerFullReconnect), + }; +}); function mockRead() { - return jest.spyOn(API, 'read').mockImplementation(() => {}); + const readSpy = jest.mocked(API.read); + readSpy.mockImplementation(() => {}); + return readSpy; } OnyxUpdateManager(); @@ -110,7 +128,8 @@ describe('actions/App', () => { }); test('openApp is not deduped against an in-flight OpenApp when it carries preservation data', async () => { - const writeOpenApp = jest.spyOn(API, 'writeWithNoDuplicatesOpenAppConflictAction').mockImplementation(() => Promise.resolve()); + const writeOpenApp = jest.mocked(API.writeWithNoDuplicatesOpenAppConflictAction); + writeOpenApp.mockImplementation(() => Promise.resolve()); App.openApp(); await waitForBatchedUpdates(); @@ -126,7 +145,7 @@ describe('actions/App', () => { }); test('trigger full reconnect', async () => { - const triggerFullReconnect = jest.spyOn(App, 'triggerFullReconnect'); + const triggerFullReconnect = jest.mocked(App.triggerFullReconnect); // When OpenApp runs App.openApp(); @@ -146,7 +165,7 @@ describe('actions/App', () => { }); test("don't trigger full reconnect", async () => { - const triggerFullReconnect = jest.spyOn(App, 'triggerFullReconnect'); + const triggerFullReconnect = jest.mocked(App.triggerFullReconnect); // When OpenApp runs App.openApp(); diff --git a/tests/actions/CompanyCardsImportTest.ts b/tests/actions/CompanyCardsImportTest.ts index f1d4a26c34a0..ddff3e955163 100644 --- a/tests/actions/CompanyCardsImportTest.ts +++ b/tests/actions/CompanyCardsImportTest.ts @@ -1,4 +1,5 @@ import {importCSVCompanyCards} from '@libs/actions/CompanyCards'; +import * as APIModule from '@libs/API'; import type {ImportCSVCompanyCardsParams} from '@libs/API/parameters'; import {WRITE_COMMANDS} from '@libs/API/types'; @@ -12,6 +13,14 @@ import Onyx from 'react-native-onyx'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +jest.mock('@libs/API', () => { + const actual = jest.requireActual('@libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + }; +}); + const POLICY_ID = 'POLICY_1'; const DOMAIN_ACCOUNT_ID = 777; const CSV_FEED = CONST.COMPANY_CARD.FEED_BANK_NAME.CSV; @@ -31,7 +40,8 @@ describe('actions/CompanyCards importCSVCompanyCards', () => { it('targets the feed-owning domain account when re-importing a domain feed surfaced via a preferred workspace', () => { // Given a domain feed (its NVPs live on the +@domain account, not the workspace account) that is re-imported - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); // When importing with an explicit domainAccountID and no cached feeds for that account importCSVCompanyCards({ @@ -74,7 +84,8 @@ describe('actions/CompanyCards importCSVCompanyCards', () => { it('does not optimistically create the feed when it already exists on the target account', () => { // Given the target account already has the feed and a nickname for it - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const existingFeeds: CardFeeds = { settings: { @@ -116,7 +127,7 @@ describe('actions/CompanyCards importCSVCompanyCards', () => { beforeEach(() => { sentImports.length = 0; - jest.spyOn(require('@libs/API'), 'write').mockImplementation((...args: unknown[]) => { + jest.mocked(APIModule.write).mockImplementation((...args: unknown[]) => { const parameters = args.at(1); if (isImportCSVCompanyCardsParams(parameters)) { sentImports.push(parameters); diff --git a/tests/actions/DomainTest.ts b/tests/actions/DomainTest.ts index b2622ba5b3d2..1d658184d8cd 100644 --- a/tests/actions/DomainTest.ts +++ b/tests/actions/DomainTest.ts @@ -26,6 +26,7 @@ import { setTwoFactorAuthExemptEmailForDomain, updateDomainSecurityGroup, } from '@libs/actions/Domain'; +import * as APIModule from '@libs/API'; import {SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import {generateAccountID} from '@libs/UserUtils'; @@ -44,6 +45,15 @@ import createMock from '../utils/createMock'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +jest.mock('@libs/API', () => { + const actual = jest.requireActual('@libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + makeRequestWithSideEffects: jest.fn(actual.makeRequestWithSideEffects), + }; +}); + OnyxUpdateManager(); describe('actions/Domain', () => { beforeAll(() => { @@ -58,7 +68,8 @@ describe('actions/Domain', () => { }); it('createDomain', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const domainKeysBeforeCreation = new Set([`${ONYXKEYS.COLLECTION.DOMAIN}123`]); createDomain('test.com', domainKeysBeforeCreation); @@ -111,7 +122,8 @@ describe('actions/Domain', () => { }); it('resetDomain', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const domainAccountID = 123; const domainName = 'test.com'; @@ -140,7 +152,8 @@ describe('actions/Domain', () => { describe('requestDomainAdminship', () => { it('optimistically marks the requester as pending', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const domainAccountID = 123; const currentUserAccountID = 456; @@ -157,7 +170,8 @@ describe('actions/Domain', () => { }); it('rolls only the requester back on failure when the domain is one the user can see', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const domainAccountID = 123; const currentUserAccountID = 456; @@ -173,7 +187,8 @@ describe('actions/Domain', () => { }); it('drops the whole entry on failure when it only exists to carry the flow, so no empty domain lingers', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const domainAccountID = 123; const currentUserAccountID = 456; @@ -218,7 +233,8 @@ describe('actions/Domain', () => { }); it('addMemberToDomain', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const domainAccountID = 123; const email = 'test@example.com'; const defaultSecurityGroupID = '1'; @@ -282,7 +298,8 @@ describe('actions/Domain', () => { }); it('addAdminToDomain - adds and clears optimistic personal details for optimistic accounts', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const domainAccountID = 123; const accountID = 456; const targetEmail = 'test@example.com'; @@ -326,7 +343,8 @@ describe('actions/Domain', () => { }); it('addAdminToDomain - does not update optimistic personal details for non-optimistic accounts', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const domainAccountID = 123; const accountID = 456; const targetEmail = 'test@example.com'; @@ -402,7 +420,8 @@ describe('actions/Domain', () => { describe('closeUserAccount', () => { it('closeUserAccount - sends DELETE_DOMAIN_MEMBER API request with correct data', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const domainAccountID = 123; const domainName = 'test.com'; const accountID = 456; @@ -467,7 +486,8 @@ describe('actions/Domain', () => { }); it('closeUserAccount - handles overrideProcessingReports flag', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const domainAccountID = 123; const domainName = 'test.com'; const targetEmail = 'user@test.com'; @@ -486,7 +506,8 @@ describe('actions/Domain', () => { describe('setDomainVacationDelegate', () => { it('sends SET_VACATION_DELEGATE request with ADD pending action when no existing delegate', () => { - const apiSideEffectSpy = jest.spyOn(require('@libs/API'), 'makeRequestWithSideEffects').mockImplementation(() => Promise.resolve()); + const apiSideEffectSpy = jest.mocked(APIModule.makeRequestWithSideEffects); + apiSideEffectSpy.mockImplementation(() => Promise.resolve()); const domainAccountID = 123; const domainMemberAccountID = 456; const creator = 'admin@test.com'; @@ -547,7 +568,8 @@ describe('actions/Domain', () => { }); it('uses UPDATE pending action when existing delegate is present', () => { - const apiSideEffectSpy = jest.spyOn(require('@libs/API'), 'makeRequestWithSideEffects').mockImplementation(() => Promise.resolve()); + const apiSideEffectSpy = jest.mocked(APIModule.makeRequestWithSideEffects); + apiSideEffectSpy.mockImplementation(() => Promise.resolve()); const domainAccountID = 123; const domainMemberAccountID = 456; const creator = 'admin@test.com'; @@ -576,7 +598,8 @@ describe('actions/Domain', () => { describe('deleteDomainVacationDelegate', () => { it('deleteDomainVacationDelegate - sends DELETE_VACATION_DELEGATE request with correct data', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const domainAccountID = 123; const domainMemberAccountID = 456; const vacationer = 'vacationer@test.com'; @@ -782,7 +805,8 @@ describe('actions/Domain', () => { const exemptEmails = ['other@test.com', targetEmail]; it('removes targetEmail from exempt emails in optimisticData when force2FA is true', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); setTwoFactorAuthExemptEmailForDomain(domainAccountID, accountID, exemptEmails, targetEmail, true); @@ -803,7 +827,8 @@ describe('actions/Domain', () => { }); it('adds targetEmail to exempt emails in optimisticData when force2FA is false and no twoFactorAuthCode is provided', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); setTwoFactorAuthExemptEmailForDomain(domainAccountID, accountID, exemptEmails, targetEmail, false); @@ -824,7 +849,8 @@ describe('actions/Domain', () => { }); it('keeps exempt emails unchanged in optimisticData when force2FA is false and twoFactorAuthCode is provided', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const twoFactorAuthCode = '123456'; setTwoFactorAuthExemptEmailForDomain(domainAccountID, accountID, exemptEmails, targetEmail, false, twoFactorAuthCode); @@ -846,7 +872,8 @@ describe('actions/Domain', () => { }); it('sets twoFactorAuthExemptEmailsError to null and adds VALIDATE_DOMAIN_TWO_FACTOR_CODE error in failureData when twoFactorAuthCode is provided', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const twoFactorAuthCode = '123456'; setTwoFactorAuthExemptEmailForDomain(domainAccountID, accountID, exemptEmails, targetEmail, true, twoFactorAuthCode); @@ -873,7 +900,8 @@ describe('actions/Domain', () => { }); it('sets twoFactorAuthExemptEmailsError to an error object and omits VALIDATE_DOMAIN_TWO_FACTOR_CODE from failureData when no twoFactorAuthCode is provided', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); setTwoFactorAuthExemptEmailForDomain(domainAccountID, accountID, exemptEmails, targetEmail, true); @@ -906,7 +934,8 @@ describe('actions/Domain', () => { describe('resetDomainMemberTwoFactorAuth', () => { it('calls RESET_DOMAIN_MEMBER_TWO_FACTOR_AUTH with correct optimistic, success, and failure data', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const domainAccountID = 123; const targetAccountID = 456; const targetEmail = 'member@test.com'; @@ -971,8 +1000,9 @@ describe('actions/Domain', () => { jest.clearAllMocks(); }); - it('calls API.write with CHANGE_DOMAIN_SECURITY_GROUP command', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => {}); + it('calls APIModule.write with CHANGE_DOMAIN_SECURITY_GROUP command', () => { + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => {}); changeDomainSecurityGroup(DOMAIN_ACCOUNT_ID, DOMAIN_NAME, EMPLOYEE_EMAIL, ACCOUNT_ID, CURRENT_SECURITY_GROUP_KEY, CURRENT_SECURITY_GROUP, TARGET_SECURITY_GROUP_KEY); expect(apiWriteSpy).toHaveBeenCalledTimes(1); @@ -980,8 +1010,9 @@ describe('actions/Domain', () => { apiWriteSpy.mockRestore(); }); - it('passes correct parameters to API.write', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => {}); + it('passes correct parameters to APIModule.write', () => { + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => {}); changeDomainSecurityGroup(DOMAIN_ACCOUNT_ID, DOMAIN_NAME, EMPLOYEE_EMAIL, ACCOUNT_ID, CURRENT_SECURITY_GROUP_KEY, CURRENT_SECURITY_GROUP, TARGET_SECURITY_GROUP_KEY); const [, parameters] = TestHelper.getRequiredWriteCall(apiWriteSpy.mock.calls, 0); @@ -995,7 +1026,8 @@ describe('actions/Domain', () => { }); it('optimisticData moves account from current to target security group and sets pending action', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => {}); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => {}); changeDomainSecurityGroup(DOMAIN_ACCOUNT_ID, DOMAIN_NAME, EMPLOYEE_EMAIL, ACCOUNT_ID, CURRENT_SECURITY_GROUP_KEY, CURRENT_SECURITY_GROUP, TARGET_SECURITY_GROUP_KEY); const [, , onyxData] = TestHelper.getRequiredWriteCall(apiWriteSpy.mock.calls, 0); @@ -1025,7 +1057,8 @@ describe('actions/Domain', () => { }); it('successData clears pending action and errors for the member', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => {}); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => {}); changeDomainSecurityGroup(DOMAIN_ACCOUNT_ID, DOMAIN_NAME, EMPLOYEE_EMAIL, ACCOUNT_ID, CURRENT_SECURITY_GROUP_KEY, CURRENT_SECURITY_GROUP, TARGET_SECURITY_GROUP_KEY); const [, , onyxData] = TestHelper.getRequiredWriteCall(apiWriteSpy.mock.calls, 0); @@ -1039,7 +1072,8 @@ describe('actions/Domain', () => { }); it('failureData reverts domain state, clears pending action and sets move member error', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => {}); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => {}); changeDomainSecurityGroup(DOMAIN_ACCOUNT_ID, DOMAIN_NAME, EMPLOYEE_EMAIL, ACCOUNT_ID, CURRENT_SECURITY_GROUP_KEY, CURRENT_SECURITY_GROUP, TARGET_SECURITY_GROUP_KEY); const [, , onyxData] = TestHelper.getRequiredWriteCall(apiWriteSpy.mock.calls, 0); @@ -1056,7 +1090,8 @@ describe('actions/Domain', () => { }); it('extracts newID correctly from targetSecurityGroupKey', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => {}); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => {}); const customTargetKey: SecurityGroupKey = `${CONST.DOMAIN.DOMAIN_SECURITY_GROUP_PREFIX}999`; changeDomainSecurityGroup(DOMAIN_ACCOUNT_ID, DOMAIN_NAME, EMPLOYEE_EMAIL, ACCOUNT_ID, CURRENT_SECURITY_GROUP_KEY, CURRENT_SECURITY_GROUP, customTargetKey); @@ -1102,7 +1137,8 @@ describe('actions/Domain', () => { describe('updateDomainSecurityGroup', () => { it('sends UPDATE_DOMAIN_SECURITY_GROUP with correct optimistic, success, and failure data', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const domainAccountID = 123; const groupID = '456'; const accountID = 789; @@ -1176,14 +1212,15 @@ describe('actions/Domain', () => { let apiWriteSpy: jest.SpyInstance; beforeEach(() => { - apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); }); afterEach(() => { apiWriteSpy.mockRestore(); }); - it('calls API.write with DELETE_DOMAIN_SECURITY_GROUP and correct parameters', () => { + it('calls APIModule.write with DELETE_DOMAIN_SECURITY_GROUP and correct parameters', () => { deleteDomainSecurityGroup(domainAccountID, groupID); expect(apiWriteSpy).toHaveBeenCalledTimes(1); @@ -1320,7 +1357,8 @@ describe('actions/Domain', () => { describe('setDefaultSecurityGroup', () => { it('sends SET_DEFAULT_DOMAIN_SECURITY_GROUP with correct optimistic, success, and failure data', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const domainAccountID = 123; const groupID = '456'; const previousGroupID = '789'; @@ -1380,7 +1418,8 @@ describe('actions/Domain', () => { }); it('handles undefined previousGroupID in failure data', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const domainAccountID = 123; const groupID = '456'; const SECURITY_GROUP_KEY = `${CONST.DOMAIN.DOMAIN_SECURITY_GROUP_PREFIX}${groupID}`; @@ -1422,7 +1461,8 @@ describe('actions/Domain', () => { }); it('sends CREATE_DOMAIN_SECURITY_GROUP with correct optimistic, success, and failure data', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const domainAccountID = 123; const newSecurityGroup: DomainSecurityGroup = { name: 'New Group', @@ -1482,7 +1522,8 @@ describe('actions/Domain', () => { }); it('optimistically sets domain_defaultSecurityGroupID to the new group when shouldSetAsDefaultGroup is true and reverts it on failure', () => { - const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const domainAccountID = 123; const previousDefaultGroupID = '999'; const newSecurityGroup: DomainSecurityGroup = { diff --git a/tests/actions/IOU/CreateDraftTransactionTest.ts b/tests/actions/IOU/CreateDraftTransactionTest.ts index 2028c4d46e78..f0e4f8509a83 100644 --- a/tests/actions/IOU/CreateDraftTransactionTest.ts +++ b/tests/actions/IOU/CreateDraftTransactionTest.ts @@ -26,7 +26,7 @@ import createRandomTransaction from '../../utils/collections/transaction'; import {getGlobalFetchMock, getOnyxData} from '../../utils/TestHelper'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; -const topMostReportID = '23423423'; +const mockTopMostReportID = '23423423'; jest.mock('@src/libs/Navigation/Navigation', () => ({ navigate: jest.fn(), dismissModal: jest.fn(), @@ -35,7 +35,7 @@ jest.mock('@src/libs/Navigation/Navigation', () => ({ navigateBackToLastSuperWideRHPScreen: jest.fn(), dismissModalWithReport: jest.fn(), goBack: jest.fn(), - getTopmostReportId: jest.fn(() => topMostReportID), + getTopmostReportId: jest.fn(() => mockTopMostReportID), setNavigationActionToMicrotaskQueue: jest.fn(), removeScreenByKey: jest.fn(), isNavigationReady: jest.fn(() => Promise.resolve()), @@ -79,15 +79,15 @@ jest.mock('@libs/deferredLayoutWrite', () => ({ })); jest.mock('@hooks/useCardFeedsForDisplay', () => jest.fn(() => ({defaultCardFeed: null, cardFeedsByPolicy: {}}))); -const unapprovedCashHash = 71801560; -const unapprovedCashSimilarSearchHash = 1832274510; +const mockUnapprovedCashHash = 71801560; +const mockUnapprovedCashSimilarSearchHash = 1832274510; jest.mock('@src/libs/SearchQueryUtils', () => { const actual = jest.requireActual('@src/libs/SearchQueryUtils'); // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { ...actual, getCurrentSearchQueryJSON: jest.fn().mockImplementation(() => ({ - hash: unapprovedCashHash, + hash: mockUnapprovedCashHash, query: 'test', type: 'expense', status: ['drafts', 'outstanding'], @@ -95,7 +95,7 @@ jest.mock('@src/libs/SearchQueryUtils', () => { flatFilters: [{key: 'reimbursable', filters: [{operator: 'eq', value: 'yes'}]}], inputQuery: '', recentSearchHash: 89, - similarSearchHash: unapprovedCashSimilarSearchHash, + similarSearchHash: mockUnapprovedCashSimilarSearchHash, sortBy: 'tag', sortOrder: 'asc', })), @@ -572,7 +572,7 @@ describe('actions/IOU', () => { await waitForBatchedUpdates(); // Then back from the confirmation page returns to the visible report, not the self DM the expense lives on - expect(getConfirmationRouteBackTo()).toBe(ROUTES.REPORT_WITH_ID.getRoute(topMostReportID)); + expect(getConfirmationRouteBackTo()).toBe(ROUTES.REPORT_WITH_ID.getRoute(mockTopMostReportID)); }); it('should fall back to the expense report when no report is visible behind the confirmation page', async () => { diff --git a/tests/actions/IOU/MoneyRequestBuilderTest.ts b/tests/actions/IOU/MoneyRequestBuilderTest.ts index e01849573ca6..ca71b8c54d34 100644 --- a/tests/actions/IOU/MoneyRequestBuilderTest.ts +++ b/tests/actions/IOU/MoneyRequestBuilderTest.ts @@ -22,7 +22,7 @@ import createMock from '../../utils/createMock'; import {getGlobalFetchMock} from '../../utils/TestHelper'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; -const topMostReportID = '23423423'; +const mockTopMostReportID = '23423423'; jest.mock('@src/libs/Navigation/Navigation', () => ({ navigate: jest.fn(), dismissModal: jest.fn(), @@ -31,7 +31,7 @@ jest.mock('@src/libs/Navigation/Navigation', () => ({ navigateBackToLastSuperWideRHPScreen: jest.fn(), dismissModalWithReport: jest.fn(), goBack: jest.fn(), - getTopmostReportId: jest.fn(() => topMostReportID), + getTopmostReportId: jest.fn(() => mockTopMostReportID), setNavigationActionToMicrotaskQueue: jest.fn(), removeScreenByKey: jest.fn(), isNavigationReady: jest.fn(() => Promise.resolve()), @@ -74,15 +74,15 @@ jest.mock('@libs/deferredLayoutWrite', () => ({ })); jest.mock('@hooks/useCardFeedsForDisplay', () => jest.fn(() => ({defaultCardFeed: null, cardFeedsByPolicy: {}}))); -const unapprovedCashHash = 71801560; -const unapprovedCashSimilarSearchHash = 1832274510; +const mockUnapprovedCashHash = 71801560; +const mockUnapprovedCashSimilarSearchHash = 1832274510; jest.mock('@src/libs/SearchQueryUtils', () => { const actual = jest.requireActual('@src/libs/SearchQueryUtils'); // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { ...actual, getCurrentSearchQueryJSON: jest.fn().mockImplementation(() => ({ - hash: unapprovedCashHash, + hash: mockUnapprovedCashHash, query: 'test', type: 'expense', status: ['drafts', 'outstanding'], @@ -90,7 +90,7 @@ jest.mock('@src/libs/SearchQueryUtils', () => { flatFilters: [{key: 'reimbursable', filters: [{operator: 'eq', value: 'yes'}]}], inputQuery: '', recentSearchHash: 89, - similarSearchHash: unapprovedCashSimilarSearchHash, + similarSearchHash: mockUnapprovedCashSimilarSearchHash, sortBy: 'tag', sortOrder: 'asc', })), diff --git a/tests/actions/IOU/MoneyRequestSettersTest.ts b/tests/actions/IOU/MoneyRequestSettersTest.ts index 4ccf51d37653..3044dd1b1a86 100644 --- a/tests/actions/IOU/MoneyRequestSettersTest.ts +++ b/tests/actions/IOU/MoneyRequestSettersTest.ts @@ -36,7 +36,7 @@ import getOnyxValue from '../../utils/getOnyxValue'; import {getCurrencyDecimalsLocal, getGlobalFetchMock} from '../../utils/TestHelper'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; -const topMostReportID = '23423423'; +const mockTopMostReportID = '23423423'; jest.mock('@src/libs/Navigation/Navigation', () => ({ navigate: jest.fn(), dismissModal: jest.fn(), @@ -45,7 +45,7 @@ jest.mock('@src/libs/Navigation/Navigation', () => ({ navigateBackToLastSuperWideRHPScreen: jest.fn(), dismissModalWithReport: jest.fn(), goBack: jest.fn(), - getTopmostReportId: jest.fn(() => topMostReportID), + getTopmostReportId: jest.fn(() => mockTopMostReportID), setNavigationActionToMicrotaskQueue: jest.fn(), removeScreenByKey: jest.fn(), isNavigationReady: jest.fn(() => Promise.resolve()), @@ -88,15 +88,15 @@ jest.mock('@libs/deferredLayoutWrite', () => ({ })); jest.mock('@hooks/useCardFeedsForDisplay', () => jest.fn(() => ({defaultCardFeed: null, cardFeedsByPolicy: {}}))); -const unapprovedCashHash = 71801560; -const unapprovedCashSimilarSearchHash = 1832274510; +const mockUnapprovedCashHash = 71801560; +const mockUnapprovedCashSimilarSearchHash = 1832274510; jest.mock('@src/libs/SearchQueryUtils', () => { const actual = jest.requireActual('@src/libs/SearchQueryUtils'); // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { ...actual, getCurrentSearchQueryJSON: jest.fn().mockImplementation(() => ({ - hash: unapprovedCashHash, + hash: mockUnapprovedCashHash, query: 'test', type: 'expense', status: ['drafts', 'outstanding'], @@ -104,7 +104,7 @@ jest.mock('@src/libs/SearchQueryUtils', () => { flatFilters: [{key: 'reimbursable', filters: [{operator: 'eq', value: 'yes'}]}], inputQuery: '', recentSearchHash: 89, - similarSearchHash: unapprovedCashSimilarSearchHash, + similarSearchHash: mockUnapprovedCashSimilarSearchHash, sortBy: 'tag', sortOrder: 'asc', })), diff --git a/tests/actions/IOU/RequestMoneyTest.ts b/tests/actions/IOU/RequestMoneyTest.ts index 3ed90edba91a..736febcfc850 100644 --- a/tests/actions/IOU/RequestMoneyTest.ts +++ b/tests/actions/IOU/RequestMoneyTest.ts @@ -61,6 +61,14 @@ import { import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; import waitForNetworkPromises from '../../utils/waitForNetworkPromises'; +jest.mock('@src/libs/API', () => { + const actual = jest.requireActual('@src/libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + }; +}); + const topMostReportID = '23423423'; jest.mock('@src/libs/Navigation/Navigation', () => ({ navigate: jest.fn(), @@ -116,8 +124,7 @@ jest.mock('@hooks/useCardFeedsForDisplay', () => jest.fn(() => ({defaultCardFeed const unapprovedCashHash = 71801560; const unapprovedCashSimilarSearchHash = 1832274510; jest.mock('@src/libs/SearchQueryUtils', () => { - const actual = jest.requireActual('@src/libs/SearchQueryUtils'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return + const actual = jest.requireActual('@src/libs/SearchQueryUtils'); return { ...actual, getCurrentSearchQueryJSON: jest.fn().mockImplementation(() => ({ @@ -2610,7 +2617,8 @@ describe('actions/IOU', () => { const isValid = (value: unknown) => !value || typeof value !== 'object' || value instanceof Blob; beforeEach(() => { - writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); }); afterEach(() => { @@ -2992,7 +3000,7 @@ describe('actions/IOU', () => { view: CONST.SEARCH.VIEW.TABLE, } as SearchQueryJSON; - const getCurrentSearchQueryJSONSpy = jest.spyOn(SearchQueryUtils, 'getCurrentSearchQueryJSON').mockReturnValue(currentSearchQueryJSON); + const getCurrentSearchQueryJSONSpy = jest.mocked(SearchQueryUtils.getCurrentSearchQueryJSON).mockReturnValue(currentSearchQueryJSON); requestMoney({ getCurrencyDecimals: getCurrencyDecimalsLocal, diff --git a/tests/actions/IOU/SearchUpdateTest.ts b/tests/actions/IOU/SearchUpdateTest.ts index 4fef7b93c0da..e71a2c9121da 100644 --- a/tests/actions/IOU/SearchUpdateTest.ts +++ b/tests/actions/IOU/SearchUpdateTest.ts @@ -25,7 +25,7 @@ import createMock from '../../utils/createMock'; import {getGlobalFetchMock} from '../../utils/TestHelper'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; -const topMostReportID = '23423423'; +const mockTopMostReportID = '23423423'; jest.mock('@src/libs/Navigation/Navigation', () => ({ navigate: jest.fn(), dismissModal: jest.fn(), @@ -34,7 +34,7 @@ jest.mock('@src/libs/Navigation/Navigation', () => ({ navigateBackToLastSuperWideRHPScreen: jest.fn(), dismissModalWithReport: jest.fn(), goBack: jest.fn(), - getTopmostReportId: jest.fn(() => topMostReportID), + getTopmostReportId: jest.fn(() => mockTopMostReportID), setNavigationActionToMicrotaskQueue: jest.fn(), removeScreenByKey: jest.fn(), isNavigationReady: jest.fn(() => Promise.resolve()), @@ -77,15 +77,15 @@ jest.mock('@libs/deferredLayoutWrite', () => ({ })); jest.mock('@hooks/useCardFeedsForDisplay', () => jest.fn(() => ({defaultCardFeed: null, cardFeedsByPolicy: {}}))); -const unapprovedCashHash = 71801560; -const unapprovedCashSimilarSearchHash = 1832274510; +const mockUnapprovedCashHash = 71801560; +const mockUnapprovedCashSimilarSearchHash = 1832274510; jest.mock('@src/libs/SearchQueryUtils', () => { const actual = jest.requireActual('@src/libs/SearchQueryUtils'); // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { ...actual, getCurrentSearchQueryJSON: jest.fn().mockImplementation(() => ({ - hash: unapprovedCashHash, + hash: mockUnapprovedCashHash, query: 'test', type: 'expense', status: ['drafts', 'outstanding'], @@ -93,7 +93,7 @@ jest.mock('@src/libs/SearchQueryUtils', () => { flatFilters: [{key: 'reimbursable', filters: [{operator: 'eq', value: 'yes'}]}], inputQuery: '', recentSearchHash: 89, - similarSearchHash: unapprovedCashSimilarSearchHash, + similarSearchHash: mockUnapprovedCashSimilarSearchHash, sortBy: 'tag', sortOrder: 'asc', })), @@ -619,7 +619,7 @@ describe('actions/IOU', () => { isInvoice: false, }); - const snapshotKey = `${ONYXKEYS.COLLECTION.SNAPSHOT}${unapprovedCashHash}`; + const snapshotKey = `${ONYXKEYS.COLLECTION.SNAPSHOT}${mockUnapprovedCashHash}`; const update = result?.optimisticData?.find((u) => u.key === snapshotKey); const transactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`; return {update, transactionKey}; diff --git a/tests/actions/IOU/SplitReportTotalsTest.ts b/tests/actions/IOU/SplitReportTotalsTest.ts index 3bb94529effa..b3901c132e17 100644 --- a/tests/actions/IOU/SplitReportTotalsTest.ts +++ b/tests/actions/IOU/SplitReportTotalsTest.ts @@ -3,6 +3,7 @@ import '@libs/actions/IOU/MoneyRequest'; import {createSplitsAndOnyxData} from '@libs/actions/IOU/Split'; import {updateSplitTransactionsFromSplitExpensesFlow} from '@libs/actions/IOU/SplitTransactionUpdate'; import initOnyxDerivedValues from '@libs/actions/OnyxDerived'; +import * as TransactionActions from '@libs/actions/Transaction'; import isReportTopmostSplitNavigator from '@libs/Navigation/helpers/isReportTopmostSplitNavigator'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import {rand64} from '@libs/NumberUtils'; @@ -27,7 +28,7 @@ import createMock from '../../utils/createMock'; import {getGlobalFetchMock, formatPhoneNumber, getCurrencyDecimalsLocal, getCurrencySymbolLocal} from '../../utils/TestHelper'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; -const topMostReportID = '23423423'; +const mockTopMostReportID = '23423423'; jest.mock('@src/libs/Navigation/Navigation', () => ({ navigate: jest.fn(), dismissModal: jest.fn(), @@ -36,7 +37,7 @@ jest.mock('@src/libs/Navigation/Navigation', () => ({ navigateBackToLastSuperWideRHPScreen: jest.fn(), dismissModalWithReport: jest.fn(), goBack: jest.fn(), - getTopmostReportId: jest.fn(() => topMostReportID), + getTopmostReportId: jest.fn(() => mockTopMostReportID), setNavigationActionToMicrotaskQueue: jest.fn(), removeScreenByKey: jest.fn(), isNavigationReady: jest.fn(() => Promise.resolve()), @@ -84,15 +85,15 @@ jest.mock('@libs/deferredLayoutWrite', () => ({ })); jest.mock('@hooks/useCardFeedsForDisplay', () => jest.fn(() => ({defaultCardFeed: null, cardFeedsByPolicy: {}}))); -const unapprovedCashHash = 71801560; -const unapprovedCashSimilarSearchHash = 1832274510; +const mockUnapprovedCashHash = 71801560; +const mockUnapprovedCashSimilarSearchHash = 1832274510; jest.mock('@src/libs/SearchQueryUtils', () => { const actual = jest.requireActual('@src/libs/SearchQueryUtils'); // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { ...actual, getCurrentSearchQueryJSON: jest.fn().mockImplementation(() => ({ - hash: unapprovedCashHash, + hash: mockUnapprovedCashHash, query: 'test', type: 'expense', status: ['drafts', 'outstanding'], @@ -100,7 +101,7 @@ jest.mock('@src/libs/SearchQueryUtils', () => { flatFilters: [{key: 'reimbursable', filters: [{operator: 'eq', value: 'yes'}]}], inputQuery: '', recentSearchHash: 89, - similarSearchHash: unapprovedCashSimilarSearchHash, + similarSearchHash: mockUnapprovedCashSimilarSearchHash, sortBy: 'tag', sortOrder: 'asc', })), @@ -114,6 +115,14 @@ jest.mock('@libs/PolicyUtils', () => ({ isPolicyOwner: jest.fn().mockImplementation((policy?: OnyxEntry, currentUserAccountID?: number) => !!currentUserAccountID && policy?.ownerAccountID === currentUserAccountID), })); +jest.mock('@libs/actions/Transaction', () => { + const actual = jest.requireActual('@libs/actions/Transaction'); + return { + ...actual, + mergeTransactionIdsHighlightOnSearchRoute: jest.fn(actual.mergeTransactionIdsHighlightOnSearchRoute), + }; +}); + const CARLOS_EMAIL = 'cmartins@expensifail.com'; const CARLOS_ACCOUNT_ID = 1; const CARLOS_PARTICIPANT: Participant = {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, role: 'member'}; @@ -435,7 +444,7 @@ describe('actions/IOU', () => { it('handleNavigateAfterExpenseCreate', async () => { const mockedIsReportTopmostSplitNavigator = jest.mocked(isReportTopmostSplitNavigator); - const spyOnMergeTransactionIdsHighlightOnSearchRoute = jest.spyOn(require('@libs/actions/Transaction'), 'mergeTransactionIdsHighlightOnSearchRoute'); + const spyOnMergeTransactionIdsHighlightOnSearchRoute = jest.mocked(TransactionActions.mergeTransactionIdsHighlightOnSearchRoute); const activeReportID = '1'; const transactionID = '1'; mockedIsReportTopmostSplitNavigator.mockReturnValue(false); @@ -842,7 +851,7 @@ describe('actions/IOU', () => { it('registers the search-route highlight (not report metadata) when splitting from the Search/Spend page', async () => { // Given the user is on the Search (Spend > Expenses) page, where the expense report is never opened jest.mocked(isSearchTopmostFullScreenRoute).mockReturnValue(true); - const spyOnMergeTransactionIdsHighlightOnSearchRoute = jest.spyOn(require('@libs/actions/Transaction'), 'mergeTransactionIdsHighlightOnSearchRoute'); + const spyOnMergeTransactionIdsHighlightOnSearchRoute = jest.mocked(TransactionActions.mergeTransactionIdsHighlightOnSearchRoute); const params = buildBaseParams({ transactionData: { reportID: EXPENSE_REPORT_ID, @@ -966,7 +975,7 @@ describe('actions/IOU', () => { it('skips the search-route highlight during a reverse split from the Search/Spend page', async () => { // Given the user is on the Search page and this is a reverse split (1 expense, existing child present) jest.mocked(isSearchTopmostFullScreenRoute).mockReturnValue(true); - const spyOnMergeTransactionIdsHighlightOnSearchRoute = jest.spyOn(require('@libs/actions/Transaction'), 'mergeTransactionIdsHighlightOnSearchRoute'); + const spyOnMergeTransactionIdsHighlightOnSearchRoute = jest.mocked(TransactionActions.mergeTransactionIdsHighlightOnSearchRoute); const existingChildTx = { transactionID: 'child-tx-1', reportID: EXPENSE_REPORT_ID, @@ -997,7 +1006,7 @@ describe('actions/IOU', () => { // skipped while offline (it waits for a server re-search), so this rail is the only thing that can // highlight the new rows - a reviewer caught the highlight silently disappearing offline. jest.mocked(isSearchTopmostFullScreenRoute).mockReturnValue(true); - const spyOnMergeTransactionIdsHighlightOnSearchRoute = jest.spyOn(require('@libs/actions/Transaction'), 'mergeTransactionIdsHighlightOnSearchRoute'); + const spyOnMergeTransactionIdsHighlightOnSearchRoute = jest.mocked(TransactionActions.mergeTransactionIdsHighlightOnSearchRoute); const params = buildBaseParams({ isOffline: true, transactionData: { diff --git a/tests/actions/IOUTest/BulkEditTest.ts b/tests/actions/IOUTest/BulkEditTest.ts index 775093285f53..cc82bf4ff098 100644 --- a/tests/actions/IOUTest/BulkEditTest.ts +++ b/tests/actions/IOUTest/BulkEditTest.ts @@ -19,6 +19,14 @@ import {getCurrencyDecimalsLocal, getCurrencySymbolLocal, getRequiredOnyxUpdates import {isObject, parseJSONRecord} from '../../utils/typeGuards'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; +jest.mock('@src/libs/API', () => { + const actual = jest.requireActual('@src/libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + }; +}); + const RORY_ACCOUNT_ID = 3; function isPartialReport(value: unknown): value is Partial { @@ -84,7 +92,8 @@ describe('actions/IOU/BulkEdit', () => { const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true); const buildOptimisticSpy = jest.spyOn(require('@libs/ReportUtils'), 'buildOptimisticModifiedExpenseReportAction'); - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); updateMultipleMoneyRequests({ personalDetailsList: undefined, @@ -159,7 +168,8 @@ describe('actions/IOU/BulkEdit', () => { const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true); // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); updateMultipleMoneyRequests({ personalDetailsList: undefined, @@ -223,7 +233,8 @@ describe('actions/IOU/BulkEdit', () => { const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true); // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); updateMultipleMoneyRequests({ personalDetailsList: undefined, @@ -296,7 +307,8 @@ describe('actions/IOU/BulkEdit', () => { const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true); const buildOptimisticSpy = jest.spyOn(require('@libs/ReportUtils'), 'buildOptimisticModifiedExpenseReportAction'); // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); updateMultipleMoneyRequests({ personalDetailsList: undefined, @@ -373,7 +385,8 @@ describe('actions/IOU/BulkEdit', () => { const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true); // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); updateMultipleMoneyRequests({ personalDetailsList: undefined, @@ -440,7 +453,8 @@ describe('actions/IOU/BulkEdit', () => { const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true); const buildOptimisticSpy = jest.spyOn(require('@libs/ReportUtils'), 'buildOptimisticModifiedExpenseReportAction'); // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); updateMultipleMoneyRequests({ personalDetailsList: undefined, @@ -505,7 +519,8 @@ describe('actions/IOU/BulkEdit', () => { }; // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); // No canEditFieldOfMoneyRequest mock — unreported expenses must bypass that check updateMultipleMoneyRequests({ @@ -1481,7 +1496,8 @@ describe('actions/IOU/BulkEdit', () => { const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true); // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); updateMultipleMoneyRequests({ personalDetailsList: undefined, @@ -1549,7 +1565,8 @@ describe('actions/IOU/BulkEdit', () => { const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true); // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); updateMultipleMoneyRequests({ personalDetailsList: undefined, @@ -1638,7 +1655,8 @@ describe('actions/IOU/BulkEdit', () => { const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true); // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); // When: bulk-editing with the shared policy (different from transaction's policy) updateMultipleMoneyRequests({ @@ -1734,7 +1752,8 @@ describe('actions/IOU/BulkEdit', () => { const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true); const buildOptimisticSpy = jest.spyOn(require('@libs/ReportUtils'), 'buildOptimisticModifiedExpenseReportAction'); // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); // When: bulk-editing reimbursable with the shared policy (different from transaction's policy) updateMultipleMoneyRequests({ @@ -1809,7 +1828,8 @@ describe('actions/IOU/BulkEdit', () => { const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true); // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); updateMultipleMoneyRequests({ personalDetailsList: undefined, @@ -1882,7 +1902,8 @@ describe('actions/IOU/BulkEdit', () => { const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true); // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); updateMultipleMoneyRequests({ personalDetailsList: undefined, @@ -1948,7 +1969,8 @@ describe('actions/IOU/BulkEdit', () => { const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true); // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); updateMultipleMoneyRequests({ personalDetailsList: undefined, @@ -2008,7 +2030,8 @@ describe('actions/IOU/BulkEdit', () => { const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true); // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); updateMultipleMoneyRequests({ personalDetailsList: undefined, @@ -2116,7 +2139,8 @@ describe('actions/IOU/BulkEdit', () => { const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true); // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); updateMultipleMoneyRequests({ personalDetailsList: undefined, @@ -2209,7 +2233,8 @@ describe('actions/IOU/BulkEdit', () => { const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true); // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); updateMultipleMoneyRequests({ transactionIDs: [txn1ID, txn2ID], @@ -2298,7 +2323,8 @@ describe('actions/IOU/BulkEdit', () => { const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true); // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); updateMultipleMoneyRequests({ transactionIDs: [txnID], @@ -2395,7 +2421,8 @@ describe('actions/IOU/BulkEdit', () => { const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true); // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); // Iter 1 (currency) → indeterminate. Iter 2 (same-currency amount) must inherit the sticky flag. updateMultipleMoneyRequests({ @@ -2480,7 +2507,8 @@ describe('actions/IOU/BulkEdit', () => { const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true); // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); // `transactions` carries both (mirrors mergedTransactions in SearchEditMultiplePage). updateMultipleMoneyRequests({ @@ -2562,7 +2590,8 @@ describe('actions/IOU/BulkEdit', () => { const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true); // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); updateMultipleMoneyRequests({ transactionIDs: [txnID], @@ -2641,7 +2670,8 @@ describe('actions/IOU/BulkEdit', () => { const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true); // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); // Merchant edit leaves totals untouched, so the gate lets the recompute through. updateMultipleMoneyRequests({ @@ -2721,7 +2751,8 @@ describe('actions/IOU/BulkEdit', () => { const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true); // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Spying on API.write to assert the attendees command and optimistic data. - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); updateMultipleMoneyRequests({ personalDetailsList: undefined, @@ -2811,7 +2842,8 @@ describe('actions/IOU/BulkEdit', () => { const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true); // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Spying on API.write to assert attendees params omit phantom reportActionID. - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); updateMultipleMoneyRequests({ personalDetailsList: undefined, @@ -2893,7 +2925,8 @@ describe('actions/IOU/BulkEdit', () => { const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true); // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Spying on API.write to assert both persist commands. - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); updateMultipleMoneyRequests({ personalDetailsList: undefined, diff --git a/tests/actions/IOUTest/DeleteMoneyRequestTest.ts b/tests/actions/IOUTest/DeleteMoneyRequestTest.ts index 1510bbf6845e..8e61af7b16bc 100644 --- a/tests/actions/IOUTest/DeleteMoneyRequestTest.ts +++ b/tests/actions/IOUTest/DeleteMoneyRequestTest.ts @@ -37,7 +37,7 @@ import PusherHelper from '../../utils/PusherHelper'; import {createGlobalFetchMock, formatPhoneNumber, getCurrencyDecimalsLocal, getCurrencySymbolLocal, getOnyxData, setPersonalDetails, signInWithTestUser} from '../../utils/TestHelper'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; -const topMostReportID = '23423423'; +const mockTopMostReportID = '23423423'; jest.mock('@src/libs/Navigation/Navigation', () => ({ navigate: jest.fn(), dismissModal: jest.fn(), @@ -46,7 +46,7 @@ jest.mock('@src/libs/Navigation/Navigation', () => ({ navigateBackToLastSuperWideRHPScreen: jest.fn(), dismissModalWithReport: jest.fn(), goBack: jest.fn(), - getTopmostReportId: jest.fn(() => topMostReportID), + getTopmostReportId: jest.fn(() => mockTopMostReportID), setNavigationActionToMicrotaskQueue: jest.fn(), removeScreenByKey: jest.fn(), isNavigationReady: jest.fn(() => Promise.resolve()), diff --git a/tests/actions/IOUTest/DuplicateTest.ts b/tests/actions/IOUTest/DuplicateTest.ts index edb97d92a5ae..46d12d88e2a4 100644 --- a/tests/actions/IOUTest/DuplicateTest.ts +++ b/tests/actions/IOUTest/DuplicateTest.ts @@ -4,6 +4,7 @@ import type {RenderAPI} from '@testing-library/react-native'; import {bulkDuplicateExpenses, bulkDuplicateReports, duplicateExpenseTransaction, duplicateReport, mergeDuplicates, resolveDuplicates} from '@libs/actions/IOU/Duplicate'; import type {BulkDuplicateReportsParams, DuplicateReportParams} from '@libs/actions/IOU/Duplicate'; import {getReportPreviewReportAction} from '@libs/actions/IOU/MoneyRequestBuilder'; +import * as TrackExpense from '@libs/actions/IOU/TrackExpense'; import initOnyxDerivedValues from '@libs/actions/OnyxDerived'; import {addComment, openReport} from '@libs/actions/Report'; import type {MergeDuplicatesParams} from '@libs/API/parameters'; @@ -17,7 +18,7 @@ import {buildOptimisticTransaction, isTimeRequest} from '@libs/TransactionUtils' import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; -import * as API from '@src/libs/API'; +import * as APIModule from '@src/libs/API'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OriginalMessageIOU, Policy, PolicyTagLists, RecentWaypoint, Report, ReportActions} from '@src/types/onyx'; import type ReportAction from '@src/types/onyx/ReportAction'; @@ -41,6 +42,14 @@ import {formatPhoneNumber, getCurrencyDecimalsLocal, getGlobalFetchMock, getOnyx import {isObject} from '../../utils/typeGuards'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; +jest.mock('@src/libs/API', () => { + const actual = jest.requireActual('@src/libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + }; +}); + const topMostReportID = '23423423'; jest.mock('@src/libs/Navigation/Navigation', () => ({ navigate: jest.fn(), @@ -71,6 +80,14 @@ jest.mock('@src/libs/actions/Report', () => { }); jest.mock('@libs/Navigation/helpers/isSearchTopmostFullScreenRoute', () => jest.fn()); +jest.mock('@libs/actions/IOU/TrackExpense', () => { + const actual = jest.requireActual('@libs/actions/IOU/TrackExpense'); + return { + ...actual, + requestMoney: jest.fn(actual.requestMoney), + }; +}); + const RORY_EMAIL = 'rory@expensifail.com'; const RORY_ACCOUNT_ID = 3; @@ -91,14 +108,15 @@ describe('actions/Duplicate', () => { }); describe('mergeDuplicates', () => { - let writeSpy: jest.SpyInstance; + let writeSpy: jest.SpiedFunction; let currencyListProvider: RenderAPI; beforeEach(async () => { jest.clearAllMocks(); global.fetch = getGlobalFetchMock(); - writeSpy = jest.spyOn(API, 'write').mockImplementation((command, params, options) => { + writeSpy = jest.mocked(APIModule.write); + writeSpy.mockImplementation((command, params, options) => { // Apply optimistic data for testing if (options?.optimisticData) { for (const update of options.optimisticData) { @@ -912,13 +930,14 @@ describe('actions/Duplicate', () => { }); describe('resolveDuplicates', () => { - let writeSpy: jest.SpyInstance; + let writeSpy: jest.SpiedFunction; beforeEach(() => { jest.clearAllMocks(); global.fetch = getGlobalFetchMock(); - // eslint-disable-next-line rulesdir/no-multiple-api-calls - writeSpy = jest.spyOn(API, 'write').mockImplementation((command, params, options) => { + + writeSpy = jest.mocked(APIModule.write); + writeSpy.mockImplementation((command, params, options) => { // Apply optimistic data for testing if (options?.optimisticData) { for (const update of options.optimisticData) { @@ -1199,7 +1218,7 @@ describe('actions/Duplicate', () => { // Then: Verify API was called // eslint-disable-next-line - expect(API.write).toHaveBeenCalledWith(WRITE_COMMANDS.RESOLVE_DUPLICATES, expect.objectContaining({}), expect.objectContaining({})); + expect(APIModule.write).toHaveBeenCalledWith(WRITE_COMMANDS.RESOLVE_DUPLICATES, expect.objectContaining({}), expect.objectContaining({})); }); it('should handle missing IOU actions gracefully', async () => { @@ -1605,7 +1624,7 @@ describe('actions/Duplicate', () => { }); describe('duplicateExpenseTransaction', () => { - let writeSpy: jest.SpyInstance; + let writeSpy: jest.SpiedFunction; let recentWaypoints: RecentWaypoint[] = []; let targetPolicyTags: OnyxEntry; @@ -1628,8 +1647,9 @@ describe('actions/Duplicate', () => { beforeEach(async () => { jest.clearAllMocks(); global.fetch = getGlobalFetchMock(); - // eslint-disable-next-line rulesdir/no-multiple-api-calls - writeSpy = jest.spyOn(API, 'write').mockImplementation((command, params, options) => { + + writeSpy = jest.mocked(APIModule.write); + writeSpy.mockImplementation((command, params, options) => { // Apply optimistic data for testing if (options?.optimisticData) { for (const update of options.optimisticData) { @@ -1657,7 +1677,7 @@ describe('actions/Duplicate', () => { }); it('threads the conciergeChat report through to requestMoney', () => { - const requestMoneySpy = jest.spyOn(require('@libs/actions/IOU/TrackExpense'), 'requestMoney'); + const requestMoneySpy = jest.mocked(TrackExpense.requestMoney); const conciergeChat = {reportID: 'concierge-duplicate-1'}; duplicateExpenseTransaction({ @@ -2906,7 +2926,7 @@ describe('actions/Duplicate', () => { }); describe('duplicateReport', () => { - let writeSpy: jest.SpyInstance; + let writeSpy: jest.SpiedFunction; const mockPolicy = createRandomPolicy(1); const mockPolicyCategories = createRandomPolicyCategories(3); @@ -2988,8 +3008,9 @@ describe('actions/Duplicate', () => { beforeEach(async () => { jest.clearAllMocks(); global.fetch = getGlobalFetchMock(); - // eslint-disable-next-line rulesdir/no-multiple-api-calls - writeSpy = jest.spyOn(API, 'write').mockImplementation((command, params, options) => { + + writeSpy = jest.mocked(APIModule.write); + writeSpy.mockImplementation((command, params, options) => { if (options?.optimisticData) { for (const update of options.optimisticData) { if (update.onyxMethod === Onyx.METHOD.MERGE) { @@ -3471,7 +3492,7 @@ describe('actions/Duplicate', () => { }); describe('bulkDuplicateExpenses', () => { - let writeSpy: jest.SpyInstance; + let writeSpy: jest.SpiedFunction; const mockPolicy: Policy = { ...createRandomPolicy(1), @@ -3486,8 +3507,9 @@ describe('actions/Duplicate', () => { beforeEach(async () => { jest.clearAllMocks(); global.fetch = getGlobalFetchMock(); - // eslint-disable-next-line rulesdir/no-multiple-api-calls - writeSpy = jest.spyOn(API, 'write').mockImplementation((command, params, options) => { + + writeSpy = jest.mocked(APIModule.write); + writeSpy.mockImplementation((command, params, options) => { if (options?.optimisticData) { for (const update of options.optimisticData) { if (update.onyxMethod === Onyx.METHOD.MERGE) { @@ -3564,10 +3586,10 @@ describe('actions/Duplicate', () => { await waitForBatchedUpdates(); - const requestMoneyCalls = writeSpy.mock.calls.filter(isWriteMockCallForCommand(WRITE_COMMANDS.REQUEST_MONEY)); + const requestMoneyCalls = (writeSpy.mock.calls as unknown[][]).filter(isWriteMockCallForCommand(WRITE_COMMANDS.REQUEST_MONEY)); expect(requestMoneyCalls).toHaveLength(3); - const iouReportIDs = new Set(requestMoneyCalls.map((call) => call[1].iouReportID)); + const iouReportIDs = new Set(requestMoneyCalls.map(([, params]) => params.iouReportID)); expect(iouReportIDs.size).toBe(1); }); @@ -3630,7 +3652,7 @@ describe('actions/Duplicate', () => { }); describe('bulkDuplicateReports', () => { - let writeSpy: jest.SpyInstance; + let writeSpy: jest.SpiedFunction; const SOURCE_POLICY_ID = 'sourcePolicy1'; const DEFAULT_POLICY_ID = 'defaultPolicy1'; @@ -3741,8 +3763,9 @@ describe('actions/Duplicate', () => { beforeEach(async () => { jest.clearAllMocks(); global.fetch = getGlobalFetchMock(); - // eslint-disable-next-line rulesdir/no-multiple-api-calls - writeSpy = jest.spyOn(API, 'write').mockImplementation((command, params, options) => { + + writeSpy = jest.mocked(APIModule.write); + writeSpy.mockImplementation((command, params, options) => { if (options?.optimisticData) { for (const update of options.optimisticData) { if (update.onyxMethod === Onyx.METHOD.MERGE) { @@ -3915,10 +3938,10 @@ describe('actions/Duplicate', () => { expect(countWriteCommandCalls(WRITE_COMMANDS.CREATE_APP_REPORT)).toBe(2); expect(countWriteCommandCalls(WRITE_COMMANDS.REQUEST_MONEY)).toBe(2); - const createReportCalls = writeSpy.mock.calls.filter(isWriteMockCallForCommand(WRITE_COMMANDS.CREATE_APP_REPORT)); + const createReportCalls = (writeSpy.mock.calls as unknown[][]).filter(isWriteMockCallForCommand(WRITE_COMMANDS.CREATE_APP_REPORT)); expect(createReportCalls).toHaveLength(2); - const reportNames = createReportCalls.map((call) => call[1].reportName); + const reportNames = createReportCalls.map(([, params]) => params.reportName); expect(reportNames).toContain('Copy of Source Policy Report'); expect(reportNames).toContain('Copy of Inaccessible Policy Report'); }); diff --git a/tests/actions/IOUTest/HoldTest.ts b/tests/actions/IOUTest/HoldTest.ts index db3a312eb97a..010636224dc6 100644 --- a/tests/actions/IOUTest/HoldTest.ts +++ b/tests/actions/IOUTest/HoldTest.ts @@ -30,13 +30,13 @@ import {createGlobalFetchMock, getCurrencyDecimalsLocal} from '../../utils/TestH import {hasDefinedProperty, isObject} from '../../utils/typeGuards'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; -const topMostReportID = '23423423'; +const mockTopMostReportID = '23423423'; jest.mock('@src/libs/Navigation/Navigation', () => ({ navigate: jest.fn(), dismissModal: jest.fn(), dismissModalWithReport: jest.fn(), goBack: jest.fn(), - getTopmostReportId: jest.fn(() => topMostReportID), + getTopmostReportId: jest.fn(() => mockTopMostReportID), setNavigationActionToMicrotaskQueue: jest.fn(), removeScreenByKey: jest.fn(), isNavigationReady: jest.fn(() => Promise.resolve()), diff --git a/tests/actions/IOUTest/PayMoneyRequestTest.ts b/tests/actions/IOUTest/PayMoneyRequestTest.ts index 0f0d3bfe16f4..2a4ef855b8ca 100644 --- a/tests/actions/IOUTest/PayMoneyRequestTest.ts +++ b/tests/actions/IOUTest/PayMoneyRequestTest.ts @@ -3,7 +3,7 @@ import {cancelPayment, completePaymentOnboarding, markReportPaymentReceived, pay import {requestMoney} from '@libs/actions/IOU/TrackExpense'; import initOnyxDerivedValues from '@libs/actions/OnyxDerived'; import {createWorkspace, generatePolicyID} from '@libs/actions/Policy/Policy'; -import {notifyNewAction} from '@libs/actions/Report'; +import * as ReportActionModule from '@libs/actions/Report'; import type * as PolicyUtils from '@libs/PolicyUtils'; import {getOriginalMessage, getReportActionHtml, getReportActionText, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {buildOptimisticIOUReport, buildOptimisticIOUReportAction} from '@libs/ReportUtils'; @@ -25,7 +25,6 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {OnyxEntry, OnyxInputValue} from 'react-native-onyx'; -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import Onyx from 'react-native-onyx'; import type {MockFetch} from '../../utils/TestHelper'; @@ -39,7 +38,7 @@ import getOnyxValue from '../../utils/getOnyxValue'; import {createGlobalFetchMock, formatPhoneNumber, getCurrencyDecimalsLocal, getOnyxData, translateLocal} from '../../utils/TestHelper'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; -const topMostReportID = '23423423'; +const mockTopMostReportID = '23423423'; function chatReportPolicyFromChat(chatReport: OnyxEntry): Policy { return {...createRandomPolicy(0), id: chatReport?.policyID ?? CONST.POLICY.ID_FAKE}; @@ -53,7 +52,7 @@ jest.mock('@src/libs/Navigation/Navigation', () => ({ navigateBackToLastSuperWideRHPScreen: jest.fn(), dismissModalWithReport: jest.fn(), goBack: jest.fn(), - getTopmostReportId: jest.fn(() => topMostReportID), + getTopmostReportId: jest.fn(() => mockTopMostReportID), setNavigationActionToMicrotaskQueue: jest.fn(), removeScreenByKey: jest.fn(), isNavigationReady: jest.fn(() => Promise.resolve()), @@ -69,11 +68,11 @@ jest.mock('@src/libs/Navigation/Navigation', () => ({ jest.mock('@react-navigation/native'); jest.mock('@src/libs/actions/Report', () => { - const originalModule = jest.requireActual('@src/libs/actions/Report'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return + const originalModule = jest.requireActual('@src/libs/actions/Report'); return { ...originalModule, notifyNewAction: jest.fn(), + completeOnboarding: jest.fn(originalModule.completeOnboarding), }; }); jest.mock('@libs/Navigation/helpers/isSearchTopmostFullScreenRoute', () => jest.fn()); @@ -866,7 +865,7 @@ describe('actions/IOU/PayMoneyRequest', () => { }) .then(() => { // When partially paying an iou report from the chat report via the report preview - const partialPayChatReport = {reportID: topMostReportID, policyID: CONST.POLICY.ID_FAKE}; + const partialPayChatReport = {reportID: mockTopMostReportID, policyID: CONST.POLICY.ID_FAKE}; payMoneyRequest({ conciergeChat: undefined, paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, @@ -890,7 +889,7 @@ describe('actions/IOU/PayMoneyRequest', () => { }) .then(() => { // Then notifyNewAction should be called on the top most report. - expect(notifyNewAction).toHaveBeenCalledWith(topMostReportID, undefined, true); + expect(ReportActionModule.notifyNewAction).toHaveBeenCalledWith(mockTopMostReportID, undefined, true); }); }); @@ -2139,7 +2138,7 @@ describe('actions/IOU/PayMoneyRequest', () => { let completeOnboardingSpy: jest.SpyInstance; beforeEach(async () => { - completeOnboardingSpy = jest.spyOn(require('@libs/actions/Report'), 'completeOnboarding').mockImplementation(jest.fn()); + completeOnboardingSpy = jest.mocked(ReportActionModule.completeOnboarding).mockImplementation(jest.fn()); await Onyx.set(ONYXKEYS.SESSION, {email: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}); await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, { [CARLOS_ACCOUNT_ID]: { diff --git a/tests/actions/IOUTest/ReceiptTest.ts b/tests/actions/IOUTest/ReceiptTest.ts index b8005fb62903..3feb7f6401b7 100644 --- a/tests/actions/IOUTest/ReceiptTest.ts +++ b/tests/actions/IOUTest/ReceiptTest.ts @@ -64,6 +64,21 @@ jest.mock('@libs/PolicyUtils', () => ({ isPaidGroupPolicy: jest.fn().mockReturnValue(true), isPolicyOwner: jest.fn().mockImplementation((policy?: OnyxEntry, currentUserAccountID?: number) => !!currentUserAccountID && policy?.ownerAccountID === currentUserAccountID), })); +jest.mock('@src/libs/API', () => { + const actual = jest.requireActual('@src/libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + }; +}); + +jest.mock('@src/libs/SearchQueryUtils', () => { + const actual = jest.requireActual('@src/libs/SearchQueryUtils'); + return { + ...actual, + getCurrentSearchQueryJSON: jest.fn(actual.getCurrentSearchQueryJSON), + }; +}); const RORY_EMAIL = 'rory@expensifail.com'; const RORY_ACCOUNT_ID = 3; @@ -138,7 +153,9 @@ describe('actions/IOU/Receipt', () => { let getCurrentSearchQueryJSONSpy: jest.SpyInstance; const mockApiWrite = () => { - return jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); + return writeSpy; }; beforeEach(() => { @@ -146,7 +163,7 @@ describe('actions/IOU/Receipt', () => { // within JavaScript's safe integer range so the transaction data and its Onyx key // continue to refer to the same ID. transactionID = Date.now().toString(); - getCurrentSearchQueryJSONSpy = jest.spyOn(SearchQueryUtils, 'getCurrentSearchQueryJSON').mockReturnValue(createMock({hash: snapshotHash})); + getCurrentSearchQueryJSONSpy = jest.mocked(SearchQueryUtils.getCurrentSearchQueryJSON).mockReturnValue(createMock({hash: snapshotHash})); }); afterEach(() => { @@ -561,7 +578,8 @@ describe('actions/IOU/Receipt', () => { it('should optimistically null the receipt and set pending field', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); await seedOnyx(); try { @@ -599,7 +617,8 @@ describe('actions/IOU/Receipt', () => { it('should call API.write with DETACH_RECEIPT command and correct params', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); await seedOnyx(); try { diff --git a/tests/actions/IOUTest/RejectMoneyRequestTest.ts b/tests/actions/IOUTest/RejectMoneyRequestTest.ts index 78e4fde5a569..51fcfb5e9b17 100644 --- a/tests/actions/IOUTest/RejectMoneyRequestTest.ts +++ b/tests/actions/IOUTest/RejectMoneyRequestTest.ts @@ -54,6 +54,13 @@ jest.mock('@src/libs/actions/Report', () => { }); jest.mock('@libs/Navigation/helpers/isSearchTopmostFullScreenRoute', () => jest.fn()); +jest.mock('@src/libs/API', () => { + const actual = jest.requireActual('@src/libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + }; +}); const RORY_EMAIL = 'rory@expensifail.com'; const RORY_ACCOUNT_ID = 3; @@ -201,7 +208,8 @@ describe('actions/IOU/RejectMoneyRequest', () => { it('should the createdIOUReportActionID parameter not be undefined when rejecting an expense to an open report', async () => { // Mock API.write for this test - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); const openingReport = { ...createRandomReport(3, undefined), @@ -337,7 +345,8 @@ describe('actions/IOU/RejectMoneyRequest', () => { it('should not create movedTransactionAction when rejecting an expense to a new draft report', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(() => Promise.resolve()); const secondTransaction = { ...createRandomTransaction(2), @@ -409,7 +418,8 @@ describe('actions/IOU/RejectMoneyRequest', () => { it('should call API.write with REJECT_EXPENSE_REPORT command', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); rejectExpenseReport(expenseReport, SUBMITTER_ACCOUNT_ID, comment, TEST_USER_ACCOUNT_ID, CURRENT_USER_DISPLAY_NAME, CURRENT_USER_AVATAR, false, undefined); await waitForBatchedUpdates(); @@ -430,7 +440,8 @@ describe('actions/IOU/RejectMoneyRequest', () => { const markdownComment = 'Rejected because **important**'; // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); rejectExpenseReport(expenseReport, SUBMITTER_ACCOUNT_ID, markdownComment, TEST_USER_ACCOUNT_ID, CURRENT_USER_DISPLAY_NAME, CURRENT_USER_AVATAR, false, undefined); await waitForBatchedUpdates(); @@ -553,7 +564,8 @@ describe('actions/IOU/RejectMoneyRequest', () => { it('should call API.write with MARK_TRANSACTION_VIOLATION_AS_RESOLVED command', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); if (!transaction?.transactionID || !iouReport?.reportID) { throw new Error('Required transaction or report data is missing'); @@ -591,7 +603,8 @@ describe('actions/IOU/RejectMoneyRequest', () => { it('should not make API call or notify when reportID is undefined', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); const {notifyNewAction} = require('@src/libs/actions/Report'); @@ -614,7 +627,8 @@ describe('actions/IOU/RejectMoneyRequest', () => { it('uses the passed transactionViolations parameter instead of the global Onyx collection', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); if (!transaction?.transactionID || !iouReport?.reportID) { throw new Error('Required transaction or report data is missing'); diff --git a/tests/actions/IOUTest/ReportWorkflowTest.ts b/tests/actions/IOUTest/ReportWorkflowTest.ts index 966d8367e725..062be51a450e 100644 --- a/tests/actions/IOUTest/ReportWorkflowTest.ts +++ b/tests/actions/IOUTest/ReportWorkflowTest.ts @@ -72,6 +72,14 @@ import {isObject} from '../../utils/typeGuards'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; import waitForBatchedUpdatesWithAct from '../../utils/waitForBatchedUpdatesWithAct'; +jest.mock('@src/libs/API', () => { + const actual = jest.requireActual('@src/libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + }; +}); + const topMostReportID = '23423423'; jest.mock('@src/libs/Navigation/Navigation', () => ({ navigate: jest.fn(), @@ -1749,7 +1757,8 @@ describe('actions/IOU/ReportWorkflow', () => { }); it('omits the API managerAccountID but keeps the existing report manager optimistically when policy employee data is missing', async () => { - const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(API.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const policyID = '1'; const submitterAccountID = 100; const correctManagerAccountID = 101; @@ -1823,7 +1832,8 @@ describe('actions/IOU/ReportWorkflow', () => { it('routes to the default approver when the submitter is a policy member but their submitsTo was removed from the workspace', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting API.write calls to verify submit payload and optimistic data. - const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(API.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const policyID = '1'; const submitterAccountID = 100; const removedApproverAccountID = 101; @@ -1901,7 +1911,8 @@ describe('actions/IOU/ReportWorkflow', () => { it('omits the API managerAccountID but keeps the existing report manager optimistically for a retracted report when policy employee data is missing', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting API.write calls to verify submit payload and optimistic data. - const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(API.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const policyID = '1'; const submitterAccountID = 100; const correctManagerAccountID = 101; @@ -1969,7 +1980,8 @@ describe('actions/IOU/ReportWorkflow', () => { it('primes the report PDF-filename NVP when shouldExportToPDF is true', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting API.write calls to verify the PDF NVP optimistic/failure data. - const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(API.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const policyID = '1'; const submitterAccountID = 100; const submitterEmail = 'submitter@example.com'; @@ -2025,7 +2037,8 @@ describe('actions/IOU/ReportWorkflow', () => { it('does not touch the PDF-filename NVP when shouldExportToPDF is not set', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting API.write calls to verify the PDF NVP is absent. - const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(API.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const policyID = '1'; const submitterAccountID = 100; const submitterEmail = 'submitter@example.com'; @@ -2077,7 +2090,8 @@ describe('actions/IOU/ReportWorkflow', () => { it('uses the updated policy approver when employee data is available', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting API.write calls to verify submit payload and optimistic data. - const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(API.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const policyID = '1'; const adminAccountID = 100; const submitterAccountID = 101; @@ -2149,7 +2163,8 @@ describe('actions/IOU/ReportWorkflow', () => { it('uses the rule approver in the optimistic next step when the existing report manager is stale', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting API.write calls to verify submit payload and optimistic data. - const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(API.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const policyID = '1'; const submitterAccountID = 100; const defaultApproverAccountID = 101; @@ -2248,7 +2263,8 @@ describe('actions/IOU/ReportWorkflow', () => { it('keeps the workspace chat outstanding when an admin submits after approver changes', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting optimistic parent chat data after submit from workspace chat. - const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(API.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const policyID = '1'; const workspaceChatReportID = '2'; const adminAccountID = 100; @@ -2411,7 +2427,8 @@ describe('actions/IOU/ReportWorkflow', () => { it('recomputes the submit approver for a retracted forwarded report', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting API.write calls to verify submit payload and optimistic data. - const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(API.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const policyID = '1'; const submitterAccountID = 100; const firstApproverAccountID = 101; @@ -2483,7 +2500,8 @@ describe('actions/IOU/ReportWorkflow', () => { it('allows submit while a retract state update is pending', () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Verifying submitReport writes while offline retract is pending. - const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(API.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const policyID = '1'; const submitterAccountID = 100; const managerAccountID = 101; @@ -2549,7 +2567,8 @@ describe('actions/IOU/ReportWorkflow', () => { it('allows submit while only nextStep is pending', () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Verifying submitReport writes when only a generic nextStep update is pending. - const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(API.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const policyID = '1'; const submitterAccountID = 100; const managerAccountID = 101; @@ -2614,7 +2633,8 @@ describe('actions/IOU/ReportWorkflow', () => { it('restores the original report state and manager when submit fails', () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting API.write calls to verify failure rollback data. - const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(API.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const policyID = '1'; const submitterAccountID = 100; const managerAccountID = 101; @@ -2664,7 +2684,8 @@ describe('actions/IOU/ReportWorkflow', () => { it('omits the API managerAccountID from search submit when policy employee data is missing', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting API.write calls to verify search submit payload. - const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(API.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const policyID = '1'; const submitterAccountID = 100; const correctManagerAccountID = 101; @@ -2711,7 +2732,8 @@ describe('actions/IOU/ReportWorkflow', () => { it('uses the popover-selected manager email for search submit managerAccountID', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting API.write calls to verify search submit payload. - const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(API.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const policyID = '1'; const submitterAccountID = 100; const defaultManagerAccountID = 101; @@ -2770,7 +2792,8 @@ describe('actions/IOU/ReportWorkflow', () => { it('sends the manager email alone from search submit when the chosen manager email has no accountID', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting API.write calls to verify search submit payload. - const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(API.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const policyID = '1'; const submitterAccountID = 100; const defaultManagerAccountID = 101; @@ -2821,7 +2844,8 @@ describe('actions/IOU/ReportWorkflow', () => { it('resolves search submit managerAccountID from employeeList when personal details are missing', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting API.write calls to verify search submit payload. - const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(API.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const policyID = '1'; const submitterAccountID = 100; const defaultManagerAccountID = 101; @@ -2884,7 +2908,8 @@ describe('actions/IOU/ReportWorkflow', () => { it('submits from search while a retract update is pending', () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Verifying search submit writes while offline retract is pending. - const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(API.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const report: Report = { ...createRandomReport(1, undefined), reportID: '1', @@ -2901,7 +2926,8 @@ describe('actions/IOU/ReportWorkflow', () => { it('submits from search while only nextStep is pending', () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Verifying search submit writes when only a generic nextStep update is pending. - const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(API.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const report: Report = { ...createRandomReport(1, undefined), reportID: '1', @@ -2918,7 +2944,8 @@ describe('actions/IOU/ReportWorkflow', () => { it('optimistically updates the report status and adds a SUBMITTED action so search submit reflects while offline', () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting API.write onyxData to verify optimistic submit payload. - const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(API.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const report: Report = { ...createRandomReport(1, undefined), reportID: '1', @@ -2969,8 +2996,6 @@ describe('actions/IOU/ReportWorkflow', () => { beforeEach(async () => { jest.clearAllMocks(); - // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting API.write calls in each delegate forwarding test. - jest.spyOn(API, 'write'); await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { [DELEGATE_ACCOUNT_ID]: { accountID: DELEGATE_ACCOUNT_ID, @@ -3197,7 +3222,7 @@ describe('actions/IOU/ReportWorkflow', () => { beforeEach(() => { jest.clearAllMocks(); // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting API.write calls to verify optimistic/failure data. - jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + jest.mocked(API.write).mockImplementation(() => Promise.resolve()); }); it('clears hasOutstandingChildRequest optimistically and restores it if the approve request fails', () => { @@ -3839,7 +3864,8 @@ describe('actions/IOU/ReportWorkflow', () => { describe('retractReport', () => { it('does not set a retract pending field that hides resubmit', () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting API.write calls to verify retract optimistic data does not hide resubmit. - const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(API.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const policy: OnyxEntry = createRandomPolicy(1); const expenseReport: Report = { ...createRandomReport(1, undefined), @@ -3905,7 +3931,8 @@ describe('actions/IOU/ReportWorkflow', () => { describe('change approver formatter forwarding', () => { it('uses the injected formatter for the optimistic approver display name', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting API.write optimistic data to verify formatter forwarding. - const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(API.write); + apiWriteSpy.mockImplementation(() => Promise.resolve()); const approverAccountID = 8332403627; const approverLogin = '+18332403627@expensify.sms'; const formatPhoneNumberSpy = jest.fn(formatPhoneNumber); diff --git a/tests/actions/IOUTest/SendInvoiceTest.ts b/tests/actions/IOUTest/SendInvoiceTest.ts index 014c00f1c4e1..02157e200651 100644 --- a/tests/actions/IOUTest/SendInvoiceTest.ts +++ b/tests/actions/IOUTest/SendInvoiceTest.ts @@ -31,6 +31,14 @@ import initCurrencyListContext from '../../utils/initCurrencyListContext'; import {createGlobalFetchMock, formatPhoneNumber, getCurrencyDecimalsLocal} from '../../utils/TestHelper'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; +jest.mock('@src/libs/API', () => { + const actual = jest.requireActual('@src/libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + }; +}); + const topMostReportID = '23423423'; jest.mock('@src/libs/Navigation/Navigation', () => ({ navigate: jest.fn(), @@ -749,7 +757,8 @@ describe('actions/SendInvoice', () => { describe('sendInvoice', () => { it('creates a new invoice chat when one has been converted from individual to business', async () => { // Mock API.write for this test - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); // Given a convertedInvoiceReport is stored in Onyx const {policy, transaction, convertedInvoiceChat}: InvoiceTestData = InvoiceData; @@ -834,7 +843,8 @@ describe('actions/SendInvoice', () => { const policyRecentlyUsedCategories: OnyxEntry = []; // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); // When sending an invoice sendInvoice({ @@ -917,7 +927,8 @@ describe('actions/SendInvoice', () => { }; // eslint-disable-next-line rulesdir/no-multiple-api-calls - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); sendInvoice({ currentUserAccountID: 123, diff --git a/tests/actions/IOUTest/SendMoneyTest.ts b/tests/actions/IOUTest/SendMoneyTest.ts index c1fc34aea282..88e57d94a143 100644 --- a/tests/actions/IOUTest/SendMoneyTest.ts +++ b/tests/actions/IOUTest/SendMoneyTest.ts @@ -14,6 +14,14 @@ import Onyx from 'react-native-onyx'; import {getCurrencyDecimalsLocal, getGlobalFetchMock} from '../../utils/TestHelper'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; +jest.mock('@src/libs/API', () => { + const actual = jest.requireActual('@src/libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + }; +}); + const topMostReportID = '23423423'; jest.mock('@src/libs/Navigation/Navigation', () => ({ @@ -105,7 +113,8 @@ describe('actions/IOU/SendMoney', () => { describe('delegateAccountID forwarding', () => { it('sets delegateAccountID on the pay IOU action when delegateAccountID is provided', async () => { const DELEGATE_ACCOUNT_ID = 999; - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); sendMoneyElsewhere({ report: {reportID: ''}, diff --git a/tests/actions/IOUTest/SplitTest.ts b/tests/actions/IOUTest/SplitTest.ts index ec996875cf91..e38f67e2558a 100644 --- a/tests/actions/IOUTest/SplitTest.ts +++ b/tests/actions/IOUTest/SplitTest.ts @@ -9,7 +9,7 @@ import initOnyxDerivedValues from '@libs/actions/OnyxDerived'; import {createWorkspace, generatePolicyID, setWorkspaceApprovalMode} from '@libs/actions/Policy/Policy'; import {addComment, notifyNewAction} from '@libs/actions/Report'; import initSplitExpense from '@libs/actions/SplitExpenses'; -import type * as API from '@libs/API'; +import * as APIlib from '@libs/API'; import {WRITE_COMMANDS} from '@libs/API/types'; import {rand64} from '@libs/NumberUtils'; import {getIOUActionForReportID, getIOUActionForTransactionID, getOriginalMessage, isActionOfType, isAddCommentAction, isDeletedAction, isMoneyRequestAction} from '@libs/ReportActionsUtils'; @@ -80,8 +80,6 @@ import {isObject, parseJSONRecord} from '../../utils/typeGuards'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; import waitForNetworkPromises from '../../utils/waitForNetworkPromises'; -const APIlib = jest.requireActual('@libs/API'); - jest.mock('@src/libs/Navigation/Navigation', () => ({ navigate: jest.fn(), dismissModal: jest.fn(), @@ -130,6 +128,13 @@ jest.mock('@libs/deferredLayoutWrite', () => ({ reserveDeferredWriteChannel: jest.fn(), })); jest.mock('@hooks/useCardFeedsForDisplay', () => jest.fn(() => ({defaultCardFeed: null, cardFeedsByPolicy: {}}))); +jest.mock('@libs/API', () => { + const actual = jest.requireActual('@libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + }; +}); const unapprovedCashHash = 71801560; jest.mock('@src/libs/SearchQueryUtils', () => { @@ -5774,7 +5779,7 @@ describe('updateSplitTransactions', () => { const allPolicyTags = await getAllPolicyTags(); const reports = getTransactionAndExpenseReports(expenseReport.reportID); - const writeSpy = jest.spyOn(APIlib, 'write'); + const writeSpy = jest.mocked(APIlib.write); updateSplitTransactions({ getCurrencyDecimals: getCurrencyDecimalsLocal, @@ -5959,7 +5964,7 @@ describe('updateSplitTransactions', () => { const originalCommentAction = Object.values(originalThreadReportActions ?? {}).find((action) => isAddCommentAction(action)); expect(originalCommentAction?.reportActionID).toBeDefined(); - const writeSpy = jest.spyOn(APIlib, 'write'); + const writeSpy = jest.mocked(APIlib.write); // Split a transaction that already has thread comments. const {splitTransactionID1, splitTransactionID2} = await splitToTwo(expenseReport, originalTransactionID, iouAction); const split1ThreadReportID = getIOUActionForReportID(expenseReport?.reportID, splitTransactionID1)?.childReportID; @@ -6084,7 +6089,7 @@ describe('updateSplitTransactions', () => { expect(remainingTransactionCommentAction?.reportActionID).toBeDefined(); expect(remainingTransactionHoldAction?.reportActionID).toBeDefined(); - const writeSpy = jest.spyOn(APIlib, 'write'); + const writeSpy = jest.mocked(APIlib.write); const remainingSplitTransaction = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION}${splitTransactionID1}`); const {allTransactions, allReports, allReportNameValuePairs, allReportActions} = await getCollections(); diff --git a/tests/actions/IOUTest/TrackExpenseTest.ts b/tests/actions/IOUTest/TrackExpenseTest.ts index bdb56be776b0..867243ecc76e 100644 --- a/tests/actions/IOUTest/TrackExpenseTest.ts +++ b/tests/actions/IOUTest/TrackExpenseTest.ts @@ -99,6 +99,13 @@ jest.mock('@libs/PolicyUtils', () => ({ isPaidGroupPolicy: jest.fn().mockReturnValue(true), isPolicyOwner: jest.fn().mockImplementation((policy?: OnyxEntry, currentUserAccountID?: number) => !!currentUserAccountID && policy?.ownerAccountID === currentUserAccountID), })); +jest.mock('@src/libs/API', () => { + const actual = jest.requireActual('@src/libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + }; +}); const CARLOS_EMAIL = 'cmartins@expensifail.com'; const CARLOS_ACCOUNT_ID = 1; @@ -2727,7 +2734,8 @@ describe('actions/IOU/TrackExpense', () => { afterEach(PusherHelper.teardown); it('should call API.write with delete money request onyx data for selfDM track expenses and return the parent report route in single transaction view', () => { - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); const result = deleteTrackExpense({ getCurrencyDecimals: getCurrencyDecimalsLocal, diff --git a/tests/actions/IOUTest/UpdateMoneyRequestTest.ts b/tests/actions/IOUTest/UpdateMoneyRequestTest.ts index 8a268625ca3d..d931621d1e70 100644 --- a/tests/actions/IOUTest/UpdateMoneyRequestTest.ts +++ b/tests/actions/IOUTest/UpdateMoneyRequestTest.ts @@ -46,6 +46,14 @@ import getOnyxValue from '../../utils/getOnyxValue'; import {createGlobalFetchMock, getCurrencyDecimalsLocal, getCurrencySymbolLocal} from '../../utils/TestHelper'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; +jest.mock('@libs/API', () => { + const actual = jest.requireActual('@libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + }; +}); + const topMostReportID = '23423423'; jest.mock('@src/libs/Navigation/Navigation', () => ({ navigate: jest.fn(), @@ -2143,7 +2151,8 @@ describe('actions/IOU/UpdateMoneyRequest', () => { describe('updateMoneyRequestDate distance rate recalculation', () => { it('calls UpdateMoneyRequestDistanceRate with created when a workspace distance expense date change selects a different rate', async () => { - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); const transactionID = 'distance_date_rate_switch'; const transactionThreadReportID = 'thread_date_rate_switch'; const expenseReportID = 'expense_report_date_rate_switch'; @@ -2277,7 +2286,8 @@ describe('actions/IOU/UpdateMoneyRequest', () => { it('calls UpdateMoneyRequestDate only when the current rate remains eligible for the new date', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting API.write calls to verify date-only update path. - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); const transactionID = 'distance_date_same_rate'; const transactionThreadReportID = 'thread_date_same_rate'; const expenseReportID = 'expense_report_date_same_rate'; @@ -2385,7 +2395,8 @@ describe('actions/IOU/UpdateMoneyRequest', () => { it('calls UpdateMoneyRequestDistanceRate when the current rate is missing from the policy', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting API.write calls to verify distance rate update path. - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); const transactionID = 'distance_date_orphan_rate'; const transactionThreadReportID = 'thread_date_orphan_rate'; const expenseReportID = 'expense_report_date_orphan_rate'; @@ -2494,7 +2505,8 @@ describe('actions/IOU/UpdateMoneyRequest', () => { it('calls UpdateMoneyRequestDate only when no mileage rate is eligible for the new date', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting API.write calls to verify date-only update path. - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); const transactionID = 'distance_date_no_eligible_rate'; const transactionThreadReportID = 'thread_date_no_eligible_rate'; const expenseReportID = 'expense_report_date_no_eligible_rate'; @@ -2606,7 +2618,8 @@ describe('actions/IOU/UpdateMoneyRequest', () => { it('calls UpdateMoneyRequestDistanceRate with created when a Self DM track distance expense date change selects a different rate', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting API.write calls to verify date-only update path. - const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + const writeSpy = jest.mocked(API.write); + writeSpy.mockImplementation(jest.fn()); const transactionID = 'distance_date_self_dm'; const transactionThreadReportID = 'thread_date_self_dm'; const selfDMReportID = 'self_dm_date_rate'; diff --git a/tests/actions/IOUTest/UpdateMoneyRequestVendorTest.ts b/tests/actions/IOUTest/UpdateMoneyRequestVendorTest.ts index a3dda82b313d..0d3f94286b35 100644 --- a/tests/actions/IOUTest/UpdateMoneyRequestVendorTest.ts +++ b/tests/actions/IOUTest/UpdateMoneyRequestVendorTest.ts @@ -12,6 +12,14 @@ import {getRequiredOnyxUpdate, getRequiredOnyxUpdates, getRequiredWriteCall} fro import {isObject} from '../../utils/typeGuards'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; +jest.mock('@libs/API', () => { + const actual = jest.requireActual('@libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + }; +}); + const TRANSACTION_ID = 'txn-vendor-test'; const baseTransaction: Transaction = { @@ -44,7 +52,8 @@ describe('updateMoneyRequestVendor', () => { }); beforeEach(() => { - writeSpy = jest.spyOn(APIActions, 'write').mockImplementation(jest.fn()); + writeSpy = jest.mocked(APIActions.write); + writeSpy.mockImplementation(jest.fn()); }); afterEach(async () => { diff --git a/tests/actions/MergeTransactionTest.ts b/tests/actions/MergeTransactionTest.ts index 793da5bc2965..12949070111b 100644 --- a/tests/actions/MergeTransactionTest.ts +++ b/tests/actions/MergeTransactionTest.ts @@ -38,6 +38,14 @@ import * as TestHelper from '../utils/TestHelper'; import {getCurrencyDecimalsLocal, getCurrencySymbolLocal} from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +jest.mock('@libs/API', () => { + const actual = jest.requireActual('@libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + }; +}); + // Helper function to create mock violations function createMockViolations(): TransactionViolation[] { return [ @@ -535,7 +543,7 @@ describe('mergeTransactionRequest', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${sourceTransaction.transactionID}`, sourceTransaction); await Onyx.set(`${ONYXKEYS.COLLECTION.MERGE_TRANSACTION}${mergeTransactionID}`, mergeTransaction); - const writeSpy = jest.spyOn(API, 'write'); + const writeSpy = jest.mocked(API.write); mockFetch?.pause?.(); diff --git a/tests/actions/OdometerTransactionUtilsTest.ts b/tests/actions/OdometerTransactionUtilsTest.ts index 2876d0bb0468..48c077e5f8e2 100644 --- a/tests/actions/OdometerTransactionUtilsTest.ts +++ b/tests/actions/OdometerTransactionUtilsTest.ts @@ -30,7 +30,7 @@ import getOnyxValue from '../utils/getOnyxValue'; import {getGlobalFetchMock} from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; -const topMostReportID = '23423423'; +const mockTopMostReportID = '23423423'; jest.mock('@src/libs/Navigation/Navigation', () => ({ navigate: jest.fn(), dismissModal: jest.fn(), @@ -39,7 +39,7 @@ jest.mock('@src/libs/Navigation/Navigation', () => ({ navigateBackToLastSuperWideRHPScreen: jest.fn(), dismissModalWithReport: jest.fn(), goBack: jest.fn(), - getTopmostReportId: jest.fn(() => topMostReportID), + getTopmostReportId: jest.fn(() => mockTopMostReportID), setNavigationActionToMicrotaskQueue: jest.fn(), removeScreenByKey: jest.fn(), isNavigationReady: jest.fn(() => Promise.resolve()), @@ -82,15 +82,15 @@ jest.mock('@libs/deferredLayoutWrite', () => ({ })); jest.mock('@hooks/useCardFeedsForDisplay', () => jest.fn(() => ({defaultCardFeed: null, cardFeedsByPolicy: {}}))); -const unapprovedCashHash = 71801560; -const unapprovedCashSimilarSearchHash = 1832274510; +const mockUnapprovedCashHash = 71801560; +const mockUnapprovedCashSimilarSearchHash = 1832274510; jest.mock('@src/libs/SearchQueryUtils', () => { const actual = jest.requireActual('@src/libs/SearchQueryUtils'); // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { ...actual, getCurrentSearchQueryJSON: jest.fn().mockImplementation(() => ({ - hash: unapprovedCashHash, + hash: mockUnapprovedCashHash, query: 'test', type: 'expense', status: ['drafts', 'outstanding'], @@ -98,7 +98,7 @@ jest.mock('@src/libs/SearchQueryUtils', () => { flatFilters: [{key: 'reimbursable', filters: [{operator: 'eq', value: 'yes'}]}], inputQuery: '', recentSearchHash: 89, - similarSearchHash: unapprovedCashSimilarSearchHash, + similarSearchHash: mockUnapprovedCashSimilarSearchHash, sortBy: 'tag', sortOrder: 'asc', })), diff --git a/tests/actions/PlaidTest.ts b/tests/actions/PlaidTest.ts index 8e33c6bb1175..27097920c0d1 100644 --- a/tests/actions/PlaidTest.ts +++ b/tests/actions/PlaidTest.ts @@ -4,10 +4,14 @@ import type {ApiRequestCommandParameters} from '@libs/API/types'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import getPlaidLinkTokenParameters from '@libs/getPlaidLinkTokenParameters'; -jest.mock('@libs/API', () => ({ - read: jest.fn(), - write: jest.fn(), -})); +jest.mock('@libs/API', () => { + const actual = jest.requireActual('@libs/API'); + return { + ...actual, + read: jest.fn(actual.read), + write: jest.fn(actual.write), + }; +}); jest.mock('@expensify/react-native-hybrid-app', () => ({ __esModule: true, default: { @@ -15,8 +19,8 @@ jest.mock('@expensify/react-native-hybrid-app', () => ({ }, })); -const readSpy = jest.spyOn(API, 'read'); -const writeSpy = jest.spyOn(API, 'write'); +const readSpy = jest.mocked(API.read); +const writeSpy = jest.mocked(API.write); describe('actions/Plaid', () => { beforeEach(() => { diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts index 462f785feb92..52b8d229584b 100644 --- a/tests/actions/PolicyTest.ts +++ b/tests/actions/PolicyTest.ts @@ -2,6 +2,7 @@ import type {GuidedSetupTask} from '@libs/actions/Report'; import * as APIModule from '@libs/API'; import {WRITE_COMMANDS} from '@libs/API/types'; import GoogleTagManager from '@libs/GoogleTagManager'; +import * as NextStepUtils from '@libs/NextStepUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import {isPolicyPayer} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -61,6 +62,43 @@ function requireCallArgument(call: unknown, index: number): unknown { jest.mock('@libs/GoogleTagManager'); +jest.mock('@libs/ReportUtils', () => { + const actual = jest.requireActual('@libs/ReportUtils'); + return { + ...actual, + prepareOnboardingOnyxData: jest.fn(actual.prepareOnboardingOnyxData), + getAllWorkspaceReports: jest.fn(actual.getAllWorkspaceReports), + getAllPolicyReports: jest.fn(actual.getAllPolicyReports), + isExpenseReport: jest.fn(actual.isExpenseReport), + hasViolations: jest.fn(actual.hasViolations), + isIOUReportUsingReport: jest.fn(actual.isIOUReportUsingReport), + }; +}); + +jest.mock('@libs/PersonalDetailsUtils', () => { + const actual = jest.requireActual('@libs/PersonalDetailsUtils'); + return { + ...actual, + getPersonalDetailByEmail: jest.fn(actual.getPersonalDetailByEmail), + }; +}); + +jest.mock('@libs/NextStepUtils', () => { + const actual = jest.requireActual('@libs/NextStepUtils'); + return { + ...actual, + buildOptimisticNextStep: jest.fn(actual.buildOptimisticNextStep), + }; +}); + +jest.mock('@libs/API', () => { + const actual = jest.requireActual('@libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + }; +}); + OnyxUpdateManager(); describe('actions/Policy', () => { beforeAll(() => { @@ -1394,7 +1432,7 @@ describe('actions/Policy', () => { await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); await waitForBatchedUpdates(); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); const policyID = Policy.generatePolicyID(); // When creating a workspace with distance rates feature enabled @@ -1438,7 +1476,7 @@ describe('actions/Policy', () => { await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); await waitForBatchedUpdates(); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); const policyID = Policy.generatePolicyID(); // When creating a workspace with isSelfTourViewed set to true @@ -1476,7 +1514,7 @@ describe('actions/Policy', () => { await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); await waitForBatchedUpdates(); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); const policyID = Policy.generatePolicyID(); // When creating a workspace with isSelfTourViewed set to false @@ -1516,7 +1554,7 @@ describe('actions/Policy', () => { // chat is passed below and prepareOnboardingOnyxData does not early-return. await waitForBatchedUpdates(); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); const policyID = Policy.generatePolicyID(); // When creating a workspace with isSelfTourViewed set to true. @@ -1565,7 +1603,7 @@ describe('actions/Policy', () => { // chat is passed below and prepareOnboardingOnyxData does not early-return. await waitForBatchedUpdates(); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); const policyID = Policy.generatePolicyID(); // When creating a workspace with isSelfTourViewed set to false. @@ -1651,7 +1689,7 @@ describe('actions/Policy', () => { await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); await waitForBatchedUpdates(); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); const policyID = Policy.generatePolicyID(); const adminEmail = 'admin@example.com'; const adminAccountID = 999; @@ -1694,7 +1732,7 @@ describe('actions/Policy', () => { await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); await waitForBatchedUpdates(); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); const policyID = Policy.generatePolicyID(); const createWorkspaceTaskReportID = 'testTaskReportID123'; @@ -1749,7 +1787,7 @@ describe('actions/Policy', () => { await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); await waitForBatchedUpdates(); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); const policyID = Policy.generatePolicyID(); // When creating a workspace and the user has already completed a guided onboarding flow, @@ -1786,7 +1824,7 @@ describe('actions/Policy', () => { await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); await waitForBatchedUpdates(); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); const policyID = Policy.generatePolicyID(); // When creating a workspace before the user has gone through guided onboarding (introSelected.choice is undefined), @@ -1822,7 +1860,7 @@ describe('actions/Policy', () => { await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); await waitForBatchedUpdates(); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); const policyID = Policy.generatePolicyID(); // Even when introSelected.choice is populated, TEST_DRIVE_RECEIVER must still enter the block via @@ -1857,10 +1895,10 @@ describe('actions/Policy', () => { await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); await waitForBatchedUpdates(); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); // Force prepareOnboardingOnyxData to return undefined so the early-return path inside the guarded block runs. // This mirrors the real-world case where the target chat (Concierge for non-MANAGE_TEAM flows) cannot be resolved. - const prepareSpy = jest.spyOn(ReportUtils, 'prepareOnboardingOnyxData').mockReturnValue(undefined); + const prepareSpy = jest.mocked(ReportUtils.prepareOnboardingOnyxData).mockReturnValue(undefined); const policyID = Policy.generatePolicyID(); // introSelected.choice is undefined so the block enters; engagementChoice is non-MANAGE_TEAM @@ -1943,7 +1981,7 @@ describe('actions/Policy', () => { await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); await waitForBatchedUpdates(); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); const policyID = Policy.generatePolicyID(); Policy.createWorkspace({ @@ -1975,7 +2013,7 @@ describe('actions/Policy', () => { await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); await waitForBatchedUpdates(); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); const policyID = Policy.generatePolicyID(); const adminEmail = 'admin@example.com'; const adminAccountID = 555; @@ -2019,7 +2057,7 @@ describe('actions/Policy', () => { await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); await waitForBatchedUpdates(); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); const adminEmail = 'admin@example.com'; const adminAccountID = 555; @@ -2369,7 +2407,7 @@ describe('actions/Policy', () => { describe('updateAddress', () => { it('should send discrete address fields with UPDATE_POLICY_ADDRESS', async () => { - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); const policyID = Policy.generatePolicyID(); Policy.updateAddress(policyID, { @@ -2407,7 +2445,7 @@ describe('actions/Policy', () => { }); it('should send an empty second line when addressStreet2 is missing', async () => { - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); const policyID = Policy.generatePolicyID(); Policy.updateAddress(policyID, { @@ -2638,8 +2676,8 @@ describe('actions/Policy', () => { }; const nonOwnedWorkspaceChats = [nonOwnedWorkspaceChat1, nonOwnedWorkspaceChat2]; - const getAllWorkspaceReportsSpy = jest.spyOn(ReportUtils, 'getAllWorkspaceReports').mockReturnValue([ownWorkspaceChat, ...nonOwnedWorkspaceChats]); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const getAllWorkspaceReportsSpy = jest.mocked(ReportUtils.getAllWorkspaceReports).mockReturnValue([ownWorkspaceChat, ...nonOwnedWorkspaceChats]); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); Policy.leaveWorkspace(ESH_ACCOUNT_ID, ESH_EMAIL, policy); await waitForBatchedUpdates(); @@ -2744,8 +2782,8 @@ describe('actions/Policy', () => { type: CONST.REPORT.TYPE.CHAT, }; - const getAllWorkspaceReportsSpy = jest.spyOn(ReportUtils, 'getAllWorkspaceReports').mockReturnValue([workspaceChat]); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const getAllWorkspaceReportsSpy = jest.mocked(ReportUtils.getAllWorkspaceReports).mockReturnValue([workspaceChat]); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); const customAccountID = 999; const customEmail = 'custom@example.com'; @@ -3234,7 +3272,7 @@ describe('actions/Policy', () => { }); it('should not call API when policy is undefined', async () => { - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); Policy.upgradeSubmit(undefined, CONST.POLICY.TYPE.TEAM, ESH_EMAIL, ESH_ACCOUNT_ID, undefined, undefined); await waitForBatchedUpdates(); @@ -3441,13 +3479,13 @@ describe('actions/Policy', () => { await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); await waitForBatchedUpdates(); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); const optimisticNextStep = createMock({messageKey: CONST.NEXT_STEP.MESSAGE_KEY.NO_FURTHER_ACTION, icon: CONST.NEXT_STEP.ICONS.CHECKMARK}); - const buildNextStepNewSpy = jest.spyOn(require('@libs/NextStepUtils'), 'buildOptimisticNextStep').mockReturnValue(optimisticNextStep); + const buildNextStepNewSpy = jest.mocked(NextStepUtils.buildOptimisticNextStep).mockReturnValue(optimisticNextStep); - const getAllPolicyReportsSpy = jest.spyOn(ReportUtils, 'getAllPolicyReports'); - const isExpenseReportSpy = jest.spyOn(ReportUtils, 'isExpenseReport'); - const hasViolationsSpy = jest.spyOn(ReportUtils, 'hasViolations'); + const getAllPolicyReportsSpy = jest.mocked(ReportUtils.getAllPolicyReports); + const isExpenseReportSpy = jest.mocked(ReportUtils.isExpenseReport); + const hasViolationsSpy = jest.mocked(ReportUtils.hasViolations); const policyID = Policy.generatePolicyID(); const fakePolicy: PolicyType = { @@ -3508,13 +3546,13 @@ describe('actions/Policy', () => { await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); await waitForBatchedUpdates(); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); const optimisticNextStep = createMock({messageKey: CONST.NEXT_STEP.MESSAGE_KEY.NO_FURTHER_ACTION, icon: CONST.NEXT_STEP.ICONS.CHECKMARK}); - const buildNextStepNewSpy = jest.spyOn(require('@libs/NextStepUtils'), 'buildOptimisticNextStep').mockReturnValue(optimisticNextStep); + const buildNextStepNewSpy = jest.mocked(NextStepUtils.buildOptimisticNextStep).mockReturnValue(optimisticNextStep); - const getAllPolicyReportsSpy = jest.spyOn(ReportUtils, 'getAllPolicyReports'); - const isExpenseReportSpy = jest.spyOn(ReportUtils, 'isExpenseReport'); - const hasViolationsSpy = jest.spyOn(ReportUtils, 'hasViolations').mockReturnValue(false); + const getAllPolicyReportsSpy = jest.mocked(ReportUtils.getAllPolicyReports); + const isExpenseReportSpy = jest.mocked(ReportUtils.isExpenseReport); + const hasViolationsSpy = jest.mocked(ReportUtils.hasViolations).mockReturnValue(false); const policyID = Policy.generatePolicyID(); const fakePolicy: PolicyType = { @@ -3573,9 +3611,9 @@ describe('actions/Policy', () => { await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); await waitForBatchedUpdates(); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); - const buildNextStepNewSpy = jest.spyOn(require('@libs/NextStepUtils'), 'buildOptimisticNextStep'); - const getAllPolicyReportsSpy = jest.spyOn(ReportUtils, 'getAllPolicyReports'); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); + const buildNextStepNewSpy = jest.mocked(NextStepUtils.buildOptimisticNextStep); + const getAllPolicyReportsSpy = jest.mocked(ReportUtils.getAllPolicyReports); const policyID = Policy.generatePolicyID(); const fakePolicy: PolicyType = { @@ -3671,7 +3709,7 @@ describe('actions/Policy', () => { await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); await waitForBatchedUpdates(); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); const policyID = Policy.generatePolicyID(); const fakePolicy: PolicyType = { @@ -3696,7 +3734,7 @@ describe('actions/Policy', () => { await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); await waitForBatchedUpdates(); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); const policyID = Policy.generatePolicyID(); const fakePolicy: PolicyType = { @@ -3721,7 +3759,7 @@ describe('actions/Policy', () => { await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); await waitForBatchedUpdates(); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); const policyID = Policy.generatePolicyID(); const employeeList = { @@ -3827,7 +3865,7 @@ describe('actions/Policy', () => { await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); await waitForBatchedUpdates(); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); const employeeWithNoForwarding = 'noforward@example.com'; const policyID = Policy.generatePolicyID(); @@ -3880,7 +3918,7 @@ describe('actions/Policy', () => { await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); await waitForBatchedUpdates(); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); const differentApprover = 'manager@example.com'; const policyID = Policy.generatePolicyID(); @@ -4717,7 +4755,7 @@ describe('actions/Policy', () => { const domain = 'example.com'; const displayNameForWorkspace = Str.UCFirst(domain.split('.').at(0) ?? ''); - jest.spyOn(PersonalDetailsUtils, 'getPersonalDetailByEmail').mockReturnValue({ + jest.mocked(PersonalDetailsUtils.getPersonalDetailByEmail).mockReturnValue({ displayName: TEST_DISPLAY_NAME, phoneNumber: TEST_PHONE_NUMBER, accountID: TEST_ACCOUNT_ID, @@ -4730,7 +4768,7 @@ describe('actions/Policy', () => { it('should generate a workspace name based on the display name when the domain is public and display name is available', () => { const displayNameForWorkspace = Str.UCFirst(TEST_DISPLAY_NAME); - jest.spyOn(PersonalDetailsUtils, 'getPersonalDetailByEmail').mockReturnValue({ + jest.mocked(PersonalDetailsUtils.getPersonalDetailByEmail).mockReturnValue({ displayName: TEST_DISPLAY_NAME, phoneNumber: TEST_PHONE_NUMBER, accountID: TEST_ACCOUNT_ID, @@ -4745,7 +4783,7 @@ describe('actions/Policy', () => { const username = emailParts.at(0) ?? ''; const displayNameForWorkspace = Str.UCFirst(username); - jest.spyOn(PersonalDetailsUtils, 'getPersonalDetailByEmail').mockReturnValue({ + jest.mocked(PersonalDetailsUtils.getPersonalDetailByEmail).mockReturnValue({ displayName: '', phoneNumber: TEST_PHONE_NUMBER, accountID: TEST_ACCOUNT_ID, @@ -4761,7 +4799,7 @@ describe('actions/Policy', () => { ...createRandomPolicy(0, CONST.POLICY.TYPE.PERSONAL, `${TEST_DISPLAY_NAME}'s Workspace 1`), }; - jest.spyOn(PersonalDetailsUtils, 'getPersonalDetailByEmail').mockReturnValue({ + jest.mocked(PersonalDetailsUtils.getPersonalDetailByEmail).mockReturnValue({ displayName: TEST_DISPLAY_NAME, phoneNumber: TEST_PHONE_NUMBER, accountID: TEST_ACCOUNT_ID, @@ -4774,7 +4812,7 @@ describe('actions/Policy', () => { }); it('should return "My Group Workspace" when the domain is SMS', () => { - jest.spyOn(PersonalDetailsUtils, 'getPersonalDetailByEmail').mockReturnValue({ + jest.mocked(PersonalDetailsUtils.getPersonalDetailByEmail).mockReturnValue({ displayName: TEST_DISPLAY_NAME, phoneNumber: TEST_PHONE_NUMBER, accountID: TEST_ACCOUNT_ID, @@ -4792,7 +4830,7 @@ describe('actions/Policy', () => { ...createRandomPolicy(0, CONST.POLICY.TYPE.PERSONAL, `${TEST_DISPLAY_NAME}'s Workspace 1`), }; - jest.spyOn(PersonalDetailsUtils, 'getPersonalDetailByEmail').mockReturnValue({ + jest.mocked(PersonalDetailsUtils.getPersonalDetailByEmail).mockReturnValue({ displayName: TEST_DISPLAY_NAME, phoneNumber: TEST_PHONE_NUMBER, accountID: TEST_ACCOUNT_ID, @@ -7489,8 +7527,8 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, iouReport); await waitForBatchedUpdates(); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); - const isIOUReportUsingReportSpy = jest.spyOn(ReportUtils, 'isIOUReportUsingReport').mockReturnValue(true); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); + const isIOUReportUsingReportSpy = jest.mocked(ReportUtils.isIOUReportUsingReport).mockReturnValue(true); Policy.createWorkspaceFromIOUPayment({ iouReport, @@ -7588,8 +7626,8 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, iouReport); await waitForBatchedUpdates(); - const isIOUReportUsingReportSpy = jest.spyOn(ReportUtils, 'isIOUReportUsingReport').mockReturnValue(true); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const isIOUReportUsingReportSpy = jest.mocked(ReportUtils.isIOUReportUsingReport).mockReturnValue(true); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); const result = Policy.createWorkspaceFromIOUPayment({ iouReport, @@ -7642,8 +7680,8 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, iouReport); await waitForBatchedUpdates(); - const isIOUReportUsingReportSpy = jest.spyOn(ReportUtils, 'isIOUReportUsingReport').mockReturnValue(true); - const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve()); + const isIOUReportUsingReportSpy = jest.mocked(ReportUtils.isIOUReportUsingReport).mockReturnValue(true); + const apiWriteSpy = jest.mocked(APIModule.write).mockImplementation(() => Promise.resolve()); Policy.createWorkspaceFromIOUPayment({ iouReport, diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index a4fd788509bd..a3655ddfb600 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -36,6 +36,7 @@ import Log from '@src/libs/Log'; import * as SequentialQueue from '@src/libs/Network/SequentialQueue'; import {setHasRadio} from '@src/libs/NetworkState'; import * as ReportUtils from '@src/libs/ReportUtils'; +import * as SearchQueryUtils from '@src/libs/SearchQueryUtils'; import type * as SearchQueryUtilsType from '@src/libs/SearchQueryUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -71,6 +72,14 @@ jest.mock('@libs/NextStepUtils', () => ({ // Only the layout-specific tests below override this, so it keeps the real implementation as its default and every // other test in this file keeps behaving exactly as it did before. jest.mock('@libs/getIsNarrowLayout', () => jest.fn(jest.requireActual<{default: () => boolean}>('@libs/getIsNarrowLayout').default)); +jest.mock('@libs/API', () => { + const actual = jest.requireActual('@libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + }; +}); + const mockGetIsNarrowLayout = jest.mocked(getIsNarrowLayout); const MOCKED_POLICY_EXPENSE_CHAT_REPORT_ID = '1234'; @@ -113,11 +122,12 @@ jest.mock('@src/libs/SearchQueryUtils', () => { const UTC = 'UTC'; jest.mock('@src/libs/actions/Report', () => { - const originalModule = jest.requireActual('@src/libs/actions/Report'); + const originalModule = jest.requireActual('@src/libs/actions/Report'); return { ...originalModule, showReportActionNotification: jest.fn(), + navigateToAndOpenReport: jest.fn(originalModule.navigateToAndOpenReport), }; }); @@ -259,7 +269,7 @@ describe('actions/Report', () => { setImmediate(jest.runOnlyPendingTimers); } global.fetch = TestHelper.createGlobalFetchMock(); - apiWriteSpy = jest.spyOn(API, 'write'); + apiWriteSpy = jest.mocked(API.write); // Clear the queue before each test to avoid test pollution SequentialQueue.resetQueue(); @@ -2994,7 +3004,7 @@ describe('actions/Report', () => { }); it('should add the report preview action to the chat snapshot when it is created', async () => { - jest.spyOn(require('@src/libs/SearchQueryUtils'), 'getCurrentSearchQueryJSON').mockImplementationOnce(() => + jest.mocked(SearchQueryUtils.getCurrentSearchQueryJSON).mockImplementationOnce(() => createMock({ hash: currentHash, inputQuery: 'test', @@ -5399,7 +5409,7 @@ describe('actions/Report', () => { it('should respect checkIfCurrentPageActive callback when creating new concierge chat', async () => { const checkIfCurrentPageActive = jest.fn(() => false); - const navigateToAndOpenReportSpy = jest.spyOn(Report, 'navigateToAndOpenReport'); + const navigateToAndOpenReportSpy = jest.mocked(Report.navigateToAndOpenReport); // Don't set CONCIERGE_REPORT_ID to simulate undefined state await waitForBatchedUpdates(); diff --git a/tests/actions/SessionTest.ts b/tests/actions/SessionTest.ts index 957dfb40b591..8316b80e84d7 100644 --- a/tests/actions/SessionTest.ts +++ b/tests/actions/SessionTest.ts @@ -40,6 +40,16 @@ import getOnyxValue from '../utils/getOnyxValue'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +jest.mock('@src/libs/API', () => { + const actual = jest.requireActual('@src/libs/API'); + return { + ...actual, + makeRequestWithSideEffects: jest.fn(actual.makeRequestWithSideEffects), + write: jest.fn(actual.write), + writeWithNoDuplicatesConflictAction: jest.fn(actual.writeWithNoDuplicatesConflictAction), + }; +}); + // We are mocking this method so that we can later test to see if it was called and what arguments it was called with. // We test HttpUtils.xhr() since this means that our API command turned into a network request and isn't only queued. HttpUtils.xhr = jest.fn>(); @@ -63,6 +73,24 @@ jest.mock('@libs/actions/Link', () => { jest.mock('@libs/getPlatform', () => jest.fn()); +jest.mock('@src/libs/actions/Session', () => { + const actual = jest.requireActual('@src/libs/actions/Session'); + return { + ...actual, + isSupportAuthToken: jest.fn(actual.isSupportAuthToken), + hasStashedSession: jest.fn(actual.hasStashedSession), + signOut: jest.fn(actual.signOut), + }; +}); + +jest.mock('@libs/Network/NetworkStore', () => { + const actual = jest.requireActual('@libs/Network/NetworkStore'); + return { + ...actual, + setAuthToken: jest.fn(actual.setAuthToken), + }; +}); + const mockedGetPlatform = jest.mocked(getPlatform); type AccountMergeUpdate = Extract, {onyxMethod: typeof Onyx.METHOD.MERGE}>; @@ -494,7 +522,8 @@ describe('Session', () => { test('SignOut should clear native startup prefetch state before LOG_OUT', async () => { const clearTokenRefreshMock = jest.mocked(clearTokenRefresh); const removeAllFromAutoprefetchMock = jest.mocked(removeAllFromAutoprefetch); - const makeRequestSpy = jest.spyOn(API, 'makeRequestWithSideEffects').mockResolvedValue(undefined); + const makeRequestSpy = jest.mocked(API.makeRequestWithSideEffects); + makeRequestSpy.mockResolvedValue(undefined); await SessionUtil.signOut({authToken: 'testAuthToken'}); @@ -558,9 +587,9 @@ describe('Session', () => { }); test('SignOutAndRedirectToSignIn should restore stashed session and redirect to OldDot supportal of supportal agent', async () => { - jest.spyOn(SessionUtil, 'isSupportAuthToken').mockReturnValue(true); - jest.spyOn(SessionUtil, 'hasStashedSession').mockReturnValue(true); - jest.spyOn(SessionUtil, 'signOut').mockResolvedValue(undefined); + jest.mocked(SessionUtil.isSupportAuthToken).mockReturnValue(true); + jest.mocked(SessionUtil.hasStashedSession).mockReturnValue(true); + jest.mocked(SessionUtil.signOut).mockResolvedValue(undefined); const onyxClearSpy = jest.spyOn(Onyx, 'clear').mockResolvedValue(undefined); const onyxMultiSetSpy = jest.spyOn(Onyx, 'multiSet').mockResolvedValue(undefined); @@ -591,9 +620,9 @@ describe('Session', () => { }); test('SignOutAndRedirectToSignIn should preserve SESSION and restore stashed session when shouldForceUseStashedSession is true', async () => { - jest.spyOn(SessionUtil, 'isSupportAuthToken').mockReturnValue(false); - jest.spyOn(SessionUtil, 'hasStashedSession').mockReturnValue(true); - jest.spyOn(SessionUtil, 'signOut').mockResolvedValue(undefined); + jest.mocked(SessionUtil.isSupportAuthToken).mockReturnValue(false); + jest.mocked(SessionUtil.hasStashedSession).mockReturnValue(true); + jest.mocked(SessionUtil.signOut).mockResolvedValue(undefined); const onyxClearSpy = jest.spyOn(Onyx, 'clear').mockResolvedValue(undefined); const onyxMultiSetSpy = jest.spyOn(Onyx, 'multiSet').mockResolvedValue(undefined); @@ -691,7 +720,8 @@ describe('Session', () => { mockedGetPlatform.mockReturnValue(CONST.PLATFORM.WEB); - const makeRequestSpy = jest.spyOn(API, 'makeRequestWithSideEffects').mockResolvedValue(undefined); + const makeRequestSpy = jest.mocked(API.makeRequestWithSideEffects); + makeRequestSpy.mockResolvedValue(undefined); await SessionUtil.signOut({signedInWithSAML: true, authToken: 'testAuthToken', autoGeneratedLogin: 'testLogin'}); await waitForBatchedUpdates(); @@ -709,7 +739,8 @@ describe('Session', () => { mockedOpenAuthSessionAsync.mockClear(); - const makeRequestSpy = jest.spyOn(API, 'makeRequestWithSideEffects').mockResolvedValue(undefined); + const makeRequestSpy = jest.mocked(API.makeRequestWithSideEffects); + makeRequestSpy.mockResolvedValue(undefined); mockedGetPlatform.mockReturnValue(CONST.PLATFORM.ANDROID); await SessionUtil.signOut({signedInWithSAML: true, authToken: 'testAuthToken', autoGeneratedLogin: 'testLogin'}); @@ -727,7 +758,8 @@ describe('Session', () => { mockedOpenAuthSessionAsync.mockImplementationOnce(() => Promise.reject(new Error('Browser session failed'))); - const makeRequestSpy = jest.spyOn(API, 'makeRequestWithSideEffects').mockResolvedValue(undefined); + const makeRequestSpy = jest.mocked(API.makeRequestWithSideEffects); + makeRequestSpy.mockResolvedValue(undefined); await SessionUtil.signOut({signedInWithSAML: true, authToken: 'testAuthToken', autoGeneratedLogin: 'testLogin'}); await waitForBatchedUpdates(); @@ -742,7 +774,8 @@ describe('Session', () => { mockedOpenAuthSessionAsync.mockClear(); - const makeRequestSpy = jest.spyOn(API, 'makeRequestWithSideEffects').mockResolvedValue(undefined); + const makeRequestSpy = jest.mocked(API.makeRequestWithSideEffects); + makeRequestSpy.mockResolvedValue(undefined); await SessionUtil.signOut({authToken: 'testAuthToken', autoGeneratedLogin: 'testLogin'}); await waitForBatchedUpdates(); @@ -755,7 +788,8 @@ describe('Session', () => { describe('replaceTwoFactorDevice', () => { test('sets isLoading and clears errors optimistically for both steps', () => { - const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined); + const writeSpy = jest.mocked(API.write); + writeSpy.mockResolvedValue(undefined); SessionUtil.replaceTwoFactorDevice('verify_old', '123456'); @@ -770,7 +804,8 @@ describe('Session', () => { }); test('verify_old success data does not clear twoFactorAuthSecretKey', () => { - const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined); + const writeSpy = jest.mocked(API.write); + writeSpy.mockResolvedValue(undefined); SessionUtil.replaceTwoFactorDevice('verify_old', '123456'); @@ -785,7 +820,8 @@ describe('Session', () => { }); test('verify_new success data clears twoFactorAuthSecretKey to signal step completion', () => { - const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined); + const writeSpy = jest.mocked(API.write); + writeSpy.mockResolvedValue(undefined); SessionUtil.replaceTwoFactorDevice('verify_new', '654321'); @@ -802,14 +838,16 @@ describe('Session', () => { describe('validateTwoFactorAuth', () => { test('forced onboarding path updates auth token before clearing Onyx without openApp', async () => { - const makeRequestSpy = jest.spyOn(API, 'makeRequestWithSideEffects').mockResolvedValue({ + const makeRequestSpy = jest.mocked(API.makeRequestWithSideEffects); + makeRequestSpy.mockResolvedValue({ authToken: 'newAuthToken', encryptedAuthToken: 'newEncryptedAuthToken', }); - const setAuthTokenSpy = jest.spyOn(NetworkStore, 'setAuthToken'); + const setAuthTokenSpy = jest.mocked(NetworkStore.setAuthToken); const multiSetSpy = jest.spyOn(Onyx, 'multiSet').mockResolvedValue(undefined); const clearSpy = jest.spyOn(Onyx, 'clear').mockResolvedValue(undefined); - const writeWithNoDuplicatesSpy = jest.spyOn(API, 'writeWithNoDuplicatesConflictAction').mockResolvedValue(undefined); + const writeWithNoDuplicatesSpy = jest.mocked(API.writeWithNoDuplicatesConflictAction); + writeWithNoDuplicatesSpy.mockResolvedValue(undefined); SessionUtil.validateTwoFactorAuth('123456', false, {shouldKeepTwoFactorAuthFlowOpen: true}); await waitForBatchedUpdates(); @@ -904,7 +942,8 @@ describe('Session', () => { describe('resendValidateCode', () => { test('sends the login argument as the email param, independent of the CREDENTIALS Onyx cache', async () => { - const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined); + const writeSpy = jest.mocked(API.write); + writeSpy.mockResolvedValue(undefined); // CREDENTIALS is empty (cleared in beforeEach), so a correct email here proves the value comes from the param, not the module cache. SessionUtil.resendValidateCode({reasonCode: null}, 'passed-in@expensify.com'); @@ -918,7 +957,8 @@ describe('Session', () => { }); test('forwards the reasonCode from reasonParams to the API call', async () => { - const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined); + const writeSpy = jest.mocked(API.write); + writeSpy.mockResolvedValue(undefined); SessionUtil.resendValidateCode({reasonCode: COMMON_CONST.VALIDATE_CODE_REASONS.SIGN_IN}, 'passed-in@expensify.com'); await waitForBatchedUpdates(); @@ -929,7 +969,8 @@ describe('Session', () => { }); test('sends an undefined email when the login argument is undefined', async () => { - const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined); + const writeSpy = jest.mocked(API.write); + writeSpy.mockResolvedValue(undefined); SessionUtil.resendValidateCode({reasonCode: null}, undefined); await waitForBatchedUpdates(); @@ -940,7 +981,8 @@ describe('Session', () => { }); test('optimistically sets loadingForm to RESEND_VALIDATE_CODE_FORM', async () => { - const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined); + const writeSpy = jest.mocked(API.write); + writeSpy.mockResolvedValue(undefined); SessionUtil.resendValidateCode({reasonCode: null}, 'passed-in@expensify.com'); await waitForBatchedUpdates(); @@ -959,7 +1001,8 @@ describe('Session', () => { describe('signUpUser', () => { test('sends the login argument as the email param, independent of the CREDENTIALS Onyx cache', async () => { - const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined); + const writeSpy = jest.mocked(API.write); + writeSpy.mockResolvedValue(undefined); // CREDENTIALS is empty (cleared in beforeEach), so a correct email here proves the value comes from the param, not the module cache. SessionUtil.signUpUser('new-user@expensify.com', undefined); @@ -973,7 +1016,8 @@ describe('Session', () => { }); test('forwards the preferredLocale to the API call', async () => { - const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined); + const writeSpy = jest.mocked(API.write); + writeSpy.mockResolvedValue(undefined); SessionUtil.signUpUser('new-user@expensify.com', CONST.LOCALES.EN); await waitForBatchedUpdates(); @@ -984,7 +1028,8 @@ describe('Session', () => { }); test('includes hasSMSMarketingConsent when it is provided', async () => { - const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined); + const writeSpy = jest.mocked(API.write); + writeSpy.mockResolvedValue(undefined); SessionUtil.signUpUser('new-user@expensify.com', undefined, true); await waitForBatchedUpdates(); @@ -995,7 +1040,8 @@ describe('Session', () => { }); test('omits hasSMSMarketingConsent when it is undefined', async () => { - const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined); + const writeSpy = jest.mocked(API.write); + writeSpy.mockResolvedValue(undefined); SessionUtil.signUpUser('new-user@expensify.com', undefined); await waitForBatchedUpdates(); @@ -1174,7 +1220,8 @@ describe('Session', () => { describe('signIn', () => { test('sends the login and validate code arguments to the API, independent of the CREDENTIALS Onyx cache', async () => { - const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined); + const writeSpy = jest.mocked(API.write); + writeSpy.mockResolvedValue(undefined); // CREDENTIALS is empty (cleared in beforeEach), so correct values here prove they come from the params, not the module cache. SessionUtil.signIn('112233', undefined, undefined, 'user@expensify.com', undefined); @@ -1188,7 +1235,8 @@ describe('Session', () => { }); test('falls back to the stored validate code when no code is entered during the 2FA step', async () => { - const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined); + const writeSpy = jest.mocked(API.write); + writeSpy.mockResolvedValue(undefined); // Empty entered validateCode + a 2FA code present → should use the stored validate code passed in. SessionUtil.signIn('', undefined, '654321', 'user@expensify.com', 'stored-code'); @@ -1200,7 +1248,8 @@ describe('Session', () => { }); test('sends the stored authToken during the 2FA step', async () => { - const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined); + const writeSpy = jest.mocked(API.write); + writeSpy.mockResolvedValue(undefined); SessionUtil.signIn('', undefined, '654321', 'user@expensify.com', 'stored-code', 'stored-auth-token'); await waitForBatchedUpdates(); @@ -1211,7 +1260,8 @@ describe('Session', () => { }); test('does not send an authToken on the initial validate code submission', async () => { - const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined); + const writeSpy = jest.mocked(API.write); + writeSpy.mockResolvedValue(undefined); // No 2FA code yet, even though a stored authToken is passed in - shouldn't be sent. SessionUtil.signIn('112233', undefined, undefined, 'user@expensify.com', undefined, 'stored-auth-token'); @@ -1225,7 +1275,8 @@ describe('Session', () => { describe('requestUnlinkValidationLink', () => { test('sends the login argument as the email param, independent of the CREDENTIALS Onyx cache', async () => { - const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined); + const writeSpy = jest.mocked(API.write); + writeSpy.mockResolvedValue(undefined); // CREDENTIALS is empty (cleared in beforeEach), so a correct email here proves the value comes from the param, not the module cache. SessionUtil.requestUnlinkValidationLink('secondary@expensify.com'); @@ -1241,7 +1292,8 @@ describe('Session', () => { describe('signInWithValidateCode', () => { test('sends the entered code as the validate code when it is not a 2FA step', async () => { - const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined); + const writeSpy = jest.mocked(API.write); + writeSpy.mockResolvedValue(undefined); SessionUtil.signInWithValidateCode(123, '112233', undefined); await waitForBatchedUpdates(); @@ -1254,7 +1306,8 @@ describe('Session', () => { }); test('uses the stored validate code instead of the entered code during a 2FA step', async () => { - const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined); + const writeSpy = jest.mocked(API.write); + writeSpy.mockResolvedValue(undefined); // twoFactorAuthCode present → should use storedValidateCode, ignoring the entered `code`. SessionUtil.signInWithValidateCode(123, 'ignored-code', undefined, '654321', 'stored-code'); @@ -1266,7 +1319,8 @@ describe('Session', () => { }); test('sends the stored authToken during the 2FA step', async () => { - const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined); + const writeSpy = jest.mocked(API.write); + writeSpy.mockResolvedValue(undefined); SessionUtil.signInWithValidateCode(123, 'ignored-code', undefined, '654321', 'stored-code', 'stored-auth-token'); await waitForBatchedUpdates(); @@ -1277,7 +1331,8 @@ describe('Session', () => { }); test('does not send an authToken on the initial validate code submission', async () => { - const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined); + const writeSpy = jest.mocked(API.write); + writeSpy.mockResolvedValue(undefined); SessionUtil.signInWithValidateCode(123, '112233', undefined, undefined, undefined, 'stored-auth-token'); await waitForBatchedUpdates(); diff --git a/tests/actions/TaskTest.ts b/tests/actions/TaskTest.ts index 06f196c00b3c..83aac0f8bf25 100644 --- a/tests/actions/TaskTest.ts +++ b/tests/actions/TaskTest.ts @@ -58,6 +58,27 @@ jest.mock('@libs/actions/Welcome'); // Keep OnyxDerived real initialization below jest.mock('@components/LocaleContextProvider'); +jest.mock('@libs/ReportUtils', () => { + const actual = jest.requireActual('@libs/ReportUtils'); + return { + ...actual, + buildOptimisticTaskReport: jest.fn(actual.buildOptimisticTaskReport), + buildOptimisticCreatedReportAction: jest.fn(actual.buildOptimisticCreatedReportAction), + buildOptimisticTaskCommentReportAction: jest.fn(actual.buildOptimisticTaskCommentReportAction), + getTaskAssigneeChatOnyxData: jest.fn(actual.getTaskAssigneeChatOnyxData), + isHiddenForCurrentUser: jest.fn(actual.isHiddenForCurrentUser), + formatReportLastMessageText: jest.fn(actual.formatReportLastMessageText), + }; +}); + +jest.mock('@libs/actions/Report', () => { + const actual = jest.requireActual('@libs/actions/Report'); + return { + ...actual, + getMostRecentReportID: jest.fn(actual.getMostRecentReportID), + }; +}); + const mockWrite = jest.mocked(API.write); type ReportActionsKey = `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS}${string}`; @@ -106,15 +127,15 @@ const mockBuildOptimisticTaskCommentReportAction = jest.fn(); const mockGetTaskAssigneeChatOnyxData = jest.fn(); const mockIsHiddenForCurrentUser = jest.fn(); const mockFormatReportLastMessageText = jest.fn(); -jest.spyOn(ReportUtils, 'buildOptimisticTaskReport').mockImplementation(mockBuildOptimisticTaskReport); -jest.spyOn(ReportUtils, 'buildOptimisticCreatedReportAction').mockImplementation(mockBuildOptimisticCreatedReportAction); -jest.spyOn(ReportUtils, 'buildOptimisticTaskCommentReportAction').mockImplementation(mockBuildOptimisticTaskCommentReportAction); -jest.spyOn(ReportUtils, 'getTaskAssigneeChatOnyxData').mockImplementation(mockGetTaskAssigneeChatOnyxData); -jest.spyOn(ReportUtils, 'isHiddenForCurrentUser').mockImplementation(mockIsHiddenForCurrentUser); -jest.spyOn(ReportUtils, 'formatReportLastMessageText').mockImplementation(mockFormatReportLastMessageText); +jest.mocked(ReportUtils.buildOptimisticTaskReport).mockImplementation(mockBuildOptimisticTaskReport); +jest.mocked(ReportUtils.buildOptimisticCreatedReportAction).mockImplementation(mockBuildOptimisticCreatedReportAction); +jest.mocked(ReportUtils.buildOptimisticTaskCommentReportAction).mockImplementation(mockBuildOptimisticTaskCommentReportAction); +jest.mocked(ReportUtils.getTaskAssigneeChatOnyxData).mockImplementation(mockGetTaskAssigneeChatOnyxData); +jest.mocked(ReportUtils.isHiddenForCurrentUser).mockImplementation(mockIsHiddenForCurrentUser); +jest.mocked(ReportUtils.formatReportLastMessageText).mockImplementation(mockFormatReportLastMessageText); // Spy on API.write but allow calls to go through -const writeSpy = jest.spyOn(API, 'write'); +const writeSpy = jest.mocked(API.write); // A report actions map containing a single visible comment — used to exercise the real // doesReportHaveVisibleActions instead of mocking it. @@ -1418,7 +1439,7 @@ describe('actions/Task', () => { let getMostRecentReportIDSpy: jest.SpyInstance; beforeEach(() => { - getMostRecentReportIDSpy = jest.spyOn(ReportModule, 'getMostRecentReportID'); + getMostRecentReportIDSpy = jest.mocked(ReportModule.getMostRecentReportID); }); afterEach(() => { @@ -1651,7 +1672,7 @@ describe('actions/Task', () => { global.fetch = getGlobalFetchMock(); - getMostRecentReportIDSpy = jest.spyOn(ReportModule, 'getMostRecentReportID'); + getMostRecentReportIDSpy = jest.mocked(ReportModule.getMostRecentReportID); await act(async () => { await Onyx.clear(); diff --git a/tests/actions/TransactionTest.ts b/tests/actions/TransactionTest.ts index ae95548b127d..f26bb8981e04 100644 --- a/tests/actions/TransactionTest.ts +++ b/tests/actions/TransactionTest.ts @@ -65,7 +65,7 @@ function changeTransactionsReport({allTransactions, transactionIDs, transactionV }); } -const topMostReportID = '23423423'; +const mockTopMostReportID = '23423423'; jest.mock('@src/libs/Navigation/Navigation', () => ({ navigate: jest.fn(), dismissModal: jest.fn(), @@ -74,7 +74,7 @@ jest.mock('@src/libs/Navigation/Navigation', () => ({ navigateBackToLastSuperWideRHPScreen: jest.fn(), dismissModalWithReport: jest.fn(), goBack: jest.fn(), - getTopmostReportId: jest.fn(() => topMostReportID), + getTopmostReportId: jest.fn(() => mockTopMostReportID), setNavigationActionToMicrotaskQueue: jest.fn(), removeScreenByKey: jest.fn(), isNavigationReady: jest.fn(() => Promise.resolve()), @@ -117,15 +117,15 @@ jest.mock('@libs/deferredLayoutWrite', () => ({ })); jest.mock('@hooks/useCardFeedsForDisplay', () => jest.fn(() => ({defaultCardFeed: null, cardFeedsByPolicy: {}}))); -const unapprovedCashHash = 71801560; -const unapprovedCashSimilarSearchHash = 1832274510; +const mockUnapprovedCashHash = 71801560; +const mockUnapprovedCashSimilarSearchHash = 1832274510; jest.mock('@src/libs/SearchQueryUtils', () => { const actual = jest.requireActual('@src/libs/SearchQueryUtils'); // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { ...actual, getCurrentSearchQueryJSON: jest.fn().mockImplementation(() => ({ - hash: unapprovedCashHash, + hash: mockUnapprovedCashHash, query: 'test', type: 'expense', status: ['drafts', 'outstanding'], @@ -133,7 +133,7 @@ jest.mock('@src/libs/SearchQueryUtils', () => { flatFilters: [{key: 'reimbursable', filters: [{operator: 'eq', value: 'yes'}]}], inputQuery: '', recentSearchHash: 89, - similarSearchHash: unapprovedCashSimilarSearchHash, + similarSearchHash: mockUnapprovedCashSimilarSearchHash, sortBy: 'tag', sortOrder: 'asc', })), diff --git a/tests/actions/UserTest.ts b/tests/actions/UserTest.ts index b0a08721b3cc..e4475bef1dcf 100644 --- a/tests/actions/UserTest.ts +++ b/tests/actions/UserTest.ts @@ -18,6 +18,14 @@ import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; jest.mock('@libs/API'); jest.mock('../../src/libs/actions/SignInRedirect'); +jest.mock('../../src/libs/actions/Device', () => { + const actual = jest.requireActual('../../src/libs/actions/Device'); + return { + ...actual, + getDeviceInfoWithID: jest.fn(actual.getDeviceInfoWithID), + }; +}); + const mockAPI = jest.mocked(API); describe('actions/User', () => { @@ -405,7 +413,7 @@ describe('actions/User', () => { describe('validateSecondaryLogin', () => { beforeEach(() => { - jest.spyOn(DeviceActions, 'getDeviceInfoWithID').mockResolvedValue('{"deviceID":"test-device"}'); + jest.mocked(DeviceActions.getDeviceInfoWithID).mockResolvedValue('{"deviceID":"test-device"}'); }); afterEach(() => { diff --git a/tests/actions/connections/DualEntry.ts b/tests/actions/connections/DualEntry.ts index 2f8e13970c53..1074069b44eb 100644 --- a/tests/actions/connections/DualEntry.ts +++ b/tests/actions/connections/DualEntry.ts @@ -21,7 +21,13 @@ import Onyx from 'react-native-onyx'; import getOnyxValue from '../../utils/getOnyxValue'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; -jest.mock('@libs/API'); +jest.mock('@libs/API', () => { + const actual = jest.requireActual('@libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + }; +}); jest.mock('@expensify/react-native-hybrid-app', () => ({ __esModule: true, default: { @@ -29,7 +35,7 @@ jest.mock('@expensify/react-native-hybrid-app', () => ({ }, })); -const writeSpy = jest.spyOn(API, 'write'); +const writeSpy = jest.mocked(API.write); const MOCK_POLICY_ID = 'MOCK_POLICY_ID'; const POLICY_KEY = `${ONYXKEYS.COLLECTION.POLICY}${MOCK_POLICY_ID}` as const; diff --git a/tests/actions/connections/NetSuite.ts b/tests/actions/connections/NetSuite.ts index a7d067d0a722..bfa29c489e61 100644 --- a/tests/actions/connections/NetSuite.ts +++ b/tests/actions/connections/NetSuite.ts @@ -22,9 +22,15 @@ import Onyx from 'react-native-onyx'; import createMock from '../../utils/createMock'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; -jest.mock('@libs/API'); +jest.mock('@libs/API', () => { + const actual = jest.requireActual('@libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + }; +}); -const writeSpy = jest.spyOn(API, 'write'); +const writeSpy = jest.mocked(API.write); const MOCK_POLICY_ID = 'MOCK_POLICY_ID'; const MOCK_CREDENTIALS = { diff --git a/tests/actions/connections/QuickbooksDesktop.ts b/tests/actions/connections/QuickbooksDesktop.ts index ae90cc25ee41..e3aef71a1349 100644 --- a/tests/actions/connections/QuickbooksDesktop.ts +++ b/tests/actions/connections/QuickbooksDesktop.ts @@ -10,7 +10,13 @@ import Onyx from 'react-native-onyx'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; -jest.mock('@libs/API'); +jest.mock('@libs/API', () => { + const actual = jest.requireActual('@libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + }; +}); jest.mock('@expensify/react-native-hybrid-app', () => ({ __esModule: true, default: { @@ -18,7 +24,7 @@ jest.mock('@expensify/react-native-hybrid-app', () => ({ }, })); -const writeSpy = jest.spyOn(API, 'write'); +const writeSpy = jest.mocked(API.write); const MOCK_POLICY_ID = 'MOCK_POLICY_ID'; diff --git a/tests/actions/connections/QuickbooksOnline.ts b/tests/actions/connections/QuickbooksOnline.ts index 26ad6b720f44..b183b7e9d921 100644 --- a/tests/actions/connections/QuickbooksOnline.ts +++ b/tests/actions/connections/QuickbooksOnline.ts @@ -20,10 +20,16 @@ import Onyx from 'react-native-onyx'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; -jest.mock('@libs/API'); +jest.mock('@libs/API', () => { + const actual = jest.requireActual('@libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + }; +}); jest.mock('@libs/ErrorUtils'); -const writeSpy = jest.spyOn(API, 'write'); +const writeSpy = jest.mocked(API.write); const MOCK_POLICY_ID = 'MOCK_POLICY_ID'; const MOCK_ACCOUNT_ID = 'account-123'; diff --git a/tests/actions/connections/SageIntacct.ts b/tests/actions/connections/SageIntacct.ts index d075664be702..b4877ddb0542 100644 --- a/tests/actions/connections/SageIntacct.ts +++ b/tests/actions/connections/SageIntacct.ts @@ -15,10 +15,16 @@ import Onyx from 'react-native-onyx'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; -jest.mock('@libs/API'); +jest.mock('@libs/API', () => { + const actual = jest.requireActual('@libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + }; +}); jest.mock('@libs/ErrorUtils'); -const writeSpy = jest.spyOn(API, 'write'); +const writeSpy = jest.mocked(API.write); const MOCK_POLICY_ID = 'MOCK_POLICY_ID'; const MOCK_ONYX_ERROR = {key: 'error'}; diff --git a/tests/actions/connections/Xero.test.ts b/tests/actions/connections/Xero.test.ts index 0d572cd0abcd..cb5dd738263d 100644 --- a/tests/actions/connections/Xero.test.ts +++ b/tests/actions/connections/Xero.test.ts @@ -10,7 +10,13 @@ import Onyx from 'react-native-onyx'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; -jest.mock('@libs/API'); +jest.mock('@libs/API', () => { + const actual = jest.requireActual('@libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + }; +}); jest.mock('@expensify/react-native-hybrid-app', () => ({ __esModule: true, default: { @@ -18,7 +24,7 @@ jest.mock('@expensify/react-native-hybrid-app', () => ({ }, })); -const writeSpy = jest.spyOn(API, 'write'); +const writeSpy = jest.mocked(API.write); const MOCK_POLICY_ID = 'MOCK_POLICY_ID'; diff --git a/tests/navigation/LinkedActionNotFoundGuardTest.tsx b/tests/navigation/LinkedActionNotFoundGuardTest.tsx index b464add1f92b..82a9e192c220 100644 --- a/tests/navigation/LinkedActionNotFoundGuardTest.tsx +++ b/tests/navigation/LinkedActionNotFoundGuardTest.tsx @@ -10,8 +10,8 @@ import {View} from 'react-native'; const REPORT_ID = '12345'; const REPORT_ACTION_ID = '67890'; -const ROUTE_KEY = 'test-route-key'; -const NAVIGATOR_KEY = 'test-navigator-key'; +const mockRouteKey = 'test-route-key'; +const mockNavigatorKey = 'test-navigator-key'; const mockSetParams = jest.fn(); const mockCleanStaleBackToParam = jest.fn(); @@ -41,12 +41,12 @@ jest.mock('@react-navigation/native', () => { return { ...actual, useRoute: () => ({ - key: ROUTE_KEY, + key: mockRouteKey, name: 'Report', params: mockRouteParams, }), useNavigation: () => ({ - getState: () => ({key: NAVIGATOR_KEY}), + getState: () => ({key: mockNavigatorKey}), }), }; }); @@ -151,7 +151,7 @@ describe('LinkedActionNotFoundGuard', () => { expect(screen.getByTestId('test-children')).toBeTruthy(); expect(mockSetParams).toHaveBeenCalledTimes(1); - expect(mockSetParams).toHaveBeenCalledWith({reportActionID: undefined}, ROUTE_KEY, NAVIGATOR_KEY); + expect(mockSetParams).toHaveBeenCalledWith({reportActionID: undefined}, mockRouteKey, mockNavigatorKey); }); it('clears reportActionID when the linked action is deleted while being viewed', () => { @@ -174,7 +174,7 @@ describe('LinkedActionNotFoundGuard', () => { ); expect(screen.getByTestId('test-children')).toBeTruthy(); - expect(mockSetParams).toHaveBeenCalledWith({reportActionID: undefined}, ROUTE_KEY, NAVIGATOR_KEY); + expect(mockSetParams).toHaveBeenCalledWith({reportActionID: undefined}, mockRouteKey, mockNavigatorKey); expect(mockCleanStaleBackToParam).toHaveBeenCalledWith(REPORT_ID, REPORT_ACTION_ID); }); @@ -200,7 +200,7 @@ describe('LinkedActionNotFoundGuard', () => { ); // The cleanup effect should clear reportActionID with the route key - expect(mockSetParams).toHaveBeenCalledWith({reportActionID: undefined}, ROUTE_KEY, NAVIGATOR_KEY); + expect(mockSetParams).toHaveBeenCalledWith({reportActionID: undefined}, mockRouteKey, mockNavigatorKey); expect(mockCleanStaleBackToParam).toHaveBeenCalledWith(REPORT_ID, REPORT_ACTION_ID); }); @@ -239,7 +239,7 @@ describe('LinkedActionNotFoundGuard', () => { expect(mockSetParams).toHaveBeenCalledTimes(1); // Verify route.key is the second argument (needed for split navigator targeting) - expect(mockSetParams).toHaveBeenCalledWith({reportActionID: undefined}, ROUTE_KEY, NAVIGATOR_KEY); + expect(mockSetParams).toHaveBeenCalledWith({reportActionID: undefined}, mockRouteKey, mockNavigatorKey); }); it('renders children without guard when no reportActionID in route', () => { diff --git a/tests/tooling/oxcTransformer.test.ts b/tests/tooling/oxcTransformer.test.ts index 6f212b14e8ba..4dab69a06085 100644 --- a/tests/tooling/oxcTransformer.test.ts +++ b/tests/tooling/oxcTransformer.test.ts @@ -31,10 +31,9 @@ describe('oxcTransformer', () => { } `; const result = oxcTransformer.process(source, path.resolve('src/libs/math.ts'), transformOptions); - expect(result.code).toContain('exports.add = add'); + expect(result.code).toContain('module.exports'); expect(result.code).not.toMatch(/^export /m); expect(result.code).not.toContain(': number'); - expect(result.map?.sources?.some((mapSource) => mapSource.endsWith('math.ts'))).toBe(true); }); it('runs React Compiler on app components', () => { @@ -59,19 +58,6 @@ describe('oxcTransformer', () => { expect(result.code).toContain('jsxDEV'); }); - it('lowers const in jest.mock factories so circular imports do not TDZ', () => { - const source = ` - const mockedReportID = '1'; - jest.mock('./foo', () => ({ - parseReportRouteParams: () => ({reportID: mockedReportID}), - })); - export const x = mockedReportID; - `; - const result = oxcTransformer.process(source, path.resolve('tests/unit/Hello.test.ts'), transformOptions); - expect(result.code).toMatch(/var mockedReportID/); - expect(result.code).not.toMatch(/\bconst mockedReportID\b/); - }); - it('hoists jest.mock above require() after CJS conversion', () => { const source = ` import foo from './foo'; @@ -79,10 +65,10 @@ describe('oxcTransformer', () => { export const x = foo; `; const result = oxcTransformer.process(source, path.resolve('tests/perf-test/Hello.perf-test.tsx'), transformOptions); - expect(result.code).toContain('_getJestObj().mock("./foo")'); + expect(result.code).toMatch(/jest\.mock\(['"]\.\/foo['"]\)/); expect(result.code).toMatch(/require\(['"]\.\/foo['"]\)/); - expect(result.code.indexOf('_getJestObj().mock')).toBeLessThan(result.code.search(/require\(['"]\.\/foo['"]\)/)); - expect(result.code).toContain('exports.x'); + expect(result.code.search(/jest\.mock\(['"]\.\/foo['"]\)/)).toBeLessThan(result.code.search(/require\(['"]\.\/foo['"]\)/)); + expect(result.code).toContain('module.exports'); }); it('lowers dynamic import() so Jest still owns the module graph', () => { diff --git a/tests/ui/AddDomainPageTest.tsx b/tests/ui/AddDomainPageTest.tsx index a89c515da9e3..d64478742298 100644 --- a/tests/ui/AddDomainPageTest.tsx +++ b/tests/ui/AddDomainPageTest.tsx @@ -33,6 +33,14 @@ import Onyx from 'react-native-onyx'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; +jest.mock('@libs/API', () => { + const actual = jest.requireActual('@libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + }; +}); + const DOMAIN_NAME = 'test.com'; const EXISTING_DOMAIN_ACCOUNT_ID = 4242; @@ -55,7 +63,8 @@ jest.mock('@hooks/useInFlightRequests', () => ({ useIsAppLoadPending: () => mockIsAppLoadPending, })); -const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); +const apiWriteSpy = jest.mocked(API.write); +apiWriteSpy.mockImplementation(() => Promise.resolve()); const navigateSpy = jest.spyOn(Navigation, 'navigate').mockImplementation(() => {}); const Stack = createPlatformStackNavigator(); diff --git a/tests/ui/BaseLoginFormTest.tsx b/tests/ui/BaseLoginFormTest.tsx index 9f0748afc737..9ddc9e88ea81 100644 --- a/tests/ui/BaseLoginFormTest.tsx +++ b/tests/ui/BaseLoginFormTest.tsx @@ -24,17 +24,17 @@ jest.mock('@react-navigation/native', () => { }; }); -const AGENT_ERROR = "Agent accounts can't be signed into directly. To use an agent, sign in with your own account and access it via Copilot."; -const INVALID_EMAIL_ERROR = 'The email entered is invalid. Please fix the format and try again.'; +const mockAgentError = "Agent accounts can't be signed into directly. To use an agent, sign in with your own account and access it via Copilot."; +const mockInvalidEmailError = 'The email entered is invalid. Please fix the format and try again.'; jest.mock('@hooks/useLocalize', () => jest.fn(() => ({ translate: jest.fn((key: string) => { switch (key) { case 'loginForm.error.agentSignInBlocked': - return AGENT_ERROR; + return mockAgentError; case 'loginForm.error.invalidFormatEmailLogin': - return INVALID_EMAIL_ERROR; + return mockInvalidEmailError; case 'loginForm.phoneOrEmail': return 'Phone or email'; case 'loginForm.loginForm': @@ -117,7 +117,7 @@ describe('BaseLoginForm', () => { fireEvent.press(continueButton); await waitFor(() => { - expect(screen.getByText(AGENT_ERROR)).toBeTruthy(); + expect(screen.getByText(mockAgentError)).toBeTruthy(); }); expect(mockBeginSignIn).not.toHaveBeenCalled(); }); @@ -132,7 +132,7 @@ describe('BaseLoginForm', () => { fireEvent.press(continueButton); await waitFor(() => { - expect(screen.getByText(AGENT_ERROR)).toBeTruthy(); + expect(screen.getByText(mockAgentError)).toBeTruthy(); }); expect(mockBeginSignIn).not.toHaveBeenCalled(); }); diff --git a/tests/ui/BaseVerifyDomainPageTest.tsx b/tests/ui/BaseVerifyDomainPageTest.tsx index 08661912de3f..3037e9bf2a92 100644 --- a/tests/ui/BaseVerifyDomainPageTest.tsx +++ b/tests/ui/BaseVerifyDomainPageTest.tsx @@ -26,6 +26,13 @@ import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; jest.mock('@components/RenderHTML', () => () => null); +jest.mock('@libs/API', () => { + const actual = jest.requireActual('@libs/API'); + return { + ...actual, + read: jest.fn(actual.read), + }; +}); const DOMAIN_ACCOUNT_ID = 123456; const DOMAIN_EMAIL = 'user@test.com'; @@ -37,7 +44,8 @@ const DOMAIN_ADMIN_ACCESS = { [`${CONST.DOMAIN.EXPENSIFY_ADMIN_ACCESS_PREFIX}0`]: TEST_USER_ACCOUNT_ID, }; -const apiReadSpy = jest.spyOn(API, 'read').mockImplementation(() => {}); +const apiReadSpy = jest.mocked(API.read); +apiReadSpy.mockImplementation(() => {}); const Stack = createPlatformStackNavigator(); diff --git a/tests/ui/ClearReportActionErrorsUITest.tsx b/tests/ui/ClearReportActionErrorsUITest.tsx index ca9cc57a0ca0..9b45f988e733 100644 --- a/tests/ui/ClearReportActionErrorsUITest.tsx +++ b/tests/ui/ClearReportActionErrorsUITest.tsx @@ -27,6 +27,13 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct' import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; jest.mock('@react-navigation/native'); +jest.mock('@src/libs/ReportActionsUtils', () => { + const actual = jest.requireActual('@src/libs/ReportActionsUtils'); + return { + ...actual, + getIOUActionForReportID: jest.fn(actual.getIOUActionForReportID), + }; +}); const ACTOR_ACCOUNT_ID = 123456789; const ACTOR_EMAIL = 'test@test.com'; @@ -57,7 +64,7 @@ describe('ClearReportActionErrors UI', () => { evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS], }); jest.spyOn(NativeNavigation, 'useRoute').mockReturnValue({key: '', name: ''}); - jest.spyOn(ReportActionUtils, 'getIOUActionForReportID').mockImplementation(getIOUActionForReportID); + jest.mocked(ReportActionUtils.getIOUActionForReportID).mockImplementation(getIOUActionForReportID); }); beforeEach(async () => { diff --git a/tests/ui/ConciergePromptBoxTest.tsx b/tests/ui/ConciergePromptBoxTest.tsx index 2197eb0f4361..e4c04dc718ed 100644 --- a/tests/ui/ConciergePromptBoxTest.tsx +++ b/tests/ui/ConciergePromptBoxTest.tsx @@ -34,14 +34,14 @@ const mockAskConciergeWithAttachment = jest.fn(); const mockPickAttachments = jest.fn(); const mockOpenPicker = jest.fn(); -const pickerHandler: {onConfirm?: (files: FileObject | FileObject[]) => void} = {}; +const mockPickerHandler: {onConfirm?: (files: FileObject | FileObject[]) => void} = {}; jest.mock('@components/Search/SearchRouter/useAskConcierge', () => jest.fn()); jest.mock('@pages/home/ForYouSection/useConciergeAttachmentPicker', () => ({ __esModule: true, default: (_reportID: string | undefined, onConfirm: (files: FileObject | FileObject[]) => void) => { - pickerHandler.onConfirm = onConfirm; + mockPickerHandler.onConfirm = onConfirm; return {pickAttachments: mockPickAttachments, PDFValidationComponent: null}; }, })); @@ -175,7 +175,7 @@ function measureLongPlaceholder(height: number) { describe('ConciergePromptBox', () => { beforeEach(() => { jest.clearAllMocks(); - pickerHandler.onConfirm = undefined; + mockPickerHandler.onConfirm = undefined; setAskConcierge(); setResponsiveLayout(false); setKeyboardShown(false); @@ -335,7 +335,7 @@ describe('ConciergePromptBox', () => { fireEvent.changeText(getInput(), 'Here it is'); // When the modal confirms - act(() => pickerHandler.onConfirm?.(files)); + act(() => mockPickerHandler.onConfirm?.(files)); // Then the attachments are sent with the message and the input is emptied expect(mockAskConciergeWithAttachment).toHaveBeenCalledWith(files, 'Here it is'); @@ -431,7 +431,7 @@ describe('ConciergePromptBox', () => { render(); // When the modal confirms - act(() => pickerHandler.onConfirm?.(files)); + act(() => mockPickerHandler.onConfirm?.(files)); // Then nothing is sent and the sign in flow opens expect(mockAskConciergeWithAttachment).not.toHaveBeenCalled(); diff --git a/tests/ui/DomainAlreadyExistsPageTest.tsx b/tests/ui/DomainAlreadyExistsPageTest.tsx index a3cd4e4ae65b..fcadfcd9e5b7 100644 --- a/tests/ui/DomainAlreadyExistsPageTest.tsx +++ b/tests/ui/DomainAlreadyExistsPageTest.tsx @@ -26,10 +26,19 @@ import Onyx from 'react-native-onyx'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; +jest.mock('@libs/API', () => { + const actual = jest.requireActual('@libs/API'); + return { + ...actual, + write: jest.fn(actual.write), + }; +}); + const DOMAIN_ACCOUNT_ID = 4242; const CURRENT_USER_ACCOUNT_ID = 1; -const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); +const apiWriteSpy = jest.mocked(API.write); +apiWriteSpy.mockImplementation(() => Promise.resolve()); const goBackSpy = jest.spyOn(Navigation, 'goBack').mockImplementation(() => {}); const Stack = createPlatformStackNavigator(); diff --git a/tests/ui/DynamicPaymentCardCurrencySelectorPageTest.tsx b/tests/ui/DynamicPaymentCardCurrencySelectorPageTest.tsx index eebf60250d4a..a53b7e2a4013 100644 --- a/tests/ui/DynamicPaymentCardCurrencySelectorPageTest.tsx +++ b/tests/ui/DynamicPaymentCardCurrencySelectorPageTest.tsx @@ -21,10 +21,10 @@ type CurrencyOption = {text: string; value: string; keyForList: string; isSelect type ConfirmButtonOptions = {showButton?: boolean; text?: string; onConfirm?: () => void; isDisabled?: boolean}; -let capturedData: CurrencyOption[] = []; -let capturedOnSelectRow: ((option: CurrencyOption) => void) | undefined; -let capturedCustomListHeader: SelectionListProps['customListHeader']; -let capturedConfirmButtonOptions: ConfirmButtonOptions | undefined; +let mockCapturedData: CurrencyOption[] = []; +let mockCapturedOnSelectRow: ((option: CurrencyOption) => void) | undefined; +let mockCapturedCustomListHeader: SelectionListProps['customListHeader']; +let mockCapturedConfirmButtonOptions: ConfirmButtonOptions | undefined; jest.mock('@hooks/usePermissions', () => jest.fn(() => ({isBetaEnabled: () => false}))); @@ -88,10 +88,10 @@ jest.mock('@components/SelectionList', () => { customListHeader?: SelectionListProps['customListHeader']; confirmButtonOptions?: ConfirmButtonOptions; }) { - capturedData = data ?? []; - capturedOnSelectRow = onSelectRow; - capturedCustomListHeader = customListHeader; - capturedConfirmButtonOptions = confirmButtonOptions; + mockCapturedData = data ?? []; + mockCapturedOnSelectRow = onSelectRow; + mockCapturedCustomListHeader = customListHeader; + mockCapturedConfirmButtonOptions = confirmButtonOptions; return (data ?? []).map((item) => item.text).join(','); } return MockSelectionList; @@ -132,10 +132,10 @@ const mockOnyx = (formDraftCurrency?: string, addCardCurrency?: string, billingC describe('DynamicPaymentCardCurrencySelectorPage', () => { beforeEach(() => { jest.clearAllMocks(); - capturedData = []; - capturedOnSelectRow = undefined; - capturedCustomListHeader = undefined; - capturedConfirmButtonOptions = undefined; + mockCapturedData = []; + mockCapturedOnSelectRow = undefined; + mockCapturedCustomListHeader = undefined; + mockCapturedConfirmButtonOptions = undefined; mockUsePermissions.mockReturnValue({isBetaEnabled: () => false}); mockUseDynamicBackPath.mockReturnValue('settings/subscription/change-billing-currency'); mockOnyx(); @@ -144,7 +144,7 @@ describe('DynamicPaymentCardCurrencySelectorPage', () => { it('hides EUR when the EUR billing beta is disabled', () => { render(); - const currencies = capturedData.map((option) => option.value); + const currencies = mockCapturedData.map((option) => option.value); expect(currencies).toEqual(['USD', 'AUD', 'GBP', 'NZD']); expect(currencies).not.toContain('EUR'); }); @@ -154,7 +154,7 @@ describe('DynamicPaymentCardCurrencySelectorPage', () => { render(); - expect(capturedData.map((option) => option.value)).toContain('EUR'); + expect(mockCapturedData.map((option) => option.value)).toContain('EUR'); }); it('marks the form draft currency as selected', () => { @@ -162,7 +162,7 @@ describe('DynamicPaymentCardCurrencySelectorPage', () => { render(); - const selected = capturedData.filter((option) => option.isSelected); + const selected = mockCapturedData.filter((option) => option.isSelected); expect(selected).toHaveLength(1); expect(selected.at(0)?.value).toBe('AUD'); }); @@ -172,7 +172,7 @@ describe('DynamicPaymentCardCurrencySelectorPage', () => { render(); - expect(capturedData.find((option) => option.isSelected)?.value).toBe('NZD'); + expect(mockCapturedData.find((option) => option.isSelected)?.value).toBe('NZD'); }); it('falls back to the billing card currency when both the draft and the add-card form are empty', () => { @@ -180,24 +180,24 @@ describe('DynamicPaymentCardCurrencySelectorPage', () => { render(); - expect(capturedData.find((option) => option.isSelected)?.value).toBe('GBP'); + expect(mockCapturedData.find((option) => option.isSelected)?.value).toBe('GBP'); }); it('moves the checkmark on select without persisting or navigating (deferred until Save)', () => { render(); - const aud = capturedData.find((option) => option.value === 'AUD'); + const aud = mockCapturedData.find((option) => option.value === 'AUD'); expect(aud).toBeDefined(); act(() => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - capturedOnSelectRow?.(aud!); + mockCapturedOnSelectRow?.(aud!); }); expect(mockSetDraftValues).not.toHaveBeenCalled(); expect(mockSetPaymentMethodCurrency).not.toHaveBeenCalled(); expect(mockGoBack).not.toHaveBeenCalled(); - const selected = capturedData.filter((option) => option.isSelected); + const selected = mockCapturedData.filter((option) => option.isSelected); expect(selected).toHaveLength(1); expect(selected.at(0)?.value).toBe('AUD'); }); @@ -205,16 +205,16 @@ describe('DynamicPaymentCardCurrencySelectorPage', () => { it('writes the chosen currency to both flows and navigates back when Save is tapped', () => { render(); - const aud = capturedData.find((option) => option.value === 'AUD'); + const aud = mockCapturedData.find((option) => option.value === 'AUD'); expect(aud).toBeDefined(); act(() => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - capturedOnSelectRow?.(aud!); + mockCapturedOnSelectRow?.(aud!); }); - expect(capturedConfirmButtonOptions?.showButton).toBe(true); + expect(mockCapturedConfirmButtonOptions?.showButton).toBe(true); act(() => { - capturedConfirmButtonOptions?.onConfirm?.(); + mockCapturedConfirmButtonOptions?.onConfirm?.(); }); expect(mockSetDraftValues).toHaveBeenCalledWith(ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM, {currency: 'AUD'}); @@ -227,16 +227,16 @@ describe('DynamicPaymentCardCurrencySelectorPage', () => { render(); - expect(capturedConfirmButtonOptions?.isDisabled).toBe(true); + expect(mockCapturedConfirmButtonOptions?.isDisabled).toBe(true); - const aud = capturedData.find((option) => option.value === 'AUD'); + const aud = mockCapturedData.find((option) => option.value === 'AUD'); expect(aud).toBeDefined(); act(() => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - capturedOnSelectRow?.(aud!); + mockCapturedOnSelectRow?.(aud!); }); - expect(capturedConfirmButtonOptions?.isDisabled).toBe(false); + expect(mockCapturedConfirmButtonOptions?.isDisabled).toBe(false); }); it('shows the currency note when opened from a flow that does not already display it (e.g. add payment card)', () => { @@ -244,7 +244,7 @@ describe('DynamicPaymentCardCurrencySelectorPage', () => { render(); - const header = capturedCustomListHeader; + const header = mockCapturedCustomListHeader; expect(header).toBeTruthy(); if (!React.isValidElement(header)) { throw new Error('Expected the captured custom list header to be a React element'); @@ -263,6 +263,6 @@ describe('DynamicPaymentCardCurrencySelectorPage', () => { // The default mocked back path is the change-billing-currency screen. render(); - expect(capturedCustomListHeader).toBeUndefined(); + expect(mockCapturedCustomListHeader).toBeUndefined(); }); }); diff --git a/tests/ui/ForYouSectionTest.tsx b/tests/ui/ForYouSectionTest.tsx index 839e3f24c637..92bced606ad6 100644 --- a/tests/ui/ForYouSectionTest.tsx +++ b/tests/ui/ForYouSectionTest.tsx @@ -119,7 +119,7 @@ jest.mock('@hooks/useThemeStyles', () => jest.mock('@hooks/useTheme', () => jest.fn(() => ({}))); -const RECEIPT_SEARCH_ASSET = {testID: 'receipt-search-icon'}; +const mockReceiptSearchAsset = {testID: 'receipt-search-icon'}; jest.mock('@hooks/useLazyAsset', () => ({ useMemoizedLazyExpensifyIcons: jest.fn(() => ({ @@ -127,7 +127,7 @@ jest.mock('@hooks/useLazyAsset', () => ({ Send: null, ThumbsUp: null, Export: null, - ReceiptSearch: RECEIPT_SEARCH_ASSET, + ReceiptSearch: mockReceiptSearchAsset, })), useMemoizedLazyIllustrations: jest.fn(() => ({ ThumbsUpStars: null, @@ -630,8 +630,8 @@ describe('ForYouSection', () => { expect(screen.getByText('Begin')).toBeOnTheScreen(); // The ReceiptSearch asset should be passed as the `icon` prop on at least one BaseWidgetItem. - // We look for any rendered element whose `icon` prop is the RECEIPT_SEARCH_ASSET reference. - const matchingNodes = unsafeRoot.findAll((node) => node.props && (node.props as {icon?: unknown}).icon === RECEIPT_SEARCH_ASSET); + // We look for any rendered element whose `icon` prop is the mockReceiptSearchAsset reference. + const matchingNodes = unsafeRoot.findAll((node) => node.props && (node.props as {icon?: unknown}).icon === mockReceiptSearchAsset); expect(matchingNodes.length).toBeGreaterThan(0); }); diff --git a/tests/ui/GroupHeaderTest.tsx b/tests/ui/GroupHeaderTest.tsx index c2a13e804f98..e9391bf6c316 100644 --- a/tests/ui/GroupHeaderTest.tsx +++ b/tests/ui/GroupHeaderTest.tsx @@ -31,16 +31,16 @@ const mockedUseResponsiveLayout = jest.mocked(useResponsiveLayout); /** The header renders its group's own sub-header; capturing its props is how the derived checkbox state and its press are read. */ type CapturedHeaderProps = {isSelectAllChecked: boolean; isIndeterminate: boolean; onCheckboxPress: () => void}; -const capturedSubHeader: {current: CapturedHeaderProps | null} = {current: null}; +const mockCapturedSubHeader: {current: CapturedHeaderProps | null} = {current: null}; jest.mock('@components/Search/SearchList/ListItem/CategoryListItemHeader', () => ({ __esModule: true, default: (props: CapturedHeaderProps) => { - capturedSubHeader.current = props; + mockCapturedSubHeader.current = props; return null; }, })); -const checkboxState = () => ({isSelectAllChecked: capturedSubHeader.current?.isSelectAllChecked, isIndeterminate: capturedSubHeader.current?.isIndeterminate}); +const checkboxState = () => ({isSelectAllChecked: mockCapturedSubHeader.current?.isSelectAllChecked, isIndeterminate: mockCapturedSubHeader.current?.isIndeterminate}); const GROUP_KEY = 'Advertising'; @@ -125,7 +125,7 @@ describe('GroupHeader', () => { beforeAll(() => Onyx.init({keys: ONYXKEYS})); beforeEach(() => { - capturedSubHeader.current = null; + mockCapturedSubHeader.current = null; mockedUseResponsiveLayout.mockReturnValue({ isInLandscapeMode: false, isLargeScreenWidth: true, @@ -170,13 +170,13 @@ describe('GroupHeader', () => { it('hands the toggle the rows it carries, so a header deselect can reach them', () => { const onCheckboxPress = renderGroupHeader(children, select('1', '2', '3')); - act(() => capturedSubHeader.current?.onCheckboxPress()); + act(() => mockCapturedSubHeader.current?.onCheckboxPress()); expect(onCheckboxPress).toHaveBeenCalledWith(expect.objectContaining({keyForList: GROUP_KEY}), children); }); it('hands the toggle no rows while it carries none, which is what makes the group its own unit', () => { const onCheckboxPress = renderGroupHeader([], select(GROUP_KEY)); - act(() => capturedSubHeader.current?.onCheckboxPress()); + act(() => mockCapturedSubHeader.current?.onCheckboxPress()); expect(onCheckboxPress).toHaveBeenCalledWith(expect.objectContaining({keyForList: GROUP_KEY}), []); }); }); diff --git a/tests/ui/IOURequestRedirectToStartPageTest.tsx b/tests/ui/IOURequestRedirectToStartPageTest.tsx index ac824f014d7e..b8b8e5e785fb 100644 --- a/tests/ui/IOURequestRedirectToStartPageTest.tsx +++ b/tests/ui/IOURequestRedirectToStartPageTest.tsx @@ -53,6 +53,14 @@ jest.mock('@libs/Navigation/Navigation', () => { }; }); +jest.mock('@userActions/IOU/MoneyRequest', () => { + const actual = jest.requireActual('@userActions/IOU/MoneyRequest'); + return { + ...actual, + clearMoneyRequest: jest.fn(actual.clearMoneyRequest), + }; +}); + function getOptimisticDraft() { return new Promise>((resolve) => { const connection = Onyx.connect({ @@ -138,7 +146,7 @@ describe('IOURequestRedirectToStartPage', () => { // returns [] instead of undefined. Checks the clear still includes OPTIMISTIC_TRANSACTION_ID, since an empty // list would remove nothing and leave the stale draft behind. it('always clears the OPTIMISTIC_TRANSACTION_ID draft even when the selector returns an empty list', async () => { - const clearSpy = jest.spyOn(MoneyRequestActions, 'clearMoneyRequest'); + const clearSpy = jest.mocked(MoneyRequestActions.clearMoneyRequest); // Given no loaded drafts (validTransactionDraftIDsSelector returns []) render( diff --git a/tests/ui/IOURequestStepAmountDraftTest.tsx b/tests/ui/IOURequestStepAmountDraftTest.tsx index 9803f75ac836..deea2a0b9765 100644 --- a/tests/ui/IOURequestStepAmountDraftTest.tsx +++ b/tests/ui/IOURequestStepAmountDraftTest.tsx @@ -30,7 +30,7 @@ import {signInWithTestUser} from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; -const preventRemoveFlags: boolean[] = []; +const mockPreventRemoveFlags: boolean[] = []; // Mock LocaleContextProvider to avoid dynamic import issues with emojis/IntlStore jest.mock('@components/LocaleContextProvider', () => { @@ -133,7 +133,7 @@ jest.mock('@react-navigation/native', () => { useNavigation: () => ({navigate: jest.fn(), addListener: jest.fn()}), useFocusEffect: jest.fn(), usePreventRemove: (shouldPreventRemove: boolean) => { - preventRemoveFlags.push(shouldPreventRemove); + mockPreventRemoveFlags.push(shouldPreventRemove); }, useRoute: jest.fn(() => ({name: 'Money_Request_Step_Amount'})), }; @@ -187,7 +187,7 @@ describe('IOURequestStepAmount - draft transactions coverage', () => { beforeEach(async () => { jest.clearAllMocks(); - preventRemoveFlags.length = 0; + mockPreventRemoveFlags.length = 0; await Onyx.clear(); await waitForBatchedUpdates(); }); @@ -314,16 +314,16 @@ describe('IOURequestStepAmount - draft transactions coverage', () => { ); await waitForBatchedUpdatesWithAct(); - expect(preventRemoveFlags.some(Boolean)).toBe(false); + expect(mockPreventRemoveFlags.some(Boolean)).toBe(false); fireEvent.press(screen.getByText('iou.flip')); await waitForBatchedUpdatesWithAct(); - expect(preventRemoveFlags.some(Boolean)).toBe(true); + expect(mockPreventRemoveFlags.some(Boolean)).toBe(true); - preventRemoveFlags.length = 0; + mockPreventRemoveFlags.length = 0; fireEvent.press(screen.getByText('iou.flip')); await waitForBatchedUpdatesWithAct(); - expect(preventRemoveFlags.some(Boolean)).toBe(false); + expect(mockPreventRemoveFlags.some(Boolean)).toBe(false); }); it('keeps the native discard guard armed when a transaction draft update arrives after a sign-only change', async () => { @@ -358,15 +358,15 @@ describe('IOURequestStepAmount - draft transactions coverage', () => { await waitForBatchedUpdatesWithAct(); fireEvent.press(screen.getByText('iou.flip')); await waitForBatchedUpdatesWithAct(); - expect(preventRemoveFlags.some(Boolean)).toBe(true); + expect(mockPreventRemoveFlags.some(Boolean)).toBe(true); - preventRemoveFlags.length = 0; + mockPreventRemoveFlags.length = 0; await act(async () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${TRANSACTION_ID}`, {amount: 500}); }); await waitForBatchedUpdatesWithAct(); - expect(preventRemoveFlags.some(Boolean)).toBe(true); + expect(mockPreventRemoveFlags.some(Boolean)).toBe(true); }); it('disarms the native discard guard when the request type changes after a sign-only change', async () => { @@ -401,15 +401,15 @@ describe('IOURequestStepAmount - draft transactions coverage', () => { await waitForBatchedUpdatesWithAct(); fireEvent.press(screen.getByText('iou.flip')); await waitForBatchedUpdatesWithAct(); - expect(preventRemoveFlags.some(Boolean)).toBe(true); + expect(mockPreventRemoveFlags.some(Boolean)).toBe(true); - preventRemoveFlags.length = 0; + mockPreventRemoveFlags.length = 0; await act(async () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${TRANSACTION_ID}`, {iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN}); }); await waitForBatchedUpdatesWithAct(); - expect(preventRemoveFlags.at(-1)).toBe(false); + expect(mockPreventRemoveFlags.at(-1)).toBe(false); }); it('arms the native discard guard when a negative amount becomes positive and disarms it when the negative sign is restored', async () => { @@ -442,16 +442,16 @@ describe('IOURequestStepAmount - draft transactions coverage', () => { ); await waitForBatchedUpdatesWithAct(); - expect(preventRemoveFlags.some(Boolean)).toBe(false); + expect(mockPreventRemoveFlags.some(Boolean)).toBe(false); fireEvent.press(screen.getByText('iou.flip')); await waitForBatchedUpdatesWithAct(); - expect(preventRemoveFlags.some(Boolean)).toBe(true); + expect(mockPreventRemoveFlags.some(Boolean)).toBe(true); - preventRemoveFlags.length = 0; + mockPreventRemoveFlags.length = 0; fireEvent.press(screen.getByText('iou.flip')); await waitForBatchedUpdatesWithAct(); - expect(preventRemoveFlags.some(Boolean)).toBe(false); + expect(mockPreventRemoveFlags.some(Boolean)).toBe(false); }); it('disarms the native discard guard when backspace clears an empty negative amount', async () => { @@ -486,7 +486,7 @@ describe('IOURequestStepAmount - draft transactions coverage', () => { await waitForBatchedUpdatesWithAct(); fireEvent.press(screen.getByText('iou.flip')); await waitForBatchedUpdatesWithAct(); - expect(preventRemoveFlags.some(Boolean)).toBe(true); + expect(mockPreventRemoveFlags.some(Boolean)).toBe(true); const amountInput = screen.getByTestId('moneyRequestAmountInput'); fireEvent.changeText(amountInput, '5'); @@ -494,10 +494,10 @@ describe('IOURequestStepAmount - draft transactions coverage', () => { fireEvent.changeText(amountInput, ''); await waitForBatchedUpdatesWithAct(); - preventRemoveFlags.length = 0; + mockPreventRemoveFlags.length = 0; fireEvent(amountInput, 'keyPress', {nativeEvent: {key: 'Backspace'}}); await waitForBatchedUpdatesWithAct(); - expect(preventRemoveFlags.some(Boolean)).toBe(false); + expect(mockPreventRemoveFlags.some(Boolean)).toBe(false); }); }); diff --git a/tests/ui/IOURequestStepDescriptionTest.tsx b/tests/ui/IOURequestStepDescriptionTest.tsx index cdc00879db05..7477f5348bf7 100644 --- a/tests/ui/IOURequestStepDescriptionTest.tsx +++ b/tests/ui/IOURequestStepDescriptionTest.tsx @@ -36,11 +36,11 @@ jest.mock('@libs/shouldForceKeyboardIfAlreadyFocused', () => ({ // Capture the `onCancel` the component wires into the discard-changes hook, without dragging in the real // navigation/modal machinery (that flow is covered by tests/unit/hooks/useDiscardChangesConfirmationNative.test.ts). -let capturedOnCancel: (() => void) | undefined; +let mockCapturedOnCancel: (() => void) | undefined; jest.mock('@hooks/useDiscardChangesConfirmation', () => ({ __esModule: true, default: (options: {onCancel?: () => void}) => { - capturedOnCancel = options.onCancel; + mockCapturedOnCancel = options.onCancel; return {suppressDiscardPrompt: jest.fn()}; }, })); @@ -84,7 +84,7 @@ const NAVIGATION = createMock { beforeEach(() => { jest.clearAllMocks(); - capturedOnCancel = undefined; + mockCapturedOnCancel = undefined; }); it('forces the soft keyboard on cancel when the platform helper opts in (iOS)', async () => { @@ -100,9 +100,9 @@ describe('IOURequestStepDescription - discard modal onCancel', () => { await waitForBatchedUpdatesWithAct(); // The component must have wired an onCancel handler into the discard-changes hook. - expect(capturedOnCancel).toBeDefined(); + expect(mockCapturedOnCancel).toBeDefined(); - act(() => capturedOnCancel?.()); + act(() => mockCapturedOnCancel?.()); // iOS: shouldDelay=true, no forced selection range, forceKeyboardIfAlreadyFocused=true (the #97823 fix). expect(mockFocusFn).toHaveBeenCalledWith(true, undefined, true); @@ -120,9 +120,9 @@ describe('IOURequestStepDescription - discard modal onCancel', () => { // Let the component's useOnyx subscriptions settle so their updates don't fire outside act(). await waitForBatchedUpdatesWithAct(); - expect(capturedOnCancel).toBeDefined(); + expect(mockCapturedOnCancel).toBeDefined(); - act(() => capturedOnCancel?.()); + act(() => mockCapturedOnCancel?.()); // Android/web: forceKeyboardIfAlreadyFocused=false, argument-identical to pre-#97823 main, so the Android // regression (focus without keyboard on the hardware-back path) is removed. diff --git a/tests/ui/IOURequestStepDistanceOdometerDiscardGuardTest.tsx b/tests/ui/IOURequestStepDistanceOdometerDiscardGuardTest.tsx index da2997abb624..58026baaac4c 100644 --- a/tests/ui/IOURequestStepDistanceOdometerDiscardGuardTest.tsx +++ b/tests/ui/IOURequestStepDistanceOdometerDiscardGuardTest.tsx @@ -27,7 +27,7 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct' // That distinction is the whole point: iOS maps the flag to `preventNativeDismiss`, so a swipe-back is decided by // the last committed value. A callback that reads refs answers correctly whenever it is called, yet still leaves // the flag stale, because React Compiler reuses the render-time result while the closure's captured values hold. -const preventRemoveFlags: boolean[] = []; +const mockPreventRemoveFlags: boolean[] = []; // Only the two React APIs this factory needs. A namespace import of 'react' trips no-restricted-imports, // and `typeof import(...)` is banned, so name them off the default import instead (types are erased). @@ -122,7 +122,7 @@ jest.mock('@react-navigation/native', () => { useNavigation: () => ({navigate: jest.fn(), addListener: jest.fn()}), useFocusEffect: jest.fn(), usePreventRemove: (shouldPreventRemove: boolean) => { - preventRemoveFlags.push(shouldPreventRemove); + mockPreventRemoveFlags.push(shouldPreventRemove); }, useRoute: jest.fn(() => ({key: 'distance-odometer', name: 'Money_Request_Distance_Create', params: {}})), }; @@ -213,10 +213,10 @@ const odometerInput = (labelKey: string) => { // ScreenWrapper calls usePreventRemove as well and always passes false here, so a single armed call in the render // pass can only have come from the discard guard. Reading the last flag alone would depend on render order. -const isGuardArmed = () => preventRemoveFlags.some(Boolean); +const isGuardArmed = () => mockPreventRemoveFlags.some(Boolean); const typeStartReading = async (value: string) => { - preventRemoveFlags.length = 0; + mockPreventRemoveFlags.length = 0; fireEvent.changeText(odometerInput('distance.odometer.startReading'), value); await waitForBatchedUpdatesWithAct(); }; @@ -228,7 +228,7 @@ describe('IOURequestStepDistanceOdometer - native discard guard arms on the read beforeEach(async () => { jest.clearAllMocks(); - preventRemoveFlags.length = 0; + mockPreventRemoveFlags.length = 0; await Onyx.clear(); await waitForBatchedUpdates(); await signInWithTestUser(ACCOUNT_ID, ACCOUNT_LOGIN); diff --git a/tests/ui/IOURequestStepHoursTest.tsx b/tests/ui/IOURequestStepHoursTest.tsx index c1c77271472b..ef74b6022bfd 100644 --- a/tests/ui/IOURequestStepHoursTest.tsx +++ b/tests/ui/IOURequestStepHoursTest.tsx @@ -5,6 +5,7 @@ import {CurrentUserPersonalDetailsProvider} from '@components/CurrentUserPersona import {LocaleContextProvider} from '@components/LocaleContextProvider'; import OnyxListItemProvider from '@components/OnyxListItemProvider'; +import * as MoneyRequestActions from '@libs/actions/IOU/MoneyRequest'; import Navigation from '@libs/Navigation/Navigation'; import IOURequestStepHours from '@pages/iou/request/step/IOURequestStepHours'; @@ -56,6 +57,17 @@ jest.mock('@hooks/useResponsiveLayout', () => () => ({ isLargeScreenWidth: true, })); +jest.mock('@libs/actions/IOU/MoneyRequest', () => { + const actual = jest.requireActual('@libs/actions/IOU/MoneyRequest'); + return { + ...actual, + setMoneyRequestAmount: jest.fn(actual.setMoneyRequestAmount), + setMoneyRequestMerchant: jest.fn(actual.setMoneyRequestMerchant), + setMoneyRequestTimeCount: jest.fn(actual.setMoneyRequestTimeCount), + setMoneyRequestTimeRate: jest.fn(actual.setMoneyRequestTimeRate), + }; +}); + const ACCOUNT_ID = 1; const ACCOUNT_LOGIN = 'test@user.com'; const TRANSACTION_ID = 'transaction-1'; @@ -93,10 +105,10 @@ describe('IOURequestStepHours', () => { jest.clearAllMocks(); await signInWithTestUser(ACCOUNT_ID, ACCOUNT_LOGIN); - setMoneyRequestAmountSpy = jest.spyOn(require('@libs/actions/IOU/MoneyRequest'), 'setMoneyRequestAmount'); - setMoneyRequestMerchantSpy = jest.spyOn(require('@libs/actions/IOU/MoneyRequest'), 'setMoneyRequestMerchant'); - setMoneyRequestTimeCountSpy = jest.spyOn(require('@libs/actions/IOU/MoneyRequest'), 'setMoneyRequestTimeCount'); - setMoneyRequestTimeRateSpy = jest.spyOn(require('@libs/actions/IOU/MoneyRequest'), 'setMoneyRequestTimeRate'); + setMoneyRequestAmountSpy = jest.mocked(MoneyRequestActions.setMoneyRequestAmount); + setMoneyRequestMerchantSpy = jest.mocked(MoneyRequestActions.setMoneyRequestMerchant); + setMoneyRequestTimeCountSpy = jest.mocked(MoneyRequestActions.setMoneyRequestTimeCount); + setMoneyRequestTimeRateSpy = jest.mocked(MoneyRequestActions.setMoneyRequestTimeRate); await act(async () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, createPolicyWithTimeTracking()); diff --git a/tests/ui/IOURequestStepScanTest.tsx b/tests/ui/IOURequestStepScanTest.tsx index edb13c7cf633..50a38500bfb8 100644 --- a/tests/ui/IOURequestStepScanTest.tsx +++ b/tests/ui/IOURequestStepScanTest.tsx @@ -26,7 +26,7 @@ import createMock from '../utils/createMock'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; -let triggerFileSelection: ((files: FileObject[]) => void) | null = null; +let mockTriggerFileSelection: ((files: FileObject[]) => void) | null = null; jest.mock('react-native-permissions', () => ({ RESULTS: {GRANTED: 'granted', DENIED: 'denied', UNAVAILABLE: 'unavailable', BLOCKED: 'blocked', LIMITED: 'limited'}, @@ -46,7 +46,7 @@ jest.mock('react-native-permissions', () => ({ jest.mock('@hooks/useFilesValidation', () => { const ReactLib = jest.requireActual('react'); return (callback: (files: FileObject[]) => void) => { - triggerFileSelection = callback; + mockTriggerFileSelection = callback; return { validateFiles: (files: FileObject[]) => callback(files), PDFValidationComponent: ReactLib.createElement(ReactLib.Fragment), @@ -96,7 +96,7 @@ describe('IOURequestStepScan', () => { }); beforeEach(() => { - triggerFileSelection = null; + mockTriggerFileSelection = null; }); afterEach(async () => { @@ -155,14 +155,14 @@ describe('IOURequestStepScan', () => { await waitForBatchedUpdatesWithAct(); - expect(triggerFileSelection).not.toBeNull(); + expect(mockTriggerFileSelection).not.toBeNull(); const replacementFile = {name: 'replacement-receipt.png', type: 'image/png', size: 100, uri: 'file://replacement-receipt.png'} as FileObject; await act(async () => { - if (!triggerFileSelection) { + if (!mockTriggerFileSelection) { return; } - triggerFileSelection([replacementFile]); + mockTriggerFileSelection([replacementFile]); }); await waitForBatchedUpdates(); @@ -213,14 +213,14 @@ describe('IOURequestStepScan', () => { }); await waitForBatchedUpdates(); - expect(triggerFileSelection).not.toBeNull(); + expect(mockTriggerFileSelection).not.toBeNull(); const secondFile = {name: 'second-receipt.png', type: 'image/png', size: 200, uri: 'file://second-receipt.png'} as FileObject; await act(async () => { - if (!triggerFileSelection) { + if (!mockTriggerFileSelection) { return; } - triggerFileSelection([secondFile]); + mockTriggerFileSelection([secondFile]); }); await waitForBatchedUpdates(); diff --git a/tests/ui/IOURequestStepTimeRateTest.tsx b/tests/ui/IOURequestStepTimeRateTest.tsx index b2eb2bfa18d3..00d4b59cc589 100644 --- a/tests/ui/IOURequestStepTimeRateTest.tsx +++ b/tests/ui/IOURequestStepTimeRateTest.tsx @@ -4,6 +4,8 @@ import {CurrencyListContextProvider} from '@components/CurrencyListContextProvid import {LocaleContextProvider} from '@components/LocaleContextProvider'; import OnyxListItemProvider from '@components/OnyxListItemProvider'; +import * as MoneyRequestActions from '@libs/actions/IOU/MoneyRequest'; + import IOURequestStepTimeRate from '@pages/iou/request/step/IOURequestStepTimeRate'; import type {IOUAction} from '@src/CONST'; @@ -60,6 +62,16 @@ jest.mock('@hooks/useResponsiveLayout', () => () => ({ isLargeScreenWidth: true, })); +jest.mock('@libs/actions/IOU/MoneyRequest', () => { + const actual = jest.requireActual('@libs/actions/IOU/MoneyRequest'); + return { + ...actual, + setMoneyRequestAmount: jest.fn(actual.setMoneyRequestAmount), + setMoneyRequestMerchant: jest.fn(actual.setMoneyRequestMerchant), + setMoneyRequestTimeRate: jest.fn(actual.setMoneyRequestTimeRate), + }; +}); + const ACCOUNT_ID = 1; const ACCOUNT_LOGIN = 'test@user.com'; const TRANSACTION_ID = 'transaction-1'; @@ -81,9 +93,9 @@ describe('IOURequestStepTimeRate', () => { jest.clearAllMocks(); await signInWithTestUser(ACCOUNT_ID, ACCOUNT_LOGIN); - setMoneyRequestAmountSpy = jest.spyOn(require('@libs/actions/IOU/MoneyRequest'), 'setMoneyRequestAmount'); - setMoneyRequestMerchantSpy = jest.spyOn(require('@libs/actions/IOU/MoneyRequest'), 'setMoneyRequestMerchant'); - setMoneyRequestTimeRateSpy = jest.spyOn(require('@libs/actions/IOU/MoneyRequest'), 'setMoneyRequestTimeRate'); + setMoneyRequestAmountSpy = jest.mocked(MoneyRequestActions.setMoneyRequestAmount); + setMoneyRequestMerchantSpy = jest.mocked(MoneyRequestActions.setMoneyRequestMerchant); + setMoneyRequestTimeRateSpy = jest.mocked(MoneyRequestActions.setMoneyRequestTimeRate); }); afterEach(async () => { diff --git a/tests/ui/ImportedMembersPageTest.tsx b/tests/ui/ImportedMembersPageTest.tsx index 713593ed257f..c7e41cb58ff6 100644 --- a/tests/ui/ImportedMembersPageTest.tsx +++ b/tests/ui/ImportedMembersPageTest.tsx @@ -60,6 +60,14 @@ jest.mock('@libs/Navigation/Navigation', () => ({ dismissModal: jest.fn(), })); +jest.mock('@libs/actions/Policy/Member', () => { + const actual = jest.requireActual('@libs/actions/Policy/Member'); + return { + ...actual, + importPolicyMembers: jest.fn(actual.importPolicyMembers), + }; +}); + // A Submit (non-Control) workspace, so the import gate's "requires Control" check fires. function buildSubmitPolicy(): Policy { return { @@ -207,7 +215,7 @@ describe('ImportedMembersPage', () => { PEOPLE_ADMIN_EMAIL, PEOPLE_ADMIN_ACCOUNT_ID, ); - const importPolicyMembersSpy = jest.spyOn(Member, 'importPolicyMembers').mockResolvedValue({ + const importPolicyMembersSpy = jest.mocked(Member.importPolicyMembers).mockResolvedValue({ titleKey: 'spreadsheet.importSuccessfulTitle', promptKey: 'spreadsheet.importMembersAdded', promptKeyParams: {count: 1}, diff --git a/tests/ui/MoneyRequestReportActionsListRejectModalTest.tsx b/tests/ui/MoneyRequestReportActionsListRejectModalTest.tsx index da7168b6e9c3..f40fcd63e920 100644 --- a/tests/ui/MoneyRequestReportActionsListRejectModalTest.tsx +++ b/tests/ui/MoneyRequestReportActionsListRejectModalTest.tsx @@ -26,7 +26,7 @@ import createMock from '../utils/createMock'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; -const FAKE_REPORT_ID = '100001'; +const mockFakeReportID = '100001'; const FAKE_POLICY_ID = 'FAKE_POLICY_001'; const FAKE_ACCOUNT_ID = 15593135; const FAKE_TRANSACTION_ID = 'FAKE_TXN_001'; @@ -41,7 +41,7 @@ jest.mock('@react-navigation/native', () => ({ return { key: 'test-key', name: SCREENS_MOCK.REPORT, - params: {reportID: FAKE_REPORT_ID}, + params: {reportID: mockFakeReportID}, }; }, })); @@ -164,7 +164,7 @@ jest.mock('@hooks/useParentReportAction', () => jest.fn(() => undefined)); jest.mock('@navigation/helpers/isSearchTopmostFullScreenRoute', () => jest.fn(() => false)); const mockReport: Report = { - reportID: FAKE_REPORT_ID, + reportID: mockFakeReportID, reportName: 'Test Expense Report', chatReportID: '200001', ownerAccountID: FAKE_ACCOUNT_ID, @@ -199,7 +199,7 @@ const mockPolicy: Policy = { const mockTransaction: Transaction = { transactionID: FAKE_TRANSACTION_ID, - reportID: FAKE_REPORT_ID, + reportID: mockFakeReportID, amount: 10000, currency: CONST.CURRENCY.USD, merchant: 'Test Merchant', @@ -209,7 +209,7 @@ const mockTransaction: Transaction = { const mockReportAction = createMock>({ reportActionID: 'ACTION_001', - reportID: FAKE_REPORT_ID, + reportID: mockFakeReportID, actionName: CONST.REPORT.ACTIONS.TYPE.IOU, created: '2025-01-01 00:00:00', actorAccountID: FAKE_ACCOUNT_ID, @@ -263,11 +263,11 @@ describe('MoneyRequestReportActionsList - Reject Educational Modal', () => { await act(async () => { await Onyx.multiSet({ [ONYXKEYS.NVP_DISMISSED_REJECT_USE_EXPLANATION]: false, - [`${ONYXKEYS.COLLECTION.REPORT}${FAKE_REPORT_ID}` as const]: mockReport, + [`${ONYXKEYS.COLLECTION.REPORT}${mockFakeReportID}` as const]: mockReport, [`${ONYXKEYS.COLLECTION.POLICY}${FAKE_POLICY_ID}` as const]: mockPolicy, [`${ONYXKEYS.COLLECTION.TRANSACTION}${FAKE_TRANSACTION_ID}` as const]: mockTransaction, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${FAKE_REPORT_ID}` as const]: {[mockReportAction.reportActionID]: mockReportAction}, - [`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${FAKE_REPORT_ID}` as const]: {isLoadingInitialReportActions: false, hasOnceLoadedReportActions: true}, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${mockFakeReportID}` as const]: {[mockReportAction.reportActionID]: mockReportAction}, + [`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${mockFakeReportID}` as const]: {isLoadingInitialReportActions: false, hasOnceLoadedReportActions: true}, [ONYXKEYS.SESSION]: {accountID: FAKE_ACCOUNT_ID, email: FAKE_EMAIL} as Session, }); }); @@ -291,7 +291,7 @@ describe('MoneyRequestReportActionsList - Reject Educational Modal', () => { await act(async () => { await Onyx.multiSet({ [ONYXKEYS.NVP_DISMISSED_REJECT_USE_EXPLANATION]: true, - [`${ONYXKEYS.COLLECTION.REPORT}${FAKE_REPORT_ID}` as const]: mockReport, + [`${ONYXKEYS.COLLECTION.REPORT}${mockFakeReportID}` as const]: mockReport, [`${ONYXKEYS.COLLECTION.POLICY}${FAKE_POLICY_ID}` as const]: mockPolicy, [`${ONYXKEYS.COLLECTION.TRANSACTION}${FAKE_TRANSACTION_ID}` as const]: mockTransaction, [ONYXKEYS.SESSION]: {accountID: FAKE_ACCOUNT_ID, email: FAKE_EMAIL} as Session, @@ -313,9 +313,9 @@ describe('MoneyRequestReportActionsList - Reject Educational Modal', () => { it('shows the empty state when only the stored loading flag is true', async () => { await act(async () => { await Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT}${FAKE_REPORT_ID}` as const]: mockReport, + [`${ONYXKEYS.COLLECTION.REPORT}${mockFakeReportID}` as const]: mockReport, [`${ONYXKEYS.COLLECTION.POLICY}${FAKE_POLICY_ID}` as const]: mockPolicy, - [`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${FAKE_REPORT_ID}` as const]: {isLoadingInitialReportActions: true, hasOnceLoadedReportActions: false}, + [`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${mockFakeReportID}` as const]: {isLoadingInitialReportActions: true, hasOnceLoadedReportActions: false}, [ONYXKEYS.SESSION]: {accountID: FAKE_ACCOUNT_ID, email: FAKE_EMAIL} as Session, }); }); @@ -330,9 +330,9 @@ describe('MoneyRequestReportActionsList - Reject Educational Modal', () => { it('keeps the loading list mounted when only the report pending state is true', async () => { await act(async () => { await Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT}${FAKE_REPORT_ID}` as const]: mockReport, + [`${ONYXKEYS.COLLECTION.REPORT}${mockFakeReportID}` as const]: mockReport, [`${ONYXKEYS.COLLECTION.POLICY}${FAKE_POLICY_ID}` as const]: mockPolicy, - [`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${FAKE_REPORT_ID}` as const]: {isLoadingInitialReportActions: false, hasOnceLoadedReportActions: false}, + [`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${mockFakeReportID}` as const]: {isLoadingInitialReportActions: false, hasOnceLoadedReportActions: false}, [ONYXKEYS.SESSION]: {accountID: FAKE_ACCOUNT_ID, email: FAKE_EMAIL} as Session, }); }); @@ -344,15 +344,15 @@ describe('MoneyRequestReportActionsList - Reject Educational Modal', () => { expect(screen.getByTestId('MockMoneyRequestReportTransactionList')).toBeTruthy(); expect(screen.getByTestId('MockInitialReportActionsSkeleton')).toBeTruthy(); expect(screen.queryByTestId('MockSearchMoneyRequestReportEmptyState')).toBeNull(); - expect(mockUseIsReportLoadPending).toHaveBeenCalledWith(FAKE_REPORT_ID); + expect(mockUseIsReportLoadPending).toHaveBeenCalledWith(mockFakeReportID); }); it('shows a warm empty report without a skeleton or loading list while a report request is pending', async () => { await act(async () => { await Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT}${FAKE_REPORT_ID}` as const]: mockReport, + [`${ONYXKEYS.COLLECTION.REPORT}${mockFakeReportID}` as const]: mockReport, [`${ONYXKEYS.COLLECTION.POLICY}${FAKE_POLICY_ID}` as const]: mockPolicy, - [`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${FAKE_REPORT_ID}` as const]: {isLoadingInitialReportActions: true, hasOnceLoadedReportActions: true}, + [`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${mockFakeReportID}` as const]: {isLoadingInitialReportActions: true, hasOnceLoadedReportActions: true}, [ONYXKEYS.SESSION]: {accountID: FAKE_ACCOUNT_ID, email: FAKE_EMAIL} as Session, }); }); @@ -371,9 +371,9 @@ describe('MoneyRequestReportActionsList - Reject Educational Modal', () => { mockUseIsReportLoadPending.mockReturnValue(true); await act(async () => { await Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT}${FAKE_REPORT_ID}` as const]: mockReport, + [`${ONYXKEYS.COLLECTION.REPORT}${mockFakeReportID}` as const]: mockReport, [`${ONYXKEYS.COLLECTION.POLICY}${FAKE_POLICY_ID}` as const]: mockPolicy, - [`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${FAKE_REPORT_ID}` as const]: {isLoadingInitialReportActions: true, hasOnceLoadedReportActions: false}, + [`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${mockFakeReportID}` as const]: {isLoadingInitialReportActions: true, hasOnceLoadedReportActions: false}, [ONYXKEYS.SESSION]: {accountID: FAKE_ACCOUNT_ID, email: FAKE_EMAIL} as Session, }); }); diff --git a/tests/ui/MoneyRequestReportPreview.test.tsx b/tests/ui/MoneyRequestReportPreview.test.tsx index 2c96d30dee71..363977c02d0e 100644 --- a/tests/ui/MoneyRequestReportPreview.test.tsx +++ b/tests/ui/MoneyRequestReportPreview.test.tsx @@ -116,6 +116,22 @@ jest.mock('@components/ProcessMoneyReportHoldMenu', () => ({ }, })); +jest.mock('@libs/ReportUtils', () => { + const actual = jest.requireActual('@libs/ReportUtils'); + return { + ...actual, + hasViolations: jest.fn(actual.hasViolations), + }; +}); + +jest.mock('@src/libs/ReportActionsUtils', () => { + const actual = jest.requireActual('@src/libs/ReportActionsUtils'); + return { + ...actual, + getIOUActionForReportID: jest.fn(actual.getIOUActionForReportID), + }; +}); + const SELECTED_BANK_ACCOUNT_ID = 9999; const getIOUActionForReportID = (reportID: string | undefined, transactionID: string | undefined) => { @@ -245,8 +261,8 @@ describe('MoneyRequestReportPreview', () => { keys: ONYXKEYS, }); jest.spyOn(NativeNavigation, 'useRoute').mockReturnValue({key: '', name: ''}); - jest.spyOn(ReportActionUtils, 'getIOUActionForReportID').mockImplementation(getIOUActionForReportID); - jest.spyOn(ReportUtils, 'hasViolations').mockImplementation(hasViolations); + jest.mocked(ReportActionUtils.getIOUActionForReportID).mockImplementation(getIOUActionForReportID); + jest.mocked(ReportUtils.hasViolations).mockImplementation(hasViolations); await TestHelper.signInWithTestUser(); }); diff --git a/tests/ui/MoneyRequestReportViewTest.tsx b/tests/ui/MoneyRequestReportViewTest.tsx index f0dc787734b5..29d599d308a3 100644 --- a/tests/ui/MoneyRequestReportViewTest.tsx +++ b/tests/ui/MoneyRequestReportViewTest.tsx @@ -60,6 +60,25 @@ jest.mock('@components/OfflineWithFeedback', () => { return jest.fn(({children}: {children: React.ReactNode}) => reactModule.createElement(reactModule.Fragment, null, children)); }); +jest.mock('@libs/MoneyRequestReportUtils', () => { + const actual = jest.requireActual('@libs/MoneyRequestReportUtils'); + return { + ...actual, + getAllNonDeletedTransactions: jest.fn(actual.getAllNonDeletedTransactions), + shouldWaitForTransactions: jest.fn(actual.shouldWaitForTransactions), + shouldDisplayReportTableView: jest.fn(actual.shouldDisplayReportTableView), + }; +}); + +jest.mock('@libs/ReportActionsUtils', () => { + const actual = jest.requireActual('@libs/ReportActionsUtils'); + return { + ...actual, + getFilteredReportActionsForReportView: jest.fn(actual.getFilteredReportActionsForReportView), + getOneTransactionThreadReportID: jest.fn(actual.getOneTransactionThreadReportID), + }; +}); + const mockUseNetwork = useNetwork as jest.MockedFunction; const mockUseOnyx = useOnyx as jest.MockedFunction; const mockUseResponsiveLayout = useResponsiveLayout as jest.MockedFunction; @@ -158,11 +177,11 @@ describe('MoneyRequestReportView', () => { // Drive the branch deterministically: no transactions, a resolved transaction-thread id (so the // report isn't treated as empty), and a non-empty filtered action set. - jest.spyOn(MoneyRequestReportUtils, 'getAllNonDeletedTransactions').mockReturnValue([]); - jest.spyOn(MoneyRequestReportUtils, 'shouldWaitForTransactions').mockReturnValue(false); - jest.spyOn(MoneyRequestReportUtils, 'shouldDisplayReportTableView').mockReturnValue(false); - jest.spyOn(ReportActionsUtils, 'getFilteredReportActionsForReportView').mockReturnValue(mockReportActions); - jest.spyOn(ReportActionsUtils, 'getOneTransactionThreadReportID').mockReturnValue('thread-1'); + jest.mocked(MoneyRequestReportUtils.getAllNonDeletedTransactions).mockReturnValue([]); + jest.mocked(MoneyRequestReportUtils.shouldWaitForTransactions).mockReturnValue(false); + jest.mocked(MoneyRequestReportUtils.shouldDisplayReportTableView).mockReturnValue(false); + jest.mocked(ReportActionsUtils.getFilteredReportActionsForReportView).mockReturnValue(mockReportActions); + jest.mocked(ReportActionsUtils.getOneTransactionThreadReportID).mockReturnValue('thread-1'); }); afterEach(async () => { @@ -191,7 +210,7 @@ describe('MoneyRequestReportView', () => { }); it('mounts the money-request table view (not the chat body or typing listener) when a table view should display', () => { - jest.spyOn(MoneyRequestReportUtils, 'shouldDisplayReportTableView').mockReturnValue(true); + jest.mocked(MoneyRequestReportUtils.shouldDisplayReportTableView).mockReturnValue(true); renderMoneyRequestReportView(jest.fn()); diff --git a/tests/ui/MultifactorAuthenticationRevokePageTest.tsx b/tests/ui/MultifactorAuthenticationRevokePageTest.tsx index 7ccbf4f173e3..1b8dd1d42b0c 100644 --- a/tests/ui/MultifactorAuthenticationRevokePageTest.tsx +++ b/tests/ui/MultifactorAuthenticationRevokePageTest.tsx @@ -108,14 +108,14 @@ type CapturedConfirmModalProps = Omit; }; -let capturedConfirmModalProps: CapturedConfirmModalProps = { +let mockCapturedConfirmModalProps: CapturedConfirmModalProps = { isVisible: false, onConfirm: () => {}, onCancel: () => {}, }; jest.mock('@components/ConfirmModal', () => { function MockConfirmModal(props: ConfirmModalProps) { - capturedConfirmModalProps = { + mockCapturedConfirmModalProps = { ...props, onCancel: props.onCancel ?? (() => {}), }; @@ -139,7 +139,7 @@ function setBiometricStatus(overrides: Partial) { describe('MultifactorAuthenticationRevokePage', () => { afterEach(() => { jest.clearAllMocks(); - capturedConfirmModalProps = { + mockCapturedConfirmModalProps = { isVisible: false, onConfirm: () => {}, onCancel: () => {}, @@ -194,9 +194,9 @@ describe('MultifactorAuthenticationRevokePage', () => { fireEvent.press(thisDeviceButton!); // Then the confirmation modal should say "this device" and the confirm button should say "Revoke access" - expect(capturedConfirmModalProps.prompt).toBe('multifactorAuthentication.revoke.confirmationPromptThisDevice'); - expect(capturedConfirmModalProps.confirmText).toBe('multifactorAuthentication.revoke.cta'); - expect(capturedConfirmModalProps.isVisible).toBe(true); + expect(mockCapturedConfirmModalProps.prompt).toBe('multifactorAuthentication.revoke.confirmationPromptThisDevice'); + expect(mockCapturedConfirmModalProps.confirmText).toBe('multifactorAuthentication.revoke.cta'); + expect(mockCapturedConfirmModalProps.isVisible).toBe(true); }); }); @@ -213,8 +213,8 @@ describe('MultifactorAuthenticationRevokePage', () => { fireEvent.press(otherDevicesButton!); // Then the modal should say "that device" and the confirm button should say "Revoke access" - expect(capturedConfirmModalProps.prompt).toBe('multifactorAuthentication.revoke.confirmationPrompt'); - expect(capturedConfirmModalProps.confirmText).toBe('multifactorAuthentication.revoke.cta'); + expect(mockCapturedConfirmModalProps.prompt).toBe('multifactorAuthentication.revoke.confirmationPrompt'); + expect(mockCapturedConfirmModalProps.confirmText).toBe('multifactorAuthentication.revoke.cta'); }); it('shows "those devices" prompt with "Revoke access" when revoking 2+ others and this device is registered', () => { @@ -230,8 +230,8 @@ describe('MultifactorAuthenticationRevokePage', () => { // Then the modal should say "those devices" and the confirm button should say "Revoke access" // because we're only revoking others, not this device - expect(capturedConfirmModalProps.prompt).toBe('multifactorAuthentication.revoke.confirmationPromptMultiple'); - expect(capturedConfirmModalProps.confirmText).toBe('multifactorAuthentication.revoke.cta'); + expect(mockCapturedConfirmModalProps.prompt).toBe('multifactorAuthentication.revoke.confirmationPromptMultiple'); + expect(mockCapturedConfirmModalProps.confirmText).toBe('multifactorAuthentication.revoke.cta'); }); it('shows "any device" prompt with "Revoke all" when revoking 2+ others and this device is not registered', () => { @@ -247,8 +247,8 @@ describe('MultifactorAuthenticationRevokePage', () => { fireEvent.press(otherDevicesButton!); // Then the modal should say "any device" and the confirm button should say "Revoke all" - expect(capturedConfirmModalProps.prompt).toBe('multifactorAuthentication.revoke.confirmationPromptAll'); - expect(capturedConfirmModalProps.confirmText).toBe('multifactorAuthentication.revoke.ctaAll'); + expect(mockCapturedConfirmModalProps.prompt).toBe('multifactorAuthentication.revoke.confirmationPromptAll'); + expect(mockCapturedConfirmModalProps.confirmText).toBe('multifactorAuthentication.revoke.ctaAll'); }); }); @@ -263,8 +263,8 @@ describe('MultifactorAuthenticationRevokePage', () => { // Then the modal should say "this device" and the confirm button should say "Revoke access" // because the only device being revoked is the one the user is currently on - expect(capturedConfirmModalProps.prompt).toBe('multifactorAuthentication.revoke.confirmationPromptThisDevice'); - expect(capturedConfirmModalProps.confirmText).toBe('multifactorAuthentication.revoke.cta'); + expect(mockCapturedConfirmModalProps.prompt).toBe('multifactorAuthentication.revoke.confirmationPromptThisDevice'); + expect(mockCapturedConfirmModalProps.confirmText).toBe('multifactorAuthentication.revoke.cta'); }); it('shows "that device" prompt with "Revoke access" when only 1 other device is registered', () => { @@ -280,8 +280,8 @@ describe('MultifactorAuthenticationRevokePage', () => { // Then the modal should say "that device" and the confirm button should say "Revoke access" // because we're revoking a single device that is not the current one - expect(capturedConfirmModalProps.prompt).toBe('multifactorAuthentication.revoke.confirmationPrompt'); - expect(capturedConfirmModalProps.confirmText).toBe('multifactorAuthentication.revoke.cta'); + expect(mockCapturedConfirmModalProps.prompt).toBe('multifactorAuthentication.revoke.confirmationPrompt'); + expect(mockCapturedConfirmModalProps.confirmText).toBe('multifactorAuthentication.revoke.cta'); }); it('shows "any device" prompt with "Revoke all" when 2+ others and this device is not registered', () => { @@ -294,8 +294,8 @@ describe('MultifactorAuthenticationRevokePage', () => { // Then the modal should say "any device" and the confirm button should say "Revoke all" // because all registered devices will be revoked - expect(capturedConfirmModalProps.prompt).toBe('multifactorAuthentication.revoke.confirmationPromptAll'); - expect(capturedConfirmModalProps.confirmText).toBe('multifactorAuthentication.revoke.ctaAll'); + expect(mockCapturedConfirmModalProps.prompt).toBe('multifactorAuthentication.revoke.confirmationPromptAll'); + expect(mockCapturedConfirmModalProps.confirmText).toBe('multifactorAuthentication.revoke.ctaAll'); }); it('shows "any device" prompt with "Revoke all" when this device + others are registered', () => { @@ -308,8 +308,8 @@ describe('MultifactorAuthenticationRevokePage', () => { // Then the modal should say "any device" and the confirm button should say "Revoke all" // because both this device and others are being revoked - expect(capturedConfirmModalProps.prompt).toBe('multifactorAuthentication.revoke.confirmationPromptAll'); - expect(capturedConfirmModalProps.confirmText).toBe('multifactorAuthentication.revoke.ctaAll'); + expect(mockCapturedConfirmModalProps.prompt).toBe('multifactorAuthentication.revoke.confirmationPromptAll'); + expect(mockCapturedConfirmModalProps.confirmText).toBe('multifactorAuthentication.revoke.ctaAll'); }); }); @@ -325,7 +325,7 @@ describe('MultifactorAuthenticationRevokePage', () => { expect(thisDeviceButton).toBeTruthy(); fireEvent.press(thisDeviceButton!); await act(async () => { - capturedConfirmModalProps.onConfirm(); + mockCapturedConfirmModalProps.onConfirm(); }); // Then the API should be called with onlyKeyID matching this device's key @@ -344,7 +344,7 @@ describe('MultifactorAuthenticationRevokePage', () => { expect(otherDevicesButton).toBeTruthy(); fireEvent.press(otherDevicesButton!); await act(async () => { - capturedConfirmModalProps.onConfirm(); + mockCapturedConfirmModalProps.onConfirm(); }); // Then the API should be called with exceptKeyID to preserve this device's registration @@ -362,7 +362,7 @@ describe('MultifactorAuthenticationRevokePage', () => { expect(otherDevicesButton).toBeTruthy(); fireEvent.press(otherDevicesButton!); await act(async () => { - capturedConfirmModalProps.onConfirm(); + mockCapturedConfirmModalProps.onConfirm(); }); // Then the API should be called with empty params to revoke all credentials @@ -378,7 +378,7 @@ describe('MultifactorAuthenticationRevokePage', () => { render(); fireEvent.press(screen.getByText('multifactorAuthentication.revoke.ctaAll')); await act(async () => { - capturedConfirmModalProps.onConfirm(); + mockCapturedConfirmModalProps.onConfirm(); }); // Then the API should be called with empty params to revoke every credential @@ -399,7 +399,7 @@ describe('MultifactorAuthenticationRevokePage', () => { fireEvent.press(thisDeviceButton!); await act(async () => { - await Promise.resolve(capturedConfirmModalProps.onConfirm()); + await Promise.resolve(mockCapturedConfirmModalProps.onConfirm()); }); expect(mockRevokeCredentials).toHaveBeenCalled(); @@ -425,13 +425,13 @@ describe('MultifactorAuthenticationRevokePage', () => { const revokeButtons = screen.getAllByText('multifactorAuthentication.revoke.revoke'); fireEvent.press(revokeButtons.at(0)!); - expect(capturedConfirmModalProps.isVisible).toBe(true); + expect(mockCapturedConfirmModalProps.isVisible).toBe(true); act(() => { - capturedConfirmModalProps.onCancel(); + mockCapturedConfirmModalProps.onCancel(); }); - expect(capturedConfirmModalProps.isVisible).toBe(false); + expect(mockCapturedConfirmModalProps.isVisible).toBe(false); }); }); @@ -461,7 +461,7 @@ describe('MultifactorAuthenticationRevokePage', () => { fireEvent.press(screen.getByText('multifactorAuthentication.revoke.ctaAll')); - expect(capturedConfirmModalProps.title).toBe('multifactorAuthentication.revoke.ctaAll'); + expect(mockCapturedConfirmModalProps.title).toBe('multifactorAuthentication.revoke.ctaAll'); }); it('shows "Revoke access" title on modal when revoking a single device', () => { @@ -472,13 +472,14 @@ describe('MultifactorAuthenticationRevokePage', () => { const revokeButtons = screen.getAllByText('multifactorAuthentication.revoke.revoke'); fireEvent.press(revokeButtons.at(0)!); - expect(capturedConfirmModalProps.title).toBe('multifactorAuthentication.revoke.cta'); + expect(mockCapturedConfirmModalProps.title).toBe('multifactorAuthentication.revoke.cta'); }); }); }); -// eslint-disable-next-line @typescript-eslint/consistent-type-imports -const {revokeMultifactorAuthenticationCredentials} = jest.requireActual('@libs/actions/MultifactorAuthentication'); +const {revokeMultifactorAuthenticationCredentials} = jest.requireActual<{ + revokeMultifactorAuthenticationCredentials: typeof revokeMultifactorAuthenticationCredentialsType; +}>('@libs/actions/MultifactorAuthentication'); describe('revokeMultifactorAuthenticationCredentials', () => { beforeEach(() => { diff --git a/tests/ui/OnboardingAccountingAndInterestedFeatures.tsx b/tests/ui/OnboardingAccountingAndInterestedFeatures.tsx index 49800908bd4d..0cfb26703cf5 100644 --- a/tests/ui/OnboardingAccountingAndInterestedFeatures.tsx +++ b/tests/ui/OnboardingAccountingAndInterestedFeatures.tsx @@ -45,12 +45,20 @@ jest.mock('@hooks/useCompleteOnboarding', () => () => ({ isLoading: false, })); +jest.mock('@libs/Browser', () => { + const actual = jest.requireActual('@libs/Browser'); + return { + ...actual, + isMobileSafari: jest.fn(actual.isMobileSafari), + }; +}); + TestHelper.setupGlobalFetchMock(); const Stack = createStackNavigator(); const navigate = jest.spyOn(Navigation, 'navigate'); const goBack = jest.spyOn(Navigation, 'goBack'); -const isMobileSafari = jest.spyOn(Browser, 'isMobileSafari'); +const isMobileSafari = jest.mocked(Browser.isMobileSafari); jest.spyOn(Navigation, 'getTopmostReportId').mockReturnValue(undefined); function renderInterestedFeaturesPage() { diff --git a/tests/ui/ReportActionAvatarsTest.tsx b/tests/ui/ReportActionAvatarsTest.tsx index dfd4b7826997..1a943a9740ca 100644 --- a/tests/ui/ReportActionAvatarsTest.tsx +++ b/tests/ui/ReportActionAvatarsTest.tsx @@ -50,7 +50,7 @@ function getAvatarData(dataSet: unknown): AvatarData { /* --- UI Mocks --- */ -const parseSource = (source: AvatarSource | IconAsset): string => { +const mockParseSource = (source: AvatarSource | IconAsset): string => { if (typeof source === 'string') { return source; } @@ -76,7 +76,7 @@ jest.mock('@components/Avatar/UserAvatar', () => { { dataSet={{ name, avatarID, - uri: parseSource(source ?? '') || 'No Source', + uri: mockParseSource(source ?? '') || 'No Source', parent: testID, }} testID="MockedAvatarData" @@ -106,7 +106,7 @@ jest.mock('@src/components/Icon', () => { return ( { }); jest.mock('@pages/inbox/report/ContextMenu/ReportActionContextMenu', () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const actual = jest.requireActual('@pages/inbox/report/ContextMenu/ReportActionContextMenu'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return + const actual = jest.requireActual('@pages/inbox/report/ContextMenu/ReportActionContextMenu'); return { ...actual, showDeleteModal: jest.fn(), diff --git a/tests/ui/ReportActionItemTest.tsx b/tests/ui/ReportActionItemTest.tsx index d507a894a3db..7d32b88d6c25 100644 --- a/tests/ui/ReportActionItemTest.tsx +++ b/tests/ui/ReportActionItemTest.tsx @@ -86,6 +86,14 @@ jest.mock('@libs/actions/Link', () => { jest.mock('@hooks/useCardFeedsForDisplay', () => jest.fn(() => ({defaultCardFeed: null, cardFeedsByPolicy: {}}))); +jest.mock('@src/libs/ReportActionsUtils', () => { + const actual = jest.requireActual('@src/libs/ReportActionsUtils'); + return { + ...actual, + getIOUActionForReportID: jest.fn(actual.getIOUActionForReportID), + }; +}); + const ACTOR_ACCOUNT_ID = 123456789; const actorEmail = 'test@test.com'; @@ -113,7 +121,7 @@ describe('ReportActionItem', () => { evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS], }); jest.spyOn(NativeNavigation, 'useRoute').mockReturnValue({key: '', name: ''}); - jest.spyOn(ReportActionUtils, 'getIOUActionForReportID').mockImplementation(getIOUActionForReportID); + jest.mocked(ReportActionUtils.getIOUActionForReportID).mockImplementation(getIOUActionForReportID); }); beforeEach(async () => { diff --git a/tests/ui/ReportActionsListTest.tsx b/tests/ui/ReportActionsListTest.tsx index 0b1aac4cf61e..0999b23daf3c 100644 --- a/tests/ui/ReportActionsListTest.tsx +++ b/tests/ui/ReportActionsListTest.tsx @@ -197,6 +197,14 @@ jest.mock('@libs/actions/Report', () => ({ })); jest.mock('@libs/telemetry/markOpenReportEnd', () => jest.fn()); +jest.mock('@libs/ReportActionsUtils', () => { + const actual = jest.requireActual('@libs/ReportActionsUtils'); + return { + ...actual, + shouldReportActionBeVisible: jest.fn(actual.shouldReportActionBeVisible), + }; +}); + const mockReport: OnyxTypes.Report = { reportID: '123', reportName: 'Test Report', @@ -630,7 +638,7 @@ describe('ReportActionsList (body)', () => { ]; const setupConciergeMocks = () => { - jest.spyOn(ReportActionsUtils, 'shouldReportActionBeVisible').mockReturnValue(true); + jest.mocked(ReportActionsUtils.shouldReportActionBeVisible).mockReturnValue(true); mockUseNetwork.mockReturnValue({isOffline: false}); mockUseOnyx.mockImplementation((key: string, options) => { if (key === ONYXKEYS.CONCIERGE_REPORT_ID) { @@ -786,7 +794,7 @@ describe('ReportActionsList (body)', () => { ]; const setupMainDMConciergeMocks = (sessionStartTime: string | null = SESSION_START, showFullHistory = false, hasOnceLoadedReportActions = true) => { - jest.spyOn(ReportActionsUtils, 'shouldReportActionBeVisible').mockReturnValue(true); + jest.mocked(ReportActionsUtils.shouldReportActionBeVisible).mockReturnValue(true); mockUseNetwork.mockReturnValue({isOffline: false}); mockUseIsInSidePanel.mockReturnValue(false); mockUseSidePanelState.mockReturnValue(defaultSidePanelState); diff --git a/tests/ui/ReportActionsTest.tsx b/tests/ui/ReportActionsTest.tsx index 18f7f63e2496..95fe3f0cf281 100644 --- a/tests/ui/ReportActionsTest.tsx +++ b/tests/ui/ReportActionsTest.tsx @@ -26,13 +26,13 @@ import Onyx from 'react-native-onyx'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; -const REPORT_ID = '123'; +const mockReportID = '123'; jest.mock('@react-navigation/native', () => { const actualNav = jest.requireActual('@react-navigation/native'); return { ...actualNav, - useRoute: jest.fn(() => ({params: {reportID: REPORT_ID}})), + useRoute: jest.fn(() => ({params: {reportID: mockReportID}})), }; }); @@ -87,7 +87,7 @@ const defaultPaginatedReportActionsResult: ReturnType { expect(screen.queryByTestId('ReportActionsSkeletonView')).toBeNull(); expect(mockMoneyRequestList).not.toHaveBeenCalled(); expect(mockReportActionsListBody).toHaveBeenCalled(); - expect(mockReportActionsListBody.mock.calls.at(-1)?.at(0)).toEqual(expect.objectContaining({reportID: REPORT_ID})); + expect(mockReportActionsListBody.mock.calls.at(-1)?.at(0)).toEqual(expect.objectContaining({reportID: mockReportID})); expect(mockUserTypingEventListener).toHaveBeenCalled(); expect(mockUserTypingEventListener.mock.calls.at(-1)?.at(0)).toEqual(expect.objectContaining({report: mockReport})); }); @@ -199,7 +199,7 @@ describe('ReportActions (orchestrator)', () => { expect(screen.getByTestId('ReportActionsSkeletonView')).toBeTruthy(); expect(mockReportActionsListBody).not.toHaveBeenCalled(); - expect(mockMarkOpenReportEnd).toHaveBeenCalledWith(REPORT_ID, mockReport, {warm: false}); + expect(mockMarkOpenReportEnd).toHaveBeenCalledWith(mockReportID, mockReport, {warm: false}); }); it('mounts the body (not the orchestrator app-load skeleton) for a Concierge report during app load', () => { diff --git a/tests/ui/ScanSkipConfirmationTest.tsx b/tests/ui/ScanSkipConfirmationTest.tsx index 6ef764df3ba1..5e5244bdf15e 100644 --- a/tests/ui/ScanSkipConfirmationTest.tsx +++ b/tests/ui/ScanSkipConfirmationTest.tsx @@ -29,7 +29,7 @@ type CreateTransactionArg = {optimisticTransactionIDs?: string[]; optimisticChat // These mocks isolate the submit-orchestration boundary so we can assert *what ScanSkipConfirmation composes* // (optimistic-id threading + dismiss-first + cleanup) without exercising the real action/navigation stack. -let triggerFileSelection: ((files: FileObject[]) => void) | null = null; +let mockTriggerFileSelection: ((files: FileObject[]) => void) | null = null; let capturedCreateTransactionArg: CreateTransactionArg | undefined; const mockCreateTransaction = jest.fn((arg: CreateTransactionArg) => { capturedCreateTransactionArg = arg; @@ -67,7 +67,7 @@ jest.mock('@pages/iou/request/step/IOURequestStepScan/hooks/useScanRouteParams', jest.mock('@hooks/useFilesValidation', () => { const ReactLib = jest.requireActual('react'); return (callback: (files: FileObject[]) => void) => { - triggerFileSelection = callback; + mockTriggerFileSelection = callback; return { validateFiles: (files: FileObject[]) => callback(files), PDFValidationComponent: ReactLib.createElement(ReactLib.Fragment), @@ -125,7 +125,7 @@ describe('ScanSkipConfirmation submit orchestration', () => { }); beforeEach(() => { - triggerFileSelection = null; + mockTriggerFileSelection = null; capturedCreateTransactionArg = undefined; }); @@ -176,11 +176,11 @@ describe('ScanSkipConfirmation submit orchestration', () => { ); await waitForBatchedUpdatesWithAct(); - expect(triggerFileSelection).not.toBeNull(); + expect(mockTriggerFileSelection).not.toBeNull(); const receiptFile = {name: 'receipt.png', type: 'image/png', size: 100, uri: 'file://receipt.png'} as FileObject; await act(async () => { - triggerFileSelection?.([receiptFile]); + mockTriggerFileSelection?.([receiptFile]); }); await waitForBatchedUpdates(); diff --git a/tests/ui/SearchPageTest.tsx b/tests/ui/SearchPageTest.tsx index caa0a79917b7..3cb0b9452c32 100644 --- a/tests/ui/SearchPageTest.tsx +++ b/tests/ui/SearchPageTest.tsx @@ -76,11 +76,11 @@ jest.mock('@react-navigation/core', () => ({ })); // FlashList never lays out here, so stand in for it to get at onEndReached. -const listProps: {onEndReached?: () => void} = {}; +const mockListProps: {onEndReached?: () => void} = {}; jest.mock('@components/Search/SearchList/BaseSearchList', () => ({ __esModule: true, default: (props: {onEndReached?: () => void}) => { - listProps.onEndReached = props.onEndReached; + mockListProps.onEndReached = props.onEndReached; return null; }, })); @@ -260,7 +260,7 @@ describe('SearchPageNarrow', () => { mockUseNetwork.mockReturnValue({isOffline: false} as ReturnType); mockSearchQueryParam.mockReturnValue(FAILED_QUERY); mockIsFocused.mockReturnValue(true); - listProps.onEndReached = undefined; + mockListProps.onEndReached = undefined; }); it('SearchPageNarrow renders correctly', async () => { @@ -411,14 +411,14 @@ describe('SearchPageNarrow', () => { jest.advanceTimersByTime(0); }); - expect(listProps.onEndReached).toBeDefined(); + expect(mockListProps.onEndReached).toBeDefined(); // Mount refresh still in flight when the end of the list is reached. await act(async () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.SNAPSHOT}${expenseQueryJSON?.hash}`, {search: {isLoading: true}}); }); await act(async () => { - listProps.onEndReached?.(); + mockListProps.onEndReached?.(); }); await act(async () => { jest.advanceTimersByTime(0); @@ -452,7 +452,7 @@ describe('SearchPageNarrow', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.SNAPSHOT}${expenseQueryJSON?.hash}`, {search: {isLoading: true}}); }); await act(async () => { - listProps.onEndReached?.(); + mockListProps.onEndReached?.(); }); await act(async () => { jest.advanceTimersByTime(0); @@ -483,7 +483,7 @@ describe('SearchPageNarrow', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.SNAPSHOT}${expenseQueryJSON?.hash}`, {search: {isLoading: true}}); }); await act(async () => { - listProps.onEndReached?.(); + mockListProps.onEndReached?.(); }); await act(async () => { jest.advanceTimersByTime(0); @@ -511,7 +511,7 @@ describe('SearchPageNarrow', () => { }); await act(async () => { - listProps.onEndReached?.(); + mockListProps.onEndReached?.(); }); await act(async () => { jest.advanceTimersByTime(0); @@ -539,7 +539,7 @@ describe('SearchPageNarrow', () => { jest.advanceTimersByTime(0); }); await act(async () => { - listProps.onEndReached?.(); + mockListProps.onEndReached?.(); }); await act(async () => { jest.advanceTimersByTime(0); @@ -559,7 +559,7 @@ describe('SearchPageNarrow', () => { }); await act(async () => { - listProps.onEndReached?.(); + mockListProps.onEndReached?.(); }); await act(async () => { jest.advanceTimersByTime(0); @@ -602,7 +602,7 @@ describe('SearchPageNarrow', () => { }); await act(async () => { - listProps.onEndReached?.(); + mockListProps.onEndReached?.(); }); await act(async () => { jest.advanceTimersByTime(0); diff --git a/tests/ui/SessionTest.tsx b/tests/ui/SessionTest.tsx index 48f936636152..040f5380e25d 100644 --- a/tests/ui/SessionTest.tsx +++ b/tests/ui/SessionTest.tsx @@ -31,6 +31,31 @@ jest.mock('@libs/BootSplash', () => ({ hide: jest.fn().mockResolvedValue(undefined), })); +jest.mock('@libs/actions/Session', () => { + const actual = jest.requireActual('@libs/actions/Session'); + return { + ...actual, + signInWithShortLivedAuthToken: jest.fn(actual.signInWithShortLivedAuthToken), + signInWithSupportAuthToken: jest.fn(actual.signInWithSupportAuthToken), + }; +}); + +jest.mock('@libs/actions/App', () => { + const actual = jest.requireActual('@libs/actions/App'); + return { + ...actual, + openApp: jest.fn(actual.openApp), + }; +}); + +jest.mock('@libs/actions/Device', () => { + const actual = jest.requireActual('@libs/actions/Device'); + return { + ...actual, + getDeviceInfoWithID: jest.fn(actual.getDeviceInfoWithID), + }; +}); + const TEST_USER_ACCOUNT_ID_1 = 123; const TEST_USER_LOGIN_1 = 'test@test.com'; // cspell:disable-next-line @@ -100,7 +125,7 @@ describe('Deep linking', () => { // the real signInWithShortLivedAuthToken so it sets NetworkStore.lastShortAuthToken — // the token-cache guard in LogInWithShortLivedAuthTokenPage reads that value to skip // re-authentication after sign-out, which is exactly what the second test asserts. - jest.spyOn(Session, 'signInWithShortLivedAuthToken').mockImplementation(() => { + jest.mocked(Session.signInWithShortLivedAuthToken).mockImplementation(() => { Onyx.multiSet({ [ONYXKEYS.CREDENTIALS]: { login: TEST_USER_LOGIN_1, @@ -132,7 +157,7 @@ describe('Deep linking', () => { // the relationship between the computed report collection key and its production value type. const reportKey: `${typeof ONYXKEYS.COLLECTION.REPORT}${string}` = `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`; const reportData = createMock>({[reportKey]: report}); - jest.spyOn(AppActions, 'openApp').mockImplementation(() => + jest.mocked(AppActions.openApp).mockImplementation(() => Onyx.multiSet({ ...reportData, [ONYXKEYS.IS_LOADING_APP]: false, @@ -258,10 +283,10 @@ describe('Support auth token login', () => { jest.restoreAllMocks(); wrapOnyxWithWaitForBatchedUpdates(Onyx); - jest.spyOn(Session, 'signInWithSupportAuthToken').mockImplementation(() => {}); + jest.mocked(Session.signInWithSupportAuthToken).mockImplementation(() => {}); // Set the keys the app needs to finish loading rather than going through a full OpenApp round-trip. - jest.spyOn(AppActions, 'openApp').mockImplementation(() => + jest.mocked(AppActions.openApp).mockImplementation(() => Onyx.multiSet({ [ONYXKEYS.IS_LOADING_APP]: false, [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, @@ -408,7 +433,7 @@ describe('signInWithShortLivedAuthToken', () => { // optimisticData never runs; the flag can therefore only be true if it was set synchronously. it('sets the in-flight guard synchronously, before the device-info promise resolves', async () => { let resolveDeviceInfo!: (value: string) => void; - jest.spyOn(Device, 'getDeviceInfoWithID').mockReturnValue( + jest.mocked(Device.getDeviceInfoWithID).mockReturnValue( new Promise((resolve) => { resolveDeviceInfo = resolve; }), diff --git a/tests/ui/WorkspaceCompanyCardFeedSelectorPageTest.tsx b/tests/ui/WorkspaceCompanyCardFeedSelectorPageTest.tsx index 896ab8073c06..f459a8f8a70c 100644 --- a/tests/ui/WorkspaceCompanyCardFeedSelectorPageTest.tsx +++ b/tests/ui/WorkspaceCompanyCardFeedSelectorPageTest.tsx @@ -14,7 +14,7 @@ import SCREENS from '@src/SCREENS'; import React from 'react'; -const POLICY_ID = 'policy123'; +const mockPolicyID = 'policy123'; let mockIsUserValidated = false; let mockIsBlockedToAddNewFeeds = false; @@ -99,7 +99,7 @@ jest.mock('@hooks/useOtherFeedsForFeedSelector', () => ({ jest.mock('@hooks/usePolicy', () => ({ __esModule: true, - default: () => ({id: POLICY_ID, name: 'Acme'}), + default: () => ({id: mockPolicyID, name: 'Acme'}), })); jest.mock('@hooks/usePolicyFeatureWriteAccess', () => ({ @@ -141,14 +141,14 @@ const mockNavigate = jest.mocked(Navigation.navigate); const mockClearAddNewCardFlow = jest.mocked(clearAddNewCardFlow); const expectedUpgradeRoute = () => - ROUTES.WORKSPACE_UPGRADE.getRoute(POLICY_ID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.companyCards.alias, ROUTES.WORKSPACE_COMPANY_CARDS_SELECT_FEED.getRoute(POLICY_ID)); + ROUTES.WORKSPACE_UPGRADE.getRoute(mockPolicyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.companyCards.alias, ROUTES.WORKSPACE_COMPANY_CARDS_SELECT_FEED.getRoute(mockPolicyID)); type WorkspaceCompanyCardFeedSelectorPageScreenProps = PlatformStackScreenProps; const route: WorkspaceCompanyCardFeedSelectorPageScreenProps['route'] = { key: 'workspace-company-cards-select-feed', name: SCREENS.WORKSPACE.COMPANY_CARDS_SELECT_FEED, - params: {policyID: POLICY_ID}, + params: {policyID: mockPolicyID}, }; // The screen does not read navigation; this inert test double only satisfies the navigator-provided prop. // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion diff --git a/tests/ui/WorkspaceCompanyCardPageEmptyStateTest.tsx b/tests/ui/WorkspaceCompanyCardPageEmptyStateTest.tsx index 4020dc2393e9..fd8d3aa820b8 100644 --- a/tests/ui/WorkspaceCompanyCardPageEmptyStateTest.tsx +++ b/tests/ui/WorkspaceCompanyCardPageEmptyStateTest.tsx @@ -10,7 +10,7 @@ import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; import React from 'react'; -const POLICY_ID = 'policy123'; +const mockPolicyID = 'policy123'; let mockIsUserValidated = false; let mockIsActingAsDelegate = false; @@ -67,7 +67,7 @@ jest.mock('@hooks/useOtherFeedsForFeedSelector', () => ({ jest.mock('@hooks/usePolicy', () => ({ __esModule: true, - default: () => ({id: POLICY_ID, policyAccountID: 123, outputCurrency: 'USD'}), + default: () => ({id: mockPolicyID, policyAccountID: 123, outputCurrency: 'USD'}), })); jest.mock('@hooks/usePolicyFeatureWriteAccess', () => ({ @@ -98,7 +98,7 @@ const mockNavigate = jest.mocked(Navigation.navigate); const mockClearAddNewCardFlow = jest.mocked(clearAddNewCardFlow); function renderEmptyState() { - return render(); + return render(); } function pressAddCards() { @@ -123,7 +123,7 @@ describe('WorkspaceCompanyCardPageEmptyState', () => { pressAddCards(); expect(mockVerifyAccountAndResume).not.toHaveBeenCalled(); - expect(mockNavigate).toHaveBeenCalledWith(ROUTES.WORKSPACE_COMPANY_CARDS_SELECT_FEED.getRoute(POLICY_ID)); + expect(mockNavigate).toHaveBeenCalledWith(ROUTES.WORKSPACE_COMPANY_CARDS_SELECT_FEED.getRoute(mockPolicyID)); expect(mockClearAddNewCardFlow).not.toHaveBeenCalled(); }); @@ -139,7 +139,7 @@ describe('WorkspaceCompanyCardPageEmptyState', () => { }); it.each([ - ['with feeds from other workspaces', [{value: 'feed1'}], () => ROUTES.WORKSPACE_COMPANY_CARDS_SELECT_FEED.getRoute(POLICY_ID)], + ['with feeds from other workspaces', [{value: 'feed1'}], () => ROUTES.WORKSPACE_COMPANY_CARDS_SELECT_FEED.getRoute(mockPolicyID)], ['without other feeds', [], () => DYNAMIC_ROUTES.WORKSPACE_COMPANY_CARDS_ADD_NEW.path], ])('defers to account verification for an unvalidated user and resumes the branching %s', async (_, otherFeeds, expectedRoute) => { mockOtherFeeds = otherFeeds; diff --git a/tests/ui/WorkspaceMoreFeaturesPageTest.tsx b/tests/ui/WorkspaceMoreFeaturesPageTest.tsx index fab7084036f2..4e1db82d5a48 100644 --- a/tests/ui/WorkspaceMoreFeaturesPageTest.tsx +++ b/tests/ui/WorkspaceMoreFeaturesPageTest.tsx @@ -48,7 +48,7 @@ jest.mock('@components/Modal/ReanimatedModal', () => { jest.mock('@hooks/useIsPolicyConnectedToUberReceiptPartner', () => ({__esModule: true, default: jest.fn(() => false)})); jest.mock('@libs/CardUtils', () => { - const actual: typeof CardUtils = jest.requireActual('@libs/CardUtils'); + const actual: typeof CardUtils = jest.requireActual('@libs/CardUtils'); return { ...actual, isSmartLimitEnabled: jest.fn(() => false), @@ -57,7 +57,7 @@ jest.mock('@libs/CardUtils', () => { }); jest.mock('@libs/PolicyUtils', () => { - const actual: typeof PolicyUtils = jest.requireActual('@libs/PolicyUtils'); + const actual: typeof PolicyUtils = jest.requireActual('@libs/PolicyUtils'); return { ...actual, hasAccountingConnections: jest.fn(() => false), @@ -65,6 +65,14 @@ jest.mock('@libs/PolicyUtils', () => { }; }); +jest.mock('@userActions/Report', () => { + const actual = jest.requireActual('@userActions/Report'); + return { + ...actual, + navigateToConciergeChat: jest.fn(actual.navigateToConciergeChat), + }; +}); + TestHelper.setupGlobalFetchMock(); const Stack = createPlatformStackNavigator(); @@ -124,7 +132,7 @@ const hasAccountingFeatureConnectionMock = jest.mocked(PolicyUtils.hasAccounting const useIsUberConnectedMock = jest.mocked(useIsPolicyConnectedToUberReceiptPartner); const navigateSpy = jest.spyOn(Navigation, 'navigate').mockImplementation(() => undefined); -const navigateToConciergeChatSpy = jest.spyOn(ReportActions, 'navigateToConciergeChat').mockImplementation(() => Promise.resolve()); +const navigateToConciergeChatSpy = jest.mocked(ReportActions.navigateToConciergeChat).mockImplementation(() => Promise.resolve()); function escapeRegExp(value: string): string { return value.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&'); diff --git a/tests/ui/WorkspacePageWithSectionsTest.tsx b/tests/ui/WorkspacePageWithSectionsTest.tsx index f31104c75ba0..7191bada2c8b 100644 --- a/tests/ui/WorkspacePageWithSectionsTest.tsx +++ b/tests/ui/WorkspacePageWithSectionsTest.tsx @@ -21,7 +21,7 @@ import Onyx from 'react-native-onyx'; import createRandomPolicy from '../utils/collections/policies'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; -const POLICY_ID = 1; +const mockPolicyID = 1; // Mock navigation hooks jest.mock('@react-navigation/native', () => { @@ -32,7 +32,7 @@ jest.mock('@react-navigation/native', () => { useRoute: () => ({ key: 'test-route', name: 'WORKSPACE_INITIAL', - params: {policyID: POLICY_ID.toString()}, + params: {policyID: mockPolicyID.toString()}, }), usePreventRemove: jest.fn(), }; @@ -53,14 +53,14 @@ jest.mock('@components/FullscreenLoadingIndicator', () => { }; }); -const mockPolicy: Policy = {...createRandomPolicy(POLICY_ID), type: CONST.POLICY.TYPE.CORPORATE, pendingAction: null, role: CONST.POLICY.ROLE.ADMIN}; +const mockPolicy: Policy = {...createRandomPolicy(mockPolicyID), type: CONST.POLICY.TYPE.CORPORATE, pendingAction: null, role: CONST.POLICY.ROLE.ADMIN}; const getDefaultProps = (props = {}) => ({ headerText: 'Test Workspace', route: { key: 'test-route', name: SCREENS.WORKSPACE.INITIAL, - params: {policyID: POLICY_ID.toString()}, + params: {policyID: mockPolicyID.toString()}, }, policy: mockPolicy, ...props, @@ -83,7 +83,7 @@ describe('WorkspacePageWithSections', () => { keys: ONYXKEYS, }); await act(async () => { - await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, mockPolicy); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${mockPolicyID}`, mockPolicy); await waitForBatchedUpdatesWithAct(); }); }); @@ -132,7 +132,7 @@ describe('WorkspacePageWithSections', () => { describe('FullPageNotFoundView behavior when deleting a workspace', () => { // The policy is read from Onyx via the withPolicy HOC (which overrides the `policy` prop), so these // tests drive the workspace state through the Onyx policy collection to mirror the real delete flow. - const policyKey = `${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}` as const; + const policyKey = `${ONYXKEYS.COLLECTION.POLICY}${mockPolicyID}` as const; beforeEach(async () => { // Render the page content (not the loading indicator) so we can assert on the not-found view directly. diff --git a/tests/ui/components/ApproveActionButtonTest.tsx b/tests/ui/components/ApproveActionButtonTest.tsx index eb46db87e41e..fba869303ad9 100644 --- a/tests/ui/components/ApproveActionButtonTest.tsx +++ b/tests/ui/components/ApproveActionButtonTest.tsx @@ -16,10 +16,10 @@ import type {UseOnyxResult} from 'react-native-onyx'; import React from 'react'; -const TEST_IOU_REPORT_ID = '1001'; +const mockTestIOUReportID = '1001'; const iouReport = { - reportID: TEST_IOU_REPORT_ID, + reportID: mockTestIOUReportID, type: CONST.REPORT.TYPE.EXPENSE, policyID: 'policy1', } as Report; @@ -66,7 +66,7 @@ const mockStartApprovedAnimation = jest.fn(); const mockOnHoldMenuOpen = jest.fn(); jest.mock('@components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContext', () => ({ __esModule: true, - useReportPreviewData: () => ({iouReportID: TEST_IOU_REPORT_ID}), + useReportPreviewData: () => ({iouReportID: mockTestIOUReportID}), useReportPreviewActionState: () => ({shouldShowPayButton: true}), useReportPreviewActions: () => ({startApprovedAnimation: mockStartApprovedAnimation, onHoldMenuOpen: mockOnHoldMenuOpen}), })); @@ -96,7 +96,7 @@ describe('ApproveActionButton', () => { mockIsDelegateAccessRestricted = false; mockedHasHeldExpenses.mockReturnValue(false); mockedUseOnyx.mockImplementation((key) => { - if (key === `${ONYXKEYS.COLLECTION.REPORT}${TEST_IOU_REPORT_ID}`) { + if (key === `${ONYXKEYS.COLLECTION.REPORT}${mockTestIOUReportID}`) { return createOnyxResult(iouReport); } return createOnyxResult(undefined); diff --git a/tests/ui/components/ButtonKeyboardShortcut.tsx b/tests/ui/components/ButtonKeyboardShortcut.tsx index 3b312364cb98..81b8031f1d3a 100644 --- a/tests/ui/components/ButtonKeyboardShortcut.tsx +++ b/tests/ui/components/ButtonKeyboardShortcut.tsx @@ -14,9 +14,9 @@ import React from 'react'; // the callback and config that ButtonKeyboardShortcut passes to // useKeyboardShortcut, letting tests invoke the callback directly — the same // pattern used across the ButtonComposed test suite. -let enterKeyCallback: ((event?: KeyboardEvent) => void) | undefined; +let mockEnterKeyCallback: ((event?: KeyboardEvent) => void) | undefined; // eslint-disable-next-line @typescript-eslint/no-explicit-any -let capturedShortcutConfig: Record | undefined; +let mockCapturedShortcutConfig: Record | undefined; jest.mock('@hooks/useKeyboardShortcut', () => // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -24,8 +24,8 @@ jest.mock('@hooks/useKeyboardShortcut', () => if (shortcut.shortcutKey !== 'Enter' || !config.isActive) { return; } - enterKeyCallback = callback; - capturedShortcutConfig = config; + mockEnterKeyCallback = callback; + mockCapturedShortcutConfig = config; }); // ────────────────────────────────────────────────────────────────────────────── @@ -53,8 +53,8 @@ const renderShortcut = (props: Partial = {}, button describe('ButtonKeyboardShortcut', () => { beforeEach(() => { - enterKeyCallback = undefined; - capturedShortcutConfig = undefined; + mockEnterKeyCallback = undefined; + mockCapturedShortcutConfig = undefined; }); afterEach(() => { @@ -69,7 +69,7 @@ describe('ButtonKeyboardShortcut', () => { renderShortcut(); // Then useKeyboardShortcut was called and the callback was captured - expect(enterKeyCallback).toBeDefined(); + expect(mockEnterKeyCallback).toBeDefined(); }); }); @@ -82,7 +82,7 @@ describe('ButtonKeyboardShortcut', () => { renderShortcut({}, {onPress}); // When the Enter key fires - enterKeyCallback?.(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); + mockEnterKeyCallback?.(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); // Then onPress is called exactly once expect(onPress).toHaveBeenCalledTimes(1); @@ -94,7 +94,7 @@ describe('ButtonKeyboardShortcut', () => { renderShortcut({}, {onPress, isDisabled: true}); // When the Enter key fires - enterKeyCallback?.(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); + mockEnterKeyCallback?.(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); // Then validateSubmitShortcut blocks the call expect(onPress).not.toHaveBeenCalled(); @@ -106,7 +106,7 @@ describe('ButtonKeyboardShortcut', () => { renderShortcut({}, {onPress, isLoading: true}); // When the Enter key fires - enterKeyCallback?.(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); + mockEnterKeyCallback?.(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); // Then validateSubmitShortcut blocks the call expect(onPress).not.toHaveBeenCalled(); @@ -121,7 +121,7 @@ describe('ButtonKeyboardShortcut', () => { renderShortcut({allowBubble: true}); // Then the config passed to useKeyboardShortcut reflects shouldBubble=true - expect(capturedShortcutConfig?.shouldBubble).toBe(true); + expect(mockCapturedShortcutConfig?.shouldBubble).toBe(true); }); it('forwards enterKeyEventListenerPriority as priority', () => { @@ -129,7 +129,7 @@ describe('ButtonKeyboardShortcut', () => { renderShortcut({enterKeyEventListenerPriority: 5}); // Then the config reflects the custom priority - expect(capturedShortcutConfig?.priority).toBe(5); + expect(mockCapturedShortcutConfig?.priority).toBe(5); }); it('is active when isPressOnEnterActive is true regardless of screen focus', () => { @@ -137,14 +137,14 @@ describe('ButtonKeyboardShortcut', () => { renderShortcut({isPressOnEnterActive: true}); // Then the callback was captured — the shortcut is active - expect(enterKeyCallback).toBeDefined(); + expect(mockEnterKeyCallback).toBeDefined(); }); it('sets shouldPreventDefault to false', () => { // The shortcut must not swallow the event so other listeners can still react. renderShortcut(); - expect(capturedShortcutConfig?.shouldPreventDefault).toBe(false); + expect(mockCapturedShortcutConfig?.shouldPreventDefault).toBe(false); }); }); diff --git a/tests/ui/components/Search/ExpenseReportListItemAvatarTest.tsx b/tests/ui/components/Search/ExpenseReportListItemAvatarTest.tsx index 31e9032e671c..e4a575b8c808 100644 --- a/tests/ui/components/Search/ExpenseReportListItemAvatarTest.tsx +++ b/tests/ui/components/Search/ExpenseReportListItemAvatarTest.tsx @@ -37,7 +37,7 @@ type AvatarData = { parent: string; }; -const parseSource = (source: AvatarSource | IconAsset): string => { +const mockParseSource = (source: AvatarSource | IconAsset): string => { if (typeof source === 'string') { return source; } @@ -64,7 +64,7 @@ jest.mock('@components/Avatar/UserAvatar', () => { { dataSet={{ name, avatarID, - uri: parseSource(source ?? '') || 'No Source', + uri: mockParseSource(source ?? '') || 'No Source', parent: testID, }} testID="MockedAvatarData" @@ -94,7 +94,7 @@ jest.mock('@src/components/Icon', () => { return ( void}; type ChildrenOnly = {children?: React.ReactNode}; // Captured callbacks across mocks let us drive the form imperatively without rendering the real FormProvider chain. -let capturedSubmit: (() => void) | null = null; -let capturedOnChangeText: ((value: string) => void) | null = null; +let mockCapturedSubmit: (() => void) | null = null; +let mockCapturedOnChangeText: ((value: string) => void) | null = null; const mockGoBack = jest.fn(); @@ -34,7 +34,7 @@ jest.mock('@hooks/useAutoFocusInput', () => () => ({inputCallbackRef: () => {}}) jest.mock('@components/Form/FormProvider', () => ({ __esModule: true, default: ({children, onSubmit}: FormProviderMockProps) => { - capturedSubmit = onSubmit ?? null; + mockCapturedSubmit = onSubmit ?? null; return children; }, })); @@ -42,7 +42,7 @@ jest.mock('@components/Form/FormProvider', () => ({ jest.mock('@components/Form/InputWrapper', () => ({ __esModule: true, default: ({onChangeText}: InputWrapperMockProps) => { - capturedOnChangeText = onChangeText ?? null; + mockCapturedOnChangeText = onChangeText ?? null; return null; }, })); @@ -75,8 +75,8 @@ function renderEdit(props: Partial = {}, onMerchantDataChange = jest. } beforeEach(() => { - capturedSubmit = null; - capturedOnChangeText = null; + mockCapturedSubmit = null; + mockCapturedOnChangeText = null; mockGoBack.mockReset(); }); @@ -88,8 +88,8 @@ describe('SpendRuleMerchantEditBase.submit — Navigation.goBack fires with {sho merchantMatchTypes: [CONST.SEARCH.SYNTAX_OPERATORS.CONTAINS, CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO], }); - act(() => capturedOnChangeText?.('')); - capturedSubmit?.(); + act(() => mockCapturedOnChangeText?.('')); + mockCapturedSubmit?.(); expect(mockGoBack).toHaveBeenCalledTimes(1); expect(mockGoBack).toHaveBeenCalledWith(undefined, {shouldSkipFocusRestore: true}); @@ -99,7 +99,7 @@ describe('SpendRuleMerchantEditBase.submit — Navigation.goBack fires with {sho it('cancel-on-new (new-merchant flow, empty name): still skips restore even though onMerchantDataChange is NOT invoked — the destination form Save button must not be hijacked', () => { const {onMerchantDataChange} = renderEdit({merchantIndex: ROUTES.NEW, merchantNames: ['Acme'], merchantMatchTypes: [CONST.SEARCH.SYNTAX_OPERATORS.CONTAINS]}); - capturedSubmit?.(); + mockCapturedSubmit?.(); expect(mockGoBack).toHaveBeenCalledTimes(1); expect(mockGoBack).toHaveBeenCalledWith(undefined, {shouldSkipFocusRestore: true}); @@ -113,8 +113,8 @@ describe('SpendRuleMerchantEditBase.submit — Navigation.goBack fires with {sho merchantMatchTypes: [CONST.SEARCH.SYNTAX_OPERATORS.CONTAINS, CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO], }); - act(() => capturedOnChangeText?.('Acme Inc')); - capturedSubmit?.(); + act(() => mockCapturedOnChangeText?.('Acme Inc')); + mockCapturedSubmit?.(); expect(mockGoBack).toHaveBeenCalledTimes(1); expect(mockGoBack).toHaveBeenCalledWith(undefined, {shouldSkipFocusRestore: true}); @@ -124,8 +124,8 @@ describe('SpendRuleMerchantEditBase.submit — Navigation.goBack fires with {sho it('new-merchant add (new flow, non-empty name): skips restore, navigates back, and appends to the arrays — same Enter-hijack defense', () => { const {onMerchantDataChange} = renderEdit({merchantIndex: ROUTES.NEW, merchantNames: ['Acme'], merchantMatchTypes: [CONST.SEARCH.SYNTAX_OPERATORS.CONTAINS]}); - act(() => capturedOnChangeText?.('Globex')); - capturedSubmit?.(); + act(() => mockCapturedOnChangeText?.('Globex')); + mockCapturedSubmit?.(); expect(mockGoBack).toHaveBeenCalledTimes(1); expect(mockGoBack).toHaveBeenCalledWith(undefined, {shouldSkipFocusRestore: true}); diff --git a/tests/ui/components/SubmitActionButtonTest.tsx b/tests/ui/components/SubmitActionButtonTest.tsx index 6819a0faa4d5..13e68df26b48 100644 --- a/tests/ui/components/SubmitActionButtonTest.tsx +++ b/tests/ui/components/SubmitActionButtonTest.tsx @@ -26,13 +26,13 @@ import React from 'react'; import createMock from '../../utils/createMock'; -const TEST_IOU_REPORT_ID = '1001'; +const mockTestIOUReportID = '1001'; const TEST_TRANSACTION_ID = '3003'; const TEST_ACCOUNT_ID = 1; const TEST_EMAIL = 'submitter@test.com'; const iouReport = { - reportID: TEST_IOU_REPORT_ID, + reportID: mockTestIOUReportID, type: CONST.REPORT.TYPE.EXPENSE, policyID: 'policy1', ownerAccountID: 2, @@ -137,7 +137,7 @@ jest.mock('@hooks/useConfirmPendingRTERAndProceed', () => ({ const mockStartSubmittingAnimation = jest.fn(); jest.mock('@components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContext', () => ({ __esModule: true, - useReportPreviewData: () => ({iouReportID: TEST_IOU_REPORT_ID, transactions: mockTransactions}), + useReportPreviewData: () => ({iouReportID: mockTestIOUReportID, transactions: mockTransactions}), useReportPreviewTransactionViolations: () => ({transactionViolations: mockTransactionViolations}), useReportPreviewAnimationState: () => ({isSubmittingAnimationRunning: false}), useReportPreviewActions: () => ({stopAnimation: jest.fn(), startSubmittingAnimation: mockStartSubmittingAnimation}), @@ -184,7 +184,7 @@ describe('SubmitActionButton', () => { // override the implementation to simulate dismissals. mockedGetTransactionViolations.mockImplementation((transaction, violations) => violations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction?.transactionID}`]); mockedUseOnyx.mockImplementation((key) => { - if (key === `${ONYXKEYS.COLLECTION.REPORT}${TEST_IOU_REPORT_ID}`) { + if (key === `${ONYXKEYS.COLLECTION.REPORT}${mockTestIOUReportID}`) { return createOnyxResult(iouReport); } return createOnyxResult(undefined); @@ -278,7 +278,7 @@ describe('SubmitActionButton', () => { // Then the decision is made from the report preview's own violations and transactions rather than a separate // Onyx read, which is what let this button drift out of sync with the report header's submit button - expect(mockedShouldBlockSubmitDueToStrictPolicyRules).toHaveBeenCalledWith(TEST_IOU_REPORT_ID, reportViolations, true, TEST_ACCOUNT_ID, TEST_EMAIL, reportTransactions); + expect(mockedShouldBlockSubmitDueToStrictPolicyRules).toHaveBeenCalledWith(mockTestIOUReportID, reportViolations, true, TEST_ACCOUNT_ID, TEST_EMAIL, reportTransactions); }); it('passes dismissal-filtered violations to the strict policy rules gate', () => { @@ -296,7 +296,7 @@ describe('SubmitActionButton', () => { // collection instead of the raw context slice, so the two Submit buttons cannot disagree on dismissed violations expect(mockedGetTransactionViolations).toHaveBeenCalledWith(reportTransactions.at(0), reportViolations, TEST_EMAIL, TEST_ACCOUNT_ID, iouReport, undefined, undefined); expect(mockedShouldBlockSubmitDueToStrictPolicyRules).toHaveBeenCalledWith( - TEST_IOU_REPORT_ID, + mockTestIOUReportID, {[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${TEST_TRANSACTION_ID}`]: []}, true, TEST_ACCOUNT_ID, diff --git a/tests/ui/components/SubmitPlanWelcomeModalTest.tsx b/tests/ui/components/SubmitPlanWelcomeModalTest.tsx index ea2919acdb5d..0a645b96108e 100644 --- a/tests/ui/components/SubmitPlanWelcomeModalTest.tsx +++ b/tests/ui/components/SubmitPlanWelcomeModalTest.tsx @@ -18,14 +18,14 @@ const mockSetSubmitMigrationModalShown = jest.fn(); // Capture the callback the modal registers with useBeforeRemove so we can simulate the modal being // removed from the navigation stack and assert the shown-state persistence fires. -let beforeRemoveCallback: (() => void) | undefined; +let mockBeforeRemoveCallback: (() => void) | undefined; jest.mock('@hooks/useLocalize', () => () => ({ translate: (key: string) => key, })); jest.mock('@hooks/useBeforeRemove', () => (callback: () => void) => { - beforeRemoveCallback = callback; + mockBeforeRemoveCallback = callback; }); jest.mock('@react-navigation/native', () => ({ @@ -66,7 +66,7 @@ describe('SubmitPlanWelcomeModal', () => { mockAutoCreateSubmitWorkspace.mockClear(); mockGoBack.mockClear(); mockSetSubmitMigrationModalShown.mockClear(); - beforeRemoveCallback = undefined; + mockBeforeRemoveCallback = undefined; }); function renderModal() { @@ -97,8 +97,8 @@ describe('SubmitPlanWelcomeModal', () => { it('marks the migration modal as shown when it is removed from the navigation stack', () => { renderModal(); - expect(beforeRemoveCallback).toBeDefined(); - beforeRemoveCallback?.(); + expect(mockBeforeRemoveCallback).toBeDefined(); + mockBeforeRemoveCallback?.(); expect(mockSetSubmitMigrationModalShown).toHaveBeenCalledTimes(1); }); diff --git a/tests/unit/APITest.ts b/tests/unit/APITest.ts index 56506012938a..9329b175dc7a 100644 --- a/tests/unit/APITest.ts +++ b/tests/unit/APITest.ts @@ -51,6 +51,14 @@ const readParams = (reportID: string): ApiRequestCommandParameters[typeof MOCK_R jest.mock('@src/libs/Log'); +jest.mock('@src/libs/Request', () => { + const actual = jest.requireActual('@src/libs/Request'); + return { + ...actual, + processWithMiddleware: jest.fn(actual.processWithMiddleware), + }; +}); + Onyx.init({ keys: ONYXKEYS, }); @@ -564,7 +572,7 @@ describe('APITests', () => { test('Sequential queue will not run until credentials are read', () => { const xhr = jest.spyOn(HttpUtils, 'xhr'); - const processWithMiddleware = jest.spyOn(Request, 'processWithMiddleware'); + const processWithMiddleware = jest.mocked(Request.processWithMiddleware); // Given a simulated a condition where the credentials have not yet been read from storage and we are offline setHasRadio(false); diff --git a/tests/unit/BulletListRendererTest.tsx b/tests/unit/BulletListRendererTest.tsx index 0534f43ecc4a..873f6f8a7c6e 100644 --- a/tests/unit/BulletListRendererTest.tsx +++ b/tests/unit/BulletListRendererTest.tsx @@ -19,7 +19,7 @@ jest.mock('@hooks/useHasTextAncestor', () => () => false); // Capture the html string ultimately passed to react-native-render-html so we can // assert the orphaned
stripping happens before the library sees the HTML. -const capturedSource: {html?: string} = {}; +const mockCapturedSource: {html?: string} = {}; function mockGetFirstTextContent(tnode?: TBlock): string { const firstChild = tnode?.children.at(0); @@ -32,7 +32,7 @@ jest.mock('react-native-render-html', () => { return { RenderHTMLConfigProvider: ({children}: {children: React.ReactNode}) => children, RenderHTMLSource: ({source}: {source: {html?: string}}) => { - capturedSource.html = source?.html; + mockCapturedSource.html = source?.html; return ReactModule.createElement(MockView); }, TNodeChildrenRenderer: ({tnode}: {tnode?: TBlock}) => ReactModule.createElement(MockText, null, mockGetFirstTextContent(tnode)), @@ -57,7 +57,7 @@ const buildULTNode = (children: Array<{tagName: string; text: string}>) => describe('Bullet list rendering', () => { beforeEach(() => { - capturedSource.html = undefined; + mockCapturedSource.html = undefined; }); describe('ULRenderer', () => { @@ -145,79 +145,79 @@ describe('Bullet list rendering', () => { describe('RenderHTML strips orphaned
tags inside
    ', () => { it('strips
    immediately before
', () => { render(); - expect(capturedSource.html).toBe('
  • One
  • Two
'); + expect(mockCapturedSource.html).toBe('
  • One
  • Two
'); }); it('strips
(no slash) immediately before ', () => { render(); - expect(capturedSource.html).toBe('
  • One
  • Two
'); + expect(mockCapturedSource.html).toBe('
  • One
  • Two
'); }); it('strips
appearing between and the next
  • ', () => { render(); - expect(capturedSource.html).toBe('
    • One
    • Two
    '); + expect(mockCapturedSource.html).toBe('
    • One
    • Two
    '); }); it('leaves a valid
      /
    • list untouched', () => { render(); - expect(capturedSource.html).toBe('
      • One
      • Two
      '); + expect(mockCapturedSource.html).toBe('
      • One
      • Two
      '); }); it('strips multiple consecutive
      before
    ', () => { render(); - expect(capturedSource.html).toBe('
    • One
    '); + expect(mockCapturedSource.html).toBe('
    • One
    '); }); it('strips multiple consecutive
    between
  • and
  • ', () => { render(); - expect(capturedSource.html).toBe('
    • One
    • Two
    '); + expect(mockCapturedSource.html).toBe('
    • One
    • Two
    '); }); it('does not strip
    outside of ', () => { render(); - expect(mockCapturedSource.html).toBe('
    • One
    • Two
    '); + expect(capturedSource.html).toBe('
    • One
    • Two
    '); }); it('strips
    appearing between
  • and the next
  • ', () => { render(); - expect(mockCapturedSource.html).toBe('
    • One
    • Two
    '); + expect(capturedSource.html).toBe('
    • One
    • Two
    '); }); it('leaves a valid
      /
    • list untouched', () => { render(); - expect(mockCapturedSource.html).toBe('
      • One
      • Two
      '); + expect(capturedSource.html).toBe('
      • One
      • Two
      '); }); it('strips multiple consecutive
      before
    ', () => { render(); - expect(mockCapturedSource.html).toBe('
    • One
    '); + expect(capturedSource.html).toBe('
    • One
    '); }); it('strips multiple consecutive
    between
  • and
  • ', () => { render(); - expect(mockCapturedSource.html).toBe('
    • One
    • Two
    '); + expect(capturedSource.html).toBe('
    • One
    • Two
    '); }); it('does not strip
    outside of