Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions src/libs/actions/Policy/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2865,12 +2865,7 @@ function buildPolicyData(options: BuildPolicyDataOptions): OnyxData<BuildPolicyD
areDistanceRatesEnabled,
};

if (
introSelected !== undefined &&
(introSelected.choice === CONST.ONBOARDING_CHOICES.TEST_DRIVE_RECEIVER || !introSelected?.createWorkspace) &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MelvinBot Help me to find out which PR introduced !introSelected?.createWorkspace check here?

engagementChoice &&
shouldAddOnboardingTasks
) {
if (introSelected !== undefined && (introSelected.choice === CONST.ONBOARDING_CHOICES.TEST_DRIVE_RECEIVER || !introSelected?.choice) && engagementChoice && shouldAddOnboardingTasks) {
const {onboardingMessages} = getOnboardingMessages();
const onboardingData = ReportUtils.prepareOnboardingOnyxData({
introSelected,
Expand Down
176 changes: 167 additions & 9 deletions tests/actions/PolicyTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,11 @@ describe('actions/Policy', () => {
// We do not pass tasks to `#admins` channel in favour of backed generated followup-list
const expectedManageTeamDefaultTasksCount = 0;

// After filtering, two actions are added to the list =- signoff message (+1) and default create action (+1)
// The user has already completed onboarding (introSelected.choice is set), so the onboarding-tasks block
// in buildPolicyData is skipped — no signoff message is attached to the new workspace's #admins room.
// Only the default CREATED action remains.
const expectedReportActionsOfTypeCreatedCount = 1;
const expectedSignOffMessagesCount = 1;
const expectedSignOffMessagesCount = 0;
expect(adminReportActions.length).toBe(expectedManageTeamDefaultTasksCount + expectedReportActionsOfTypeCreatedCount + expectedSignOffMessagesCount);

let reportActionsOfTypeCreatedCount = 0;
Expand Down Expand Up @@ -1219,16 +1221,16 @@ describe('actions/Policy', () => {
const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve());
const policyID = Policy.generatePolicyID();

// When creating a workspace with isSelfTourViewed set to true
// Use LOOKING_AROUND as introSelected.choice to ensure VIEW_TOUR task is included
// (VIEW_TOUR is filtered out when both introSelected.choice and engagementChoice are MANAGE_TEAM)
// When creating a workspace with isSelfTourViewed set to true.
// introSelected.choice is left undefined to simulate a user who hasn't completed onboarding yet —
// that's the only state in which the onboarding-tasks block runs (see buildPolicyData guard).
Policy.createWorkspace({
policyOwnerEmail: ESH_EMAIL,
makeMeAdmin: true,
policyName: WORKSPACE_NAME,
policyID,
engagementChoice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM,
introSelected: {choice: CONST.ONBOARDING_CHOICES.LOOKING_AROUND},
introSelected: {},
currentUserAccountIDParam: ESH_ACCOUNT_ID,
currentUserEmailParam: ESH_EMAIL,
isSelfTourViewed: true,
Expand Down Expand Up @@ -1262,15 +1264,16 @@ describe('actions/Policy', () => {
const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve());
const policyID = Policy.generatePolicyID();

// When creating a workspace with isSelfTourViewed set to false
// Use LOOKING_AROUND as introSelected.choice to ensure VIEW_TOUR task is included
// When creating a workspace with isSelfTourViewed set to false.
// introSelected.choice is left undefined to simulate a user who hasn't completed onboarding yet —
// that's the only state in which the onboarding-tasks block runs (see buildPolicyData guard).
Policy.createWorkspace({
policyOwnerEmail: ESH_EMAIL,
makeMeAdmin: true,
policyName: WORKSPACE_NAME,
policyID,
engagementChoice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM,
introSelected: {choice: CONST.ONBOARDING_CHOICES.LOOKING_AROUND},
introSelected: {},
currentUserAccountIDParam: ESH_ACCOUNT_ID,
currentUserEmailParam: ESH_EMAIL,
isSelfTourViewed: false,
Expand Down Expand Up @@ -1373,6 +1376,161 @@ describe('actions/Policy', () => {
apiWriteSpy.mockRestore();
});

// The guard in buildPolicyData decides whether to attach onboarding tasks (guidedSetupData)
// to the new workspace. Users who already finished a Concierge-based onboarding flow
// (TRACK_WORKSPACE / EMPLOYER / SUBMIT / CHAT_SPLIT / LOOKING_AROUND / PERSONAL_SPEND)
// never have introSelected.createWorkspace set, so the previous `!introSelected?.createWorkspace`
// check evaluated truthy and leaked the tasks into the new workspace's #admins room.
// The fix swaps that for `!introSelected?.choice`, which is populated by every completeOnboarding call.
const onboardedChoices = [
CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE,
CONST.ONBOARDING_CHOICES.EMPLOYER,
CONST.ONBOARDING_CHOICES.SUBMIT,
CONST.ONBOARDING_CHOICES.CHAT_SPLIT,
CONST.ONBOARDING_CHOICES.LOOKING_AROUND,
CONST.ONBOARDING_CHOICES.PERSONAL_SPEND,
CONST.ONBOARDING_CHOICES.MANAGE_TEAM,
] as const;

it.each(onboardedChoices)('should not add onboarding tasks when user already completed onboarding (choice=%s)', async (choice) => {
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();

// When creating a workspace and the user has already completed a guided onboarding flow,
// introSelected.choice is populated but introSelected.createWorkspace is not (Concierge-based flows
// never set it; MANAGE_TEAM only sets it for the *first* workspace, not subsequent ones).
Policy.createWorkspace({
policyOwnerEmail: ESH_EMAIL,
makeMeAdmin: true,
policyName: WORKSPACE_NAME,
policyID,
engagementChoice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM,
introSelected: {choice},
currentUserAccountIDParam: ESH_ACCOUNT_ID,
currentUserEmailParam: ESH_EMAIL,
isSelfTourViewed: false,
betas: undefined,
hasActiveAdminPolicies: false,
});
await waitForBatchedUpdates();

// Then guidedSetupData should NOT be sent, so the #admins room of the new workspace stays empty of onboarding tasks.
const apiCallArgs = apiWriteSpy.mock.calls.find((call) => call.at(0) === WRITE_COMMANDS.CREATE_WORKSPACE);
expect(apiCallArgs).toBeDefined();
const params = apiCallArgs?.[1] as {guidedSetupData?: string};
expect(params.guidedSetupData).toBeUndefined();

apiWriteSpy.mockRestore();
});

it('should add onboarding tasks when the user has not yet selected an onboarding choice', 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();

// When creating a workspace before the user has gone through guided onboarding (introSelected.choice is undefined),
// the block should run so that onboarding tasks are attached to the new workspace.
Policy.createWorkspace({
policyOwnerEmail: ESH_EMAIL,
makeMeAdmin: true,
policyName: WORKSPACE_NAME,
policyID,
engagementChoice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM,
introSelected: {},
currentUserAccountIDParam: ESH_ACCOUNT_ID,
currentUserEmailParam: ESH_EMAIL,
isSelfTourViewed: false,
betas: undefined,
hasActiveAdminPolicies: false,
});
await waitForBatchedUpdates();

// Then guidedSetupData should be sent.
const apiCallArgs = apiWriteSpy.mock.calls.find((call) => call.at(0) === WRITE_COMMANDS.CREATE_WORKSPACE);
expect(apiCallArgs).toBeDefined();
const params = apiCallArgs?.[1] as {guidedSetupData?: string};
expect(params.guidedSetupData).toBeDefined();

apiWriteSpy.mockRestore();
});

it('should add onboarding tasks for TEST_DRIVE_RECEIVER even when choice is 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 policyID = Policy.generatePolicyID();

// Even when introSelected.choice is populated, TEST_DRIVE_RECEIVER must still enter the block via
// the first disjunct so that the downstream Concierge createWorkspace task gets completed.
Policy.createWorkspace({
policyOwnerEmail: ESH_EMAIL,
makeMeAdmin: true,
policyName: WORKSPACE_NAME,
policyID,
engagementChoice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM,
introSelected: {choice: CONST.ONBOARDING_CHOICES.TEST_DRIVE_RECEIVER},
currentUserAccountIDParam: ESH_ACCOUNT_ID,
currentUserEmailParam: ESH_EMAIL,
isSelfTourViewed: false,
betas: undefined,
hasActiveAdminPolicies: false,
});
await waitForBatchedUpdates();

const apiCallArgs = apiWriteSpy.mock.calls.find((call) => call.at(0) === WRITE_COMMANDS.CREATE_WORKSPACE);
expect(apiCallArgs).toBeDefined();
const params = apiCallArgs?.[1] as {guidedSetupData?: string};
expect(params.guidedSetupData).toBeDefined();

apiWriteSpy.mockRestore();
});

it('should bail out of onboarding-tasks block if prepareOnboardingOnyxData returns undefined', 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());
// Force prepareOnboardingOnyxData to return undefined so the early-return path inside the guarded block runs.
// This mirrors the real-world case where the target chat (Concierge for non-MANAGE_TEAM flows) cannot be resolved.
const prepareSpy = jest.spyOn(ReportUtils, 'prepareOnboardingOnyxData').mockReturnValue(undefined);
const policyID = Policy.generatePolicyID();

// introSelected.choice is undefined so the block enters; engagementChoice is non-MANAGE_TEAM
// so prepareOnboardingOnyxData would normally route to Concierge — and since the mock returns undefined,
// buildPolicyData should return early without setting guidedSetupData.
Policy.createWorkspace({
policyOwnerEmail: ESH_EMAIL,
makeMeAdmin: true,
policyName: WORKSPACE_NAME,
policyID,
engagementChoice: CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE,
introSelected: {},
currentUserAccountIDParam: ESH_ACCOUNT_ID,
currentUserEmailParam: ESH_EMAIL,
isSelfTourViewed: false,
betas: undefined,
hasActiveAdminPolicies: false,
});
await waitForBatchedUpdates();

expect(prepareSpy).toHaveBeenCalled();
const apiCallArgs = apiWriteSpy.mock.calls.find((call) => call.at(0) === WRITE_COMMANDS.CREATE_WORKSPACE);
expect(apiCallArgs).toBeDefined();
const params = apiCallArgs?.[1] as {guidedSetupData?: string; bespokeWelcomeMessage?: string};
// Early-return path should not populate the onboarding fields.
expect(params.guidedSetupData).toBeUndefined();
expect(params.bespokeWelcomeMessage).toBeUndefined();

prepareSpy.mockRestore();
apiWriteSpy.mockRestore();
});

it('should publish a workspace created event if this is their first policy', () => {
Policy.createWorkspace({
policyOwnerEmail: ESH_EMAIL,
Expand Down
Loading