From 59932e76a3b9249e18931bf9f63564115b503310 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:04:14 +0530 Subject: [PATCH 1/6] Fix member row errors on bulk approve-only change --- src/hooks/useApproveOnlyRoleBlockedModal.ts | 10 +-- src/libs/actions/Policy/Member.ts | 33 +++++-- tests/actions/PolicyMemberTest.ts | 95 +++++++++++++++++++++ 3 files changed, 125 insertions(+), 13 deletions(-) diff --git a/src/hooks/useApproveOnlyRoleBlockedModal.ts b/src/hooks/useApproveOnlyRoleBlockedModal.ts index 37f1e61cb985..351244c523e1 100644 --- a/src/hooks/useApproveOnlyRoleBlockedModal.ts +++ b/src/hooks/useApproveOnlyRoleBlockedModal.ts @@ -2,7 +2,7 @@ * Subscribes to BLOCKED_ROLE_CHANGE in Onyx and shows the blocked-reasons modal on the global modal stack * when the backend refuses an approve-only role change. */ -import {clearBlockedRoleChange, clearUpdateMemberRoleError} from '@userActions/Policy/Member'; +import {clearApproveOnlyRoleBlockedErrors, clearBlockedRoleChange} from '@userActions/Policy/Member'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -49,12 +49,10 @@ function useApproveOnlyRoleBlockedModal() { const {policyID, email, blockedReasons = []} = blockedRoleChange; - // `OnyxUpdates.apply` runs `request.failureData` on the microtask after `response.onyxData`, which sets a - // generic inline error on the member row. Clear that row error once `failureData` has settled so the modal - // is the only feedback shown for a blocked role change. - if (policyID && email) { + // Clear row errors once failureData has settled so the modal is the only feedback shown. + if (policyID) { Promise.resolve().then(() => { - clearUpdateMemberRoleError(policyID, email); + clearApproveOnlyRoleBlockedErrors(policyID, email ?? ''); }); } diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index 384ee06abfc8..13b7077895f7 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -81,6 +81,8 @@ Onyx.connectWithoutView({ }, }); +const inFlightApproveOnlyEmailsByPolicy: Record = {}; + /** * Build optimistic data for adding members to the announcement/admins room */ @@ -727,6 +729,11 @@ function updateWorkspaceMembersRole(policy: OnyxEntry, selectedMemberEma if (isSubmitPolicy(policy)) { return; } + if (newRole === CONST.POLICY.ROLE.APPROVE_ONLY) { + inFlightApproveOnlyEmailsByPolicy[policy.id] = selectedMemberEmails; + } else { + delete inFlightApproveOnlyEmailsByPolicy[policy.id]; + } const {optimisticData, successData, failureData, memberRoles} = buildUpdateWorkspaceMembersRoleOnyxData(policy, selectedMemberEmails, selectedMemberAccountIDs, newRole); const params: UpdateWorkspaceMembersRoleParams = { @@ -1275,17 +1282,28 @@ function clearAddMemberError(policyID: string, login: string, accountID: number) /** * Removes an error after trying to update a member role */ -function clearUpdateMemberRoleError(policyID: string, login: string) { +function clearUpdateMemberRoleError(policyID: string, login: string | string[]) { + const logins = Array.isArray(login) ? login : [login]; + const employeeListUpdate = logins.reduce>((acc, email) => { + acc[email] = { + pendingAction: null, + errors: null, + }; + return acc; + }, {}); + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - employeeList: { - [login]: { - pendingAction: null, - errors: null, - }, - }, + employeeList: employeeListUpdate, }); } +function clearApproveOnlyRoleBlockedErrors(policyID: string, email: string) { + const recordedEmails = inFlightApproveOnlyEmailsByPolicy[policyID] ?? []; + delete inFlightApproveOnlyEmailsByPolicy[policyID]; + const emailsToClear = email && !recordedEmails.includes(email) ? [...recordedEmails, email] : recordedEmails; + clearUpdateMemberRoleError(policyID, emailsToClear.length > 0 ? emailsToClear : email); +} + function clearBlockedRoleChange() { Onyx.set(ONYXKEYS.BLOCKED_ROLE_CHANGE, null); } @@ -1592,6 +1610,7 @@ export { clearDeleteMemberError, clearAddMemberError, clearUpdateMemberRoleError, + clearApproveOnlyRoleBlockedErrors, clearBlockedRoleChange, openWorkspaceMembersPage, setWorkspaceInviteMembersDraft, diff --git a/tests/actions/PolicyMemberTest.ts b/tests/actions/PolicyMemberTest.ts index 332f9f26b14e..bbe204b88af5 100644 --- a/tests/actions/PolicyMemberTest.ts +++ b/tests/actions/PolicyMemberTest.ts @@ -214,6 +214,101 @@ describe('actions/PolicyMember', () => { }); }); }); + + describe('clearUpdateMemberRoleError', () => { + it('clears errors for single and multiple member logins', async () => { + const member1 = 'member1@test.com'; + const member2 = 'member2@test.com'; + const fakePolicy: PolicyType = { + ...createRandomPolicy(0), + employeeList: { + [member1]: { + email: member1, + role: CONST.POLICY.ROLE.USER, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + errors: {error1: 'Failed'}, + }, + [member2]: { + email: member2, + role: CONST.POLICY.ROLE.USER, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + errors: {error2: 'Failed'}, + }, + }, + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + + Member.clearUpdateMemberRoleError(fakePolicy.id, member1); + await waitForBatchedUpdates(); + + let policy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`); + expect(policy?.employeeList?.[member1]?.errors).toBeNull(); + expect(policy?.employeeList?.[member1]?.pendingAction).toBeNull(); + expect(policy?.employeeList?.[member2]?.errors).toEqual({error2: 'Failed'}); + + Member.clearUpdateMemberRoleError(fakePolicy.id, [member2]); + await waitForBatchedUpdates(); + + policy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`); + expect(policy?.employeeList?.[member2]?.errors).toBeNull(); + expect(policy?.employeeList?.[member2]?.pendingAction).toBeNull(); + }); + }); + + describe('clearApproveOnlyRoleBlockedErrors', () => { + it('clears errors for all members in a bulk approve-only role change', async () => { + const member1 = createPersonalDetails(1); + const member2 = createPersonalDetails(2); + const memberLogin1 = member1.login ?? ''; + const memberLogin2 = member2.login ?? ''; + const fakePolicy: PolicyType = { + ...createRandomPolicy(0, CONST.POLICY.TYPE.CORPORATE), + employeeList: { + [memberLogin1]: { + email: memberLogin1, + role: CONST.POLICY.ROLE.USER, + }, + [memberLogin2]: { + email: memberLogin2, + role: CONST.POLICY.ROLE.USER, + }, + }, + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + mockFetch?.pause?.(); + + Member.updateWorkspaceMembersRole(fakePolicy, [memberLogin1, memberLogin2], [member1.accountID, member2.accountID], CONST.POLICY.ROLE.APPROVE_ONLY); + await waitForBatchedUpdates(); + + // Simulate failure data setting error on both member rows + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, { + employeeList: { + [memberLogin1]: { + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + errors: {error: 'Failed'}, + }, + [memberLogin2]: { + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + errors: {error: 'Failed'}, + }, + }, + }); + + // When backend blocks role change reporting memberLogin1 + Member.clearApproveOnlyRoleBlockedErrors(fakePolicy.id, memberLogin1); + await waitForBatchedUpdates(); + + const policy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`); + expect(policy?.employeeList?.[memberLogin1]?.errors).toBeNull(); + expect(policy?.employeeList?.[memberLogin1]?.pendingAction).toBeNull(); + expect(policy?.employeeList?.[memberLogin2]?.errors).toBeNull(); + expect(policy?.employeeList?.[memberLogin2]?.pendingAction).toBeNull(); + + await mockFetch?.resume?.(); + }); + }); describe('requestWorkspaceOwnerChange', () => { it('Change the workspace`s owner', async () => { const fakePolicy: PolicyType = createRandomPolicy(0); From d66ac89fe511a0c392748bd973eb62b56d68d93f Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 24 Oct 2026 12:35:21 +0530 Subject: [PATCH 2/6] Omit failure row error for approve-only role changes --- src/hooks/useApproveOnlyRoleBlockedModal.ts | 12 +-- src/libs/actions/Policy/Member.ts | 37 +++------ tests/actions/PolicyMemberTest.ts | 87 +++------------------ 3 files changed, 23 insertions(+), 113 deletions(-) diff --git a/src/hooks/useApproveOnlyRoleBlockedModal.ts b/src/hooks/useApproveOnlyRoleBlockedModal.ts index 351244c523e1..5f14c88a0951 100644 --- a/src/hooks/useApproveOnlyRoleBlockedModal.ts +++ b/src/hooks/useApproveOnlyRoleBlockedModal.ts @@ -2,7 +2,7 @@ * Subscribes to BLOCKED_ROLE_CHANGE in Onyx and shows the blocked-reasons modal on the global modal stack * when the backend refuses an approve-only role change. */ -import {clearApproveOnlyRoleBlockedErrors, clearBlockedRoleChange} from '@userActions/Policy/Member'; +import {clearBlockedRoleChange} from '@userActions/Policy/Member'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -47,15 +47,7 @@ function useApproveOnlyRoleBlockedModal() { lastShownRef.current = blockedRoleChange; isModalOpenRef.current = true; - const {policyID, email, blockedReasons = []} = blockedRoleChange; - - // Clear row errors once failureData has settled so the modal is the only feedback shown. - if (policyID) { - Promise.resolve().then(() => { - clearApproveOnlyRoleBlockedErrors(policyID, email ?? ''); - }); - } - + const {blockedReasons = []} = blockedRoleChange; const reasonLines = blockedReasons.filter((reason) => reason in REASON_TRANSLATION_KEYS).map((reason) => `• ${translate(REASON_TRANSLATION_KEYS[reason])}`); const hasReasonLines = reasonLines.length > 0; diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index 13b7077895f7..2c3c266cd27f 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -81,8 +81,6 @@ Onyx.connectWithoutView({ }, }); -const inFlightApproveOnlyEmailsByPolicy: Record = {}; - /** * Build optimistic data for adding members to the announcement/admins room */ @@ -660,8 +658,8 @@ function buildUpdateWorkspaceMembersRoleOnyxData(policy: OnyxEntry, sele // eslint-disable-next-line no-param-reassign member[current.email] = { ...(previousEmployeeList[current.email] ?? {}), - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.editor.genericFailureMessage'), + pendingAction: newRole === CONST.POLICY.ROLE.APPROVE_ONLY ? null : CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + errors: newRole === CONST.POLICY.ROLE.APPROVE_ONLY ? null : ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.editor.genericFailureMessage'), }; return member; }, {}), @@ -729,11 +727,6 @@ function updateWorkspaceMembersRole(policy: OnyxEntry, selectedMemberEma if (isSubmitPolicy(policy)) { return; } - if (newRole === CONST.POLICY.ROLE.APPROVE_ONLY) { - inFlightApproveOnlyEmailsByPolicy[policy.id] = selectedMemberEmails; - } else { - delete inFlightApproveOnlyEmailsByPolicy[policy.id]; - } const {optimisticData, successData, failureData, memberRoles} = buildUpdateWorkspaceMembersRoleOnyxData(policy, selectedMemberEmails, selectedMemberAccountIDs, newRole); const params: UpdateWorkspaceMembersRoleParams = { @@ -1282,28 +1275,17 @@ function clearAddMemberError(policyID: string, login: string, accountID: number) /** * Removes an error after trying to update a member role */ -function clearUpdateMemberRoleError(policyID: string, login: string | string[]) { - const logins = Array.isArray(login) ? login : [login]; - const employeeListUpdate = logins.reduce>((acc, email) => { - acc[email] = { - pendingAction: null, - errors: null, - }; - return acc; - }, {}); - +function clearUpdateMemberRoleError(policyID: string, login: string) { Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - employeeList: employeeListUpdate, + employeeList: { + [login]: { + pendingAction: null, + errors: null, + }, + }, }); } -function clearApproveOnlyRoleBlockedErrors(policyID: string, email: string) { - const recordedEmails = inFlightApproveOnlyEmailsByPolicy[policyID] ?? []; - delete inFlightApproveOnlyEmailsByPolicy[policyID]; - const emailsToClear = email && !recordedEmails.includes(email) ? [...recordedEmails, email] : recordedEmails; - clearUpdateMemberRoleError(policyID, emailsToClear.length > 0 ? emailsToClear : email); -} - function clearBlockedRoleChange() { Onyx.set(ONYXKEYS.BLOCKED_ROLE_CHANGE, null); } @@ -1610,7 +1592,6 @@ export { clearDeleteMemberError, clearAddMemberError, clearUpdateMemberRoleError, - clearApproveOnlyRoleBlockedErrors, clearBlockedRoleChange, openWorkspaceMembersPage, setWorkspaceInviteMembersDraft, diff --git a/tests/actions/PolicyMemberTest.ts b/tests/actions/PolicyMemberTest.ts index bbe204b88af5..88a35055eb55 100644 --- a/tests/actions/PolicyMemberTest.ts +++ b/tests/actions/PolicyMemberTest.ts @@ -213,100 +213,37 @@ describe('actions/PolicyMember', () => { }); }); }); - }); - describe('clearUpdateMemberRoleError', () => { - it('clears errors for single and multiple member logins', async () => { + it('does not set errors in failureData when changing role to approve-only', () => { const member1 = 'member1@test.com'; const member2 = 'member2@test.com'; const fakePolicy: PolicyType = { - ...createRandomPolicy(0), + ...createRandomPolicy(0, CONST.POLICY.TYPE.CORPORATE), employeeList: { [member1]: { email: member1, role: CONST.POLICY.ROLE.USER, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - errors: {error1: 'Failed'}, }, [member2]: { email: member2, role: CONST.POLICY.ROLE.USER, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - errors: {error2: 'Failed'}, - }, - }, - }; - - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); - - Member.clearUpdateMemberRoleError(fakePolicy.id, member1); - await waitForBatchedUpdates(); - - let policy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`); - expect(policy?.employeeList?.[member1]?.errors).toBeNull(); - expect(policy?.employeeList?.[member1]?.pendingAction).toBeNull(); - expect(policy?.employeeList?.[member2]?.errors).toEqual({error2: 'Failed'}); - - Member.clearUpdateMemberRoleError(fakePolicy.id, [member2]); - await waitForBatchedUpdates(); - - policy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`); - expect(policy?.employeeList?.[member2]?.errors).toBeNull(); - expect(policy?.employeeList?.[member2]?.pendingAction).toBeNull(); - }); - }); - - describe('clearApproveOnlyRoleBlockedErrors', () => { - it('clears errors for all members in a bulk approve-only role change', async () => { - const member1 = createPersonalDetails(1); - const member2 = createPersonalDetails(2); - const memberLogin1 = member1.login ?? ''; - const memberLogin2 = member2.login ?? ''; - const fakePolicy: PolicyType = { - ...createRandomPolicy(0, CONST.POLICY.TYPE.CORPORATE), - employeeList: { - [memberLogin1]: { - email: memberLogin1, - role: CONST.POLICY.ROLE.USER, - }, - [memberLogin2]: { - email: memberLogin2, - role: CONST.POLICY.ROLE.USER, }, }, }; - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); - mockFetch?.pause?.(); - - Member.updateWorkspaceMembersRole(fakePolicy, [memberLogin1, memberLogin2], [member1.accountID, member2.accountID], CONST.POLICY.ROLE.APPROVE_ONLY); - await waitForBatchedUpdates(); - - // Simulate failure data setting error on both member rows - await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, { - employeeList: { - [memberLogin1]: { - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - errors: {error: 'Failed'}, - }, - [memberLogin2]: { - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - errors: {error: 'Failed'}, - }, - }, - }); + const approveOnlyData = Member.buildUpdateWorkspaceMembersRoleOnyxData(fakePolicy, [member1, member2], [1, 2], CONST.POLICY.ROLE.APPROVE_ONLY); + const failureEmployeeList = (approveOnlyData.failureData[0] as {value: {employeeList: Record}}).value.employeeList; - // When backend blocks role change reporting memberLogin1 - Member.clearApproveOnlyRoleBlockedErrors(fakePolicy.id, memberLogin1); - await waitForBatchedUpdates(); + expect(failureEmployeeList[member1].errors).toBeNull(); + expect(failureEmployeeList[member1].pendingAction).toBeNull(); + expect(failureEmployeeList[member2].errors).toBeNull(); + expect(failureEmployeeList[member2].pendingAction).toBeNull(); - const policy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`); - expect(policy?.employeeList?.[memberLogin1]?.errors).toBeNull(); - expect(policy?.employeeList?.[memberLogin1]?.pendingAction).toBeNull(); - expect(policy?.employeeList?.[memberLogin2]?.errors).toBeNull(); - expect(policy?.employeeList?.[memberLogin2]?.pendingAction).toBeNull(); + const adminData = Member.buildUpdateWorkspaceMembersRoleOnyxData(fakePolicy, [member1], [1], CONST.POLICY.ROLE.ADMIN); + const adminFailureEmployeeList = (adminData.failureData[0] as {value: {employeeList: Record}}).value.employeeList; - await mockFetch?.resume?.(); + expect(adminFailureEmployeeList[member1].errors).toBeTruthy(); + expect(adminFailureEmployeeList[member1].pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); }); }); describe('requestWorkspaceOwnerChange', () => { From 529eed4ba4f319fec86b2c1333eca766d8fd5820 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:28:55 +0530 Subject: [PATCH 3/6] Use canRoleCreateExpenses to gate failure row errors --- src/libs/actions/Policy/Member.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index 2c3c266cd27f..cfbcf994b8a1 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -648,6 +648,8 @@ function buildUpdateWorkspaceMembersRoleOnyxData(policy: OnyxEntry, sele }, ]; + // Roles that cannot create expenses show a dedicated modal on refusal, so skip inline row errors. + const canCreateExpenses = canRoleCreateExpenses(newRole); const failureData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -658,8 +660,8 @@ function buildUpdateWorkspaceMembersRoleOnyxData(policy: OnyxEntry, sele // eslint-disable-next-line no-param-reassign member[current.email] = { ...(previousEmployeeList[current.email] ?? {}), - pendingAction: newRole === CONST.POLICY.ROLE.APPROVE_ONLY ? null : CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - errors: newRole === CONST.POLICY.ROLE.APPROVE_ONLY ? null : ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.editor.genericFailureMessage'), + pendingAction: canCreateExpenses ? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE : null, + errors: canCreateExpenses ? ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.editor.genericFailureMessage') : null, }; return member; }, {}), From bb568b3bc341fe5a1150ab30a642d48a52796b7c Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:52:16 +0530 Subject: [PATCH 4/6] Fix type assertion and errors assignment in failureData --- src/libs/actions/Policy/Member.ts | 2 +- tests/actions/PolicyMemberTest.ts | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index cfbcf994b8a1..3a6dd2832a59 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -661,7 +661,7 @@ function buildUpdateWorkspaceMembersRoleOnyxData(policy: OnyxEntry, sele member[current.email] = { ...(previousEmployeeList[current.email] ?? {}), pendingAction: canCreateExpenses ? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE : null, - errors: canCreateExpenses ? ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.editor.genericFailureMessage') : null, + ...(canCreateExpenses && {errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.editor.genericFailureMessage')}), }; return member; }, {}), diff --git a/tests/actions/PolicyMemberTest.ts b/tests/actions/PolicyMemberTest.ts index 88a35055eb55..3fd00a86a3b5 100644 --- a/tests/actions/PolicyMemberTest.ts +++ b/tests/actions/PolicyMemberTest.ts @@ -232,18 +232,18 @@ describe('actions/PolicyMember', () => { }; const approveOnlyData = Member.buildUpdateWorkspaceMembersRoleOnyxData(fakePolicy, [member1, member2], [1, 2], CONST.POLICY.ROLE.APPROVE_ONLY); - const failureEmployeeList = (approveOnlyData.failureData[0] as {value: {employeeList: Record}}).value.employeeList; - - expect(failureEmployeeList[member1].errors).toBeNull(); - expect(failureEmployeeList[member1].pendingAction).toBeNull(); - expect(failureEmployeeList[member2].errors).toBeNull(); - expect(failureEmployeeList[member2].pendingAction).toBeNull(); + const approveOnlyUpdate = approveOnlyData.failureData.at(0); + const employeeList = approveOnlyUpdate && 'employeeList' in (approveOnlyUpdate.value ?? {}) ? approveOnlyUpdate.value.employeeList : undefined; + expect(employeeList?.[member1]?.errors).toBeUndefined(); + expect(employeeList?.[member1]?.pendingAction).toBeNull(); + expect(employeeList?.[member2]?.errors).toBeUndefined(); + expect(employeeList?.[member2]?.pendingAction).toBeNull(); const adminData = Member.buildUpdateWorkspaceMembersRoleOnyxData(fakePolicy, [member1], [1], CONST.POLICY.ROLE.ADMIN); - const adminFailureEmployeeList = (adminData.failureData[0] as {value: {employeeList: Record}}).value.employeeList; - - expect(adminFailureEmployeeList[member1].errors).toBeTruthy(); - expect(adminFailureEmployeeList[member1].pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + const adminUpdate = adminData.failureData.at(0); + const adminEmployeeList = adminUpdate && 'employeeList' in (adminUpdate.value ?? {}) ? adminUpdate.value.employeeList : undefined; + expect(adminEmployeeList?.[member1]?.errors).toBeTruthy(); + expect(adminEmployeeList?.[member1]?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); }); }); describe('requestWorkspaceOwnerChange', () => { From c84e9de71645c1832ab203b2fc3241fc1f7d6ee2 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:01:51 +0530 Subject: [PATCH 5/6] Use Jest objectContaining in PolicyMemberTest --- tests/actions/PolicyMemberTest.ts | 46 ++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/tests/actions/PolicyMemberTest.ts b/tests/actions/PolicyMemberTest.ts index 3fd00a86a3b5..985fd29a8dd4 100644 --- a/tests/actions/PolicyMemberTest.ts +++ b/tests/actions/PolicyMemberTest.ts @@ -232,18 +232,44 @@ describe('actions/PolicyMember', () => { }; const approveOnlyData = Member.buildUpdateWorkspaceMembersRoleOnyxData(fakePolicy, [member1, member2], [1, 2], CONST.POLICY.ROLE.APPROVE_ONLY); - const approveOnlyUpdate = approveOnlyData.failureData.at(0); - const employeeList = approveOnlyUpdate && 'employeeList' in (approveOnlyUpdate.value ?? {}) ? approveOnlyUpdate.value.employeeList : undefined; - expect(employeeList?.[member1]?.errors).toBeUndefined(); - expect(employeeList?.[member1]?.pendingAction).toBeNull(); - expect(employeeList?.[member2]?.errors).toBeUndefined(); - expect(employeeList?.[member2]?.pendingAction).toBeNull(); + expect(approveOnlyData.failureData).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + value: expect.objectContaining({ + employeeList: { + [member1]: { + email: member1, + role: CONST.POLICY.ROLE.USER, + pendingAction: null, + }, + [member2]: { + email: member2, + role: CONST.POLICY.ROLE.USER, + pendingAction: null, + }, + }, + }), + }), + ]), + ); const adminData = Member.buildUpdateWorkspaceMembersRoleOnyxData(fakePolicy, [member1], [1], CONST.POLICY.ROLE.ADMIN); - const adminUpdate = adminData.failureData.at(0); - const adminEmployeeList = adminUpdate && 'employeeList' in (adminUpdate.value ?? {}) ? adminUpdate.value.employeeList : undefined; - expect(adminEmployeeList?.[member1]?.errors).toBeTruthy(); - expect(adminEmployeeList?.[member1]?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(adminData.failureData).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + value: expect.objectContaining({ + employeeList: expect.objectContaining({ + [member1]: expect.objectContaining({ + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + errors: expect.any(Object), + }), + }), + }), + }), + ]), + ); }); }); describe('requestWorkspaceOwnerChange', () => { From 23cbf39bda6f8eeb253b774e7623ed51d3b097d4 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:41:16 +0530 Subject: [PATCH 6/6] Use TestHelper anyObject for error matcher --- tests/actions/PolicyMemberTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/actions/PolicyMemberTest.ts b/tests/actions/PolicyMemberTest.ts index 985fd29a8dd4..85dbcb778ddc 100644 --- a/tests/actions/PolicyMemberTest.ts +++ b/tests/actions/PolicyMemberTest.ts @@ -263,7 +263,7 @@ describe('actions/PolicyMember', () => { employeeList: expect.objectContaining({ [member1]: expect.objectContaining({ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - errors: expect.any(Object), + errors: TestHelper.anyObject, }), }), }),