From ea403730cf96c8d0f37869b1f4dd7d306f604e5a Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Wed, 15 Apr 2026 12:21:35 +0200 Subject: [PATCH 1/4] feat(onboarding) - add dynamic steps --- example/src/Onboarding.tsx | 27 +++++------ src/flows/Onboarding/hooks.tsx | 51 +++++++++++++------- src/flows/Onboarding/utils.ts | 85 ++++++++++++++++++++++++++-------- 3 files changed, 109 insertions(+), 54 deletions(-) diff --git a/example/src/Onboarding.tsx b/example/src/Onboarding.tsx index 3be3691f9..7e1a1fe20 100644 --- a/example/src/Onboarding.tsx +++ b/example/src/Onboarding.tsx @@ -35,13 +35,6 @@ export const InviteSection = ({ ); }; -const STEPS = [ - 'Select Country', - 'Basic Information', - 'Contract Details', - 'Benefits', - 'Review & Invite', -]; type MultiStepFormProps = { onboardingBag: OnboardingRenderProps['onboardingBag']; @@ -213,7 +206,7 @@ const OnBoardingRender = ({ }: MultiStepFormProps) => { const currentStepIndex = onboardingBag.stepState.currentStep.index; - const stepTitle = STEPS[currentStepIndex]; + const stepTitle = onboardingBag.steps[currentStepIndex].label; if (onboardingBag.isLoading) { return

Loading...

; @@ -223,14 +216,16 @@ const OnBoardingRender = ({ <>
diff --git a/src/flows/Onboarding/hooks.tsx b/src/flows/Onboarding/hooks.tsx index b7ee9d207..7d91f745f 100644 --- a/src/flows/Onboarding/hooks.tsx +++ b/src/flows/Onboarding/hooks.tsx @@ -11,8 +11,8 @@ import { getContractDetailsSchemaVersion, getBasicInformationSchemaVersion, reviewStepAllowedEmploymentStatus, - STEPS, - STEPS_WITHOUT_SELECT_COUNTRY, + buildSteps, + StepKeys, } from '@/src/flows/Onboarding/utils'; import { prettifyFormValues } from '@/src/lib/utils'; import { @@ -53,12 +53,10 @@ const jsonSchemaToEmployment: Partial< contract_details: 'contract_details', }; -const stepToFormSchemaMap: Record< - keyof typeof STEPS, - JSONSchemaFormType | null -> = { +const stepToFormSchemaMap: Record = { select_country: null, basic_information: 'employment_basic_information', + engagement_agreement_details: null, contract_details: 'contract_details', benefits: null, review: null, @@ -147,12 +145,20 @@ export const useOnboarding = ({ isUpdating: Boolean(employmentId), }, }); - const stepsToUse = skipSteps?.includes('select_country') - ? STEPS_WITHOUT_SELECT_COUNTRY - : STEPS; + + const [includeEngagementAgreementDetails] = useState(false); + + const { steps, stepsArray } = useMemo( + () => + buildSteps({ + includeSelectCountry: !skipSteps?.includes('select_country'), + includeEngagementAgreementDetails, + }), + [includeEngagementAgreementDetails, skipSteps], + ); const onStepChange = useCallback( - (step: Step) => { + (step: Step) => { updateErrorContext({ step: step.name, }); @@ -168,10 +174,7 @@ export const useOnboarding = ({ nextStep, goToStep, setStepValues, - } = useStepState( - stepsToUse as Record>, - onStepChange, - ); + } = useStepState(steps, onStepChange); const fieldsMetaRef = useRef<{ select_country: Meta; @@ -446,7 +449,7 @@ export const useOnboarding = ({ const initialValuesBenefitOffers = useMemo(() => { if (stepState.currentStep.name === 'benefits') { const benefitsFormValues = { - ...stepState.values?.[stepState.currentStep.name as keyof typeof STEPS], // Restore values for the current step + ...stepState.values?.[stepState.currentStep.name as StepKeys], // Restore values for the current step ...fieldValues, }; return mergeWith({}, benefitOffers, benefitsFormValues); @@ -459,10 +462,12 @@ export const useOnboarding = ({ fieldValues, ]); - const stepFields: Record = useMemo( + const stepFields: Record = useMemo( () => ({ select_country: selectCountryForm?.fields || [], basic_information: basicInformationForm?.fields || [], + // TODO: Fix later when we have the engagement agreement details form + engagement_agreement_details: [], contract_details: contractDetailsForm?.fields || [], benefits: benefitOffersSchema?.fields || [], review: [], @@ -476,11 +481,13 @@ export const useOnboarding = ({ ); const stepFieldsWithFlatFieldsets: Record< - keyof typeof STEPS, + StepKeys, JSFFieldset | null | undefined > = { select_country: null, basic_information: basicInformationForm?.meta['x-jsf-fieldsets'], + // TODO: Fix later when we have the engagement agreement details form + engagement_agreement_details: null, contract_details: contractDetailsForm?.meta['x-jsf-fieldsets'], benefits: null, review: null, @@ -638,6 +645,8 @@ export const useOnboarding = ({ basic_information: basicInformationInitialValues, contract_details: contractDetailsInitialValues, benefits: benefitsInitialValues, + // TODO: Fix later when we have the engagement agreement details form + engagement_agreement_details: {}, review: {}, }); @@ -789,7 +798,7 @@ export const useOnboarding = ({ nextStep(); } - function goTo(step: keyof typeof STEPS) { + function goTo(step: StepKeys) { goToStep(step); } @@ -959,5 +968,11 @@ export const useOnboarding = ({ * @returns {boolean} */ canInvite, + + /** + * Steps array + * @returns {Array<{name: string, visible: boolean, index: number, label: string}>} + */ + steps: stepsArray, }; }; diff --git a/src/flows/Onboarding/utils.ts b/src/flows/Onboarding/utils.ts index 4cf726e70..022de5501 100644 --- a/src/flows/Onboarding/utils.ts +++ b/src/flows/Onboarding/utils.ts @@ -1,33 +1,78 @@ import { Employment, OnboardingFlowProps } from '@/src/flows/Onboarding/types'; import { Step } from '@/src/flows/useStepState'; -type StepKeys = +export type StepKeys = | 'select_country' | 'basic_information' + | 'engagement_agreement_details' | 'contract_details' | 'benefits' | 'review'; -export const STEPS: Record> = { - select_country: { - index: 0, - name: 'select_country', - }, - basic_information: { index: 1, name: 'basic_information' }, - contract_details: { index: 2, name: 'contract_details' }, - benefits: { index: 3, name: 'benefits' }, - review: { index: 4, name: 'review' }, -} as const; +type StepConfig = { + includeSelectCountry?: boolean; + includeEngagementAgreementDetails?: boolean; +}; + +export function buildSteps(config: StepConfig = {}) { + const stepDefinitions: Array<{ + name: StepKeys; + label: string; + visible: boolean; + }> = [ + { + name: 'select_country', + label: 'Select Country', + visible: Boolean(config?.includeSelectCountry), + }, + { + name: 'basic_information', + label: 'Basic Information', + visible: true, + }, + { + name: 'engagement_agreement_details', + label: 'Engagement Agreement Details', + visible: Boolean(config?.includeEngagementAgreementDetails), + }, + { + name: 'contract_details', + label: 'Contract Details', + visible: true, + }, + { + name: 'benefits', + label: 'Benefits', + visible: true, + }, + { + name: 'review', + label: 'Review', + visible: true, + }, + ]; + + const stepsArray = stepDefinitions.map((step, index) => ({ + name: step.name, + index, + label: step.label, + visible: step.visible, + })); + + const steps = stepsArray.reduce( + (acc, step) => { + acc[step.name] = { + index: step.index, + name: step.name, + visible: step.visible, + }; + return acc; + }, + {} as Record>, + ); -export const STEPS_WITHOUT_SELECT_COUNTRY: Record< - Exclude, - Step> -> = { - basic_information: { index: 0, name: 'basic_information' }, - contract_details: { index: 1, name: 'contract_details' }, - benefits: { index: 2, name: 'benefits' }, - review: { index: 3, name: 'review' }, -} as const; + return { steps, stepsArray }; +} /** * Array of employment statuses that are allowed to proceed to the review step. From a895b5eb0ee081ecc42cae587adf5d388c4ca3f7 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Wed, 15 Apr 2026 12:35:05 +0200 Subject: [PATCH 2/4] feat(onboarding) - add feature flag for dynamic steps --- example/src/Onboarding.tsx | 4 +- src/flows/Onboarding/hooks.tsx | 13 ++- src/flows/Onboarding/types.ts | 5 +- src/flows/Onboarding/utils.test.ts | 168 +++++++++++++++++++++++++++++ src/flows/Onboarding/utils.ts | 31 ++++-- 5 files changed, 210 insertions(+), 11 deletions(-) create mode 100644 src/flows/Onboarding/utils.test.ts diff --git a/example/src/Onboarding.tsx b/example/src/Onboarding.tsx index 7e1a1fe20..d7b6c4ce7 100644 --- a/example/src/Onboarding.tsx +++ b/example/src/Onboarding.tsx @@ -206,6 +206,8 @@ const OnBoardingRender = ({ }: MultiStepFormProps) => { const currentStepIndex = onboardingBag.stepState.currentStep.index; + // When using dynamic_steps feature, you need to filter and use step.index for comparison + // Otherwise, you can use the steps array directly with sequential indices const stepTitle = onboardingBag.steps[currentStepIndex].label; if (onboardingBag.isLoading) { @@ -259,7 +261,7 @@ const OnboardingWithProps = ({ employmentId={employmentId} externalId={externalId} options={{ - features: ['onboarding_reserves'], + features: ['onboarding_reserves', 'dynamic_steps'], jsonSchemaVersion: { employment_basic_information: 3, }, diff --git a/src/flows/Onboarding/hooks.tsx b/src/flows/Onboarding/hooks.tsx index 7d91f745f..0f8904797 100644 --- a/src/flows/Onboarding/hooks.tsx +++ b/src/flows/Onboarding/hooks.tsx @@ -148,13 +148,16 @@ export const useOnboarding = ({ const [includeEngagementAgreementDetails] = useState(false); + const useDynamicSteps = options?.features?.includes('dynamic_steps') ?? false; + const { steps, stepsArray } = useMemo( () => buildSteps({ includeSelectCountry: !skipSteps?.includes('select_country'), includeEngagementAgreementDetails, + useDynamicSteps, }), - [includeEngagementAgreementDetails, skipSteps], + [includeEngagementAgreementDetails, skipSteps, useDynamicSteps], ); const onStepChange = useCallback( @@ -970,7 +973,13 @@ export const useOnboarding = ({ canInvite, /** - * Steps array + * Steps array for dynamic step navigation + * When 'dynamic_steps' feature is enabled: + * - Returns all steps including hidden ones with their original indices + * - Consumers must filter by `visible` property + * When disabled (default): + * - Returns only visible steps with sequential indices + * - Maintains backwards compatibility with existing implementations * @returns {Array<{name: string, visible: boolean, index: number, label: string}>} */ steps: stepsArray, diff --git a/src/flows/Onboarding/types.ts b/src/flows/Onboarding/types.ts index 14d87b7d4..78cc58aeb 100644 --- a/src/flows/Onboarding/types.ts +++ b/src/flows/Onboarding/types.ts @@ -49,7 +49,7 @@ export type OnboardingRenderProps = { }; }; -type OnboardingFeatures = 'onboarding_reserves'; +type OnboardingFeatures = 'onboarding_reserves' | 'dynamic_steps'; /** * JSON schema version configuration for a specific country @@ -144,7 +144,8 @@ export type OnboardingFlowProps = { /** * The features to use for the onboarding. * This is used to enable or disable features for the onboarding. - * Currently only supports enabling the onboarding reserves feature. + * - 'onboarding_reserves': Enable onboarding reserves feature + * - 'dynamic_steps': Enable dynamic step generation with visibility control (opt-in, will be default in next major version) */ features?: OnboardingFeatures[]; }; diff --git a/src/flows/Onboarding/utils.test.ts b/src/flows/Onboarding/utils.test.ts new file mode 100644 index 000000000..ed4088b93 --- /dev/null +++ b/src/flows/Onboarding/utils.test.ts @@ -0,0 +1,168 @@ +import { describe, it, expect } from 'vitest'; +import { buildSteps } from './utils'; + +describe('buildSteps', () => { + describe('legacy behavior (useDynamicSteps: false)', () => { + it('should return sequential indices when select_country is included', () => { + const { steps, stepsArray } = buildSteps({ + includeSelectCountry: true, + useDynamicSteps: false, + }); + + // All visible steps should have sequential indices + expect(stepsArray).toHaveLength(5); + expect(stepsArray[0]).toMatchObject({ + name: 'select_country', + index: 0, + visible: true, + }); + expect(stepsArray[1]).toMatchObject({ + name: 'basic_information', + index: 1, + visible: true, + }); + expect(stepsArray[2]).toMatchObject({ + name: 'contract_details', + index: 2, + visible: true, + }); + expect(stepsArray[3]).toMatchObject({ + name: 'benefits', + index: 3, + visible: true, + }); + expect(stepsArray[4]).toMatchObject({ + name: 'review', + index: 4, + visible: true, + }); + + // Steps object should match + expect(steps.select_country.index).toBe(0); + expect(steps.basic_information.index).toBe(1); + expect(steps.review.index).toBe(4); + }); + + it('should return sequential indices when select_country is excluded', () => { + const { steps, stepsArray } = buildSteps({ + includeSelectCountry: false, + useDynamicSteps: false, + }); + + // Only visible steps, with sequential indices starting at 0 + expect(stepsArray).toHaveLength(4); + expect(stepsArray[0]).toMatchObject({ + name: 'basic_information', + index: 0, // Sequential from 0 + visible: true, + }); + expect(stepsArray[1]).toMatchObject({ + name: 'contract_details', + index: 1, + visible: true, + }); + expect(stepsArray[2]).toMatchObject({ + name: 'benefits', + index: 2, + visible: true, + }); + expect(stepsArray[3]).toMatchObject({ + name: 'review', + index: 3, + visible: true, + }); + + // Steps object should use sequential indices + expect(steps.basic_information.index).toBe(0); + expect(steps.contract_details.index).toBe(1); + expect(steps.review.index).toBe(3); + }); + }); + + describe('new dynamic behavior (useDynamicSteps: true)', () => { + it('should preserve original indices for all steps including hidden ones', () => { + const { steps, stepsArray } = buildSteps({ + includeSelectCountry: false, + useDynamicSteps: true, + }); + + // All steps should be present, including hidden ones + expect(stepsArray).toHaveLength(6); + + // Hidden step should still be in the array with original index + expect(stepsArray[0]).toMatchObject({ + name: 'select_country', + index: 0, // Original index preserved + visible: false, + }); + + // Visible steps keep their original indices + expect(stepsArray[1]).toMatchObject({ + name: 'basic_information', + index: 1, // Original index, not renumbered + visible: true, + }); + + expect(stepsArray[2]).toMatchObject({ + name: 'engagement_agreement_details', + index: 2, + visible: false, + }); + + expect(stepsArray[3]).toMatchObject({ + name: 'contract_details', + index: 3, + visible: true, + }); + + // Steps object should preserve original indices + expect(steps.select_country.index).toBe(0); + expect(steps.basic_information.index).toBe(1); + expect(steps.contract_details.index).toBe(3); + expect(steps.review.index).toBe(5); + }); + + it('should include engagement_agreement_details when enabled', () => { + const { stepsArray } = buildSteps({ + includeSelectCountry: true, + includeEngagementAgreementDetails: true, + useDynamicSteps: true, + }); + + expect(stepsArray).toHaveLength(6); + expect(stepsArray[2]).toMatchObject({ + name: 'engagement_agreement_details', + index: 2, + visible: true, + }); + }); + }); + + describe('backwards compatibility', () => { + it('should default to legacy behavior when useDynamicSteps is not specified', () => { + const { stepsArray } = buildSteps({ + includeSelectCountry: false, + }); + + // Should filter out hidden steps and use sequential indices + expect(stepsArray).toHaveLength(4); + expect(stepsArray[0].name).toBe('basic_information'); + expect(stepsArray[0].index).toBe(0); // Sequential + }); + + it('should not break existing step index logic', () => { + const { steps } = buildSteps({ + includeSelectCountry: true, + useDynamicSteps: false, + }); + + // Simulate what existing code does: accessing steps by index + const currentStepIndex = 2; // e.g., contract_details + const stepAtIndex = Object.values(steps).find( + (s) => s.index === currentStepIndex, + ); + + expect(stepAtIndex?.name).toBe('contract_details'); + }); + }); +}); diff --git a/src/flows/Onboarding/utils.ts b/src/flows/Onboarding/utils.ts index 022de5501..baa7f5f07 100644 --- a/src/flows/Onboarding/utils.ts +++ b/src/flows/Onboarding/utils.ts @@ -12,6 +12,7 @@ export type StepKeys = type StepConfig = { includeSelectCountry?: boolean; includeEngagementAgreementDetails?: boolean; + useDynamicSteps?: boolean; }; export function buildSteps(config: StepConfig = {}) { @@ -52,12 +53,30 @@ export function buildSteps(config: StepConfig = {}) { }, ]; - const stepsArray = stepDefinitions.map((step, index) => ({ - name: step.name, - index, - label: step.label, - visible: step.visible, - })); + // When useDynamicSteps is false (default/legacy behavior): + // - Filter out hidden steps first + // - Assign sequential indices (0, 1, 2, 3...) + // - This maintains backwards compatibility with existing implementations + // + // When useDynamicSteps is true (new behavior): + // - Keep all steps including hidden ones + // - Preserve original indices even for hidden steps + // - Consumers must filter by `visible` property + const stepsArray = config.useDynamicSteps + ? stepDefinitions.map((step, index) => ({ + name: step.name, + index, + label: step.label, + visible: step.visible, + })) + : stepDefinitions + .filter((step) => step.visible) + .map((step, index) => ({ + name: step.name, + index, + label: step.label, + visible: step.visible, + })); const steps = stepsArray.reduce( (acc, step) => { From be9dfc695ad71af204a61f5b24baffe294832327 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Wed, 15 Apr 2026 12:43:37 +0200 Subject: [PATCH 3/4] add commented old code --- example/src/Onboarding.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/example/src/Onboarding.tsx b/example/src/Onboarding.tsx index d7b6c4ce7..456668334 100644 --- a/example/src/Onboarding.tsx +++ b/example/src/Onboarding.tsx @@ -200,6 +200,14 @@ const MultiStepForm = ({ components, onboardingBag }: MultiStepFormProps) => { } }; +/* const STEPS = [ + 'Select Country', + 'Basic Information', + 'Contract Details', + 'Benefits', + 'Review & Invite', +]; + */ const OnBoardingRender = ({ onboardingBag, components, @@ -208,6 +216,7 @@ const OnBoardingRender = ({ // When using dynamic_steps feature, you need to filter and use step.index for comparison // Otherwise, you can use the steps array directly with sequential indices + //const stepTitle = STEPS[currentStepIndex]; const stepTitle = onboardingBag.steps[currentStepIndex].label; if (onboardingBag.isLoading) { @@ -218,6 +227,14 @@ const OnBoardingRender = ({ <>
    + {/* {STEPS.map((step, index) => ( +
  • + {step} +
  • + ))} */} {onboardingBag.steps .filter((step) => step.visible) .map((step, index) => ( From d9eae8e0c87129c22af7bc8fac89f61b0c6e83d3 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Wed, 15 Apr 2026 13:01:49 +0200 Subject: [PATCH 4/4] fix utils test --- src/flows/Onboarding/tests/utils.test.ts | 167 ++++++++++++++++++++++ src/flows/Onboarding/utils.test.ts | 168 ----------------------- 2 files changed, 167 insertions(+), 168 deletions(-) delete mode 100644 src/flows/Onboarding/utils.test.ts diff --git a/src/flows/Onboarding/tests/utils.test.ts b/src/flows/Onboarding/tests/utils.test.ts index 0fc83c124..6d5c2302e 100644 --- a/src/flows/Onboarding/tests/utils.test.ts +++ b/src/flows/Onboarding/tests/utils.test.ts @@ -1,4 +1,5 @@ import { + buildSteps, getBasicInformationSchemaVersion, getBenefitOffersSchemaVersion, getContractDetailsSchemaVersion, @@ -65,3 +66,169 @@ describe('getBasicInformationSchemaVersion', () => { expect(result).toEqual(2); }); }); + +describe('buildSteps', () => { + describe('legacy behavior (useDynamicSteps: false)', () => { + it('should return sequential indices when select_country is included', () => { + const { steps, stepsArray } = buildSteps({ + includeSelectCountry: true, + useDynamicSteps: false, + }); + + // All visible steps should have sequential indices + expect(stepsArray).toHaveLength(5); + expect(stepsArray[0]).toMatchObject({ + name: 'select_country', + index: 0, + visible: true, + }); + expect(stepsArray[1]).toMatchObject({ + name: 'basic_information', + index: 1, + visible: true, + }); + expect(stepsArray[2]).toMatchObject({ + name: 'contract_details', + index: 2, + visible: true, + }); + expect(stepsArray[3]).toMatchObject({ + name: 'benefits', + index: 3, + visible: true, + }); + expect(stepsArray[4]).toMatchObject({ + name: 'review', + index: 4, + visible: true, + }); + + // Steps object should match + expect(steps.select_country.index).toBe(0); + expect(steps.basic_information.index).toBe(1); + expect(steps.review.index).toBe(4); + }); + + it('should return sequential indices when select_country is excluded', () => { + const { steps, stepsArray } = buildSteps({ + includeSelectCountry: false, + useDynamicSteps: false, + }); + + // Only visible steps, with sequential indices starting at 0 + expect(stepsArray).toHaveLength(4); + expect(stepsArray[0]).toMatchObject({ + name: 'basic_information', + index: 0, // Sequential from 0 + visible: true, + }); + expect(stepsArray[1]).toMatchObject({ + name: 'contract_details', + index: 1, + visible: true, + }); + expect(stepsArray[2]).toMatchObject({ + name: 'benefits', + index: 2, + visible: true, + }); + expect(stepsArray[3]).toMatchObject({ + name: 'review', + index: 3, + visible: true, + }); + + // Steps object should use sequential indices + expect(steps.basic_information.index).toBe(0); + expect(steps.contract_details.index).toBe(1); + expect(steps.review.index).toBe(3); + }); + }); + + describe('new dynamic behavior (useDynamicSteps: true)', () => { + it('should preserve original indices for all steps including hidden ones', () => { + const { steps, stepsArray } = buildSteps({ + includeSelectCountry: false, + useDynamicSteps: true, + }); + + // All steps should be present, including hidden ones + expect(stepsArray).toHaveLength(6); + + // Hidden step should still be in the array with original index + expect(stepsArray[0]).toMatchObject({ + name: 'select_country', + index: 0, // Original index preserved + visible: false, + }); + + // Visible steps keep their original indices + expect(stepsArray[1]).toMatchObject({ + name: 'basic_information', + index: 1, // Original index, not renumbered + visible: true, + }); + + expect(stepsArray[2]).toMatchObject({ + name: 'engagement_agreement_details', + index: 2, + visible: false, + }); + + expect(stepsArray[3]).toMatchObject({ + name: 'contract_details', + index: 3, + visible: true, + }); + + // Steps object should preserve original indices + expect(steps.select_country.index).toBe(0); + expect(steps.basic_information.index).toBe(1); + expect(steps.contract_details.index).toBe(3); + expect(steps.review.index).toBe(5); + }); + + it('should include engagement_agreement_details when enabled', () => { + const { stepsArray } = buildSteps({ + includeSelectCountry: true, + includeEngagementAgreementDetails: true, + useDynamicSteps: true, + }); + + expect(stepsArray).toHaveLength(6); + expect(stepsArray[2]).toMatchObject({ + name: 'engagement_agreement_details', + index: 2, + visible: true, + }); + }); + }); + + describe('backwards compatibility', () => { + it('should default to legacy behavior when useDynamicSteps is not specified', () => { + const { stepsArray } = buildSteps({ + includeSelectCountry: false, + }); + + // Should filter out hidden steps and use sequential indices + expect(stepsArray).toHaveLength(4); + expect(stepsArray[0].name).toBe('basic_information'); + expect(stepsArray[0].index).toBe(0); // Sequential + }); + + it('should not break existing step index logic', () => { + const { steps } = buildSteps({ + includeSelectCountry: true, + useDynamicSteps: false, + }); + + // Simulate what existing code does: accessing steps by index + const currentStepIndex = 2; // e.g., contract_details + const stepAtIndex = Object.values(steps).find( + (s) => s.index === currentStepIndex, + ); + + expect(stepAtIndex?.name).toBe('contract_details'); + }); + }); +}); diff --git a/src/flows/Onboarding/utils.test.ts b/src/flows/Onboarding/utils.test.ts deleted file mode 100644 index ed4088b93..000000000 --- a/src/flows/Onboarding/utils.test.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { buildSteps } from './utils'; - -describe('buildSteps', () => { - describe('legacy behavior (useDynamicSteps: false)', () => { - it('should return sequential indices when select_country is included', () => { - const { steps, stepsArray } = buildSteps({ - includeSelectCountry: true, - useDynamicSteps: false, - }); - - // All visible steps should have sequential indices - expect(stepsArray).toHaveLength(5); - expect(stepsArray[0]).toMatchObject({ - name: 'select_country', - index: 0, - visible: true, - }); - expect(stepsArray[1]).toMatchObject({ - name: 'basic_information', - index: 1, - visible: true, - }); - expect(stepsArray[2]).toMatchObject({ - name: 'contract_details', - index: 2, - visible: true, - }); - expect(stepsArray[3]).toMatchObject({ - name: 'benefits', - index: 3, - visible: true, - }); - expect(stepsArray[4]).toMatchObject({ - name: 'review', - index: 4, - visible: true, - }); - - // Steps object should match - expect(steps.select_country.index).toBe(0); - expect(steps.basic_information.index).toBe(1); - expect(steps.review.index).toBe(4); - }); - - it('should return sequential indices when select_country is excluded', () => { - const { steps, stepsArray } = buildSteps({ - includeSelectCountry: false, - useDynamicSteps: false, - }); - - // Only visible steps, with sequential indices starting at 0 - expect(stepsArray).toHaveLength(4); - expect(stepsArray[0]).toMatchObject({ - name: 'basic_information', - index: 0, // Sequential from 0 - visible: true, - }); - expect(stepsArray[1]).toMatchObject({ - name: 'contract_details', - index: 1, - visible: true, - }); - expect(stepsArray[2]).toMatchObject({ - name: 'benefits', - index: 2, - visible: true, - }); - expect(stepsArray[3]).toMatchObject({ - name: 'review', - index: 3, - visible: true, - }); - - // Steps object should use sequential indices - expect(steps.basic_information.index).toBe(0); - expect(steps.contract_details.index).toBe(1); - expect(steps.review.index).toBe(3); - }); - }); - - describe('new dynamic behavior (useDynamicSteps: true)', () => { - it('should preserve original indices for all steps including hidden ones', () => { - const { steps, stepsArray } = buildSteps({ - includeSelectCountry: false, - useDynamicSteps: true, - }); - - // All steps should be present, including hidden ones - expect(stepsArray).toHaveLength(6); - - // Hidden step should still be in the array with original index - expect(stepsArray[0]).toMatchObject({ - name: 'select_country', - index: 0, // Original index preserved - visible: false, - }); - - // Visible steps keep their original indices - expect(stepsArray[1]).toMatchObject({ - name: 'basic_information', - index: 1, // Original index, not renumbered - visible: true, - }); - - expect(stepsArray[2]).toMatchObject({ - name: 'engagement_agreement_details', - index: 2, - visible: false, - }); - - expect(stepsArray[3]).toMatchObject({ - name: 'contract_details', - index: 3, - visible: true, - }); - - // Steps object should preserve original indices - expect(steps.select_country.index).toBe(0); - expect(steps.basic_information.index).toBe(1); - expect(steps.contract_details.index).toBe(3); - expect(steps.review.index).toBe(5); - }); - - it('should include engagement_agreement_details when enabled', () => { - const { stepsArray } = buildSteps({ - includeSelectCountry: true, - includeEngagementAgreementDetails: true, - useDynamicSteps: true, - }); - - expect(stepsArray).toHaveLength(6); - expect(stepsArray[2]).toMatchObject({ - name: 'engagement_agreement_details', - index: 2, - visible: true, - }); - }); - }); - - describe('backwards compatibility', () => { - it('should default to legacy behavior when useDynamicSteps is not specified', () => { - const { stepsArray } = buildSteps({ - includeSelectCountry: false, - }); - - // Should filter out hidden steps and use sequential indices - expect(stepsArray).toHaveLength(4); - expect(stepsArray[0].name).toBe('basic_information'); - expect(stepsArray[0].index).toBe(0); // Sequential - }); - - it('should not break existing step index logic', () => { - const { steps } = buildSteps({ - includeSelectCountry: true, - useDynamicSteps: false, - }); - - // Simulate what existing code does: accessing steps by index - const currentStepIndex = 2; // e.g., contract_details - const stepAtIndex = Object.values(steps).find( - (s) => s.index === currentStepIndex, - ); - - expect(stepAtIndex?.name).toBe('contract_details'); - }); - }); -});