From 53e415d3eba51fce686025d21519a477102256f8 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Thu, 22 Jan 2026 19:13:03 +0700 Subject: [PATCH 1/7] Implement optimistic state updates for employee approvals --- src/libs/actions/Policy/Policy.ts | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index de476dfe0bc2..f1e2e215cfe7 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -1,7 +1,7 @@ /* eslint-disable max-lines */ import {PUBLIC_DOMAINS_SET, Str} from 'expensify-common'; import escapeRegExp from 'lodash/escapeRegExp'; -import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import type {OnyxCollection, OnyxCollectionInputValue, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {TupleToUnion, ValueOf} from 'type-fest'; import type {ReportExportType} from '@components/ButtonWithDropdownMenu/types'; @@ -111,6 +111,7 @@ import type { Policy, PolicyCategories, PolicyCategory, + PolicyEmployee, Report, ReportAction, ReportActions, @@ -813,6 +814,30 @@ function setWorkspaceApprovalMode(policyID: string, approver: string, approvalMo approvalMode, }; + const optimisticMembersState: OnyxCollectionInputValue = {}; + + if (approvalMode === CONST.POLICY.APPROVAL_MODE.OPTIONAL) { + for (const employeeEmail of Object.keys(policy?.employeeList ?? {})) { + const employee = policy?.employeeList?.[employeeEmail]; + + if (employee?.submitsTo) { + optimisticMembersState[employeeEmail] = { + submitsTo: policy?.owner, + }; + } + if (employee?.forwardsTo) { + optimisticMembersState[employeeEmail] = { + forwardsTo: '', + }; + } + if (employee?.overLimitForwardsTo) { + optimisticMembersState[employeeEmail] = { + forwardsTo: '', + }; + } + } + } + const optimisticData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -820,6 +845,7 @@ function setWorkspaceApprovalMode(policyID: string, approver: string, approvalMo value: { ...value, pendingFields: {approvalMode: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, + employeeList: optimisticMembersState, }, }, ]; From 27a2cb71f026eaf21fc72fcfe35932494feed542 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Thu, 22 Jan 2026 19:27:43 +0700 Subject: [PATCH 2/7] Refactor optimistic state updates in setWorkspaceApprovalMode to use a single updates object for employee properties --- src/libs/actions/Policy/Policy.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index f1e2e215cfe7..da4b34e11806 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -819,21 +819,20 @@ function setWorkspaceApprovalMode(policyID: string, approver: string, approvalMo if (approvalMode === CONST.POLICY.APPROVAL_MODE.OPTIONAL) { for (const employeeEmail of Object.keys(policy?.employeeList ?? {})) { const employee = policy?.employeeList?.[employeeEmail]; + const updates: Partial = {}; if (employee?.submitsTo) { - optimisticMembersState[employeeEmail] = { - submitsTo: policy?.owner, - }; + updates.submitsTo = policy?.owner; } if (employee?.forwardsTo) { - optimisticMembersState[employeeEmail] = { - forwardsTo: '', - }; + updates.forwardsTo = ''; } if (employee?.overLimitForwardsTo) { - optimisticMembersState[employeeEmail] = { - forwardsTo: '', - }; + updates.overLimitForwardsTo = ''; + } + + if (Object.keys(updates).length > 0) { + optimisticMembersState[employeeEmail] = updates; } } } From f32082ea75faab640ec46f012f3eb4c561f76252 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Sun, 8 Feb 2026 13:00:06 +0700 Subject: [PATCH 3/7] Add DISABLE_POLICY_APPROVALS command --- src/libs/API/parameters/DisablePolicyApprovalsParams.ts | 5 +++++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 ++ src/libs/actions/Policy/Policy.ts | 7 ++++++- 4 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 src/libs/API/parameters/DisablePolicyApprovalsParams.ts diff --git a/src/libs/API/parameters/DisablePolicyApprovalsParams.ts b/src/libs/API/parameters/DisablePolicyApprovalsParams.ts new file mode 100644 index 000000000000..bac98ac52103 --- /dev/null +++ b/src/libs/API/parameters/DisablePolicyApprovalsParams.ts @@ -0,0 +1,5 @@ +type DisablePolicyApprovalsParams = { + policyID: string; +}; + +export default DisablePolicyApprovalsParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 9eb594c8ee36..01e240f11cd4 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -211,6 +211,7 @@ export type {default as SetWorkspaceAutoHarvestingParams} from './SetWorkspaceAu export type {default as SetWorkspaceAutoReportingFrequencyParams} from './SetWorkspaceAutoReportingFrequencyParams'; export type {default as SetWorkspaceAutoReportingMonthlyOffsetParams} from './SetWorkspaceAutoReportingMonthlyOffsetParams'; export type {default as SetWorkspaceApprovalModeParams} from './SetWorkspaceApprovalModeParams'; +export type {default as DisablePolicyApprovalsParams} from './DisablePolicyApprovalsParams'; export type {default as SetWorkspacePayerParams} from './SetWorkspacePayerParams'; export type {default as SetWorkspaceReimbursementParams} from './SetWorkspaceReimbursementParams'; export type {default as SetWorkspaceDefaultSpendCategoryParams} from './SetWorkspaceDefaultSpendCategoryParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index c52c56909486..e63b497c8471 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -18,6 +18,7 @@ const WRITE_COMMANDS = { SET_WORKSPACE_AUTO_REPORTING_FREQUENCY: 'SetWorkspaceAutoReportingFrequency', SET_WORKSPACE_AUTO_REPORTING_MONTHLY_OFFSET: 'SetWorkspaceAutoReportingOffset', SET_WORKSPACE_APPROVAL_MODE: 'SetWorkspaceApprovalMode', + DISABLE_POLICY_APPROVALS: 'DisablePolicyApprovals', SET_WORKSPACE_PAYER: 'SetWorkspacePayer', SET_WORKSPACE_REIMBURSEMENT: 'SetWorkspaceReimbursement', SET_POLICY_DEFAULT_REPORT_TITLE: 'SetPolicyDefaultReportTitle', @@ -781,6 +782,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_WORKSPACE_AUTO_REPORTING_FREQUENCY]: Parameters.SetWorkspaceAutoReportingFrequencyParams; [WRITE_COMMANDS.SET_WORKSPACE_AUTO_REPORTING_MONTHLY_OFFSET]: Parameters.SetWorkspaceAutoReportingMonthlyOffsetParams; [WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE]: Parameters.SetWorkspaceApprovalModeParams; + [WRITE_COMMANDS.DISABLE_POLICY_APPROVALS]: Parameters.DisablePolicyApprovalsParams; [WRITE_COMMANDS.SET_WORKSPACE_PAYER]: Parameters.SetWorkspacePayerParams; [WRITE_COMMANDS.SET_WORKSPACE_REIMBURSEMENT]: Parameters.SetWorkspaceReimbursementParams; [WRITE_COMMANDS.SET_POLICY_DEFAULT_REPORT_TITLE]: Parameters.SetPolicyDefaultReportTitleParams; diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 918c88b7b765..408a4e47f272 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -16,6 +16,7 @@ import type { CreateWorkspaceParams, DeleteWorkspaceAvatarParams, DeleteWorkspaceParams, + DisablePolicyApprovalsParams, DisablePolicyBillableModeParams, DowngradeToTeamParams, DuplicateWorkspaceParams, @@ -957,7 +958,11 @@ function setWorkspaceApprovalMode(policyID: string, approver: string, approvalMo isAutoApprovalEnabled: false, }), }; - API.write(WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE, params, {optimisticData, failureData, successData}); + if (approvalMode === CONST.POLICY.APPROVAL_MODE.OPTIONAL) { + API.write(WRITE_COMMANDS.DISABLE_POLICY_APPROVALS, {policyID}, {optimisticData, failureData, successData}); + } else { + API.write(WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE, params, {optimisticData, failureData, successData}); + } } function setWorkspacePayer(policyID: string, reimburserEmail: string) { From d7c8c3371107afb9afc841237084ff3b820c75e6 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Sun, 8 Feb 2026 13:11:38 +0700 Subject: [PATCH 4/7] add params --- src/libs/actions/Policy/Policy.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 408a4e47f272..ecce2b549560 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -950,17 +950,21 @@ function setWorkspaceApprovalMode(policyID: string, approver: string, approvalMo }, ]; - const params: SetWorkspaceApprovalModeParams = { - policyID, - value: JSON.stringify({ - ...value, - // This property should now be set to false for all Collect policies - isAutoApprovalEnabled: false, - }), - }; if (approvalMode === CONST.POLICY.APPROVAL_MODE.OPTIONAL) { - API.write(WRITE_COMMANDS.DISABLE_POLICY_APPROVALS, {policyID}, {optimisticData, failureData, successData}); + const params: DisablePolicyApprovalsParams = { + policyID, + }; + API.write(WRITE_COMMANDS.DISABLE_POLICY_APPROVALS, params, {optimisticData, failureData, successData}); } else { + const params: SetWorkspaceApprovalModeParams = { + policyID, + value: JSON.stringify({ + ...value, + // This property should now be set to false for all Collect policies + isAutoApprovalEnabled: false, + }), + }; + // eslint-disable-next-line rulesdir/no-multiple-api-calls API.write(WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE, params, {optimisticData, failureData, successData}); } } From 55d6ee45d532029f45eb691426e0d8dd2826eeaf Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Fri, 13 Feb 2026 01:22:15 +0700 Subject: [PATCH 5/7] Update PolicyTest to reflect change from SET_WORKSPACE_APPROVAL_MODE to DISABLE_POLICY_APPROVALS --- tests/actions/PolicyTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts index db610a35295d..29be93739e66 100644 --- a/tests/actions/PolicyTest.ts +++ b/tests/actions/PolicyTest.ts @@ -1180,7 +1180,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); expect(apiWriteSpy).toHaveBeenCalledWith( - WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE, + WRITE_COMMANDS.DISABLE_POLICY_APPROVALS, expect.anything(), expect.objectContaining({ optimisticData: expect.arrayContaining([ From 9a72afd88afe325f2d4ed9086336787559a08d9b Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Fri, 13 Feb 2026 01:24:51 +0700 Subject: [PATCH 6/7] add more test cases --- tests/actions/PolicyTest.ts | 320 ++++++++++++++++++++++++++++++++++++ 1 file changed, 320 insertions(+) diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts index 29be93739e66..4b8aa834aec2 100644 --- a/tests/actions/PolicyTest.ts +++ b/tests/actions/PolicyTest.ts @@ -1237,6 +1237,326 @@ describe('actions/Policy', () => { buildNextStepNewSpy.mockRestore(); getAllPolicyReportsSpy.mockRestore(); }); + + it('should optimistically clear forwardsTo and overLimitForwardsTo when switching to OPTIONAL mode', async () => { + (fetch as MockFetch)?.pause?.(); + await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); + + const policyID = Policy.generatePolicyID(); + const employeeList = { + [ESH_EMAIL]: { + email: ESH_EMAIL, + submitsTo: ESH_EMAIL, + forwardsTo: EMPLOYEE_EMAIL, + overLimitForwardsTo: EMPLOYEE_EMAIL, + role: CONST.POLICY.ROLE.ADMIN, + }, + [EMPLOYEE_EMAIL]: { + email: EMPLOYEE_EMAIL, + submitsTo: ESH_EMAIL, + forwardsTo: ESH_EMAIL, + role: CONST.POLICY.ROLE.USER, + }, + }; + + const fakePolicy: PolicyType = { + ...createRandomPolicy(0, CONST.POLICY.TYPE.TEAM), + id: policyID, + approvalMode: CONST.POLICY.APPROVAL_MODE.ADVANCED, + approver: ESH_EMAIL, + owner: ESH_EMAIL, + employeeList, + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await waitForBatchedUpdates(); + + Policy.setWorkspaceApprovalMode(policyID, ESH_EMAIL, CONST.POLICY.APPROVAL_MODE.OPTIONAL); + await waitForBatchedUpdates(); + + const policy: OnyxEntry = await new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + callback: (workspace) => { + Onyx.disconnect(connection); + resolve(workspace); + }, + }); + }); + + // forwardsTo and overLimitForwardsTo should be cleared + expect(policy?.employeeList?.[ESH_EMAIL]?.forwardsTo).toBe(''); + expect(policy?.employeeList?.[ESH_EMAIL]?.overLimitForwardsTo).toBe(''); + // submitsTo should be set to the policy owner + expect(policy?.employeeList?.[ESH_EMAIL]?.submitsTo).toBe(ESH_EMAIL); + + expect(policy?.employeeList?.[EMPLOYEE_EMAIL]?.forwardsTo).toBe(''); + // submitsTo should be set to the policy owner + expect(policy?.employeeList?.[EMPLOYEE_EMAIL]?.submitsTo).toBe(ESH_EMAIL); + // overLimitForwardsTo was not set, so it should remain undefined + expect(policy?.employeeList?.[EMPLOYEE_EMAIL]?.overLimitForwardsTo).toBeUndefined(); + + expect(policy?.pendingFields?.approvalMode).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + + (fetch as MockFetch)?.resume?.(); + }); + + it('should call DISABLE_POLICY_APPROVALS when switching to OPTIONAL mode', async () => { + await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); + await waitForBatchedUpdates(); + + const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + + const policyID = Policy.generatePolicyID(); + const fakePolicy: PolicyType = { + ...createRandomPolicy(0, CONST.POLICY.TYPE.TEAM), + id: policyID, + approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + approver: ESH_EMAIL, + owner: ESH_EMAIL, + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await waitForBatchedUpdates(); + + Policy.setWorkspaceApprovalMode(policyID, ESH_EMAIL, CONST.POLICY.APPROVAL_MODE.OPTIONAL); + await waitForBatchedUpdates(); + + expect(apiWriteSpy).toHaveBeenCalledWith( + WRITE_COMMANDS.DISABLE_POLICY_APPROVALS, + expect.objectContaining({policyID}), + expect.anything(), + ); + + apiWriteSpy.mockRestore(); + }); + + it('should call SET_WORKSPACE_APPROVAL_MODE when switching to non-OPTIONAL mode', async () => { + await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); + await waitForBatchedUpdates(); + + const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + + const policyID = Policy.generatePolicyID(); + const fakePolicy: PolicyType = { + ...createRandomPolicy(0, CONST.POLICY.TYPE.TEAM), + id: policyID, + approvalMode: CONST.POLICY.APPROVAL_MODE.OPTIONAL, + approver: ESH_EMAIL, + owner: ESH_EMAIL, + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await waitForBatchedUpdates(); + + Policy.setWorkspaceApprovalMode(policyID, ESH_EMAIL, CONST.POLICY.APPROVAL_MODE.BASIC); + await waitForBatchedUpdates(); + + expect(apiWriteSpy).toHaveBeenCalledWith( + WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE, + expect.objectContaining({policyID}), + expect.anything(), + ); + + apiWriteSpy.mockRestore(); + }); + + it('should not modify employee list when switching to non-OPTIONAL mode', async () => { + await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); + await waitForBatchedUpdates(); + + const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + + const policyID = Policy.generatePolicyID(); + const employeeList = { + [ESH_EMAIL]: { + email: ESH_EMAIL, + submitsTo: ESH_EMAIL, + forwardsTo: EMPLOYEE_EMAIL, + overLimitForwardsTo: EMPLOYEE_EMAIL, + role: CONST.POLICY.ROLE.ADMIN, + }, + [EMPLOYEE_EMAIL]: { + email: EMPLOYEE_EMAIL, + submitsTo: ESH_EMAIL, + forwardsTo: ESH_EMAIL, + role: CONST.POLICY.ROLE.USER, + }, + }; + + const fakePolicy: PolicyType = { + ...createRandomPolicy(0, CONST.POLICY.TYPE.TEAM), + id: policyID, + approvalMode: CONST.POLICY.APPROVAL_MODE.OPTIONAL, + approver: ESH_EMAIL, + owner: ESH_EMAIL, + employeeList, + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await waitForBatchedUpdates(); + + Policy.setWorkspaceApprovalMode(policyID, ESH_EMAIL, CONST.POLICY.APPROVAL_MODE.BASIC); + await waitForBatchedUpdates(); + + // optimisticMembersState should be empty for non-OPTIONAL mode + const writeOptions = apiWriteSpy.mock.calls.at(0)?.at(2) as {optimisticData?: Array<{key?: string; value?: Record}>} | undefined; + const policyOptimisticData = writeOptions?.optimisticData?.find((u) => u?.key === `${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + expect(policyOptimisticData?.value?.employeeList).toEqual({}); + + apiWriteSpy.mockRestore(); + }); + + it('should restore original employee list on API failure when switching to OPTIONAL mode', async () => { + await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); + + const policyID = Policy.generatePolicyID(); + const employeeList = { + [ESH_EMAIL]: { + email: ESH_EMAIL, + submitsTo: ESH_EMAIL, + forwardsTo: EMPLOYEE_EMAIL, + overLimitForwardsTo: EMPLOYEE_EMAIL, + role: CONST.POLICY.ROLE.ADMIN, + }, + [EMPLOYEE_EMAIL]: { + email: EMPLOYEE_EMAIL, + submitsTo: ESH_EMAIL, + forwardsTo: ESH_EMAIL, + role: CONST.POLICY.ROLE.USER, + }, + }; + + const fakePolicy: PolicyType = { + ...createRandomPolicy(0, CONST.POLICY.TYPE.TEAM), + id: policyID, + approvalMode: CONST.POLICY.APPROVAL_MODE.ADVANCED, + approver: ESH_EMAIL, + owner: ESH_EMAIL, + employeeList, + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await waitForBatchedUpdates(); + + // Simulate API failure + mockFetch?.fail?.(); + + Policy.setWorkspaceApprovalMode(policyID, ESH_EMAIL, CONST.POLICY.APPROVAL_MODE.OPTIONAL); + await waitForBatchedUpdates(); + + const policy: OnyxEntry = await new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + callback: (workspace) => { + Onyx.disconnect(connection); + resolve(workspace); + }, + }); + }); + + // On failure, employee list should be restored to original + expect(policy?.employeeList).toEqual(employeeList); + // Approval mode should revert to original + expect(policy?.approvalMode).toBe(CONST.POLICY.APPROVAL_MODE.ADVANCED); + // Pending field should be cleared + expect(policy?.pendingFields?.approvalMode).toBeFalsy(); + // Error field should be set + expect(policy?.errorFields?.approvalMode).toBeTruthy(); + + mockFetch?.succeed?.(); + }); + + it('should only update employees that have submitsTo, forwardsTo, or overLimitForwardsTo set', async () => { + await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); + await waitForBatchedUpdates(); + + const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const employeeWithNoForwarding = 'noforward@example.com'; + + const policyID = Policy.generatePolicyID(); + const employeeList = { + [ESH_EMAIL]: { + email: ESH_EMAIL, + submitsTo: ESH_EMAIL, + forwardsTo: EMPLOYEE_EMAIL, + role: CONST.POLICY.ROLE.ADMIN, + }, + [employeeWithNoForwarding]: { + email: employeeWithNoForwarding, + role: CONST.POLICY.ROLE.USER, + }, + }; + + const fakePolicy: PolicyType = { + ...createRandomPolicy(0, CONST.POLICY.TYPE.TEAM), + id: policyID, + approvalMode: CONST.POLICY.APPROVAL_MODE.ADVANCED, + approver: ESH_EMAIL, + owner: ESH_EMAIL, + employeeList, + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await waitForBatchedUpdates(); + + Policy.setWorkspaceApprovalMode(policyID, ESH_EMAIL, CONST.POLICY.APPROVAL_MODE.OPTIONAL); + await waitForBatchedUpdates(); + + const writeOptions = apiWriteSpy.mock.calls.at(0)?.at(2) as { + optimisticData?: Array<{key?: string; value?: {employeeList?: Record}}>; + } | undefined; + const policyOptimisticData = writeOptions?.optimisticData?.find((u) => u?.key === `${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + const optimisticEmployeeList = policyOptimisticData?.value?.employeeList as Record> | undefined; + + // Employee with submitsTo and forwardsTo should be updated + expect(optimisticEmployeeList?.[ESH_EMAIL]).toBeDefined(); + expect(optimisticEmployeeList?.[ESH_EMAIL]?.submitsTo).toBe(ESH_EMAIL); + expect(optimisticEmployeeList?.[ESH_EMAIL]?.forwardsTo).toBe(''); + + // Employee with no forwarding fields should not be in the updates + expect(optimisticEmployeeList?.[employeeWithNoForwarding]).toBeUndefined(); + + apiWriteSpy.mockRestore(); + }); + + it('should set submitsTo to policy owner for all employees when switching to OPTIONAL', async () => { + await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); + await waitForBatchedUpdates(); + + const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const differentApprover = 'manager@example.com'; + + const policyID = Policy.generatePolicyID(); + const employeeList = { + [EMPLOYEE_EMAIL]: { + email: EMPLOYEE_EMAIL, + submitsTo: differentApprover, + forwardsTo: differentApprover, + role: CONST.POLICY.ROLE.USER, + }, + }; + + const fakePolicy: PolicyType = { + ...createRandomPolicy(0, CONST.POLICY.TYPE.TEAM), + id: policyID, + approvalMode: CONST.POLICY.APPROVAL_MODE.ADVANCED, + approver: ESH_EMAIL, + owner: ESH_EMAIL, + employeeList, + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await waitForBatchedUpdates(); + + Policy.setWorkspaceApprovalMode(policyID, ESH_EMAIL, CONST.POLICY.APPROVAL_MODE.OPTIONAL); + await waitForBatchedUpdates(); + + const writeOptions = apiWriteSpy.mock.calls.at(0)?.at(2) as { + optimisticData?: Array<{key?: string; value?: {employeeList?: Record>}}>; + } | undefined; + const policyOptimisticData = writeOptions?.optimisticData?.find((u) => u?.key === `${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + const optimisticEmployeeList = policyOptimisticData?.value?.employeeList; + + // submitsTo should be set to the policy owner (ESH_EMAIL), not the previous approver + expect(optimisticEmployeeList?.[EMPLOYEE_EMAIL]?.submitsTo).toBe(ESH_EMAIL); + expect(optimisticEmployeeList?.[EMPLOYEE_EMAIL]?.forwardsTo).toBe(''); + + apiWriteSpy.mockRestore(); + }); }); describe('deleteWorkspace', () => { From e2cd04a658d89b603f2fe7081c38501b1bef7fc3 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Fri, 13 Feb 2026 01:28:08 +0700 Subject: [PATCH 7/7] run prettier --- tests/actions/PolicyTest.ts | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts index 4b8aa834aec2..376ad27d6b1f 100644 --- a/tests/actions/PolicyTest.ts +++ b/tests/actions/PolicyTest.ts @@ -1320,11 +1320,7 @@ describe('actions/Policy', () => { Policy.setWorkspaceApprovalMode(policyID, ESH_EMAIL, CONST.POLICY.APPROVAL_MODE.OPTIONAL); await waitForBatchedUpdates(); - expect(apiWriteSpy).toHaveBeenCalledWith( - WRITE_COMMANDS.DISABLE_POLICY_APPROVALS, - expect.objectContaining({policyID}), - expect.anything(), - ); + expect(apiWriteSpy).toHaveBeenCalledWith(WRITE_COMMANDS.DISABLE_POLICY_APPROVALS, expect.objectContaining({policyID}), expect.anything()); apiWriteSpy.mockRestore(); }); @@ -1349,11 +1345,7 @@ describe('actions/Policy', () => { Policy.setWorkspaceApprovalMode(policyID, ESH_EMAIL, CONST.POLICY.APPROVAL_MODE.BASIC); await waitForBatchedUpdates(); - expect(apiWriteSpy).toHaveBeenCalledWith( - WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE, - expect.objectContaining({policyID}), - expect.anything(), - ); + expect(apiWriteSpy).toHaveBeenCalledWith(WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE, expect.objectContaining({policyID}), expect.anything()); apiWriteSpy.mockRestore(); }); @@ -1497,9 +1489,11 @@ describe('actions/Policy', () => { Policy.setWorkspaceApprovalMode(policyID, ESH_EMAIL, CONST.POLICY.APPROVAL_MODE.OPTIONAL); await waitForBatchedUpdates(); - const writeOptions = apiWriteSpy.mock.calls.at(0)?.at(2) as { - optimisticData?: Array<{key?: string; value?: {employeeList?: Record}}>; - } | undefined; + const writeOptions = apiWriteSpy.mock.calls.at(0)?.at(2) as + | { + optimisticData?: Array<{key?: string; value?: {employeeList?: Record}}>; + } + | undefined; const policyOptimisticData = writeOptions?.optimisticData?.find((u) => u?.key === `${ONYXKEYS.COLLECTION.POLICY}${policyID}`); const optimisticEmployeeList = policyOptimisticData?.value?.employeeList as Record> | undefined; @@ -1545,9 +1539,11 @@ describe('actions/Policy', () => { Policy.setWorkspaceApprovalMode(policyID, ESH_EMAIL, CONST.POLICY.APPROVAL_MODE.OPTIONAL); await waitForBatchedUpdates(); - const writeOptions = apiWriteSpy.mock.calls.at(0)?.at(2) as { - optimisticData?: Array<{key?: string; value?: {employeeList?: Record>}}>; - } | undefined; + const writeOptions = apiWriteSpy.mock.calls.at(0)?.at(2) as + | { + optimisticData?: Array<{key?: string; value?: {employeeList?: Record>}}>; + } + | undefined; const policyOptimisticData = writeOptions?.optimisticData?.find((u) => u?.key === `${ONYXKEYS.COLLECTION.POLICY}${policyID}`); const optimisticEmployeeList = policyOptimisticData?.value?.employeeList;