From 8c4a481890681412a160057730b251404089db91 Mon Sep 17 00:00:00 2001 From: KJ21-ENG <140263938+KJ21-ENG@users.noreply.github.com> Date: Wed, 29 Jul 2026 01:48:18 +0530 Subject: [PATCH 1/4] tests: clean unsafe type assertions --- tests/actions/WorkflowTest.ts | 51 +- tests/navigation/getStateFromPathTests.ts | 120 +++-- .../useFilterFormValues.perf-test.tsx | 59 +-- tests/unit/FileUtilsTest.ts | 85 ++-- tests/unit/PolicyUtilsTest.ts | 98 ++-- tests/unit/SearchEditMultipleUtilsTest.ts | 82 ++-- .../useAutoCreateSubmitWorkspace.test.ts | 103 ++-- tests/unit/navigateToWorkspacesPageTest.ts | 29 +- tests/unit/selectors/PolicyTest.ts | 55 +-- tests/unit/usePolicyIndicatorChecksTest.ts | 447 ++++++++++-------- 10 files changed, 626 insertions(+), 503 deletions(-) diff --git a/tests/actions/WorkflowTest.ts b/tests/actions/WorkflowTest.ts index 5cf6d6810f94..4048bf018e81 100644 --- a/tests/actions/WorkflowTest.ts +++ b/tests/actions/WorkflowTest.ts @@ -15,8 +15,9 @@ import Onyx from 'react-native-onyx'; import type {MockFetch} from '../utils/TestHelper'; import createRandomPolicy from '../utils/collections/policies'; +import createMock from '../utils/createMock'; import getOnyxValue from '../utils/getOnyxValue'; -import {getGlobalFetchMock, getOnyxData} from '../utils/TestHelper'; +import {createGlobalFetchMock, getOnyxData} from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; jest.mock('@src/libs/WorkflowUtils', () => { @@ -33,8 +34,8 @@ jest.mock('@src/libs/actions/Task', () => ({ completeTask: jest.fn(), })); -const calculateApproversMock = calculateApprovers as jest.Mock; -const completeTaskMock = Task.completeTask as jest.Mock; +const calculateApproversMock = jest.mocked(calculateApprovers); +const completeTaskMock = jest.mocked(Task.completeTask); OnyxUpdateManager(); @@ -67,8 +68,8 @@ describe('actions/Workflow', () => { let mockFetch: MockFetch; beforeEach(() => { - global.fetch = getGlobalFetchMock(); - mockFetch = fetch as MockFetch; + mockFetch = createGlobalFetchMock(); + global.fetch = mockFetch; calculateApproversMock.mockClear(); calculateApproversMock.mockImplementation(() => []); completeTaskMock.mockClear(); @@ -156,7 +157,7 @@ describe('actions/Workflow', () => { it('should clear pendingFields when the API is success', async () => { mockFetch.pause(); - const policy = { + const policy = createMock({ id: '123456789', name: "Mkzie2+bnmsn@gmail.com's Workspace", role: 'admin', @@ -188,7 +189,7 @@ describe('actions/Workflow', () => { forwardsTo: '', }, }, - } as unknown as Policy; + }); const approvalWorkflow = { members: [ @@ -253,7 +254,7 @@ describe('actions/Workflow', () => { it('should auto-complete the addExpenseApprovals task when creating an approval workflow', async () => { mockFetch.pause(); - const policy = { + const policy = createMock({ id: '123456789', name: 'Test Workspace', role: 'admin', @@ -273,7 +274,7 @@ describe('actions/Workflow', () => { submitsTo: ownerEmail, }, }, - } as unknown as Policy; + }); const addExpenseApprovalsTaskReport: Report = { reportID: '999', @@ -317,7 +318,7 @@ describe('actions/Workflow', () => { it('should not auto-complete the task if it is already approved', async () => { mockFetch.pause(); - const policy = { + const policy = createMock({ id: '123456789', name: 'Test Workspace', role: 'admin', @@ -337,7 +338,7 @@ describe('actions/Workflow', () => { submitsTo: ownerEmail, }, }, - } as unknown as Policy; + }); const addExpenseApprovalsTaskReport: Report = { reportID: '999', @@ -381,7 +382,7 @@ describe('actions/Workflow', () => { it('should not auto-complete the task if addExpenseApprovalsTaskReport is undefined', async () => { mockFetch.pause(); - const policy = { + const policy = createMock({ id: '123456789', name: 'Test Workspace', role: 'admin', @@ -401,7 +402,7 @@ describe('actions/Workflow', () => { submitsTo: ownerEmail, }, }, - } as unknown as Policy; + }); const approvalWorkflow = { members: [ @@ -443,7 +444,7 @@ describe('actions/Workflow', () => { // Given a policy with two workflows: // - Default workflow: employee1 submits to owner, owner forwards to employee2 (multi-level) // - Second workflow: employee3 submits to employee1 - const policy = { + const policy = createMock({ id: '123456789', name: 'Test Workspace', role: 'admin', @@ -477,7 +478,7 @@ describe('actions/Workflow', () => { submitsTo: employee1Email, }, }, - } as unknown as Policy; + }); // The second workflow to remove: employee3 submits to employee1 const approvalWorkflow = { @@ -512,7 +513,7 @@ describe('actions/Workflow', () => { // Given a policy with two workflows: // - Default workflow: employee1 submits to owner (single-level, no forwardsTo but has overLimitForwardsTo) // - Second workflow: employee3 submits to employee2 - const policy = { + const policy = createMock({ id: '123456789', name: 'Test Workspace', role: 'admin', @@ -548,7 +549,7 @@ describe('actions/Workflow', () => { submitsTo: employee2Email, }, }, - } as unknown as Policy; + }); const approvalWorkflow = { members: [{email: employee3Email, displayName: employee3Email}], @@ -581,7 +582,7 @@ describe('actions/Workflow', () => { // Given a policy with two workflows: // - Default workflow: employee1 submits to owner (single-level, no forwardsTo) // - Second workflow: employee3 submits to employee2 - const policy = { + const policy = createMock({ id: '123456789', name: 'Test Workspace', role: 'admin', @@ -615,7 +616,7 @@ describe('actions/Workflow', () => { submitsTo: employee2Email, }, }, - } as unknown as Policy; + }); // The second workflow to remove: employee3 submits to employee2 const approvalWorkflow = { @@ -651,7 +652,7 @@ describe('actions/Workflow', () => { // Given a policy with a default workflow that has two approvers: // owner forwards to employee2 (multi-level) - const policy = { + const policy = createMock({ id: '123456789', name: 'Test Workspace', role: 'admin', @@ -679,7 +680,7 @@ describe('actions/Workflow', () => { submitsTo: ownerEmail, }, }, - } as unknown as Policy; + }); // The updated workflow: only one approver (owner), second approver removed const approvalWorkflow = { @@ -715,7 +716,7 @@ describe('actions/Workflow', () => { mockFetch.pause(); // Given a policy where default approver has both forwardsTo and overLimitForwardsTo - const policy = { + const policy = createMock({ id: '123456789', name: 'Test Workspace', role: 'admin', @@ -745,7 +746,7 @@ describe('actions/Workflow', () => { submitsTo: ownerEmail, }, }, - } as unknown as Policy; + }); // Remove employee2 as second approver (forwardsTo will be cleared) // but overLimitForwardsTo to employee1 remains @@ -781,7 +782,7 @@ describe('actions/Workflow', () => { mockFetch.pause(); // Given a policy with a default workflow that has two approvers - const policy = { + const policy = createMock({ id: '123456789', name: 'Test Workspace', role: 'admin', @@ -809,7 +810,7 @@ describe('actions/Workflow', () => { submitsTo: ownerEmail, }, }, - } as unknown as Policy; + }); // The updated workflow: change second approver from employee2 to employee1 const approvalWorkflow = { diff --git a/tests/navigation/getStateFromPathTests.ts b/tests/navigation/getStateFromPathTests.ts index 6609d618bc13..fd0154902754 100644 --- a/tests/navigation/getStateFromPathTests.ts +++ b/tests/navigation/getStateFromPathTests.ts @@ -6,6 +6,8 @@ import type {Route} from '@src/ROUTES'; import {getStateFromPath as RNGetStateFromPath} from '@react-navigation/native'; +import createMock from '../utils/createMock'; + jest.mock('@react-navigation/native', () => ({ getStateFromPath: jest.fn(), })); @@ -25,8 +27,8 @@ jest.mock('@libs/Navigation/linkingConfig/config', () => ({ dynamicTabPatternToTabPaths: new Map(), })); -jest.mock('@src/ROUTES', () => ({ - DYNAMIC_ROUTES: { +function mockDynamicRoutes() { + return { SUFFIX_A: { path: 'suffix-a', entryScreens: ['BaseScreen'], @@ -59,15 +61,34 @@ jest.mock('@src/ROUTES', () => ({ path: 'tag-settings/:orderWeight/:tagName', entryScreens: ['TagsRootScreen'], }, - }, + }; +} + +jest.mock('@src/ROUTES', () => ({ + DYNAMIC_ROUTES: mockDynamicRoutes(), })); jest.mock('@libs/Navigation/helpers/getMatchingNewRoute', () => jest.fn()); jest.mock('@libs/Navigation/helpers/dynamicRoutesUtils/getStateForDynamicRoute', () => jest.fn()); +function getStateFromSyntheticPath(path: string): ReturnType { + // These paths are synthetic fixtures for the mocked route configuration, not entries in the production Route union. + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Deliberately pass synthetic route paths through the production parser. + return getStateFromPath(path as Route); +} + +type DynamicRoutePath = Parameters[0]; +type DynamicRouteKey = Parameters[1]; +type DynamicRouteState = ReturnType; +type SyntheticDynamicRouteKey = keyof ReturnType; + +function isSyntheticDynamicRouteKey(key: string): key is SyntheticDynamicRouteKey { + return Object.hasOwn(mockDynamicRoutes(), key); +} + describe('getStateFromPath', () => { - const mockRNGetStateFromPath = RNGetStateFromPath as jest.Mock; - const mockGetStateForDynamicRoute = getStateForDynamicRoute as jest.Mock; + const mockRNGetStateFromPath = jest.mocked(RNGetStateFromPath); + const mockGetStateForDynamicRoute = jest.mocked(getStateForDynamicRoute); const mockLogWarn = jest.spyOn(Log, 'warn'); const focusedRouteParams = {baseParam: '123'}; @@ -78,27 +99,37 @@ describe('getStateFromPath', () => { const dynamicMultiSegLayerState = {routes: [{name: 'DynamicMultiSegLayerScreen'}]}; const dynamicWildcardState = {routes: [{name: 'DynamicWildcardScreen'}]}; + function getSyntheticDynamicRouteState(dynamicRouteKey: string): DynamicRouteState { + if (!isSyntheticDynamicRouteKey(dynamicRouteKey)) { + throw new Error(`Unexpected production dynamic route key in synthetic mock: ${dynamicRouteKey}`); + } + + switch (dynamicRouteKey) { + case 'SUFFIX_A': + return createMock(dynamicSuffixAState); + case 'SUFFIX_B': + return createMock(dynamicSuffixBState); + case 'MULTI_SEG': + return createMock(dynamicMultiSegState); + case 'MULTI_SEG_LAYER': + return createMock(dynamicMultiSegLayerState); + case 'WILDCARD_SUFFIX': + return createMock(dynamicWildcardState); + case 'SUFFIX_B_UNAUTHORIZED': + case 'AMBIGUOUS_STATIC': + case 'TAG_SETTINGS_PARAM': + return createMock({routes: [{name: 'UnknownDynamic'}]}); + default: + throw new Error('Missing synthetic mock implementation for dynamic route key'); + } + } + beforeEach(() => { jest.clearAllMocks(); mockRNGetStateFromPath.mockReturnValue(baseRouteState); - mockGetStateForDynamicRoute.mockImplementation((_path: string, dynamicRouteKey: string) => { - if (dynamicRouteKey === 'SUFFIX_A') { - return dynamicSuffixAState; - } - if (dynamicRouteKey === 'SUFFIX_B') { - return dynamicSuffixBState; - } - if (dynamicRouteKey === 'MULTI_SEG') { - return dynamicMultiSegState; - } - if (dynamicRouteKey === 'MULTI_SEG_LAYER') { - return dynamicMultiSegLayerState; - } - if (dynamicRouteKey === 'WILDCARD_SUFFIX') { - return dynamicWildcardState; - } - return {routes: [{name: 'UnknownDynamic'}]}; - }); + mockGetStateForDynamicRoute.mockImplementation( + (_path: DynamicRoutePath, dynamicRouteKey: DynamicRouteKey): DynamicRouteState => getSyntheticDynamicRouteState(String(dynamicRouteKey)), + ); }); it('should delegate to RN getStateFromPath for standard routes (non-dynamic)', () => { @@ -106,7 +137,7 @@ describe('getStateFromPath', () => { const expectedState = {routes: [{name: 'BaseProfile'}]}; mockRNGetStateFromPath.mockReturnValue(expectedState); - const result = getStateFromPath(path as unknown as Route); + const result = getStateFromSyntheticPath(path); expect(result).toBe(expectedState); }); @@ -114,7 +145,7 @@ describe('getStateFromPath', () => { it('should generate dynamic state when authorized screen is focused', () => { const fullPath = '/base/suffix-a'; - const result = getStateFromPath(fullPath as unknown as Route); + const result = getStateFromSyntheticPath(fullPath); expect(result).toBe(dynamicSuffixAState); expect(mockGetStateForDynamicRoute).toHaveBeenCalledWith(fullPath, 'SUFFIX_A', focusedRouteParams, undefined); @@ -125,7 +156,7 @@ describe('getStateFromPath', () => { const standardState = {routes: [{name: 'FallbackRoute'}]}; mockRNGetStateFromPath.mockReturnValue(standardState); - const result = getStateFromPath(fullPath as unknown as Route); + const result = getStateFromSyntheticPath(fullPath); expect(result).toBe(standardState); expect(mockLogWarn).toHaveBeenCalledWith(expect.stringContaining('None of the')); @@ -135,7 +166,7 @@ describe('getStateFromPath', () => { it('should authorize a layered suffix when the inner dynamic screen is listed in entryScreens', () => { const fullPath = '/base/suffix-a/suffix-b'; - const result = getStateFromPath(fullPath as unknown as Route); + const result = getStateFromSyntheticPath(fullPath); expect(result).toBe(dynamicSuffixBState); expect(mockGetStateForDynamicRoute).toHaveBeenCalledWith('/base/suffix-a', 'SUFFIX_A', focusedRouteParams, undefined); @@ -152,7 +183,7 @@ describe('getStateFromPath', () => { return baseRouteState; }); - const result = getStateFromPath(fullPath as unknown as Route); + const result = getStateFromSyntheticPath(fullPath); expect(result).toBe(standardState); expect(mockGetStateForDynamicRoute).toHaveBeenCalledWith('/base/suffix-a', 'SUFFIX_A', focusedRouteParams, undefined); @@ -162,7 +193,7 @@ describe('getStateFromPath', () => { it('should pass the full layered path including query params to the outer dynamic route builder', () => { const fullPath = '/base/suffix-a/suffix-b?param=val'; - getStateFromPath(fullPath as unknown as Route); + getStateFromSyntheticPath(fullPath); expect(mockGetStateForDynamicRoute).toHaveBeenCalledWith(fullPath, 'SUFFIX_B', focusedRouteParams, undefined); }); @@ -170,7 +201,7 @@ describe('getStateFromPath', () => { it('should support a multi-segment inner suffix inside the layered path', () => { const fullPath = '/base/deep/suffix-a/suffix-b-from-multi'; - const result = getStateFromPath(fullPath as unknown as Route); + const result = getStateFromSyntheticPath(fullPath); expect(result).toBe(dynamicMultiSegLayerState); expect(mockGetStateForDynamicRoute).toHaveBeenCalledWith('/base/deep/suffix-a', 'MULTI_SEG', focusedRouteParams, undefined); @@ -182,7 +213,7 @@ describe('getStateFromPath', () => { it('should authorize any focused screen when entryScreens contains wildcard', () => { const fullPath = '/base/wildcard-suffix'; - const result = getStateFromPath(fullPath as unknown as Route); + const result = getStateFromSyntheticPath(fullPath); expect(result).toBe(dynamicWildcardState); expect(mockGetStateForDynamicRoute).toHaveBeenCalledWith(fullPath, 'WILDCARD_SUFFIX', focusedRouteParams, undefined); @@ -192,7 +223,7 @@ describe('getStateFromPath', () => { it('should authorize wildcard in a layered scenario where the inner screen is not explicitly listed', () => { const fullPath = '/base/suffix-a/wildcard-suffix'; - const result = getStateFromPath(fullPath as unknown as Route); + const result = getStateFromSyntheticPath(fullPath); expect(result).toBe(dynamicWildcardState); expect(mockGetStateForDynamicRoute).toHaveBeenCalledWith('/base/suffix-a', 'SUFFIX_A', focusedRouteParams, undefined); @@ -210,15 +241,20 @@ describe('getStateFromPath', () => { const ambiguousStaticState = {routes: [{name: 'AmbiguousStaticScreen'}]}; beforeEach(() => { - const baseImpl = mockGetStateForDynamicRoute.getMockImplementation(); - mockGetStateForDynamicRoute.mockImplementation((path: string, dynamicRouteKey: string) => { - if (dynamicRouteKey === 'AMBIGUOUS_STATIC') { - return ambiguousStaticState; + mockGetStateForDynamicRoute.mockImplementation((_path: DynamicRoutePath, dynamicRouteKey: DynamicRouteKey): DynamicRouteState => { + const syntheticDynamicRouteKey = String(dynamicRouteKey); + if (!isSyntheticDynamicRouteKey(syntheticDynamicRouteKey)) { + throw new Error(`Unexpected production dynamic route key in ambiguous synthetic mock: ${syntheticDynamicRouteKey}`); } - if (dynamicRouteKey === 'TAG_SETTINGS_PARAM') { - return tagSettingsParamState; + + switch (syntheticDynamicRouteKey) { + case 'AMBIGUOUS_STATIC': + return createMock(ambiguousStaticState); + case 'TAG_SETTINGS_PARAM': + return createMock(tagSettingsParamState); + default: + throw new Error(`Missing ambiguous synthetic mock implementation for dynamic route key: ${syntheticDynamicRouteKey}`); } - return baseImpl?.(path, dynamicRouteKey) as unknown; }); }); @@ -233,7 +269,7 @@ describe('getStateFromPath', () => { return unknownState; }); - const result = getStateFromPath(fullPath as unknown as Route); + const result = getStateFromSyntheticPath(fullPath); expect(result).toBe(fallbackState); expect(mockLogWarn).toHaveBeenCalledWith(expect.stringContaining('None of the')); @@ -244,7 +280,7 @@ describe('getStateFromPath', () => { const fullPath = '/tags-root/tag-settings/0/gl-code'; mockRNGetStateFromPath.mockReturnValue(tagsRootState); - const result = getStateFromPath(fullPath as unknown as Route); + const result = getStateFromSyntheticPath(fullPath); expect(result).toBe(tagSettingsParamState); expect(mockGetStateForDynamicRoute).toHaveBeenCalledWith(fullPath, 'TAG_SETTINGS_PARAM', expect.objectContaining({orderWeight: '0', tagName: 'gl-code'}), undefined); @@ -255,7 +291,7 @@ describe('getStateFromPath', () => { const fullPath = '/category-settings/gl-code'; mockRNGetStateFromPath.mockReturnValue(categorySettingsState); - const result = getStateFromPath(fullPath as unknown as Route); + const result = getStateFromSyntheticPath(fullPath); expect(result).toBe(ambiguousStaticState); expect(mockGetStateForDynamicRoute).toHaveBeenCalledWith(fullPath, 'AMBIGUOUS_STATIC', expect.objectContaining(categorySettingsParams), undefined); diff --git a/tests/perf-test/useFilterFormValues.perf-test.tsx b/tests/perf-test/useFilterFormValues.perf-test.tsx index 529e2fb3de2e..74e5f572338d 100644 --- a/tests/perf-test/useFilterFormValues.perf-test.tsx +++ b/tests/perf-test/useFilterFormValues.perf-test.tsx @@ -19,6 +19,7 @@ import {measureFunction, measureRenders} from 'reassure'; import createCollection from '../utils/collections/createCollection'; import createRandomPolicy from '../utils/collections/policies'; +import createMock from '../utils/createMock'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; @@ -69,12 +70,14 @@ describe('useFilterFormValues', () => { const categories = createCollection( (_, index) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${index}`, () => - Object.fromEntries( - Array.from({length: 40}, (_unused, i) => [ - `Category${i}`, - {name: `Category${i}`, enabled: true, unencodedName: `Category${i}`, areCommentsRequired: false, externalID: `${i}`, origin: 'abc'}, - ]), - ) as unknown as PolicyCategories, + createMock( + Object.fromEntries( + Array.from({length: 40}, (_unused, i) => [ + `Category${i}`, + {name: `Category${i}`, enabled: true, unencodedName: `Category${i}`, areCommentsRequired: false, externalID: `${i}`, origin: 'abc'}, + ]), + ), + ), CATEGORY_COUNT, ); @@ -85,13 +88,13 @@ describe('useFilterFormValues', () => { const tags = createCollection( (_, index) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${index}`, () => - ({ + createMock({ Department: { name: 'Department', required: true, tags: Object.fromEntries(Array.from({length: 30}, (_unused, i) => [`Tag${i}`, {name: `Tag${i}`, enabled: true}])), }, - }) as unknown as PolicyTagLists, + }), TAG_COUNT, ); @@ -102,18 +105,18 @@ describe('useFilterFormValues', () => { const policies = createCollection( (_, index) => `${ONYXKEYS.COLLECTION.POLICY}${index}`, (index) => - ({ + createMock({ ...createRandomPolicy(index), connections: { quickbooksOnline: { config: {realmId: `realm${index}`, companyName: `Company ${index}`}, - lastSync: {isConnected: true, lastSyncDate: new Date().toISOString()}, + lastSync: {isConnected: true}, }, }, exportLayouts: Object.fromEntries(Array.from({length: 5}, (_unused, i) => [`template${i}`, {name: `Template ${i}`}])), employeeList: Object.fromEntries(Array.from({length: 100}, (_unused, i) => [`user${i}@test.com`, {email: `user${i}@test.com`, role: 'user'}])), taxRates: {name: 'Tax', defaultExternalID: '', defaultValue: '10%', foreignTaxDefault: '', taxes: {}}, - }) as unknown as Policy, + }), POLICY_COUNT, ); @@ -124,7 +127,7 @@ describe('useFilterFormValues', () => { const policies = createCollection( (_, index) => `${ONYXKEYS.COLLECTION.POLICY}${index}`, (index) => - ({ + createMock({ ...createRandomPolicy(index), connections: { quickbooksOnline: { @@ -135,7 +138,7 @@ describe('useFilterFormValues', () => { taxRates: {name: 'Tax', defaultExternalID: '', defaultValue: '10%', foreignTaxDefault: '', taxes: {}}, fieldList: Object.fromEntries(Array.from({length: 10}, (_unused, i) => [`field${i}`, {name: `Field ${i}`, type: 'text'}])), employeeList: Object.fromEntries(Array.from({length: 100}, (_unused, i) => [`user${i}@test.com`, {email: `user${i}@test.com`, role: 'user'}])), - }) as unknown as Policy, + }), POLICY_COUNT, ); @@ -146,12 +149,12 @@ describe('useFilterFormValues', () => { const policies = createCollection( (_, index) => `${ONYXKEYS.COLLECTION.POLICY}${index}`, (index) => - ({ + createMock({ ...createRandomPolicy(index), connections: { quickbooksOnline: { config: {realmId: `realm${index}`, companyName: `Company ${index}`}, - lastSync: {isConnected: true, lastSyncDate: new Date().toISOString()}, + lastSync: {isConnected: true}, }, }, exportLayouts: Object.fromEntries(Array.from({length: 5}, (_unused, i) => [`template${i}`, {name: `Template ${i}`}])), @@ -159,7 +162,7 @@ describe('useFilterFormValues', () => { employeeList: Object.fromEntries(Array.from({length: 100}, (_unused, i) => [`user${i}@test.com`, {email: `user${i}@test.com`, role: 'user'}])), taxRates: {name: 'Tax', defaultExternalID: '', defaultValue: '10%', foreignTaxDefault: '', taxes: {}}, fieldList: Object.fromEntries(Array.from({length: 10}, (_unused, i) => [`field${i}`, {name: `Field ${i}`, type: 'text'}])), - }) as unknown as Policy, + }), POLICY_COUNT, ); @@ -187,24 +190,24 @@ describe('useFilterFormValues', () => { ); const reports = createCollection( (_, index) => `${ONYXKEYS.COLLECTION.REPORT}${index}`, - (index) => ({reportID: `${index}`, reportName: `Report ${index}`}) as unknown as Report, + (index) => createMock({reportID: `${index}`, reportName: `Report ${index}`}), REPORT_COUNT, ); const categories = createCollection( (_, index) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${index}`, - () => Object.fromEntries(Array.from({length: 40}, (_unused, i) => [`Category${i}`, {name: `Category${i}`, enabled: true}])) as unknown as PolicyCategories, + () => createMock(Object.fromEntries(Array.from({length: 40}, (_unused, i) => [`Category${i}`, {name: `Category${i}`, enabled: true}]))), CATEGORY_COUNT, ); const tags = createCollection( (_, index) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${index}`, () => - ({ + createMock({ Department: { name: 'Department', required: true, tags: Object.fromEntries(Array.from({length: 30}, (_unused, i) => [`Tag${i}`, {name: `Tag${i}`, enabled: true}])), }, - }) as unknown as PolicyTagLists, + }), TAG_COUNT, ); const taxRates = getAllTaxRates(policies); @@ -237,24 +240,24 @@ describe('useFilterFormValues', () => { ); const categories = createCollection( (_, index) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${index}`, - () => Object.fromEntries(Array.from({length: 40}, (_unused, i) => [`Category${i}`, {name: `Category${i}`, enabled: true}])) as unknown as PolicyCategories, + () => createMock(Object.fromEntries(Array.from({length: 40}, (_unused, i) => [`Category${i}`, {name: `Category${i}`, enabled: true}]))), CATEGORY_COUNT, ); const tags = createCollection( (_, index) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${index}`, () => - ({ + createMock({ Department: { name: 'Department', required: true, tags: Object.fromEntries(Array.from({length: 30}, (_unused, i) => [`Tag${i}`, {name: `Tag${i}`, enabled: true}])), }, - }) as unknown as PolicyTagLists, + }), TAG_COUNT, ); const reports = createCollection( (_, index) => `${ONYXKEYS.COLLECTION.REPORT}${index}`, - (index) => ({reportID: `${index}`, reportName: `Report ${index}`}) as unknown as Report, + (index) => createMock({reportID: `${index}`, reportName: `Report ${index}`}), REPORT_COUNT, ); await Onyx.mergeCollection(ONYXKEYS.COLLECTION.POLICY, policies); @@ -296,24 +299,24 @@ describe('useFilterFormValues', () => { ); const categories = createCollection( (_, index) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${index}`, - () => Object.fromEntries(Array.from({length: 40}, (_unused, i) => [`Category${i}`, {name: `Category${i}`, enabled: true}])) as unknown as PolicyCategories, + () => createMock(Object.fromEntries(Array.from({length: 40}, (_unused, i) => [`Category${i}`, {name: `Category${i}`, enabled: true}]))), CATEGORY_COUNT, ); const tags = createCollection( (_, index) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${index}`, () => - ({ + createMock({ Department: { name: 'Department', required: true, tags: Object.fromEntries(Array.from({length: 30}, (_unused, i) => [`Tag${i}`, {name: `Tag${i}`, enabled: true}])), }, - }) as unknown as PolicyTagLists, + }), TAG_COUNT, ); const reports = createCollection( (_, index) => `${ONYXKEYS.COLLECTION.REPORT}${index}`, - (index) => ({reportID: `${index}`, reportName: `Report ${index}`}) as unknown as Report, + (index) => createMock({reportID: `${index}`, reportName: `Report ${index}`}), REPORT_COUNT, ); await Onyx.mergeCollection(ONYXKEYS.COLLECTION.POLICY, policies); diff --git a/tests/unit/FileUtilsTest.ts b/tests/unit/FileUtilsTest.ts index 365c7e1f970e..75d8abc1bbdc 100644 --- a/tests/unit/FileUtilsTest.ts +++ b/tests/unit/FileUtilsTest.ts @@ -17,6 +17,8 @@ import CONST from '@src/CONST'; import {Platform} from 'react-native'; import ImageSize from 'react-native-image-size'; +import createMock from '../utils/createMock'; + jest.useFakeTimers(); jest.mock('react-native-image-size'); @@ -118,54 +120,50 @@ describe('FileUtils', () => { }); describe('canvasFallback', () => { - const mockCreateImageBitmap = jest.fn(); - const mockCanvas = { + const mockCreateImageBitmap = jest.fn, Parameters>(); + const mockCanvas = createMock({ width: 0, height: 0, - getContext: jest.fn(), - toBlob: jest.fn(), - }; - const mockCtx = { + getContext: () => null, + toBlob: () => undefined, + }); + const mockGetContext = jest.spyOn(mockCanvas, 'getContext'); + const mockToBlob = jest.spyOn(mockCanvas, 'toBlob'); + const mockCtx = createMock({ drawImage: jest.fn(), - }; - const mockCreateElement = jest.fn(); + }); + const mockCreateElement = jest.fn, Parameters>(); + const mockDocument = {createElement: mockCreateElement} satisfies Pick; const mockURL = { - createObjectURL: jest.fn(() => 'blob:mock-url'), - }; + createObjectURL: jest.fn, Parameters>(() => 'blob:mock-url'), + } satisfies Pick; beforeEach(() => { jest.clearAllMocks(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - (global as any).createImageBitmap = mockCreateImageBitmap; - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - (global as any).document = { - createElement: mockCreateElement, - }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - (global as any).URL = mockURL; + Object.defineProperty(globalThis, 'createImageBitmap', {configurable: true, enumerable: true, value: mockCreateImageBitmap, writable: true}); + Object.defineProperty(globalThis, 'document', {configurable: true, enumerable: true, value: mockDocument, writable: true}); + Object.defineProperty(globalThis, 'URL', {configurable: true, enumerable: true, value: mockURL, writable: true}); mockCreateElement.mockReturnValue(mockCanvas); - mockCanvas.getContext.mockReturnValue(mockCtx); - mockCreateImageBitmap.mockResolvedValue({ - width: 1000, - height: 800, - close: jest.fn(), - }); + mockGetContext.mockReturnValue(mockCtx); + mockCreateImageBitmap.mockResolvedValue( + createMock>>({ + width: 1000, + height: 800, + close: jest.fn(), + }), + ); }); afterEach(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - delete (global as any).createImageBitmap; - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - delete (global as any).document; - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - delete (global as any).URL; + Reflect.deleteProperty(globalThis, 'createImageBitmap'); + Reflect.deleteProperty(globalThis, 'document'); + Reflect.deleteProperty(globalThis, 'URL'); }); it('should reject when createImageBitmap is undefined', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - delete (global as any).createImageBitmap; + Reflect.deleteProperty(globalThis, 'createImageBitmap'); const blob = new Blob(['test'], {type: 'image/heic'}); @@ -175,24 +173,23 @@ describe('FileUtils', () => { it('should successfully convert HEIC to JPEG', async () => { const blob = new Blob(['test'], {type: 'image/heic'}); const mockBlob = new Blob(['converted'], {type: 'image/jpeg'}); - mockCanvas.toBlob.mockImplementation((callback: (blob: Blob | null) => void) => callback(mockBlob)); + mockToBlob.mockImplementation((callback: (blob: Blob | null) => void) => callback(mockBlob)); const result = await canvasFallback(blob, 'expense.heic'); expect(result).toBeInstanceOf(File); expect(result.type).toBe(CONST.IMAGE_FILE_FORMAT.JPEG); expect(result.name).toBe('expense.jpg'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - expect((result as any).uri).toBe('blob:mock-url'); + expect(result).toHaveProperty('uri', 'blob:mock-url'); }); it('should scale down large images', async () => { const blob = new Blob(['test'], {type: 'image/heic'}); - const mockImageBitmap = {width: 8192, height: 4000, close: jest.fn()}; + const mockImageBitmap = createMock>>({width: 8192, height: 4000, close: jest.fn()}); mockCreateImageBitmap.mockResolvedValue(mockImageBitmap); const mockBlob = new Blob(['converted'], {type: 'image/jpeg'}); - mockCanvas.toBlob.mockImplementation((callback: (blob: Blob | null) => void) => callback(mockBlob)); + mockToBlob.mockImplementation((callback: (blob: Blob | null) => void) => callback(mockBlob)); await canvasFallback(blob, 'test.heic'); @@ -202,14 +199,14 @@ describe('FileUtils', () => { it('should reject when canvas context is null', async () => { const blob = new Blob(['test'], {type: 'image/heic'}); - mockCanvas.getContext.mockReturnValue(null); + mockGetContext.mockReturnValue(null); await expect(canvasFallback(blob, 'test.heic')).rejects.toThrow('Could not get canvas context'); }); it('should reject when toBlob returns null', async () => { const blob = new Blob(['test'], {type: 'image/heic'}); - mockCanvas.toBlob.mockImplementation((callback: (blob: Blob | null) => void) => callback(null)); + mockToBlob.mockImplementation((callback: (blob: Blob | null) => void) => callback(null)); await expect(canvasFallback(blob, 'test.heic')).rejects.toThrow('Canvas conversion failed - returned null blob'); }); @@ -222,7 +219,7 @@ describe('FileUtils', () => { describe('with file:// URLs (native)', () => { it('should return scaled dimensions for normal-sized images', async () => { - (ImageSize.getSize as jest.Mock).mockResolvedValue({width: 4000, height: 3000}); + jest.mocked(ImageSize.getSize).mockResolvedValue({width: 4000, height: 3000}); const file = {uri: 'file://test.jpg', name: 'test.jpg', type: 'image/jpeg'}; const result = await getImageDimensionsAfterResize(file); @@ -233,7 +230,7 @@ describe('FileUtils', () => { it('should throw IMAGE_DIMENSIONS_TOO_LARGE error when image exceeds maximum pixel count', async () => { // 10000 x 6000 = 60 million pixels, which exceeds MAX_IMAGE_PIXEL_COUNT (50 million) - (ImageSize.getSize as jest.Mock).mockResolvedValue({width: 10000, height: 6000}); + jest.mocked(ImageSize.getSize).mockResolvedValue({width: 10000, height: 6000}); const file = {uri: 'file://large-image.jpg', name: 'large-image.jpg', type: 'image/jpeg'}; @@ -242,7 +239,7 @@ describe('FileUtils', () => { it('should not throw for images at exactly the maximum pixel count', async () => { // Exactly 50 million pixels (e.g., 10000 x 5000) - (ImageSize.getSize as jest.Mock).mockResolvedValue({width: 10000, height: 5000}); + jest.mocked(ImageSize.getSize).mockResolvedValue({width: 10000, height: 5000}); const file = {uri: 'file://max-size.jpg', name: 'max-size.jpg', type: 'image/jpeg'}; @@ -409,7 +406,7 @@ describe('FileUtils', () => { // Create an invalid/unrecognized blob (not JPEG or PNG) const invalidBlob = new Blob([new Uint8Array([0x00, 0x00, 0x00, 0x00])], {type: 'image/webp'}); mockFetchWithBlob(invalidBlob); - (ImageSize.getSize as jest.Mock).mockResolvedValue({width: 800, height: 600}); + jest.mocked(ImageSize.getSize).mockResolvedValue({width: 800, height: 600}); const file = {uri: 'blob:http://localhost/unknown-format', name: 'test.webp', type: 'image/webp'}; const result = await getImageDimensionsAfterResize(file); @@ -499,7 +496,7 @@ describe('FileUtils', () => { }); describe('getFileValidationErrorText', () => { - const mockTranslate = ((path: string) => path) as LocaleContextProps['translate']; + const mockTranslate: LocaleContextProps['translate'] = (...parameters) => parameters[0]; it('should return correct error text for IMAGE_DIMENSIONS_TOO_LARGE', () => { const result = getFileValidationErrorText(mockTranslate, {error: CONST.FILE_VALIDATION_ERRORS.IMAGE_DIMENSIONS_TOO_LARGE}); diff --git a/tests/unit/PolicyUtilsTest.ts b/tests/unit/PolicyUtilsTest.ts index 4c2c893b01e4..aafa7fac0d5c 100644 --- a/tests/unit/PolicyUtilsTest.ts +++ b/tests/unit/PolicyUtilsTest.ts @@ -613,7 +613,9 @@ describe('PolicyUtils', () => { }); describe('getRateDisplayValue', () => { it('should return an empty string for NaN', () => { - const rate = getRateDisplayValue('invalid' as unknown as number, toLocaleDigitMock); + // This deliberately malformed string exercises the runtime NaN guard while preserving the production return type. + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- The invalid string is the scenario under test. + const rate = getRateDisplayValue('invalid' as unknown as Parameters[0], toLocaleDigitMock); expect(rate).toEqual(''); }); @@ -703,7 +705,9 @@ describe('PolicyUtils', () => { describe('getUnitRateValue', () => { it('should return an empty string for NaN', () => { - const rate = getUnitRateValue(toLocaleDigitMock, {rate: 'invalid' as unknown as number}); + // This deliberately malformed string exercises the runtime NaN guard while preserving the production return type. + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- The invalid rate is the scenario under test. + const rate = getUnitRateValue(toLocaleDigitMock, {rate: 'invalid' as unknown as NonNullable[1]>['rate']}); expect(rate).toEqual(''); }); @@ -815,10 +819,12 @@ describe('PolicyUtils', () => { category: '', reportID: expenseReport.reportID, }; - await Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`]: transaction2, - } as unknown as OnyxMultiSetInput); + await Onyx.multiSet( + createMock({ + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`]: transaction2, + }), + ); expect(getSubmitToAccountID(policy, expenseReport, employeeEmail)).toBe(categoryApprover1AccountID); }); it('should return default approver if rule approver is submitter and prevent self approval is enabled', async () => { @@ -874,10 +880,12 @@ describe('PolicyUtils', () => { created: DateUtils.subtractMillisecondsFromDateTime(testDate, 1), reportID: expenseReport.reportID, }; - await Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`]: transaction2, - } as unknown as OnyxMultiSetInput); + await Onyx.multiSet( + createMock({ + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`]: transaction2, + }), + ); expect(getSubmitToAccountID(policy, expenseReport, employeeEmail)).toBe(categoryApprover2AccountID); }); it('should return the first rule approver who is not the current submitter', async () => { @@ -919,11 +927,13 @@ describe('PolicyUtils', () => { created: testDate, }; - await Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`]: transaction2, - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction3.transactionID}`]: transaction3, - } as unknown as OnyxMultiSetInput); + await Onyx.multiSet( + createMock({ + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`]: transaction2, + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction3.transactionID}`]: transaction3, + }), + ); expect(getSubmitToAccountID(policy, expenseReport, categoryApprover1Email)).toBe(tagApprover1AccountID); }); @@ -957,10 +967,12 @@ describe('PolicyUtils', () => { created: DateUtils.subtractMillisecondsFromDateTime(testDate, 1), reportID: expenseReport.reportID, }; - await Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`]: transaction2, - } as unknown as OnyxMultiSetInput); + await Onyx.multiSet( + createMock({ + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`]: transaction2, + }), + ); expect(getSubmitToAccountID(policy, expenseReport, employeeEmail)).toBe(tagApprover1AccountID); }); it('should return the tag approver of the first transaction sorted by created if we have many transaction tags match with the tag approver rule', async () => { @@ -992,10 +1004,12 @@ describe('PolicyUtils', () => { created: DateUtils.subtractMillisecondsFromDateTime(testDate, 1), reportID: expenseReport.reportID, }; - await Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`]: transaction2, - } as unknown as OnyxMultiSetInput); + await Onyx.multiSet( + createMock({ + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`]: transaction2, + }), + ); expect(getSubmitToAccountID(policy, expenseReport, employeeEmail)).toBe(tagApprover2AccountID); }); }); @@ -1012,24 +1026,25 @@ describe('PolicyUtils', () => { ...createRandomPolicy(1, CONST.POLICY.TYPE.CORPORATE), role: '', }; - const result = shouldShowPolicy(policy as OnyxEntry, false, CARLOS_EMAIL); + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- The empty role deliberately models malformed persisted policy data. + const result = shouldShowPolicy(policy as unknown as Parameters[0], false, CARLOS_EMAIL); // The result should be false since it is an archived paid policy. expect(result).toBe(false); }); it('should return true', () => { // Given a paid policy. - const policy = {...createRandomPolicy(1, CONST.POLICY.TYPE.CORPORATE), pendingAction: null}; - const result = shouldShowPolicy(policy as OnyxEntry, false, CARLOS_EMAIL); + const policy = createMock>({...createRandomPolicy(1, CONST.POLICY.TYPE.CORPORATE), pendingAction: null}); + const result = shouldShowPolicy(policy, false, CARLOS_EMAIL); // The result should be true, since it is an active paid policy. expect(result).toBe(true); }); it('should return false', () => { // Given a control workspace which is pending delete. - const policy = { + const policy = createMock>({ ...createRandomPolicy(1, CONST.POLICY.TYPE.CORPORATE), pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - }; - const result = shouldShowPolicy(policy as OnyxEntry, false, CARLOS_EMAIL); + }); + const result = shouldShowPolicy(policy, false, CARLOS_EMAIL); // The result should be false since it is a policy which is pending deletion. expect(result).toEqual(false); }); @@ -1407,35 +1422,35 @@ describe('PolicyUtils', () => { describe('isPolicyMemberWithoutPendingDelete', () => { it('should return true if the policy member is not pending delete', () => { - const policy = { + const policy = createMock({ id: '1', employeeList: { [employeeEmail]: {email: employeeEmail, role: CONST.POLICY.ROLE.USER}, }, - }; - const result = isPolicyMemberWithoutPendingDelete(employeeEmail, policy as unknown as Policy); + }); + const result = isPolicyMemberWithoutPendingDelete(employeeEmail, policy); expect(result).toBe(true); }); it('should return false if the policy member is pending delete', () => { - const policy = { + const policy = createMock({ id: '1', employeeList: { [employeeEmail]: {email: employeeEmail, role: CONST.POLICY.ROLE.USER, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}, }, - }; - const result = isPolicyMemberWithoutPendingDelete(employeeEmail, policy as unknown as Policy); + }); + const result = isPolicyMemberWithoutPendingDelete(employeeEmail, policy); expect(result).toBe(false); }); it('should return false if the policy member is not found', () => { - const policy = { + const policy = createMock({ id: '1', employeeList: { [employeeEmail]: {email: employeeEmail, role: CONST.POLICY.ROLE.USER}, }, - }; - const result = isPolicyMemberWithoutPendingDelete('fakeEmail', policy as unknown as Policy); + }); + const result = isPolicyMemberWithoutPendingDelete('fakeEmail', policy); expect(result).toBe(false); }); }); @@ -3211,7 +3226,12 @@ describe('PolicyUtils', () => { it('handles undefined and empty inputs gracefully', () => { expect(getExpensifyTeamExclusions(undefined, undefined, 'customer@acme.com')).toEqual({}); - expect(getExpensifyTeamExclusions(null as unknown as OnyxEntry, null as unknown as OnyxCollection, 'customer@acme.com')).toEqual({}); + // The null values deliberately exercise the runtime guard for malformed Onyx data. + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Null is the malformed-input scenario under test. + const nullPersonalDetails = null as unknown as Parameters[0]; + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Null is the malformed-input scenario under test. + const nullPolicies = null as unknown as Parameters[1]; + expect(getExpensifyTeamExclusions(nullPersonalDetails, nullPolicies, 'customer@acme.com')).toEqual({}); expect(getExpensifyTeamExclusions({}, {}, 'customer@acme.com')).toEqual({}); }); diff --git a/tests/unit/SearchEditMultipleUtilsTest.ts b/tests/unit/SearchEditMultipleUtilsTest.ts index 4ba46fb62d4e..e782f2d4aa79 100644 --- a/tests/unit/SearchEditMultipleUtilsTest.ts +++ b/tests/unit/SearchEditMultipleUtilsTest.ts @@ -11,6 +11,7 @@ import { withSnapshotReports, withSnapshotTransactions, } from '../../src/pages/Search/SearchEditMultiple/SearchEditMultipleUtils'; +import createMock from '../utils/createMock'; const POLICY_A = 'policyA'; const POLICY_B = 'policyB'; @@ -19,21 +20,30 @@ const REPORT_ID = 'report1'; const TRANSACTION_ID_1 = 'tx1'; const TRANSACTION_ID_2 = 'tx2'; const TRANSACTION_ID_3 = 'tx3'; +const TRANSACTION_DATA_KEY_1: `${typeof ONYXKEYS.COLLECTION.TRANSACTION}${string}` = `${ONYXKEYS.COLLECTION.TRANSACTION}${TRANSACTION_ID_1}`; +const REPORT_DATA_KEY: `${typeof ONYXKEYS.COLLECTION.REPORT}${string}` = `${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`; function makeTransaction(transactionID: string, reportID: string): Transaction { - return {transactionID, reportID, amount: 100, currency: 'USD', created: '2025-01-01', comment: {}} as Transaction; + return createMock({ + transactionID, + reportID, + amount: 100, + currency: 'USD', + created: '2025-01-01', + comment: {}, + }); } function makeReport(reportID: string, policyID: string): Report { - return {reportID, policyID, type: 'expense'} as unknown as Report; + return createMock({reportID, policyID, type: 'expense'}); } describe('SearchEditMultipleUtils', () => { describe('withSnapshotTransactions', () => { it('fills missing transactions from snapshot', () => { - const snapshotData = { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${TRANSACTION_ID_1}`]: makeTransaction(TRANSACTION_ID_1, REPORT_ID), - } as unknown as SearchResults['data']; + const snapshotData = createMock({ + [TRANSACTION_DATA_KEY_1]: makeTransaction(TRANSACTION_ID_1, REPORT_ID), + }); const merged = withSnapshotTransactions(undefined, snapshotData); @@ -44,9 +54,9 @@ describe('SearchEditMultipleUtils', () => { const existing = { [`${ONYXKEYS.COLLECTION.TRANSACTION}${TRANSACTION_ID_1}`]: makeTransaction(TRANSACTION_ID_1, 'existingReport'), }; - const snapshotData = { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${TRANSACTION_ID_1}`]: makeTransaction(TRANSACTION_ID_1, 'snapshotReport'), - } as unknown as SearchResults['data']; + const snapshotData = createMock({ + [TRANSACTION_DATA_KEY_1]: makeTransaction(TRANSACTION_ID_1, 'snapshotReport'), + }); const merged = withSnapshotTransactions(existing, snapshotData); @@ -66,9 +76,9 @@ describe('SearchEditMultipleUtils', () => { describe('withSnapshotReports', () => { it('fills missing reports from snapshot', () => { - const snapshotData = { - [`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`]: makeReport(REPORT_ID, POLICY_A), - } as unknown as SearchResults['data']; + const snapshotData = createMock({ + [REPORT_DATA_KEY]: makeReport(REPORT_ID, POLICY_A), + }); const merged = withSnapshotReports(undefined, snapshotData); @@ -79,9 +89,9 @@ describe('SearchEditMultipleUtils', () => { const existing = { [`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`]: makeReport(REPORT_ID, POLICY_A), }; - const snapshotData = { - [`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`]: makeReport(REPORT_ID, POLICY_B), - } as unknown as SearchResults['data']; + const snapshotData = createMock({ + [REPORT_DATA_KEY]: makeReport(REPORT_ID, POLICY_B), + }); const merged = withSnapshotReports(existing, snapshotData); @@ -95,9 +105,9 @@ describe('SearchEditMultipleUtils', () => { const allTransactions = { [`${ONYXKEYS.COLLECTION.TRANSACTION}${TRANSACTION_ID_1}`]: makeTransaction(TRANSACTION_ID_1, REPORT_ID), }; - const snapshotData = { - [`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`]: makeReport(REPORT_ID, POLICY_A), - } as unknown as SearchResults['data']; + const snapshotData = createMock({ + [REPORT_DATA_KEY]: makeReport(REPORT_ID, POLICY_A), + }); // Without snapshot merge — falls back to activePolicyID const withoutMerge = getSearchBulkEditPolicyID([TRANSACTION_ID_1], POLICY_B, allTransactions, undefined); @@ -110,10 +120,10 @@ describe('SearchEditMultipleUtils', () => { }); it('resolves policyID when transaction is only in snapshot', () => { - const snapshotData = { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${TRANSACTION_ID_1}`]: makeTransaction(TRANSACTION_ID_1, REPORT_ID), - [`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`]: makeReport(REPORT_ID, POLICY_A), - } as unknown as SearchResults['data']; + const snapshotData = createMock({ + [TRANSACTION_DATA_KEY_1]: makeTransaction(TRANSACTION_ID_1, REPORT_ID), + [REPORT_DATA_KEY]: makeReport(REPORT_ID, POLICY_A), + }); const mergedTransactions = withSnapshotTransactions(undefined, snapshotData); const mergedReports = withSnapshotReports(undefined, snapshotData); @@ -143,9 +153,21 @@ describe('SearchEditMultipleUtils', () => { }); describe('hasCustomUnitMerchantInSelection', () => { - const manualTransaction = {transactionID: TRANSACTION_ID_1, reportID: 'report1', comment: {}} as Transaction; - const perDiemTransaction = {transactionID: TRANSACTION_ID_2, reportID: CONST.REPORT.UNREPORTED_REPORT_ID, iouRequestType: CONST.IOU.REQUEST_TYPE.PER_DIEM} as unknown as Transaction; - const distanceTransaction = {transactionID: TRANSACTION_ID_3, reportID: CONST.REPORT.UNREPORTED_REPORT_ID, iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE} as unknown as Transaction; + const manualTransaction = createMock({ + transactionID: TRANSACTION_ID_1, + reportID: 'report1', + comment: {}, + }); + const perDiemTransaction = createMock({ + transactionID: TRANSACTION_ID_2, + reportID: CONST.REPORT.UNREPORTED_REPORT_ID, + iouRequestType: CONST.IOU.REQUEST_TYPE.PER_DIEM, + }); + const distanceTransaction = createMock({ + transactionID: TRANSACTION_ID_3, + reportID: CONST.REPORT.UNREPORTED_REPORT_ID, + iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE, + }); it('returns true when any transaction is an unreported per-diem request', () => { const contexts = [{transaction: manualTransaction}, {transaction: perDiemTransaction}]; @@ -168,9 +190,9 @@ describe('SearchEditMultipleUtils', () => { }); describe('areAllTransactionsExpenseCompatible', () => { - const expenseReport = {reportID: 'expenseReport1', type: CONST.REPORT.TYPE.EXPENSE} as Report; - const iouReport = {reportID: 'iouReport1', type: CONST.REPORT.TYPE.IOU} as Report; - const invoiceReport = {reportID: 'invoiceReport1', type: CONST.REPORT.TYPE.INVOICE} as Report; + const expenseReport = createMock({reportID: 'expenseReport1', type: CONST.REPORT.TYPE.EXPENSE}); + const iouReport = createMock({reportID: 'iouReport1', type: CONST.REPORT.TYPE.IOU}); + const invoiceReport = createMock({reportID: 'invoiceReport1', type: CONST.REPORT.TYPE.INVOICE}); it('returns true when every reported transaction is on an expense report', () => { const contexts = [ @@ -212,9 +234,9 @@ describe('SearchEditMultipleUtils', () => { }); describe('isBulkEditTaxTrackingEnabled', () => { - const taxEnabledPolicy = {id: POLICY_A, tax: {trackingEnabled: true}} as unknown as Policy; - const taxDisabledPolicy = {id: POLICY_B, tax: {trackingEnabled: false}} as unknown as Policy; - const activeTaxEnabledPolicy = {id: POLICY_C, tax: {trackingEnabled: true}} as unknown as Policy; + const taxEnabledPolicy = createMock({id: POLICY_A, tax: {trackingEnabled: true}}); + const taxDisabledPolicy = createMock({id: POLICY_B, tax: {trackingEnabled: false}}); + const activeTaxEnabledPolicy = createMock({id: POLICY_C, tax: {trackingEnabled: true}}); it('returns true when all transactions are unreported and the bulk-edit workspace has tax enabled', () => { const contexts = [ diff --git a/tests/unit/hooks/useAutoCreateSubmitWorkspace.test.ts b/tests/unit/hooks/useAutoCreateSubmitWorkspace.test.ts index 86b03581c251..a03d40f6009e 100644 --- a/tests/unit/hooks/useAutoCreateSubmitWorkspace.test.ts +++ b/tests/unit/hooks/useAutoCreateSubmitWorkspace.test.ts @@ -1,5 +1,7 @@ import {renderHook} from '@testing-library/react-native'; +import type {LocalizedTranslate} from '@components/LocaleContextProvider'; + import useAutoCreateSubmitWorkspace from '@hooks/useAutoCreateSubmitWorkspace'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useHasActiveAdminPolicies from '@hooks/useHasActiveAdminPolicies'; @@ -16,6 +18,10 @@ import * as Welcome from '@userActions/Welcome'; import CONST from '@src/CONST'; +import type {UseOnyxResult} from 'react-native-onyx'; + +import createMock from '../../utils/createMock'; + jest.mock('@hooks/useOnyx', () => { return {__esModule: true, default: jest.fn(() => [undefined])}; }); @@ -26,11 +32,10 @@ jest.mock('@hooks/useLocalize'); jest.mock('@hooks/usePreferredPolicy'); jest.mock('@hooks/useOnboardingMessages'); -const mockTranslate = jest.fn((key: string) => key); +const mockTranslate: LocalizedTranslate = (...parameters) => parameters[0]; const mockFormatPhoneNumber = jest.fn((phone: string) => phone); -// Single shared assertion so individual tests can re-mock useOnyx without repeating the unsafe cast -const mockUseOnyx = useOnyx as jest.Mock; +const mockUseOnyx = jest.mocked(useOnyx); const MOCK_SESSION = { accountID: 12345, @@ -39,42 +44,46 @@ const MOCK_SESSION = { const MOCK_POLICY_ID = 'mock-policy-id'; const MOCK_ADMINS_CHAT_REPORT_ID = 'mock-admins-chat-report-id'; -const MOCK_ONBOARDING_MESSAGE = {message: 'Welcome!', video: undefined, tasks: []}; +const MOCK_ONBOARDING_MESSAGE = createMock['onboardingMessages'][typeof CONST.ONBOARDING_CHOICES.EMPLOYER]>({ + message: 'Welcome!', + video: undefined, + tasks: [], +}); function setupDefaultMocks() { mockUseOnyx.mockImplementation((key: string) => { if (key === 'session') { - return [MOCK_SESSION]; + return [MOCK_SESSION, {status: 'loaded'}] satisfies UseOnyxResult; } if (key === 'betas') { - return [[]]; + return [[], {status: 'loaded'}] satisfies UseOnyxResult; } if (key.startsWith('policy_')) { - return [false]; + return [false, {status: 'loaded'}] satisfies UseOnyxResult; } - return [undefined]; + return [undefined, {status: 'loaded'}] satisfies UseOnyxResult; }); - (useCurrentUserPersonalDetails as jest.Mock).mockReturnValue({ + jest.mocked(useCurrentUserPersonalDetails).mockReturnValue({ accountID: MOCK_SESSION.accountID, login: MOCK_SESSION.email, localCurrencyCode: 'USD', }); - (useLocalize as jest.Mock).mockReturnValue({ + jest.mocked(useLocalize).mockReturnValue({ translate: mockTranslate, formatPhoneNumber: mockFormatPhoneNumber, }); - (usePreferredPolicy as jest.Mock).mockReturnValue({ + jest.mocked(usePreferredPolicy).mockReturnValue({ isRestrictedToPreferredPolicy: false, preferredPolicyID: undefined, isRestrictedPolicyCreation: false, }); - (useHasActiveAdminPolicies as jest.Mock).mockReturnValue(false); + jest.mocked(useHasActiveAdminPolicies).mockReturnValue(false); - (useOnboardingMessages as jest.Mock).mockReturnValue({ + jest.mocked(useOnboardingMessages).mockReturnValue({ onboardingMessages: { [CONST.ONBOARDING_CHOICES.EMPLOYER]: MOCK_ONBOARDING_MESSAGE, }, @@ -82,10 +91,12 @@ function setupDefaultMocks() { } describe('useAutoCreateSubmitWorkspace', () => { - const createWorkspaceSpy = jest.spyOn(Policy, 'createWorkspace').mockReturnValue({ - policyID: MOCK_POLICY_ID, - adminsChatReportID: MOCK_ADMINS_CHAT_REPORT_ID, - } as ReturnType); + const createWorkspaceSpy = jest.spyOn(Policy, 'createWorkspace').mockReturnValue( + createMock>({ + policyID: MOCK_POLICY_ID, + adminsChatReportID: MOCK_ADMINS_CHAT_REPORT_ID, + }), + ); const completeOnboardingSpy = jest.spyOn(Report, 'completeOnboarding').mockImplementation(jest.fn()); const setOnboardingAdminsChatReportIDSpy = jest.spyOn(Welcome, 'setOnboardingAdminsChatReportID').mockImplementation(jest.fn()); const setOnboardingPolicyIDSpy = jest.spyOn(Welcome, 'setOnboardingPolicyID').mockImplementation(jest.fn()); @@ -175,23 +186,23 @@ describe('useAutoCreateSubmitWorkspace', () => { const existingPolicyID = 'existing-policy-id'; const existingAdminsReportID = 'existing-admins-report-id'; - (useOnyx as jest.Mock).mockImplementation((key: string) => { + mockUseOnyx.mockImplementation((key: string) => { if (key === 'onboardingPolicyID') { - return [existingPolicyID]; + return [existingPolicyID, {status: 'loaded'}] satisfies UseOnyxResult; } if (key === 'onboardingAdminsChatReportID') { - return [existingAdminsReportID]; + return [existingAdminsReportID, {status: 'loaded'}] satisfies UseOnyxResult; } if (key === 'session') { - return [MOCK_SESSION]; + return [MOCK_SESSION, {status: 'loaded'}] satisfies UseOnyxResult; } if (key === 'betas') { - return [[]]; + return [[], {status: 'loaded'}] satisfies UseOnyxResult; } if (key.startsWith('policy_')) { - return [false]; + return [false, {status: 'loaded'}] satisfies UseOnyxResult; } - return [undefined]; + return [undefined, {status: 'loaded'}] satisfies UseOnyxResult; }); // When the onboarding flow runs @@ -211,23 +222,23 @@ describe('useAutoCreateSubmitWorkspace', () => { it('skips workspace creation when the user is already a paid group policy admin', () => { // Given a user who is already an admin of a paid group policy - (useOnyx as jest.Mock).mockImplementation((key: string) => { + mockUseOnyx.mockImplementation((key: string) => { if (key === 'session') { - return [MOCK_SESSION]; + return [MOCK_SESSION, {status: 'loaded'}] satisfies UseOnyxResult; } if (key === 'betas') { - return [[]]; + return [[], {status: 'loaded'}] satisfies UseOnyxResult; } if (key === 'onboardingPolicyID') { - return [undefined]; + return [undefined, {status: 'loaded'}] satisfies UseOnyxResult; } if (key === 'onboardingAdminsChatReportID') { - return [undefined]; + return [undefined, {status: 'loaded'}] satisfies UseOnyxResult; } if (key.startsWith('policy_')) { - return [true]; + return [true, {status: 'loaded'}] satisfies UseOnyxResult; } - return [undefined]; + return [undefined, {status: 'loaded'}] satisfies UseOnyxResult; }); // When onboarding completes @@ -241,7 +252,7 @@ describe('useAutoCreateSubmitWorkspace', () => { it('skips workspace creation when the user domain restricts policy creation', () => { // Given a user whose domain security group has enableRestrictedPolicyCreation set to true - (usePreferredPolicy as jest.Mock).mockReturnValue({ + jest.mocked(usePreferredPolicy).mockReturnValue({ isRestrictedToPreferredPolicy: false, preferredPolicyID: undefined, isRestrictedPolicyCreation: true, @@ -259,7 +270,7 @@ describe('useAutoCreateSubmitWorkspace', () => { it('still completes onboarding and navigates even when workspace creation is skipped', async () => { // Given a user who cannot create a workspace due to domain restrictions - (usePreferredPolicy as jest.Mock).mockReturnValue({ + jest.mocked(usePreferredPolicy).mockReturnValue({ isRestrictedToPreferredPolicy: false, preferredPolicyID: undefined, isRestrictedPolicyCreation: true, @@ -287,10 +298,10 @@ describe('useAutoCreateSubmitWorkspace', () => { }; mockUseOnyx.mockImplementation((key: string, options?: {selector?: (policies: unknown) => unknown}) => { if (key === 'session') { - return [MOCK_SESSION]; + return [MOCK_SESSION, {status: 'loaded'}] satisfies UseOnyxResult; } if (key === 'betas') { - return [[]]; + return [[], {status: 'loaded'}] satisfies UseOnyxResult; } if (key.startsWith('policy_')) { // Run the hook's real selectors against a fake policy collection so both the @@ -298,12 +309,13 @@ describe('useAutoCreateSubmitWorkspace', () => { // Unrelated policy-collection selectors (e.g. useLastWorkspaceNumber's) expect extra // arguments this mock doesn't provide, so fall back to undefined when they throw. try { - return [options?.selector?.({[`policy_${existingSubmitPolicy.id}`]: existingSubmitPolicy})]; + const selectedPolicy = options?.selector?.({[`policy_${existingSubmitPolicy.id}`]: existingSubmitPolicy}); + return [selectedPolicy, {status: 'loaded'}] satisfies UseOnyxResult; } catch { - return [undefined]; + return [undefined, {status: 'loaded'}] satisfies UseOnyxResult; } } - return [undefined]; + return [undefined, {status: 'loaded'}] satisfies UseOnyxResult; }); // When the user confirms the Submit plan welcome modal again @@ -327,19 +339,20 @@ describe('useAutoCreateSubmitWorkspace', () => { }; mockUseOnyx.mockImplementation((key: string, options?: {selector?: (policies: unknown) => unknown}) => { if (key === 'session') { - return [MOCK_SESSION]; + return [MOCK_SESSION, {status: 'loaded'}] satisfies UseOnyxResult; } if (key === 'betas') { - return [[]]; + return [[], {status: 'loaded'}] satisfies UseOnyxResult; } if (key.startsWith('policy_')) { try { - return [options?.selector?.({[`policy_${existingSubmitPolicy.id}`]: existingSubmitPolicy})]; + const selectedPolicy = options?.selector?.({[`policy_${existingSubmitPolicy.id}`]: existingSubmitPolicy}); + return [selectedPolicy, {status: 'loaded'}] satisfies UseOnyxResult; } catch { - return [undefined]; + return [undefined, {status: 'loaded'}] satisfies UseOnyxResult; } } - return [undefined]; + return [undefined, {status: 'loaded'}] satisfies UseOnyxResult; }); // When the onboarding flow runs (shouldCompleteOnboarding defaults to true) @@ -355,7 +368,7 @@ describe('useAutoCreateSubmitWorkspace', () => { it('uses the localCurrencyCode from personal details for workspace currency', () => { // Given a user whose personal details have localCurrencyCode set to GBP - (useCurrentUserPersonalDetails as jest.Mock).mockReturnValue({ + jest.mocked(useCurrentUserPersonalDetails).mockReturnValue({ accountID: MOCK_SESSION.accountID, login: MOCK_SESSION.email, localCurrencyCode: 'GBP', @@ -375,7 +388,7 @@ describe('useAutoCreateSubmitWorkspace', () => { it('falls back to USD when localCurrencyCode is not available', () => { // Given a user whose personal details do not have a localCurrencyCode set - (useCurrentUserPersonalDetails as jest.Mock).mockReturnValue({ + jest.mocked(useCurrentUserPersonalDetails).mockReturnValue({ accountID: MOCK_SESSION.accountID, login: MOCK_SESSION.email, localCurrencyCode: undefined, diff --git a/tests/unit/navigateToWorkspacesPageTest.ts b/tests/unit/navigateToWorkspacesPageTest.ts index 6c43fa725857..b675b4921fda 100644 --- a/tests/unit/navigateToWorkspacesPageTest.ts +++ b/tests/unit/navigateToWorkspacesPageTest.ts @@ -15,6 +15,7 @@ import type {NavigationState, PartialState} from '@react-navigation/native'; import {StackActions, TabActions} from '@react-navigation/native'; import createRandomPolicy from '../utils/collections/policies'; +import createMock from '../utils/createMock'; jest.mock('@libs/Navigation/navigationRef'); jest.mock('@libs/Navigation/Navigation'); @@ -42,7 +43,7 @@ const baseParams = {currentUserLogin: 'test@example.com', shouldUseNarrowLayout: const TAB_NAV_STATE_KEY = 'tab-nav-key-123'; const tabNavigatorRoute = { name: NAVIGATORS.TAB_NAVIGATOR, - state: {key: TAB_NAV_STATE_KEY} as PartialState, + state: createMock>({key: TAB_NAV_STATE_KEY}), }; describe('navigateToWorkspacesPage', () => { @@ -51,7 +52,7 @@ describe('navigateToWorkspacesPage', () => { }); function mockIntercept() { - (interceptAnonymousUser as jest.Mock).mockImplementation((callback: () => void) => { + jest.mocked(interceptAnonymousUser).mockImplementation((callback: () => void) => { callback(); }); } @@ -89,8 +90,8 @@ describe('navigateToWorkspacesPage', () => { }); it('dispatches jumpTo WORKSPACE_NAVIGATOR when a TAB_NAVIGATOR is already on top (workspace, wide layout)', () => { - (PolicyUtils.shouldShowPolicy as jest.Mock).mockReturnValue(true); - (PolicyUtils.isPendingDeletePolicy as jest.Mock).mockReturnValue(false); + jest.mocked(PolicyUtils.shouldShowPolicy).mockReturnValue(true); + jest.mocked(PolicyUtils.isPendingDeletePolicy).mockReturnValue(false); mockIntercept(); navigateToWorkspacesPage({ @@ -107,8 +108,8 @@ describe('navigateToWorkspacesPage', () => { }); it('pops workspace split to root then jumps to tab on narrow layout when a sub-page is focused (no flicker)', () => { - (PolicyUtils.shouldShowPolicy as jest.Mock).mockReturnValue(true); - (PolicyUtils.isPendingDeletePolicy as jest.Mock).mockReturnValue(false); + jest.mocked(PolicyUtils.shouldShowPolicy).mockReturnValue(true); + jest.mocked(PolicyUtils.isPendingDeletePolicy).mockReturnValue(false); mockIntercept(); const WORKSPACE_SPLIT_STATE_KEY = 'workspace-split-state-key-456'; @@ -134,8 +135,8 @@ describe('navigateToWorkspacesPage', () => { }); it('skips popToTop on narrow layout when WorkspaceInitialPage is already focused', () => { - (PolicyUtils.shouldShowPolicy as jest.Mock).mockReturnValue(true); - (PolicyUtils.isPendingDeletePolicy as jest.Mock).mockReturnValue(false); + jest.mocked(PolicyUtils.shouldShowPolicy).mockReturnValue(true); + jest.mocked(PolicyUtils.isPendingDeletePolicy).mockReturnValue(false); mockIntercept(); const WORKSPACE_SPLIT_STATE_KEY = 'workspace-split-state-key-789'; @@ -164,7 +165,7 @@ describe('navigateToWorkspacesPage', () => { navigateToWorkspacesPage({ ...baseParams, - domain: {accountID: 123} as unknown as Domain, + domain: createMock({accountID: 123}), lastTabNavigatorRoute: tabNavigatorRoute, lastWorkspacesTabNavigatorRoute: {name: NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR}, }); @@ -181,7 +182,7 @@ describe('navigateToWorkspacesPage', () => { navigateToWorkspacesPage({ ...baseParams, - domain: {accountID: 123} as unknown as Domain, + domain: createMock({accountID: 123}), // TAB_NAVIGATOR present but with no state (so no existingTabNavStateKey to jump to). lastTabNavigatorRoute: {name: NAVIGATORS.TAB_NAVIGATOR}, lastWorkspacesTabNavigatorRoute: {name: NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR}, @@ -192,8 +193,8 @@ describe('navigateToWorkspacesPage', () => { }); it('navigates to WORKSPACES_LIST if policy is pending delete', () => { - (PolicyUtils.shouldShowPolicy as jest.Mock).mockReturnValue(true); - (PolicyUtils.isPendingDeletePolicy as jest.Mock).mockReturnValue(true); + jest.mocked(PolicyUtils.shouldShowPolicy).mockReturnValue(true); + jest.mocked(PolicyUtils.isPendingDeletePolicy).mockReturnValue(true); mockIntercept(); navigateToWorkspacesPage({ @@ -206,8 +207,8 @@ describe('navigateToWorkspacesPage', () => { }); it('navigates to WORKSPACES_LIST if shouldShowPolicy is false for the user', () => { - (PolicyUtils.shouldShowPolicy as jest.Mock).mockReturnValue(false); - (PolicyUtils.isPendingDeletePolicy as jest.Mock).mockReturnValue(false); + jest.mocked(PolicyUtils.shouldShowPolicy).mockReturnValue(false); + jest.mocked(PolicyUtils.isPendingDeletePolicy).mockReturnValue(false); mockIntercept(); navigateToWorkspacesPage({ diff --git a/tests/unit/selectors/PolicyTest.ts b/tests/unit/selectors/PolicyTest.ts index f3b11c16cbf7..00be79f20c0d 100644 --- a/tests/unit/selectors/PolicyTest.ts +++ b/tests/unit/selectors/PolicyTest.ts @@ -13,6 +13,7 @@ import { } from '@selectors/Policy'; import createRandomPolicy from '../../utils/collections/policies'; +import createMock from '../../utils/createMock'; const OWNER_ACCOUNT_ID = 1; @@ -78,14 +79,14 @@ function buildSelectorPolicy(id: number, overrides: Partial): Policy { function buildNetSuitePolicy(id: number, overrides: Partial = {}): Policy { return buildSelectorPolicy(id, { role: CONST.POLICY.ROLE.ADMIN, - connections: { + connections: createMock({ netsuite: { verified: true, lastSync: { isSuccessful: true, }, }, - } as Policy['connections'], + }), ...overrides, }); } @@ -93,14 +94,14 @@ function buildNetSuitePolicy(id: number, overrides: Partial = {}): Polic function buildSageIntacctPolicy(id: number, overrides: Partial = {}): Policy { return buildSelectorPolicy(id, { role: CONST.POLICY.ROLE.ADMIN, - connections: { + connections: createMock({ intacct: { lastSync: { isConnected: true, isSuccessful: true, }, }, - } as Policy['connections'], + }), ...overrides, }); } @@ -156,7 +157,7 @@ describe('activeAdminPoliciesSelector', () => { describe('adminPoliciesConnectedToQBDSelector', () => { it('returns admin policies with QBD connections', () => { const policies: OnyxCollection = { - policy1: buildSelectorPolicy(1, {name: 'QBD Policy', role: CONST.POLICY.ROLE.ADMIN, connections: {quickbooksDesktop: {}} as Policy['connections']}), + policy1: buildSelectorPolicy(1, {name: 'QBD Policy', role: CONST.POLICY.ROLE.ADMIN, connections: createMock({quickbooksDesktop: {}})}), policy2: buildSelectorPolicy(2, {name: 'No Connection', role: CONST.POLICY.ROLE.ADMIN}), }; @@ -167,7 +168,7 @@ describe('adminPoliciesConnectedToQBDSelector', () => { it('excludes non-admin policies with QBD connections', () => { const policies: OnyxCollection = { - policy1: buildSelectorPolicy(1, {name: 'User QBD', role: CONST.POLICY.ROLE.USER, connections: {quickbooksDesktop: {}} as Policy['connections']}), + policy1: buildSelectorPolicy(1, {name: 'User QBD', role: CONST.POLICY.ROLE.USER, connections: createMock({quickbooksDesktop: {}})}), }; expect(adminPoliciesConnectedToQBDSelector(policies)).toHaveLength(0); @@ -189,26 +190,26 @@ describe('reusablePoliciesConnectedToSelector for QBD', () => { policy1: buildSelectorPolicy(1, { name: 'Current Workspace', role: CONST.POLICY.ROLE.ADMIN, - connections: { + connections: createMock({ quickbooksDesktop: { lastSync: { isConnected: true, isSuccessful: true, }, }, - } as Policy['connections'], + }), }), policy2: buildSelectorPolicy(2, { name: 'Healthy Workspace', role: CONST.POLICY.ROLE.ADMIN, - connections: { + connections: createMock({ quickbooksDesktop: { lastSync: { isConnected: true, isSuccessful: true, }, }, - } as Policy['connections'], + }), }), }; @@ -224,14 +225,14 @@ describe('reusablePoliciesConnectedToSelector for QBD', () => { policy1: buildSelectorPolicy(1, { name: 'Current Workspace', role: CONST.POLICY.ROLE.ADMIN, - connections: { + connections: createMock({ quickbooksDesktop: { lastSync: { isConnected: true, isSuccessful: true, }, }, - } as Policy['connections'], + }), }), }; @@ -248,7 +249,7 @@ describe('reusablePoliciesConnectedToSelector for QBD', () => { policy2: buildSelectorPolicy(2, { name: 'Unsynced Workspace', role: CONST.POLICY.ROLE.ADMIN, - connections: {quickbooksDesktop: {}} as Policy['connections'], + connections: createMock({quickbooksDesktop: {}}), }), }; @@ -262,7 +263,7 @@ describe('reusablePoliciesConnectedToSelector for QBD', () => { policy2: buildSelectorPolicy(2, { name: 'Errored Workspace', role: CONST.POLICY.ROLE.ADMIN, - connections: { + connections: createMock({ quickbooksDesktop: { lastSync: { errorDate: new Date().toISOString(), @@ -270,7 +271,7 @@ describe('reusablePoliciesConnectedToSelector for QBD', () => { isSuccessful: false, }, }, - } as Policy['connections'], + }), }), }; @@ -286,7 +287,7 @@ describe('reusablePoliciesConnectedToSelector for QBD', () => { policy2: buildSelectorPolicy(2, { name: 'Retrying Workspace', role: CONST.POLICY.ROLE.ADMIN, - connections: { + connections: createMock({ quickbooksDesktop: { lastSync: { errorDate: new Date().toISOString(), @@ -294,7 +295,7 @@ describe('reusablePoliciesConnectedToSelector for QBD', () => { isSuccessful: false, }, }, - } as Policy['connections'], + }), }), }; @@ -308,14 +309,14 @@ describe('hasReusablePoliciesConnectedToSelector for QBD', () => { const policies: OnyxCollection = { policy1: buildSelectorPolicy(1, { role: CONST.POLICY.ROLE.ADMIN, - connections: { + connections: createMock({ quickbooksDesktop: { lastSync: { isConnected: true, isSuccessful: true, }, }, - } as Policy['connections'], + }), }), }; @@ -345,7 +346,7 @@ describe('reusablePoliciesConnectedToSelector for NetSuite', () => { policy2: buildNetSuitePolicy(2, {name: 'Healthy Workspace'}), policy3: buildNetSuitePolicy(3, { name: 'Broken Workspace', - connections: { + connections: createMock({ netsuite: { verified: true, lastSync: { @@ -353,15 +354,15 @@ describe('reusablePoliciesConnectedToSelector for NetSuite', () => { isSuccessful: false, }, }, - } as Policy['connections'], + }), }), policy4: buildNetSuitePolicy(4, { name: 'Unverified Workspace', - connections: { + connections: createMock({ netsuite: { verified: false, }, - } as Policy['connections'], + }), }), }; @@ -377,7 +378,7 @@ describe('reusablePoliciesConnectedToSelector for Sage Intacct', () => { policy2: buildSageIntacctPolicy(2, {name: 'Healthy Workspace'}), policy3: buildSageIntacctPolicy(3, { name: 'Broken Workspace', - connections: { + connections: createMock({ intacct: { lastSync: { errorDate: new Date().toISOString(), @@ -385,13 +386,13 @@ describe('reusablePoliciesConnectedToSelector for Sage Intacct', () => { isSuccessful: false, }, }, - } as Policy['connections'], + }), }), policy4: buildSageIntacctPolicy(4, { name: 'Unverified Workspace', - connections: { + connections: createMock({ intacct: {}, - } as Policy['connections'], + }), }), }; diff --git a/tests/unit/usePolicyIndicatorChecksTest.ts b/tests/unit/usePolicyIndicatorChecksTest.ts index b76c218ca59c..38d70d224d1c 100644 --- a/tests/unit/usePolicyIndicatorChecksTest.ts +++ b/tests/unit/usePolicyIndicatorChecksTest.ts @@ -10,6 +10,7 @@ import type {OnyxMultiSetInput} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; +import createMock from '../utils/createMock'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; const userID = 'admin@expensify.com'; @@ -32,17 +33,19 @@ describe('usePolicyIndicatorChecks', () => { describe('policy error statuses', () => { it('returns HAS_POLICY_ERRORS when policy has errors', async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.SESSION]: {email: userID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - errors: {policyError: 'Something went wrong'}, - }, - } as unknown as OnyxMultiSetInput); + await Onyx.multiSet( + createMock({ + [ONYXKEYS.SESSION]: {email: userID}, + [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + errors: {policyError: 'Something went wrong'}, + }, + }), + ); await waitForBatchedUpdatesWithAct(); }); @@ -55,17 +58,19 @@ describe('usePolicyIndicatorChecks', () => { it('returns HAS_CUSTOM_UNITS_ERROR when custom units have errors', async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.SESSION]: {email: userID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - customUnits: {errors: {customUnitError: 'Invalid custom unit'}}, - }, - } as unknown as OnyxMultiSetInput); + await Onyx.multiSet( + createMock({ + [ONYXKEYS.SESSION]: {email: userID}, + [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + customUnits: {errors: {customUnitError: 'Invalid custom unit'}}, + }, + }), + ); await waitForBatchedUpdatesWithAct(); }); @@ -78,22 +83,24 @@ describe('usePolicyIndicatorChecks', () => { it('returns HAS_EMPLOYEE_LIST_ERROR when employee list has errors', async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.SESSION]: {email: userID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - employeeList: { - [otherUserID]: { - email: otherUserID, - errors: {employeeError: 'Employee error'}, + await Onyx.multiSet( + createMock({ + [ONYXKEYS.SESSION]: {email: userID}, + [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + employeeList: { + [otherUserID]: { + email: otherUserID, + errors: {employeeError: 'Employee error'}, + }, }, }, - }, - } as unknown as OnyxMultiSetInput); + }), + ); await waitForBatchedUpdatesWithAct(); }); @@ -106,29 +113,31 @@ describe('usePolicyIndicatorChecks', () => { it('returns HAS_SYNC_ERRORS when sync has errors', async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.SESSION]: {email: userID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - connections: { - quickbooksOnline: { - lastSync: { - errorMessage: 'Sync failed', - isSuccessful: false, - errorDate: new Date().toISOString(), + await Onyx.multiSet( + createMock({ + [ONYXKEYS.SESSION]: {email: userID}, + [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + connections: { + quickbooksOnline: { + lastSync: { + errorMessage: 'Sync failed', + isSuccessful: false, + errorDate: new Date().toISOString(), + }, }, }, }, - }, - [`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${WORKSPACE.policyID}` as const]: { - stageInProgress: null, - connectionName: 'quickbooksOnline', - }, - } as unknown as OnyxMultiSetInput); + [`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${WORKSPACE.policyID}` as const]: { + stageInProgress: null, + connectionName: 'quickbooksOnline', + }, + }), + ); await waitForBatchedUpdatesWithAct(); }); @@ -141,24 +150,26 @@ describe('usePolicyIndicatorChecks', () => { it('returns HAS_QBO_EXPORT_ERROR when QBO export has errors', async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.SESSION]: {email: userID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - connections: { - quickbooksOnline: { - config: { - reimbursableExpensesExportDestination: 'VENDOR_BILL', - reimbursableExpensesAccount: undefined, + await Onyx.multiSet( + createMock({ + [ONYXKEYS.SESSION]: {email: userID}, + [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + connections: { + quickbooksOnline: { + config: { + reimbursableExpensesExportDestination: 'VENDOR_BILL', + reimbursableExpensesAccount: undefined, + }, }, }, }, - }, - } as unknown as OnyxMultiSetInput); + }), + ); await waitForBatchedUpdatesWithAct(); }); @@ -171,19 +182,21 @@ describe('usePolicyIndicatorChecks', () => { it('returns HAS_UBER_CREDENTIALS_ERROR when Uber credentials have errors', async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.SESSION]: {email: userID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - receiptPartners: { - uber: {error: 'Invalid Uber credentials'}, + await Onyx.multiSet( + createMock({ + [ONYXKEYS.SESSION]: {email: userID}, + [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + receiptPartners: { + uber: {error: 'Invalid Uber credentials'}, + }, }, - }, - } as unknown as OnyxMultiSetInput); + }), + ); await waitForBatchedUpdatesWithAct(); }); @@ -196,23 +209,25 @@ describe('usePolicyIndicatorChecks', () => { it('returns HAS_POLICY_ADMIN_CARD_FEED_ERRORS when admin has card feed errors', async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.SESSION]: {email: userID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - }, - [ONYXKEYS.CARD_LIST]: { - card1: { - bank: CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE, - fundID: String(WORKSPACE.policyAccountID), - lastScrapeResult: 403, + await Onyx.multiSet( + createMock({ + [ONYXKEYS.SESSION]: {email: userID}, + [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, }, - }, - } as unknown as OnyxMultiSetInput); + [ONYXKEYS.CARD_LIST]: { + card1: { + bank: CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE, + fundID: String(WORKSPACE.policyAccountID), + lastScrapeResult: 403, + }, + }, + }), + ); await waitForBatchedUpdatesWithAct(); }); @@ -232,24 +247,26 @@ describe('usePolicyIndicatorChecks', () => { it('returns HAS_MERGE_HR_SETUP_NEEDED when merge HR setup is needed', async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.SESSION]: {email: userID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - connections: { - [CONST.POLICY.CONNECTIONS.NAME.MERGE_HR]: { - config: {integration: 'workday'}, - data: {groups: [{id: 'g1', name: 'Eng', type: 'Department'}]}, - lastSync: {syncStatus: CONST.MERGE_HR.SYNC_STATUS.DONE}, + await Onyx.multiSet( + createMock({ + [ONYXKEYS.SESSION]: {email: userID}, + [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + connections: { + [CONST.POLICY.CONNECTIONS.NAME.MERGE_HR]: { + config: {integration: 'workday'}, + data: {groups: [{id: 'g1', name: 'Eng', type: 'Department'}]}, + lastSync: {syncStatus: CONST.MERGE_HR.SYNC_STATUS.DONE}, + }, }, }, - }, - [ONYXKEYS.CARD_LIST]: {}, - } as unknown as OnyxMultiSetInput); + [ONYXKEYS.CARD_LIST]: {}, + }), + ); await waitForBatchedUpdatesWithAct(); }); @@ -263,24 +280,26 @@ describe('usePolicyIndicatorChecks', () => { it('does not return HAS_MERGE_HR_SETUP_NEEDED for non-admin users', async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.SESSION]: {email: otherUserID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'user', - policyAccountID: WORKSPACE.policyAccountID, - connections: { - [CONST.POLICY.CONNECTIONS.NAME.MERGE_HR]: { - config: {integration: 'workday'}, - data: {groups: [{id: 'g1', name: 'Eng', type: 'Department'}]}, - lastSync: {syncStatus: CONST.MERGE_HR.SYNC_STATUS.DONE}, + await Onyx.multiSet( + createMock({ + [ONYXKEYS.SESSION]: {email: otherUserID}, + [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'user', + policyAccountID: WORKSPACE.policyAccountID, + connections: { + [CONST.POLICY.CONNECTIONS.NAME.MERGE_HR]: { + config: {integration: 'workday'}, + data: {groups: [{id: 'g1', name: 'Eng', type: 'Department'}]}, + lastSync: {syncStatus: CONST.MERGE_HR.SYNC_STATUS.DONE}, + }, }, }, - }, - [ONYXKEYS.CARD_LIST]: {}, - } as unknown as OnyxMultiSetInput); + [ONYXKEYS.CARD_LIST]: {}, + }), + ); await waitForBatchedUpdatesWithAct(); }); @@ -300,11 +319,13 @@ describe('usePolicyIndicatorChecks', () => { it('returns HAS_DOMAIN_ERRORS when domain has errors', async () => { await act(async () => { - await Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.DOMAIN_ERRORS}domain1` as const]: { - errors: {domainError: 'Domain error'}, - }, - } as unknown as OnyxMultiSetInput); + await Onyx.multiSet( + createMock({ + [`${ONYXKEYS.COLLECTION.DOMAIN_ERRORS}domain1` as const]: { + errors: {domainError: 'Domain error'}, + }, + }), + ); await waitForBatchedUpdatesWithAct(); }); @@ -320,17 +341,19 @@ describe('usePolicyIndicatorChecks', () => { await Onyx.clear(); await waitForBatchedUpdatesWithAct(); await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.SESSION]: {email: userID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - }, - [ONYXKEYS.CARD_LIST]: {}, - } as unknown as OnyxMultiSetInput); + await Onyx.multiSet( + createMock({ + [ONYXKEYS.SESSION]: {email: userID}, + [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + }, + [ONYXKEYS.CARD_LIST]: {}, + }), + ); await waitForBatchedUpdatesWithAct(); }); }); @@ -366,25 +389,27 @@ describe('usePolicyIndicatorChecks', () => { describe('error priority', () => { beforeAll(async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.SESSION]: {email: userID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - errors: {policyError: 'Policy error'}, - customUnits: {errors: {customUnitError: 'Custom unit error'}}, - employeeList: { - [otherUserID]: { - email: otherUserID, - errors: {employeeError: 'Employee error'}, + await Onyx.multiSet( + createMock({ + [ONYXKEYS.SESSION]: {email: userID}, + [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + errors: {policyError: 'Policy error'}, + customUnits: {errors: {customUnitError: 'Custom unit error'}}, + employeeList: { + [otherUserID]: { + email: otherUserID, + errors: {employeeError: 'Employee error'}, + }, }, }, - }, - [ONYXKEYS.CARD_LIST]: {}, - } as unknown as OnyxMultiSetInput); + [ONYXKEYS.CARD_LIST]: {}, + }), + ); await waitForBatchedUpdatesWithAct(); }); }); @@ -407,35 +432,37 @@ describe('usePolicyIndicatorChecks', () => { it('returns both sync error and merge HR setup statuses when policy has both conditions', async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.SESSION]: {email: userID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - connections: { - quickbooksOnline: { - lastSync: { - errorMessage: 'Sync failed', - isSuccessful: false, - errorDate: new Date().toISOString(), + await Onyx.multiSet( + createMock({ + [ONYXKEYS.SESSION]: {email: userID}, + [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + connections: { + quickbooksOnline: { + lastSync: { + errorMessage: 'Sync failed', + isSuccessful: false, + errorDate: new Date().toISOString(), + }, + }, + [CONST.POLICY.CONNECTIONS.NAME.MERGE_HR]: { + config: {integration: 'workday'}, + data: {groups: [{id: 'g1', name: 'Eng', type: 'Department'}]}, + lastSync: {syncStatus: CONST.MERGE_HR.SYNC_STATUS.DONE}, }, }, - [CONST.POLICY.CONNECTIONS.NAME.MERGE_HR]: { - config: {integration: 'workday'}, - data: {groups: [{id: 'g1', name: 'Eng', type: 'Department'}]}, - lastSync: {syncStatus: CONST.MERGE_HR.SYNC_STATUS.DONE}, - }, }, - }, - [`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${WORKSPACE.policyID}` as const]: { - stageInProgress: null, - connectionName: 'quickbooksOnline', - }, - [ONYXKEYS.CARD_LIST]: {}, - } as unknown as OnyxMultiSetInput); + [`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${WORKSPACE.policyID}` as const]: { + stageInProgress: null, + connectionName: 'quickbooksOnline', + }, + [ONYXKEYS.CARD_LIST]: {}, + }), + ); await waitForBatchedUpdatesWithAct(); }); @@ -457,25 +484,27 @@ describe('usePolicyIndicatorChecks', () => { beforeAll(async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.SESSION]: {email: userID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - }, - [`${ONYXKEYS.COLLECTION.POLICY}${SECOND_WORKSPACE.policyID}` as const]: { - id: SECOND_WORKSPACE.policyID, - name: SECOND_WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: SECOND_WORKSPACE.policyAccountID, - errors: {policyError: 'Something went wrong'}, - }, - [ONYXKEYS.CARD_LIST]: {}, - } as unknown as OnyxMultiSetInput); + await Onyx.multiSet( + createMock({ + [ONYXKEYS.SESSION]: {email: userID}, + [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + }, + [`${ONYXKEYS.COLLECTION.POLICY}${SECOND_WORKSPACE.policyID}` as const]: { + id: SECOND_WORKSPACE.policyID, + name: SECOND_WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: SECOND_WORKSPACE.policyAccountID, + errors: {policyError: 'Something went wrong'}, + }, + [ONYXKEYS.CARD_LIST]: {}, + }), + ); await waitForBatchedUpdatesWithAct(); }); }); From d87a0972afe2e4e1e27503002a19c92b1eeca9f8 Mon Sep 17 00:00:00 2001 From: KJ21-ENG <140263938+KJ21-ENG@users.noreply.github.com> Date: Wed, 29 Jul 2026 04:47:04 +0530 Subject: [PATCH 2/4] tests: repair cleanup typecheck findings --- tests/actions/WorkflowTest.ts | 3 +- tests/unit/FileUtilsTest.ts | 19 +++-- tests/unit/SearchEditMultipleUtilsTest.ts | 32 ++++---- .../useAutoCreateSubmitWorkspace.test.ts | 73 ++++++++++++------- 4 files changed, 73 insertions(+), 54 deletions(-) diff --git a/tests/actions/WorkflowTest.ts b/tests/actions/WorkflowTest.ts index 4048bf018e81..4bee24cab2af 100644 --- a/tests/actions/WorkflowTest.ts +++ b/tests/actions/WorkflowTest.ts @@ -242,8 +242,9 @@ describe('actions/Workflow', () => { await mockFetch.resume(); let updatedPolicy: Policy | undefined; + const policyKey: `${typeof ONYXKEYS.COLLECTION.POLICY}${string}` = `${ONYXKEYS.COLLECTION.POLICY}${policy.id}`; await getOnyxData({ - key: `${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, + key: policyKey, callback: (val) => (updatedPolicy = val), }); diff --git a/tests/unit/FileUtilsTest.ts b/tests/unit/FileUtilsTest.ts index 75d8abc1bbdc..f214e4a40344 100644 --- a/tests/unit/FileUtilsTest.ts +++ b/tests/unit/FileUtilsTest.ts @@ -120,20 +120,23 @@ describe('FileUtils', () => { }); describe('canvasFallback', () => { + type Canvas2DGetContext = (contextId: '2d', options?: CanvasRenderingContext2DSettings) => CanvasRenderingContext2D | null; + type Canvas2DCanvas = Pick & {getContext: Canvas2DGetContext}; + const mockCreateImageBitmap = jest.fn, Parameters>(); - const mockCanvas = createMock({ + const mockGetContext = jest.fn, Parameters>(() => null); + const mockCanvas = createMock({ width: 0, height: 0, - getContext: () => null, + getContext: mockGetContext, toBlob: () => undefined, }); - const mockGetContext = jest.spyOn(mockCanvas, 'getContext'); const mockToBlob = jest.spyOn(mockCanvas, 'toBlob'); const mockCtx = createMock({ drawImage: jest.fn(), }); - const mockCreateElement = jest.fn, Parameters>(); - const mockDocument = {createElement: mockCreateElement} satisfies Pick; + const mockCreateElement = jest.fn(); + const mockDocument = {createElement: mockCreateElement} satisfies {createElement: (tagName: string) => Canvas2DCanvas}; const mockURL = { createObjectURL: jest.fn, Parameters>(() => 'blob:mock-url'), } satisfies Pick; @@ -146,7 +149,7 @@ describe('FileUtils', () => { Object.defineProperty(globalThis, 'URL', {configurable: true, enumerable: true, value: mockURL, writable: true}); mockCreateElement.mockReturnValue(mockCanvas); - mockGetContext.mockReturnValue(mockCtx); + mockGetContext.mockImplementation(() => mockCtx); mockCreateImageBitmap.mockResolvedValue( createMock>>({ width: 1000, @@ -496,7 +499,9 @@ describe('FileUtils', () => { }); describe('getFileValidationErrorText', () => { - const mockTranslate: LocaleContextProps['translate'] = (...parameters) => parameters[0]; + const mockTranslate: LocaleContextProps['translate'] = (path, ...parameters) => { + return parameters.length > 0 ? path : path; + }; it('should return correct error text for IMAGE_DIMENSIONS_TOO_LARGE', () => { const result = getFileValidationErrorText(mockTranslate, {error: CONST.FILE_VALIDATION_ERRORS.IMAGE_DIMENSIONS_TOO_LARGE}); diff --git a/tests/unit/SearchEditMultipleUtilsTest.ts b/tests/unit/SearchEditMultipleUtilsTest.ts index e782f2d4aa79..0ee82ef505e9 100644 --- a/tests/unit/SearchEditMultipleUtilsTest.ts +++ b/tests/unit/SearchEditMultipleUtilsTest.ts @@ -41,9 +41,8 @@ function makeReport(reportID: string, policyID: string): Report { describe('SearchEditMultipleUtils', () => { describe('withSnapshotTransactions', () => { it('fills missing transactions from snapshot', () => { - const snapshotData = createMock({ - [TRANSACTION_DATA_KEY_1]: makeTransaction(TRANSACTION_ID_1, REPORT_ID), - }); + const snapshotData = createMock({}); + snapshotData[TRANSACTION_DATA_KEY_1] = makeTransaction(TRANSACTION_ID_1, REPORT_ID); const merged = withSnapshotTransactions(undefined, snapshotData); @@ -54,9 +53,8 @@ describe('SearchEditMultipleUtils', () => { const existing = { [`${ONYXKEYS.COLLECTION.TRANSACTION}${TRANSACTION_ID_1}`]: makeTransaction(TRANSACTION_ID_1, 'existingReport'), }; - const snapshotData = createMock({ - [TRANSACTION_DATA_KEY_1]: makeTransaction(TRANSACTION_ID_1, 'snapshotReport'), - }); + const snapshotData = createMock({}); + snapshotData[TRANSACTION_DATA_KEY_1] = makeTransaction(TRANSACTION_ID_1, 'snapshotReport'); const merged = withSnapshotTransactions(existing, snapshotData); @@ -76,9 +74,8 @@ describe('SearchEditMultipleUtils', () => { describe('withSnapshotReports', () => { it('fills missing reports from snapshot', () => { - const snapshotData = createMock({ - [REPORT_DATA_KEY]: makeReport(REPORT_ID, POLICY_A), - }); + const snapshotData = createMock({}); + snapshotData[REPORT_DATA_KEY] = makeReport(REPORT_ID, POLICY_A); const merged = withSnapshotReports(undefined, snapshotData); @@ -89,9 +86,8 @@ describe('SearchEditMultipleUtils', () => { const existing = { [`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`]: makeReport(REPORT_ID, POLICY_A), }; - const snapshotData = createMock({ - [REPORT_DATA_KEY]: makeReport(REPORT_ID, POLICY_B), - }); + const snapshotData = createMock({}); + snapshotData[REPORT_DATA_KEY] = makeReport(REPORT_ID, POLICY_B); const merged = withSnapshotReports(existing, snapshotData); @@ -105,9 +101,8 @@ describe('SearchEditMultipleUtils', () => { const allTransactions = { [`${ONYXKEYS.COLLECTION.TRANSACTION}${TRANSACTION_ID_1}`]: makeTransaction(TRANSACTION_ID_1, REPORT_ID), }; - const snapshotData = createMock({ - [REPORT_DATA_KEY]: makeReport(REPORT_ID, POLICY_A), - }); + const snapshotData = createMock({}); + snapshotData[REPORT_DATA_KEY] = makeReport(REPORT_ID, POLICY_A); // Without snapshot merge — falls back to activePolicyID const withoutMerge = getSearchBulkEditPolicyID([TRANSACTION_ID_1], POLICY_B, allTransactions, undefined); @@ -120,10 +115,9 @@ describe('SearchEditMultipleUtils', () => { }); it('resolves policyID when transaction is only in snapshot', () => { - const snapshotData = createMock({ - [TRANSACTION_DATA_KEY_1]: makeTransaction(TRANSACTION_ID_1, REPORT_ID), - [REPORT_DATA_KEY]: makeReport(REPORT_ID, POLICY_A), - }); + const snapshotData = createMock({}); + snapshotData[TRANSACTION_DATA_KEY_1] = makeTransaction(TRANSACTION_ID_1, REPORT_ID); + snapshotData[REPORT_DATA_KEY] = makeReport(REPORT_ID, POLICY_A); const mergedTransactions = withSnapshotTransactions(undefined, snapshotData); const mergedReports = withSnapshotReports(undefined, snapshotData); diff --git a/tests/unit/hooks/useAutoCreateSubmitWorkspace.test.ts b/tests/unit/hooks/useAutoCreateSubmitWorkspace.test.ts index a03d40f6009e..924fb06686de 100644 --- a/tests/unit/hooks/useAutoCreateSubmitWorkspace.test.ts +++ b/tests/unit/hooks/useAutoCreateSubmitWorkspace.test.ts @@ -17,8 +17,10 @@ import * as Report from '@userActions/Report'; import * as Welcome from '@userActions/Welcome'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy as PolicyType} from '@src/types/onyx'; -import type {UseOnyxResult} from 'react-native-onyx'; +import type {OnyxCollection, UseOnyxOptions, UseOnyxResult} from 'react-native-onyx'; import createMock from '../../utils/createMock'; @@ -32,11 +34,20 @@ jest.mock('@hooks/useLocalize'); jest.mock('@hooks/usePreferredPolicy'); jest.mock('@hooks/useOnboardingMessages'); -const mockTranslate: LocalizedTranslate = (...parameters) => parameters[0]; +const mockTranslate: LocalizedTranslate = (path, ...parameters) => { + return parameters.length > 0 ? path : path; +}; const mockFormatPhoneNumber = jest.fn((phone: string) => phone); const mockUseOnyx = jest.mocked(useOnyx); +type PolicySelectorResult = number | boolean | string | undefined; +type PolicyCollectionUseOnyxOptions = UseOnyxOptions; + +function isPolicyCollectionOptions(options: unknown): options is PolicyCollectionUseOnyxOptions { + return typeof options === 'object' && options !== null && 'selector' in options && typeof options.selector === 'function'; +} + const MOCK_SESSION = { accountID: 12345, email: 'test@expensify.com', @@ -70,10 +81,12 @@ function setupDefaultMocks() { localCurrencyCode: 'USD', }); - jest.mocked(useLocalize).mockReturnValue({ - translate: mockTranslate, - formatPhoneNumber: mockFormatPhoneNumber, - }); + jest.mocked(useLocalize).mockReturnValue( + createMock>({ + translate: mockTranslate, + formatPhoneNumber: mockFormatPhoneNumber, + }), + ); jest.mocked(usePreferredPolicy).mockReturnValue({ isRestrictedToPreferredPolicy: false, @@ -83,11 +96,13 @@ function setupDefaultMocks() { jest.mocked(useHasActiveAdminPolicies).mockReturnValue(false); - jest.mocked(useOnboardingMessages).mockReturnValue({ - onboardingMessages: { - [CONST.ONBOARDING_CHOICES.EMPLOYER]: MOCK_ONBOARDING_MESSAGE, - }, - }); + jest.mocked(useOnboardingMessages).mockReturnValue( + createMock>({ + onboardingMessages: { + [CONST.ONBOARDING_CHOICES.EMPLOYER]: MOCK_ONBOARDING_MESSAGE, + }, + }), + ); } describe('useAutoCreateSubmitWorkspace', () => { @@ -291,26 +306,26 @@ describe('useAutoCreateSubmitWorkspace', () => { it('navigates to the existing Submit workspace when an already-onboarded caller skips creation', async () => { // Given an already-onboarded user (the Submit plan welcome modal passes shouldCompleteOnboarding = false) // who is an editor/admin of an existing Submit workspace, so no new workspace should be created - const existingSubmitPolicy = { + const existingSubmitPolicy = createMock({ id: 'existing-submit-policy-id', type: CONST.POLICY.TYPE.SUBMIT, role: CONST.POLICY.ROLE.ADMIN, - }; - mockUseOnyx.mockImplementation((key: string, options?: {selector?: (policies: unknown) => unknown}) => { + }); + mockUseOnyx.mockImplementation((key: string, options?: unknown) => { if (key === 'session') { return [MOCK_SESSION, {status: 'loaded'}] satisfies UseOnyxResult; } if (key === 'betas') { return [[], {status: 'loaded'}] satisfies UseOnyxResult; } - if (key.startsWith('policy_')) { - // Run the hook's real selectors against a fake policy collection so both the - // hasEditableGroupPolicy and existingSubmitPolicyID subscriptions resolve correctly. - // Unrelated policy-collection selectors (e.g. useLastWorkspaceNumber's) expect extra - // arguments this mock doesn't provide, so fall back to undefined when they throw. + if (key === ONYXKEYS.COLLECTION.POLICY && isPolicyCollectionOptions(options)) { + const policyCollection: OnyxCollection = {[`${ONYXKEYS.COLLECTION.POLICY}${existingSubmitPolicy.id}`]: existingSubmitPolicy}; try { - const selectedPolicy = options?.selector?.({[`policy_${existingSubmitPolicy.id}`]: existingSubmitPolicy}); - return [selectedPolicy, {status: 'loaded'}] satisfies UseOnyxResult; + const selectedPolicy = options.selector?.(policyCollection); + if (selectedPolicy === null || selectedPolicy === undefined) { + return [undefined, {status: 'loaded'}] satisfies UseOnyxResult; + } + return [selectedPolicy, {status: 'loaded'}] satisfies UseOnyxResult>; } catch { return [undefined, {status: 'loaded'}] satisfies UseOnyxResult; } @@ -332,22 +347,26 @@ describe('useAutoCreateSubmitWorkspace', () => { it('keeps the Home fallback for onboarding callers when creation is skipped', async () => { // Given an onboarding user who already has an editable group workspace, so creation is skipped - const existingSubmitPolicy = { + const existingSubmitPolicy = createMock({ id: 'existing-submit-policy-id', type: CONST.POLICY.TYPE.SUBMIT, role: CONST.POLICY.ROLE.ADMIN, - }; - mockUseOnyx.mockImplementation((key: string, options?: {selector?: (policies: unknown) => unknown}) => { + }); + mockUseOnyx.mockImplementation((key: string, options?: unknown) => { if (key === 'session') { return [MOCK_SESSION, {status: 'loaded'}] satisfies UseOnyxResult; } if (key === 'betas') { return [[], {status: 'loaded'}] satisfies UseOnyxResult; } - if (key.startsWith('policy_')) { + if (key === ONYXKEYS.COLLECTION.POLICY && isPolicyCollectionOptions(options)) { + const policyCollection: OnyxCollection = {[`${ONYXKEYS.COLLECTION.POLICY}${existingSubmitPolicy.id}`]: existingSubmitPolicy}; try { - const selectedPolicy = options?.selector?.({[`policy_${existingSubmitPolicy.id}`]: existingSubmitPolicy}); - return [selectedPolicy, {status: 'loaded'}] satisfies UseOnyxResult; + const selectedPolicy = options.selector?.(policyCollection); + if (selectedPolicy === null || selectedPolicy === undefined) { + return [undefined, {status: 'loaded'}] satisfies UseOnyxResult; + } + return [selectedPolicy, {status: 'loaded'}] satisfies UseOnyxResult>; } catch { return [undefined, {status: 'loaded'}] satisfies UseOnyxResult; } From ea332a6781c6813b874687ac0446634857edbb3e Mon Sep 17 00:00:00 2001 From: KJ21-ENG <140263938+KJ21-ENG@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:23:25 +0530 Subject: [PATCH 3/4] tests: repair Onyx fixture typecheck findings --- tests/unit/PolicyUtilsTest.ts | 44 +- tests/unit/usePolicyIndicatorChecksTest.ts | 442 +++++++++------------ 2 files changed, 207 insertions(+), 279 deletions(-) diff --git a/tests/unit/PolicyUtilsTest.ts b/tests/unit/PolicyUtilsTest.ts index aafa7fac0d5c..d9ea9526657a 100644 --- a/tests/unit/PolicyUtilsTest.ts +++ b/tests/unit/PolicyUtilsTest.ts @@ -72,7 +72,7 @@ import ROUTES from '@src/ROUTES'; import type {PersonalDetailsList, Policy, PolicyEmployeeList, PolicyTagLists, Report, Transaction} from '@src/types/onyx'; import type {Connections, QBONonReimbursableExportAccountType, SageIntacctExportConfig} from '@src/types/onyx/Policy'; -import type {OnyxCollection, OnyxEntry, OnyxMultiSetInput} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; @@ -819,12 +819,8 @@ describe('PolicyUtils', () => { category: '', reportID: expenseReport.reportID, }; - await Onyx.multiSet( - createMock({ - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`]: transaction2, - }), - ); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`, transaction1); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`, transaction2); expect(getSubmitToAccountID(policy, expenseReport, employeeEmail)).toBe(categoryApprover1AccountID); }); it('should return default approver if rule approver is submitter and prevent self approval is enabled', async () => { @@ -880,12 +876,8 @@ describe('PolicyUtils', () => { created: DateUtils.subtractMillisecondsFromDateTime(testDate, 1), reportID: expenseReport.reportID, }; - await Onyx.multiSet( - createMock({ - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`]: transaction2, - }), - ); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`, transaction1); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`, transaction2); expect(getSubmitToAccountID(policy, expenseReport, employeeEmail)).toBe(categoryApprover2AccountID); }); it('should return the first rule approver who is not the current submitter', async () => { @@ -927,13 +919,9 @@ describe('PolicyUtils', () => { created: testDate, }; - await Onyx.multiSet( - createMock({ - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`]: transaction2, - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction3.transactionID}`]: transaction3, - }), - ); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`, transaction1); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`, transaction2); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction3.transactionID}`, transaction3); expect(getSubmitToAccountID(policy, expenseReport, categoryApprover1Email)).toBe(tagApprover1AccountID); }); @@ -967,12 +955,8 @@ describe('PolicyUtils', () => { created: DateUtils.subtractMillisecondsFromDateTime(testDate, 1), reportID: expenseReport.reportID, }; - await Onyx.multiSet( - createMock({ - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`]: transaction2, - }), - ); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`, transaction1); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`, transaction2); expect(getSubmitToAccountID(policy, expenseReport, employeeEmail)).toBe(tagApprover1AccountID); }); it('should return the tag approver of the first transaction sorted by created if we have many transaction tags match with the tag approver rule', async () => { @@ -1004,12 +988,8 @@ describe('PolicyUtils', () => { created: DateUtils.subtractMillisecondsFromDateTime(testDate, 1), reportID: expenseReport.reportID, }; - await Onyx.multiSet( - createMock({ - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`]: transaction2, - }), - ); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`, transaction1); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`, transaction2); expect(getSubmitToAccountID(policy, expenseReport, employeeEmail)).toBe(tagApprover2AccountID); }); }); diff --git a/tests/unit/usePolicyIndicatorChecksTest.ts b/tests/unit/usePolicyIndicatorChecksTest.ts index 38d70d224d1c..a13150d3fd50 100644 --- a/tests/unit/usePolicyIndicatorChecksTest.ts +++ b/tests/unit/usePolicyIndicatorChecksTest.ts @@ -5,8 +5,7 @@ import usePolicyIndicatorChecks from '@hooks/usePolicyIndicatorChecks'; import CONST from '@src/CONST'; import initOnyxDerivedValues from '@src/libs/actions/OnyxDerived'; import ONYXKEYS from '@src/ONYXKEYS'; - -import type {OnyxMultiSetInput} from 'react-native-onyx'; +import type {CustomUnit} from '@src/types/onyx/Policy'; import Onyx from 'react-native-onyx'; @@ -22,6 +21,11 @@ const WORKSPACE = { policyName: 'Test Workspace', }; +// The backend stores custom-unit errors under the reserved `customUnits.errors` key, although the typed policy map models valid custom units only. +function createCustomUnitErrorFixture(customUnitError: string): CustomUnit & {customUnitError: string} { + return Object.assign(createMock({}), {customUnitError}); +} + describe('usePolicyIndicatorChecks', () => { beforeAll(() => { Onyx.init({ @@ -33,19 +37,15 @@ describe('usePolicyIndicatorChecks', () => { describe('policy error statuses', () => { it('returns HAS_POLICY_ERRORS when policy has errors', async () => { await act(async () => { - await Onyx.multiSet( - createMock({ - [ONYXKEYS.SESSION]: {email: userID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - errors: {policyError: 'Something went wrong'}, - }, - }), - ); + await Onyx.set(ONYXKEYS.SESSION, {email: userID}); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + errors: {policyError: 'Something went wrong'}, + }); await waitForBatchedUpdatesWithAct(); }); @@ -58,19 +58,15 @@ describe('usePolicyIndicatorChecks', () => { it('returns HAS_CUSTOM_UNITS_ERROR when custom units have errors', async () => { await act(async () => { - await Onyx.multiSet( - createMock({ - [ONYXKEYS.SESSION]: {email: userID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - customUnits: {errors: {customUnitError: 'Invalid custom unit'}}, - }, - }), - ); + await Onyx.set(ONYXKEYS.SESSION, {email: userID}); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + customUnits: {errors: createCustomUnitErrorFixture('Invalid custom unit')}, + }); await waitForBatchedUpdatesWithAct(); }); @@ -83,24 +79,20 @@ describe('usePolicyIndicatorChecks', () => { it('returns HAS_EMPLOYEE_LIST_ERROR when employee list has errors', async () => { await act(async () => { - await Onyx.multiSet( - createMock({ - [ONYXKEYS.SESSION]: {email: userID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - employeeList: { - [otherUserID]: { - email: otherUserID, - errors: {employeeError: 'Employee error'}, - }, - }, + await Onyx.set(ONYXKEYS.SESSION, {email: userID}); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + employeeList: { + [otherUserID]: { + email: otherUserID, + errors: {employeeError: 'Employee error'}, }, - }), - ); + }, + }); await waitForBatchedUpdatesWithAct(); }); @@ -113,31 +105,27 @@ describe('usePolicyIndicatorChecks', () => { it('returns HAS_SYNC_ERRORS when sync has errors', async () => { await act(async () => { - await Onyx.multiSet( - createMock({ - [ONYXKEYS.SESSION]: {email: userID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - connections: { - quickbooksOnline: { - lastSync: { - errorMessage: 'Sync failed', - isSuccessful: false, - errorDate: new Date().toISOString(), - }, - }, + await Onyx.set(ONYXKEYS.SESSION, {email: userID}); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + connections: { + quickbooksOnline: { + lastSync: { + errorMessage: 'Sync failed', + isSuccessful: false, + errorDate: new Date().toISOString(), }, }, - [`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${WORKSPACE.policyID}` as const]: { - stageInProgress: null, - connectionName: 'quickbooksOnline', - }, - }), - ); + }, + }); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${WORKSPACE.policyID}` as const, { + stageInProgress: null, + connectionName: 'quickbooksOnline', + }); await waitForBatchedUpdatesWithAct(); }); @@ -150,26 +138,22 @@ describe('usePolicyIndicatorChecks', () => { it('returns HAS_QBO_EXPORT_ERROR when QBO export has errors', async () => { await act(async () => { - await Onyx.multiSet( - createMock({ - [ONYXKEYS.SESSION]: {email: userID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - connections: { - quickbooksOnline: { - config: { - reimbursableExpensesExportDestination: 'VENDOR_BILL', - reimbursableExpensesAccount: undefined, - }, - }, + await Onyx.set(ONYXKEYS.SESSION, {email: userID}); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + connections: { + quickbooksOnline: { + config: { + reimbursableExpensesExportDestination: CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL, + reimbursableExpensesAccount: undefined, }, }, - }), - ); + }, + }); await waitForBatchedUpdatesWithAct(); }); @@ -182,21 +166,17 @@ describe('usePolicyIndicatorChecks', () => { it('returns HAS_UBER_CREDENTIALS_ERROR when Uber credentials have errors', async () => { await act(async () => { - await Onyx.multiSet( - createMock({ - [ONYXKEYS.SESSION]: {email: userID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - receiptPartners: { - uber: {error: 'Invalid Uber credentials'}, - }, - }, - }), - ); + await Onyx.set(ONYXKEYS.SESSION, {email: userID}); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + receiptPartners: { + uber: {error: 'Invalid Uber credentials'}, + }, + }); await waitForBatchedUpdatesWithAct(); }); @@ -209,25 +189,21 @@ describe('usePolicyIndicatorChecks', () => { it('returns HAS_POLICY_ADMIN_CARD_FEED_ERRORS when admin has card feed errors', async () => { await act(async () => { - await Onyx.multiSet( - createMock({ - [ONYXKEYS.SESSION]: {email: userID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - }, - [ONYXKEYS.CARD_LIST]: { - card1: { - bank: CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE, - fundID: String(WORKSPACE.policyAccountID), - lastScrapeResult: 403, - }, - }, - }), - ); + await Onyx.set(ONYXKEYS.SESSION, {email: userID}); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + }); + await Onyx.set(ONYXKEYS.CARD_LIST, { + card1: { + bank: CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE, + fundID: String(WORKSPACE.policyAccountID), + lastScrapeResult: 403, + }, + }); await waitForBatchedUpdatesWithAct(); }); @@ -247,26 +223,22 @@ describe('usePolicyIndicatorChecks', () => { it('returns HAS_MERGE_HR_SETUP_NEEDED when merge HR setup is needed', async () => { await act(async () => { - await Onyx.multiSet( - createMock({ - [ONYXKEYS.SESSION]: {email: userID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - connections: { - [CONST.POLICY.CONNECTIONS.NAME.MERGE_HR]: { - config: {integration: 'workday'}, - data: {groups: [{id: 'g1', name: 'Eng', type: 'Department'}]}, - lastSync: {syncStatus: CONST.MERGE_HR.SYNC_STATUS.DONE}, - }, - }, + await Onyx.set(ONYXKEYS.SESSION, {email: userID}); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + connections: { + [CONST.POLICY.CONNECTIONS.NAME.MERGE_HR]: { + config: {integration: 'workday'}, + data: {groups: [{id: 'g1', name: 'Eng', type: 'Department'}]}, + lastSync: {syncStatus: CONST.MERGE_HR.SYNC_STATUS.DONE}, }, - [ONYXKEYS.CARD_LIST]: {}, - }), - ); + }, + }); + await Onyx.set(ONYXKEYS.CARD_LIST, {}); await waitForBatchedUpdatesWithAct(); }); @@ -280,26 +252,22 @@ describe('usePolicyIndicatorChecks', () => { it('does not return HAS_MERGE_HR_SETUP_NEEDED for non-admin users', async () => { await act(async () => { - await Onyx.multiSet( - createMock({ - [ONYXKEYS.SESSION]: {email: otherUserID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'user', - policyAccountID: WORKSPACE.policyAccountID, - connections: { - [CONST.POLICY.CONNECTIONS.NAME.MERGE_HR]: { - config: {integration: 'workday'}, - data: {groups: [{id: 'g1', name: 'Eng', type: 'Department'}]}, - lastSync: {syncStatus: CONST.MERGE_HR.SYNC_STATUS.DONE}, - }, - }, + await Onyx.set(ONYXKEYS.SESSION, {email: otherUserID}); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'user', + policyAccountID: WORKSPACE.policyAccountID, + connections: { + [CONST.POLICY.CONNECTIONS.NAME.MERGE_HR]: { + config: {integration: 'workday'}, + data: {groups: [{id: 'g1', name: 'Eng', type: 'Department'}]}, + lastSync: {syncStatus: CONST.MERGE_HR.SYNC_STATUS.DONE}, }, - [ONYXKEYS.CARD_LIST]: {}, - }), - ); + }, + }); + await Onyx.set(ONYXKEYS.CARD_LIST, {}); await waitForBatchedUpdatesWithAct(); }); @@ -319,13 +287,9 @@ describe('usePolicyIndicatorChecks', () => { it('returns HAS_DOMAIN_ERRORS when domain has errors', async () => { await act(async () => { - await Onyx.multiSet( - createMock({ - [`${ONYXKEYS.COLLECTION.DOMAIN_ERRORS}domain1` as const]: { - errors: {domainError: 'Domain error'}, - }, - }), - ); + await Onyx.set(`${ONYXKEYS.COLLECTION.DOMAIN_ERRORS}domain1` as const, { + errors: {domainError: 'Domain error'}, + }); await waitForBatchedUpdatesWithAct(); }); @@ -341,19 +305,15 @@ describe('usePolicyIndicatorChecks', () => { await Onyx.clear(); await waitForBatchedUpdatesWithAct(); await act(async () => { - await Onyx.multiSet( - createMock({ - [ONYXKEYS.SESSION]: {email: userID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - }, - [ONYXKEYS.CARD_LIST]: {}, - }), - ); + await Onyx.set(ONYXKEYS.SESSION, {email: userID}); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + }); + await Onyx.set(ONYXKEYS.CARD_LIST, {}); await waitForBatchedUpdatesWithAct(); }); }); @@ -389,27 +349,23 @@ describe('usePolicyIndicatorChecks', () => { describe('error priority', () => { beforeAll(async () => { await act(async () => { - await Onyx.multiSet( - createMock({ - [ONYXKEYS.SESSION]: {email: userID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - errors: {policyError: 'Policy error'}, - customUnits: {errors: {customUnitError: 'Custom unit error'}}, - employeeList: { - [otherUserID]: { - email: otherUserID, - errors: {employeeError: 'Employee error'}, - }, - }, + await Onyx.set(ONYXKEYS.SESSION, {email: userID}); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + errors: {policyError: 'Policy error'}, + customUnits: {errors: createCustomUnitErrorFixture('Custom unit error')}, + employeeList: { + [otherUserID]: { + email: otherUserID, + errors: {employeeError: 'Employee error'}, }, - [ONYXKEYS.CARD_LIST]: {}, - }), - ); + }, + }); + await Onyx.set(ONYXKEYS.CARD_LIST, {}); await waitForBatchedUpdatesWithAct(); }); }); @@ -432,37 +388,33 @@ describe('usePolicyIndicatorChecks', () => { it('returns both sync error and merge HR setup statuses when policy has both conditions', async () => { await act(async () => { - await Onyx.multiSet( - createMock({ - [ONYXKEYS.SESSION]: {email: userID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - connections: { - quickbooksOnline: { - lastSync: { - errorMessage: 'Sync failed', - isSuccessful: false, - errorDate: new Date().toISOString(), - }, - }, - [CONST.POLICY.CONNECTIONS.NAME.MERGE_HR]: { - config: {integration: 'workday'}, - data: {groups: [{id: 'g1', name: 'Eng', type: 'Department'}]}, - lastSync: {syncStatus: CONST.MERGE_HR.SYNC_STATUS.DONE}, - }, + await Onyx.set(ONYXKEYS.SESSION, {email: userID}); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + connections: { + quickbooksOnline: { + lastSync: { + errorMessage: 'Sync failed', + isSuccessful: false, + errorDate: new Date().toISOString(), }, }, - [`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${WORKSPACE.policyID}` as const]: { - stageInProgress: null, - connectionName: 'quickbooksOnline', + [CONST.POLICY.CONNECTIONS.NAME.MERGE_HR]: { + config: {integration: 'workday'}, + data: {groups: [{id: 'g1', name: 'Eng', type: 'Department'}]}, + lastSync: {syncStatus: CONST.MERGE_HR.SYNC_STATUS.DONE}, }, - [ONYXKEYS.CARD_LIST]: {}, - }), - ); + }, + }); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${WORKSPACE.policyID}` as const, { + stageInProgress: null, + connectionName: 'quickbooksOnline', + }); + await Onyx.set(ONYXKEYS.CARD_LIST, {}); await waitForBatchedUpdatesWithAct(); }); @@ -484,27 +436,23 @@ describe('usePolicyIndicatorChecks', () => { beforeAll(async () => { await act(async () => { - await Onyx.multiSet( - createMock({ - [ONYXKEYS.SESSION]: {email: userID}, - [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const]: { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - }, - [`${ONYXKEYS.COLLECTION.POLICY}${SECOND_WORKSPACE.policyID}` as const]: { - id: SECOND_WORKSPACE.policyID, - name: SECOND_WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: SECOND_WORKSPACE.policyAccountID, - errors: {policyError: 'Something went wrong'}, - }, - [ONYXKEYS.CARD_LIST]: {}, - }), - ); + await Onyx.set(ONYXKEYS.SESSION, {email: userID}); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + }); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${SECOND_WORKSPACE.policyID}` as const, { + id: SECOND_WORKSPACE.policyID, + name: SECOND_WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: SECOND_WORKSPACE.policyAccountID, + errors: {policyError: 'Something went wrong'}, + }); + await Onyx.set(ONYXKEYS.CARD_LIST, {}); await waitForBatchedUpdatesWithAct(); }); }); From 51ac7272dc9ca1df94a27e1aeaf10ddc6ef364a8 Mon Sep 17 00:00:00 2001 From: KJ21-ENG <140263938+KJ21-ENG@users.noreply.github.com> Date: Fri, 31 Jul 2026 03:06:33 +0530 Subject: [PATCH 4/4] tests: finalize cleanup verification repairs --- tests/navigation/getStateFromPathTests.ts | 20 +- tests/unit/FileUtilsTest.ts | 12 +- tests/unit/PolicyUtilsTest.ts | 39 +- .../useAutoCreateSubmitWorkspace.test.ts | 144 ++---- tests/unit/usePolicyIndicatorChecksTest.ts | 416 ++++++++++-------- 5 files changed, 327 insertions(+), 304 deletions(-) diff --git a/tests/navigation/getStateFromPathTests.ts b/tests/navigation/getStateFromPathTests.ts index fd0154902754..1becc39e3149 100644 --- a/tests/navigation/getStateFromPathTests.ts +++ b/tests/navigation/getStateFromPathTests.ts @@ -98,6 +98,7 @@ describe('getStateFromPath', () => { const dynamicMultiSegState = {routes: [{name: 'DynamicMultiSegScreen', params: focusedRouteParams}]}; const dynamicMultiSegLayerState = {routes: [{name: 'DynamicMultiSegLayerScreen'}]}; const dynamicWildcardState = {routes: [{name: 'DynamicWildcardScreen'}]}; + const unknownDynamicRouteState = createMock({routes: [{name: 'UnknownDynamic'}]}); function getSyntheticDynamicRouteState(dynamicRouteKey: string): DynamicRouteState { if (!isSyntheticDynamicRouteKey(dynamicRouteKey)) { @@ -118,18 +119,21 @@ describe('getStateFromPath', () => { case 'SUFFIX_B_UNAUTHORIZED': case 'AMBIGUOUS_STATIC': case 'TAG_SETTINGS_PARAM': - return createMock({routes: [{name: 'UnknownDynamic'}]}); - default: + return unknownDynamicRouteState; + default: { + const exhaustiveDynamicRouteKey: never = dynamicRouteKey; + String(exhaustiveDynamicRouteKey); throw new Error('Missing synthetic mock implementation for dynamic route key'); + } } } + const defaultDynamicRouteImplementation = (_path: DynamicRoutePath, dynamicRouteKey: DynamicRouteKey): DynamicRouteState => getSyntheticDynamicRouteState(String(dynamicRouteKey)); + beforeEach(() => { jest.clearAllMocks(); mockRNGetStateFromPath.mockReturnValue(baseRouteState); - mockGetStateForDynamicRoute.mockImplementation( - (_path: DynamicRoutePath, dynamicRouteKey: DynamicRouteKey): DynamicRouteState => getSyntheticDynamicRouteState(String(dynamicRouteKey)), - ); + mockGetStateForDynamicRoute.mockImplementation(defaultDynamicRouteImplementation); }); it('should delegate to RN getStateFromPath for standard routes (non-dynamic)', () => { @@ -241,10 +245,10 @@ describe('getStateFromPath', () => { const ambiguousStaticState = {routes: [{name: 'AmbiguousStaticScreen'}]}; beforeEach(() => { - mockGetStateForDynamicRoute.mockImplementation((_path: DynamicRoutePath, dynamicRouteKey: DynamicRouteKey): DynamicRouteState => { + mockGetStateForDynamicRoute.mockImplementation((path: DynamicRoutePath, dynamicRouteKey: DynamicRouteKey): DynamicRouteState => { const syntheticDynamicRouteKey = String(dynamicRouteKey); if (!isSyntheticDynamicRouteKey(syntheticDynamicRouteKey)) { - throw new Error(`Unexpected production dynamic route key in ambiguous synthetic mock: ${syntheticDynamicRouteKey}`); + return defaultDynamicRouteImplementation(path, dynamicRouteKey); } switch (syntheticDynamicRouteKey) { @@ -253,7 +257,7 @@ describe('getStateFromPath', () => { case 'TAG_SETTINGS_PARAM': return createMock(tagSettingsParamState); default: - throw new Error(`Missing ambiguous synthetic mock implementation for dynamic route key: ${syntheticDynamicRouteKey}`); + return defaultDynamicRouteImplementation(path, dynamicRouteKey); } }); }); diff --git a/tests/unit/FileUtilsTest.ts b/tests/unit/FileUtilsTest.ts index f214e4a40344..380ee4ad5b5e 100644 --- a/tests/unit/FileUtilsTest.ts +++ b/tests/unit/FileUtilsTest.ts @@ -120,11 +120,13 @@ describe('FileUtils', () => { }); describe('canvasFallback', () => { - type Canvas2DGetContext = (contextId: '2d', options?: CanvasRenderingContext2DSettings) => CanvasRenderingContext2D | null; - type Canvas2DCanvas = Pick & {getContext: Canvas2DGetContext}; + type Canvas2DCanvas = Pick & { + getContext: (contextId: '2d', options?: CanvasRenderingContext2DSettings) => CanvasRenderingContext2D | null; + }; const mockCreateImageBitmap = jest.fn, Parameters>(); - const mockGetContext = jest.fn, Parameters>(() => null); + const mockGetContext = jest.fn, Parameters>(() => null); + const mockCloseImageBitmap = jest.fn, Parameters>(); const mockCanvas = createMock({ width: 0, height: 0, @@ -154,7 +156,7 @@ describe('FileUtils', () => { createMock>>({ width: 1000, height: 800, - close: jest.fn(), + close: mockCloseImageBitmap, }), ); }); @@ -188,7 +190,7 @@ describe('FileUtils', () => { it('should scale down large images', async () => { const blob = new Blob(['test'], {type: 'image/heic'}); - const mockImageBitmap = createMock>>({width: 8192, height: 4000, close: jest.fn()}); + const mockImageBitmap = createMock>>({width: 8192, height: 4000, close: mockCloseImageBitmap}); mockCreateImageBitmap.mockResolvedValue(mockImageBitmap); const mockBlob = new Blob(['converted'], {type: 'image/jpeg'}); diff --git a/tests/unit/PolicyUtilsTest.ts b/tests/unit/PolicyUtilsTest.ts index d9ea9526657a..e854db0c4d81 100644 --- a/tests/unit/PolicyUtilsTest.ts +++ b/tests/unit/PolicyUtilsTest.ts @@ -71,6 +71,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {PersonalDetailsList, Policy, PolicyEmployeeList, PolicyTagLists, Report, Transaction} from '@src/types/onyx'; import type {Connections, QBONonReimbursableExportAccountType, SageIntacctExportConfig} from '@src/types/onyx/Policy'; +import type {TransactionCollectionDataSet} from '@src/types/onyx/Transaction'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; @@ -87,6 +88,7 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct' import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; const CARLOS_EMAIL = 'cmartins@expensifail.com'; + function toLocaleDigitMock(dot: string): string { return dot; } @@ -819,8 +821,11 @@ describe('PolicyUtils', () => { category: '', reportID: expenseReport.reportID, }; - await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`, transaction1); - await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`, transaction2); + const transactionData: TransactionCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`]: transaction2, + }; + await Onyx.multiSet({...transactionData}); expect(getSubmitToAccountID(policy, expenseReport, employeeEmail)).toBe(categoryApprover1AccountID); }); it('should return default approver if rule approver is submitter and prevent self approval is enabled', async () => { @@ -876,8 +881,11 @@ describe('PolicyUtils', () => { created: DateUtils.subtractMillisecondsFromDateTime(testDate, 1), reportID: expenseReport.reportID, }; - await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`, transaction1); - await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`, transaction2); + const transactionData: TransactionCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`]: transaction2, + }; + await Onyx.multiSet({...transactionData}); expect(getSubmitToAccountID(policy, expenseReport, employeeEmail)).toBe(categoryApprover2AccountID); }); it('should return the first rule approver who is not the current submitter', async () => { @@ -919,9 +927,12 @@ describe('PolicyUtils', () => { created: testDate, }; - await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`, transaction1); - await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`, transaction2); - await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction3.transactionID}`, transaction3); + const transactionData: TransactionCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`]: transaction2, + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction3.transactionID}`]: transaction3, + }; + await Onyx.multiSet({...transactionData}); expect(getSubmitToAccountID(policy, expenseReport, categoryApprover1Email)).toBe(tagApprover1AccountID); }); @@ -955,8 +966,11 @@ describe('PolicyUtils', () => { created: DateUtils.subtractMillisecondsFromDateTime(testDate, 1), reportID: expenseReport.reportID, }; - await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`, transaction1); - await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`, transaction2); + const transactionData: TransactionCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`]: transaction2, + }; + await Onyx.multiSet({...transactionData}); expect(getSubmitToAccountID(policy, expenseReport, employeeEmail)).toBe(tagApprover1AccountID); }); it('should return the tag approver of the first transaction sorted by created if we have many transaction tags match with the tag approver rule', async () => { @@ -988,8 +1002,11 @@ describe('PolicyUtils', () => { created: DateUtils.subtractMillisecondsFromDateTime(testDate, 1), reportID: expenseReport.reportID, }; - await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`, transaction1); - await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`, transaction2); + const transactionData: TransactionCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`]: transaction2, + }; + await Onyx.multiSet({...transactionData}); expect(getSubmitToAccountID(policy, expenseReport, employeeEmail)).toBe(tagApprover2AccountID); }); }); diff --git a/tests/unit/hooks/useAutoCreateSubmitWorkspace.test.ts b/tests/unit/hooks/useAutoCreateSubmitWorkspace.test.ts index 924fb06686de..b0593776bc83 100644 --- a/tests/unit/hooks/useAutoCreateSubmitWorkspace.test.ts +++ b/tests/unit/hooks/useAutoCreateSubmitWorkspace.test.ts @@ -7,7 +7,6 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useHasActiveAdminPolicies from '@hooks/useHasActiveAdminPolicies'; import useLocalize from '@hooks/useLocalize'; import useOnboardingMessages from '@hooks/useOnboardingMessages'; -import useOnyx from '@hooks/useOnyx'; import usePreferredPolicy from '@hooks/usePreferredPolicy'; import * as navigateAfterOnboarding from '@libs/navigateAfterOnboarding'; @@ -19,14 +18,12 @@ import * as Welcome from '@userActions/Welcome'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy as PolicyType} from '@src/types/onyx'; +import type Session from '@src/types/onyx/Session'; -import type {OnyxCollection, UseOnyxOptions, UseOnyxResult} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; import createMock from '../../utils/createMock'; - -jest.mock('@hooks/useOnyx', () => { - return {__esModule: true, default: jest.fn(() => [undefined])}; -}); +import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; jest.mock('@hooks/useCurrentUserPersonalDetails'); jest.mock('@hooks/useHasActiveAdminPolicies'); @@ -39,19 +36,12 @@ const mockTranslate: LocalizedTranslate = (path, ...parameters) => { }; const mockFormatPhoneNumber = jest.fn((phone: string) => phone); -const mockUseOnyx = jest.mocked(useOnyx); - -type PolicySelectorResult = number | boolean | string | undefined; -type PolicyCollectionUseOnyxOptions = UseOnyxOptions; - -function isPolicyCollectionOptions(options: unknown): options is PolicyCollectionUseOnyxOptions { - return typeof options === 'object' && options !== null && 'selector' in options && typeof options.selector === 'function'; -} +type MockSession = Session & Required>; -const MOCK_SESSION = { +const MOCK_SESSION = createMock({ accountID: 12345, email: 'test@expensify.com', -}; +}); const MOCK_POLICY_ID = 'mock-policy-id'; const MOCK_ADMINS_CHAT_REPORT_ID = 'mock-admins-chat-report-id'; @@ -62,19 +52,6 @@ const MOCK_ONBOARDING_MESSAGE = createMock { - if (key === 'session') { - return [MOCK_SESSION, {status: 'loaded'}] satisfies UseOnyxResult; - } - if (key === 'betas') { - return [[], {status: 'loaded'}] satisfies UseOnyxResult; - } - if (key.startsWith('policy_')) { - return [false, {status: 'loaded'}] satisfies UseOnyxResult; - } - return [undefined, {status: 'loaded'}] satisfies UseOnyxResult; - }); - jest.mocked(useCurrentUserPersonalDetails).mockReturnValue({ accountID: MOCK_SESSION.accountID, login: MOCK_SESSION.email, @@ -117,8 +94,18 @@ describe('useAutoCreateSubmitWorkspace', () => { const setOnboardingPolicyIDSpy = jest.spyOn(Welcome, 'setOnboardingPolicyID').mockImplementation(jest.fn()); const navigateSpy = jest.spyOn(navigateAfterOnboarding, 'navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue').mockImplementation(jest.fn()); - beforeEach(() => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + }); + + beforeEach(async () => { jest.clearAllMocks(); + await Onyx.clear(); + await Onyx.multiSet({ + [ONYXKEYS.SESSION]: MOCK_SESSION, + [ONYXKEYS.BETAS]: [], + }); + await waitForBatchedUpdates(); setupDefaultMocks(); }); @@ -195,30 +182,17 @@ describe('useAutoCreateSubmitWorkspace', () => { expect(navigateSpy).toHaveBeenCalledWith(MOCK_POLICY_ID, expect.any(Boolean)); }); - it('reuses the existing onboarding workspace instead of creating a new one', () => { + it('reuses the existing onboarding workspace instead of creating a new one', async () => { // Given a user who already has an onboardingPolicyID set (e.g. assigned by an admin // or from a previous partial onboarding attempt) const existingPolicyID = 'existing-policy-id'; const existingAdminsReportID = 'existing-admins-report-id'; - mockUseOnyx.mockImplementation((key: string) => { - if (key === 'onboardingPolicyID') { - return [existingPolicyID, {status: 'loaded'}] satisfies UseOnyxResult; - } - if (key === 'onboardingAdminsChatReportID') { - return [existingAdminsReportID, {status: 'loaded'}] satisfies UseOnyxResult; - } - if (key === 'session') { - return [MOCK_SESSION, {status: 'loaded'}] satisfies UseOnyxResult; - } - if (key === 'betas') { - return [[], {status: 'loaded'}] satisfies UseOnyxResult; - } - if (key.startsWith('policy_')) { - return [false, {status: 'loaded'}] satisfies UseOnyxResult; - } - return [undefined, {status: 'loaded'}] satisfies UseOnyxResult; + await Onyx.multiSet({ + [ONYXKEYS.ONBOARDING_POLICY_ID]: existingPolicyID, + [ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID]: existingAdminsReportID, }); + await waitForBatchedUpdates(); // When the onboarding flow runs const {result} = renderHook(() => useAutoCreateSubmitWorkspace()); @@ -235,26 +209,16 @@ describe('useAutoCreateSubmitWorkspace', () => { ); }); - it('skips workspace creation when the user is already a paid group policy admin', () => { + it('skips workspace creation when the user is already a paid group policy admin', async () => { // Given a user who is already an admin of a paid group policy - mockUseOnyx.mockImplementation((key: string) => { - if (key === 'session') { - return [MOCK_SESSION, {status: 'loaded'}] satisfies UseOnyxResult; - } - if (key === 'betas') { - return [[], {status: 'loaded'}] satisfies UseOnyxResult; - } - if (key === 'onboardingPolicyID') { - return [undefined, {status: 'loaded'}] satisfies UseOnyxResult; - } - if (key === 'onboardingAdminsChatReportID') { - return [undefined, {status: 'loaded'}] satisfies UseOnyxResult; - } - if (key.startsWith('policy_')) { - return [true, {status: 'loaded'}] satisfies UseOnyxResult; - } - return [undefined, {status: 'loaded'}] satisfies UseOnyxResult; + const existingPaidPolicy = createMock({ + id: 'existing-paid-policy-id', + name: 'Existing Paid Workspace', + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.ADMIN, }); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${existingPaidPolicy.id}`, existingPaidPolicy); + await waitForBatchedUpdates(); // When onboarding completes const {result} = renderHook(() => useAutoCreateSubmitWorkspace()); @@ -308,30 +272,12 @@ describe('useAutoCreateSubmitWorkspace', () => { // who is an editor/admin of an existing Submit workspace, so no new workspace should be created const existingSubmitPolicy = createMock({ id: 'existing-submit-policy-id', + name: 'Existing Submit Workspace', type: CONST.POLICY.TYPE.SUBMIT, role: CONST.POLICY.ROLE.ADMIN, }); - mockUseOnyx.mockImplementation((key: string, options?: unknown) => { - if (key === 'session') { - return [MOCK_SESSION, {status: 'loaded'}] satisfies UseOnyxResult; - } - if (key === 'betas') { - return [[], {status: 'loaded'}] satisfies UseOnyxResult; - } - if (key === ONYXKEYS.COLLECTION.POLICY && isPolicyCollectionOptions(options)) { - const policyCollection: OnyxCollection = {[`${ONYXKEYS.COLLECTION.POLICY}${existingSubmitPolicy.id}`]: existingSubmitPolicy}; - try { - const selectedPolicy = options.selector?.(policyCollection); - if (selectedPolicy === null || selectedPolicy === undefined) { - return [undefined, {status: 'loaded'}] satisfies UseOnyxResult; - } - return [selectedPolicy, {status: 'loaded'}] satisfies UseOnyxResult>; - } catch { - return [undefined, {status: 'loaded'}] satisfies UseOnyxResult; - } - } - return [undefined, {status: 'loaded'}] satisfies UseOnyxResult; - }); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${existingSubmitPolicy.id}`, existingSubmitPolicy); + await waitForBatchedUpdates(); // When the user confirms the Submit plan welcome modal again const {result} = renderHook(() => useAutoCreateSubmitWorkspace()); @@ -349,30 +295,12 @@ describe('useAutoCreateSubmitWorkspace', () => { // Given an onboarding user who already has an editable group workspace, so creation is skipped const existingSubmitPolicy = createMock({ id: 'existing-submit-policy-id', + name: 'Existing Submit Workspace', type: CONST.POLICY.TYPE.SUBMIT, role: CONST.POLICY.ROLE.ADMIN, }); - mockUseOnyx.mockImplementation((key: string, options?: unknown) => { - if (key === 'session') { - return [MOCK_SESSION, {status: 'loaded'}] satisfies UseOnyxResult; - } - if (key === 'betas') { - return [[], {status: 'loaded'}] satisfies UseOnyxResult; - } - if (key === ONYXKEYS.COLLECTION.POLICY && isPolicyCollectionOptions(options)) { - const policyCollection: OnyxCollection = {[`${ONYXKEYS.COLLECTION.POLICY}${existingSubmitPolicy.id}`]: existingSubmitPolicy}; - try { - const selectedPolicy = options.selector?.(policyCollection); - if (selectedPolicy === null || selectedPolicy === undefined) { - return [undefined, {status: 'loaded'}] satisfies UseOnyxResult; - } - return [selectedPolicy, {status: 'loaded'}] satisfies UseOnyxResult>; - } catch { - return [undefined, {status: 'loaded'}] satisfies UseOnyxResult; - } - } - return [undefined, {status: 'loaded'}] satisfies UseOnyxResult; - }); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${existingSubmitPolicy.id}`, existingSubmitPolicy); + await waitForBatchedUpdates(); // When the onboarding flow runs (shouldCompleteOnboarding defaults to true) const {result} = renderHook(() => useAutoCreateSubmitWorkspace()); diff --git a/tests/unit/usePolicyIndicatorChecksTest.ts b/tests/unit/usePolicyIndicatorChecksTest.ts index a13150d3fd50..eabc6c707fb0 100644 --- a/tests/unit/usePolicyIndicatorChecksTest.ts +++ b/tests/unit/usePolicyIndicatorChecksTest.ts @@ -5,7 +5,11 @@ import usePolicyIndicatorChecks from '@hooks/usePolicyIndicatorChecks'; import CONST from '@src/CONST'; import initOnyxDerivedValues from '@src/libs/actions/OnyxDerived'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy} from '@src/types/onyx'; import type {CustomUnit} from '@src/types/onyx/Policy'; +import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; + +import type {OnyxInputValue, OnyxMultiSetInput} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; @@ -22,8 +26,17 @@ const WORKSPACE = { }; // The backend stores custom-unit errors under the reserved `customUnits.errors` key, although the typed policy map models valid custom units only. -function createCustomUnitErrorFixture(customUnitError: string): CustomUnit & {customUnitError: string} { - return Object.assign(createMock({}), {customUnitError}); +type CustomUnitErrorMap = NonNullable; +type PolicyCollectionKey = `${typeof ONYXKEYS.COLLECTION.POLICY}${string}`; +type PolicyWithCustomUnitError = Omit & {customUnits: {errors: CustomUnitErrorMap}}; +type MalformedPolicyCollectionDataSet = Record>; + +function createCustomUnitErrorFixture(customUnitError: string): CustomUnitErrorMap { + return {customUnitError}; +} + +function createPolicyCollectionDataSet(policy: Policy) { + return toCollectionDataSet(ONYXKEYS.COLLECTION.POLICY, [policy], (item) => item.id); } describe('usePolicyIndicatorChecks', () => { @@ -37,15 +50,20 @@ describe('usePolicyIndicatorChecks', () => { describe('policy error statuses', () => { it('returns HAS_POLICY_ERRORS when policy has errors', async () => { await act(async () => { - await Onyx.set(ONYXKEYS.SESSION, {email: userID}); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - errors: {policyError: 'Something went wrong'}, - }); + const policyCollectionDataSet = createPolicyCollectionDataSet( + createMock({ + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + errors: {policyError: 'Something went wrong'}, + }), + ); + await Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: userID}, + ...policyCollectionDataSet, + } satisfies OnyxMultiSetInput); await waitForBatchedUpdatesWithAct(); }); @@ -58,8 +76,7 @@ describe('usePolicyIndicatorChecks', () => { it('returns HAS_CUSTOM_UNITS_ERROR when custom units have errors', async () => { await act(async () => { - await Onyx.set(ONYXKEYS.SESSION, {email: userID}); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { + const customUnitErrorPolicy = createMock({ id: WORKSPACE.policyID, name: WORKSPACE.policyName, owner: userID, @@ -67,6 +84,13 @@ describe('usePolicyIndicatorChecks', () => { policyAccountID: WORKSPACE.policyAccountID, customUnits: {errors: createCustomUnitErrorFixture('Invalid custom unit')}, }); + const customUnitErrorDataSet: MalformedPolicyCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}`]: customUnitErrorPolicy, + }; + await Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: userID}, + ...customUnitErrorDataSet, + } satisfies OnyxMultiSetInput); await waitForBatchedUpdatesWithAct(); }); @@ -79,20 +103,24 @@ describe('usePolicyIndicatorChecks', () => { it('returns HAS_EMPLOYEE_LIST_ERROR when employee list has errors', async () => { await act(async () => { - await Onyx.set(ONYXKEYS.SESSION, {email: userID}); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - employeeList: { - [otherUserID]: { - email: otherUserID, - errors: {employeeError: 'Employee error'}, - }, - }, - }); + await Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: userID}, + ...createPolicyCollectionDataSet( + createMock({ + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + employeeList: { + [otherUserID]: { + email: otherUserID, + errors: {employeeError: 'Employee error'}, + }, + }, + }), + ), + } satisfies OnyxMultiSetInput); await waitForBatchedUpdatesWithAct(); }); @@ -105,27 +133,31 @@ describe('usePolicyIndicatorChecks', () => { it('returns HAS_SYNC_ERRORS when sync has errors', async () => { await act(async () => { - await Onyx.set(ONYXKEYS.SESSION, {email: userID}); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - connections: { - quickbooksOnline: { - lastSync: { - errorMessage: 'Sync failed', - isSuccessful: false, - errorDate: new Date().toISOString(), + await Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: userID}, + ...createPolicyCollectionDataSet( + createMock({ + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + connections: { + quickbooksOnline: { + lastSync: { + errorMessage: 'Sync failed', + isSuccessful: false, + errorDate: new Date().toISOString(), + }, + }, }, - }, + }), + ), + [`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${WORKSPACE.policyID}` as const]: { + stageInProgress: null, + connectionName: 'quickbooksOnline', }, - }); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${WORKSPACE.policyID}` as const, { - stageInProgress: null, - connectionName: 'quickbooksOnline', - }); + } satisfies OnyxMultiSetInput); await waitForBatchedUpdatesWithAct(); }); @@ -138,22 +170,26 @@ describe('usePolicyIndicatorChecks', () => { it('returns HAS_QBO_EXPORT_ERROR when QBO export has errors', async () => { await act(async () => { - await Onyx.set(ONYXKEYS.SESSION, {email: userID}); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - connections: { - quickbooksOnline: { - config: { - reimbursableExpensesExportDestination: CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL, - reimbursableExpensesAccount: undefined, + await Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: userID}, + ...createPolicyCollectionDataSet( + createMock({ + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + connections: { + quickbooksOnline: { + config: { + reimbursableExpensesExportDestination: CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL, + reimbursableExpensesAccount: undefined, + }, + }, }, - }, - }, - }); + }), + ), + } satisfies OnyxMultiSetInput); await waitForBatchedUpdatesWithAct(); }); @@ -166,17 +202,21 @@ describe('usePolicyIndicatorChecks', () => { it('returns HAS_UBER_CREDENTIALS_ERROR when Uber credentials have errors', async () => { await act(async () => { - await Onyx.set(ONYXKEYS.SESSION, {email: userID}); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - receiptPartners: { - uber: {error: 'Invalid Uber credentials'}, - }, - }); + await Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: userID}, + ...createPolicyCollectionDataSet( + createMock({ + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + receiptPartners: { + uber: {error: 'Invalid Uber credentials'}, + }, + }), + ), + } satisfies OnyxMultiSetInput); await waitForBatchedUpdatesWithAct(); }); @@ -189,21 +229,25 @@ describe('usePolicyIndicatorChecks', () => { it('returns HAS_POLICY_ADMIN_CARD_FEED_ERRORS when admin has card feed errors', async () => { await act(async () => { - await Onyx.set(ONYXKEYS.SESSION, {email: userID}); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - }); - await Onyx.set(ONYXKEYS.CARD_LIST, { - card1: { - bank: CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE, - fundID: String(WORKSPACE.policyAccountID), - lastScrapeResult: 403, + await Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: userID}, + ...createPolicyCollectionDataSet( + createMock({ + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + }), + ), + [ONYXKEYS.CARD_LIST]: { + card1: { + bank: CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE, + fundID: String(WORKSPACE.policyAccountID), + lastScrapeResult: 403, + }, }, - }); + } satisfies OnyxMultiSetInput); await waitForBatchedUpdatesWithAct(); }); @@ -223,22 +267,26 @@ describe('usePolicyIndicatorChecks', () => { it('returns HAS_MERGE_HR_SETUP_NEEDED when merge HR setup is needed', async () => { await act(async () => { - await Onyx.set(ONYXKEYS.SESSION, {email: userID}); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - connections: { - [CONST.POLICY.CONNECTIONS.NAME.MERGE_HR]: { - config: {integration: 'workday'}, - data: {groups: [{id: 'g1', name: 'Eng', type: 'Department'}]}, - lastSync: {syncStatus: CONST.MERGE_HR.SYNC_STATUS.DONE}, - }, - }, - }); - await Onyx.set(ONYXKEYS.CARD_LIST, {}); + await Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: userID}, + ...createPolicyCollectionDataSet( + createMock({ + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + connections: { + [CONST.POLICY.CONNECTIONS.NAME.MERGE_HR]: { + config: {integration: 'workday'}, + data: {groups: [{id: 'g1', name: 'Eng', type: 'Department'}]}, + lastSync: {syncStatus: CONST.MERGE_HR.SYNC_STATUS.DONE}, + }, + }, + }), + ), + [ONYXKEYS.CARD_LIST]: {}, + } satisfies OnyxMultiSetInput); await waitForBatchedUpdatesWithAct(); }); @@ -252,22 +300,26 @@ describe('usePolicyIndicatorChecks', () => { it('does not return HAS_MERGE_HR_SETUP_NEEDED for non-admin users', async () => { await act(async () => { - await Onyx.set(ONYXKEYS.SESSION, {email: otherUserID}); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'user', - policyAccountID: WORKSPACE.policyAccountID, - connections: { - [CONST.POLICY.CONNECTIONS.NAME.MERGE_HR]: { - config: {integration: 'workday'}, - data: {groups: [{id: 'g1', name: 'Eng', type: 'Department'}]}, - lastSync: {syncStatus: CONST.MERGE_HR.SYNC_STATUS.DONE}, - }, - }, - }); - await Onyx.set(ONYXKEYS.CARD_LIST, {}); + await Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: otherUserID}, + ...createPolicyCollectionDataSet( + createMock({ + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'user', + policyAccountID: WORKSPACE.policyAccountID, + connections: { + [CONST.POLICY.CONNECTIONS.NAME.MERGE_HR]: { + config: {integration: 'workday'}, + data: {groups: [{id: 'g1', name: 'Eng', type: 'Department'}]}, + lastSync: {syncStatus: CONST.MERGE_HR.SYNC_STATUS.DONE}, + }, + }, + }), + ), + [ONYXKEYS.CARD_LIST]: {}, + } satisfies OnyxMultiSetInput); await waitForBatchedUpdatesWithAct(); }); @@ -305,15 +357,19 @@ describe('usePolicyIndicatorChecks', () => { await Onyx.clear(); await waitForBatchedUpdatesWithAct(); await act(async () => { - await Onyx.set(ONYXKEYS.SESSION, {email: userID}); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - }); - await Onyx.set(ONYXKEYS.CARD_LIST, {}); + await Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: userID}, + ...createPolicyCollectionDataSet( + createMock({ + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + }), + ), + [ONYXKEYS.CARD_LIST]: {}, + } satisfies OnyxMultiSetInput); await waitForBatchedUpdatesWithAct(); }); }); @@ -349,8 +405,7 @@ describe('usePolicyIndicatorChecks', () => { describe('error priority', () => { beforeAll(async () => { await act(async () => { - await Onyx.set(ONYXKEYS.SESSION, {email: userID}); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { + const customUnitErrorPolicy = createMock({ id: WORKSPACE.policyID, name: WORKSPACE.policyName, owner: userID, @@ -365,7 +420,14 @@ describe('usePolicyIndicatorChecks', () => { }, }, }); - await Onyx.set(ONYXKEYS.CARD_LIST, {}); + const customUnitErrorDataSet: MalformedPolicyCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}`]: customUnitErrorPolicy, + }; + await Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: userID}, + ...customUnitErrorDataSet, + [ONYXKEYS.CARD_LIST]: {}, + } satisfies OnyxMultiSetInput); await waitForBatchedUpdatesWithAct(); }); }); @@ -388,33 +450,37 @@ describe('usePolicyIndicatorChecks', () => { it('returns both sync error and merge HR setup statuses when policy has both conditions', async () => { await act(async () => { - await Onyx.set(ONYXKEYS.SESSION, {email: userID}); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - connections: { - quickbooksOnline: { - lastSync: { - errorMessage: 'Sync failed', - isSuccessful: false, - errorDate: new Date().toISOString(), + await Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: userID}, + ...createPolicyCollectionDataSet( + createMock({ + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + connections: { + quickbooksOnline: { + lastSync: { + errorMessage: 'Sync failed', + isSuccessful: false, + errorDate: new Date().toISOString(), + }, + }, + [CONST.POLICY.CONNECTIONS.NAME.MERGE_HR]: { + config: {integration: 'workday'}, + data: {groups: [{id: 'g1', name: 'Eng', type: 'Department'}]}, + lastSync: {syncStatus: CONST.MERGE_HR.SYNC_STATUS.DONE}, + }, }, - }, - [CONST.POLICY.CONNECTIONS.NAME.MERGE_HR]: { - config: {integration: 'workday'}, - data: {groups: [{id: 'g1', name: 'Eng', type: 'Department'}]}, - lastSync: {syncStatus: CONST.MERGE_HR.SYNC_STATUS.DONE}, - }, + }), + ), + [`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${WORKSPACE.policyID}` as const]: { + stageInProgress: null, + connectionName: 'quickbooksOnline', }, - }); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${WORKSPACE.policyID}` as const, { - stageInProgress: null, - connectionName: 'quickbooksOnline', - }); - await Onyx.set(ONYXKEYS.CARD_LIST, {}); + [ONYXKEYS.CARD_LIST]: {}, + } satisfies OnyxMultiSetInput); await waitForBatchedUpdatesWithAct(); }); @@ -436,23 +502,29 @@ describe('usePolicyIndicatorChecks', () => { beforeAll(async () => { await act(async () => { - await Onyx.set(ONYXKEYS.SESSION, {email: userID}); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${WORKSPACE.policyID}` as const, { - id: WORKSPACE.policyID, - name: WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: WORKSPACE.policyAccountID, - }); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${SECOND_WORKSPACE.policyID}` as const, { - id: SECOND_WORKSPACE.policyID, - name: SECOND_WORKSPACE.policyName, - owner: userID, - role: 'admin', - policyAccountID: SECOND_WORKSPACE.policyAccountID, - errors: {policyError: 'Something went wrong'}, - }); - await Onyx.set(ONYXKEYS.CARD_LIST, {}); + await Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: userID}, + ...createPolicyCollectionDataSet( + createMock({ + id: WORKSPACE.policyID, + name: WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: WORKSPACE.policyAccountID, + }), + ), + ...createPolicyCollectionDataSet( + createMock({ + id: SECOND_WORKSPACE.policyID, + name: SECOND_WORKSPACE.policyName, + owner: userID, + role: 'admin', + policyAccountID: SECOND_WORKSPACE.policyAccountID, + errors: {policyError: 'Something went wrong'}, + }), + ), + [ONYXKEYS.CARD_LIST]: {}, + } satisfies OnyxMultiSetInput); await waitForBatchedUpdatesWithAct(); }); });