From 043a3f32bac372ca342f289d7411706307133774 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Tue, 24 Jun 2025 12:01:34 +0200 Subject: [PATCH 01/25] add code to support jumping to a certain step --- src/flows/Onboarding/hooks.tsx | 282 ++++++++++++++++++++++++++------- src/flows/useStepState.ts | 44 ++--- 2 files changed, 254 insertions(+), 72 deletions(-) diff --git a/src/flows/Onboarding/hooks.tsx b/src/flows/Onboarding/hooks.tsx index 00461d44..eb56e678 100644 --- a/src/flows/Onboarding/hooks.tsx +++ b/src/flows/Onboarding/hooks.tsx @@ -5,7 +5,7 @@ import { } from '@/src/client'; import { Fields } from '@remoteoss/json-schema-form'; -import { useStepState, Step } from '@/src/flows/useStepState'; +import { useStepState, Step, StepState } from '@/src/flows/useStepState'; import { prettifyFormValues, reviewStepAllowedEmploymentStatus, @@ -19,7 +19,7 @@ import { import { mutationToPromise } from '@/src/lib/mutations'; import { FieldValues } from 'react-hook-form'; import { OnboardingFlowParams } from '@/src/flows/Onboarding/types'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import mergeWith from 'lodash.mergewith'; import { useBenefitOffers, @@ -67,8 +67,6 @@ const getLoadingStates = ({ employmentStatus, employmentId, currentStepName, - basicInformationFields, - contractDetailsFields, }: { isLoadingBasicInformationForm: boolean; isLoadingContractDetailsForm: boolean; @@ -80,8 +78,6 @@ const getLoadingStates = ({ employmentStatus?: Employment['status']; employmentId?: string; currentStepName: string; - basicInformationFields: Fields; - contractDetailsFields: Fields; }) => { const initialLoading = isLoadingBasicInformationForm || @@ -102,14 +98,143 @@ const getLoadingStates = ({ const isLoading = initialLoading || shouldHandleReadOnlyEmployment; - const isNavigatingToReview = Boolean( - shouldHandleReadOnlyEmployment && - !initialLoading && - basicInformationFields.length > 0 && - contractDetailsFields.length > 0, + return { isLoading, isEmploymentReadOnly }; +}; + +const useNavigationConditions = ({ + employmentId, + employment, + initialLoading, + stepFields, + stepState, + isEmploymentReadOnly, + benefitOffers, + initialNavigationRef, +}: { + employmentId?: string; + employment?: Employment; + initialLoading: boolean; + stepFields: Record; + stepState: StepState; + isEmploymentReadOnly?: boolean; + benefitOffers: unknown; + initialNavigationRef: React.RefObject<{ + review: boolean; + contractDetails: boolean; + benefits: boolean; + }>; +}) => { + // Common conditions extracted as variables + const hasBasicInformation = + employment && Object.keys(employment.basic_information || {}).length > 0; + const hasContractDetails = + employment && Object.keys(employment.contract_details || {}).length > 0; + const areSchemasLoaded = + stepFields['basic_information'].length > 0 && + stepFields['contract_details'].length > 0; + const hasBenefitsSchema = stepFields['benefits'].length > 0; + const hasBenefitOffers = + benefitOffers && Object.keys(benefitOffers).length > 0; + const hasEmptyBenefitOffers = + benefitOffers && Object.keys(benefitOffers).length === 0; + const hasLoadedEmployment = Boolean( + employmentId && !initialLoading && employment, ); - return { isLoading, isNavigatingToReview, isEmploymentReadOnly }; + const isNavigatingToReviewWhenEmploymentIsFinal = useMemo(() => { + return Boolean( + hasLoadedEmployment && + isEmploymentReadOnly && + areSchemasLoaded && + stepState.currentStep.name !== 'review' && + !initialNavigationRef.current.review, + ); + }, [ + hasLoadedEmployment, + isEmploymentReadOnly, + areSchemasLoaded, + stepState.currentStep.name, + initialNavigationRef, + ]); + + const isNavigatingToReview = useMemo(() => { + return Boolean( + hasLoadedEmployment && + hasBasicInformation && + hasContractDetails && + areSchemasLoaded && + hasBenefitsSchema && + hasBenefitOffers && + stepState.currentStep.name !== 'review' && + !isNavigatingToReviewWhenEmploymentIsFinal && + !initialNavigationRef.current.review, + ); + }, [ + hasLoadedEmployment, + hasBasicInformation, + hasContractDetails, + areSchemasLoaded, + hasBenefitsSchema, + hasBenefitOffers, + stepState.currentStep.name, + isNavigatingToReviewWhenEmploymentIsFinal, + initialNavigationRef, + ]); + + const isNavigatingToContractDetails = useMemo(() => { + return Boolean( + hasLoadedEmployment && + hasBasicInformation && + !hasContractDetails && + areSchemasLoaded && + stepState.currentStep.name !== 'contract_details' && + !isNavigatingToReviewWhenEmploymentIsFinal && + !isNavigatingToReview && + !initialNavigationRef.current.contractDetails, + ); + }, [ + hasLoadedEmployment, + hasBasicInformation, + hasContractDetails, + areSchemasLoaded, + stepState.currentStep.name, + isNavigatingToReviewWhenEmploymentIsFinal, + isNavigatingToReview, + initialNavigationRef, + ]); + + const isNavigatingToBenefits = useMemo(() => { + return Boolean( + hasLoadedEmployment && + hasBasicInformation && + hasContractDetails && + hasBenefitsSchema && + hasEmptyBenefitOffers && + stepState.currentStep.name !== 'benefits' && + !isNavigatingToReviewWhenEmploymentIsFinal && + !isNavigatingToReview && + !isNavigatingToContractDetails && + !initialNavigationRef.current.benefits, + ); + }, [ + hasLoadedEmployment, + hasBasicInformation, + hasContractDetails, + hasBenefitsSchema, + hasEmptyBenefitOffers, + stepState.currentStep.name, + isNavigatingToReviewWhenEmploymentIsFinal, + isNavigatingToReview, + isNavigatingToContractDetails, + initialNavigationRef, + ]); + + return { + isNavigatingToReviewWhenEmploymentIsFinal, + isNavigatingToReview, + isNavigatingToContractDetails, + isNavigatingToBenefits, + }; }; export const useOnboarding = ({ @@ -412,7 +537,7 @@ export const useOnboarding = ({ ], ); - const { isLoading, isNavigatingToReview, isEmploymentReadOnly } = useMemo( + const { isLoading: initialLoading, isEmploymentReadOnly } = useMemo( () => getLoadingStates({ isLoadingBasicInformationForm, @@ -424,8 +549,6 @@ export const useOnboarding = ({ isLoadingCountries, employmentId, employmentStatus: employmentStatus, - basicInformationFields: stepFields.basic_information, - contractDetailsFields: stepFields.contract_details, currentStepName: currentStepName, }), [ @@ -438,55 +561,108 @@ export const useOnboarding = ({ isLoadingCountries, employmentId, employmentStatus, - stepFields.basic_information, - stepFields.contract_details, currentStepName, ], ); - useEffect(() => { - if (isNavigatingToReview) { - fieldsMetaRef.current = { - select_country: prettifyFormValues( - selectCountryInitialValues, - stepFields.select_country, - ), - basic_information: prettifyFormValues( - basicInformationInitialValues, - stepFields.basic_information, - ), - contract_details: prettifyFormValues( - contractDetailsInitialValues, - stepFields.contract_details, - ), - benefits: prettifyFormValues( - benefitsInitialValues, - stepFields.benefits, - ), - }; + // Single ref to track all initial navigations + const initialNavigationRef = useRef({ + review: false, + contractDetails: false, + benefits: false, + }); - setStepValues({ - select_country: selectCountryInitialValues, - basic_information: basicInformationInitialValues, - contract_details: contractDetailsInitialValues, - benefits: benefitsInitialValues, - review: {}, - }); + const { + isNavigatingToReviewWhenEmploymentIsFinal, + isNavigatingToReview, + isNavigatingToContractDetails, + isNavigatingToBenefits, + } = useNavigationConditions({ + employmentId: internalEmploymentId, + employment, + initialLoading, + stepFields, + stepState, + isEmploymentReadOnly, + benefitOffers, + initialNavigationRef, + }); - goToStep('review'); - } + const isLoading = + initialLoading || + isNavigatingToReviewWhenEmploymentIsFinal || + isNavigatingToReview || + isNavigatingToContractDetails || + isNavigatingToBenefits; + + const initializeStepValues = useCallback(() => { + fieldsMetaRef.current = { + select_country: prettifyFormValues( + selectCountryInitialValues, + stepFields.select_country, + ), + basic_information: prettifyFormValues( + basicInformationInitialValues, + stepFields.basic_information, + ), + contract_details: prettifyFormValues( + contractDetailsInitialValues, + stepFields.contract_details, + ), + benefits: prettifyFormValues(benefitsInitialValues, stepFields.benefits), + }; + + setStepValues({ + select_country: selectCountryInitialValues, + basic_information: basicInformationInitialValues, + contract_details: contractDetailsInitialValues, + benefits: benefitsInitialValues, + review: {}, + }); }, [ + selectCountryInitialValues, + stepFields.select_country, + stepFields.basic_information, + stepFields.contract_details, + stepFields.benefits, basicInformationInitialValues, - benefitsInitialValues, contractDetailsInitialValues, + benefitsInitialValues, + setStepValues, + ]); + + useEffect(() => { + if (isLoading) { + return; + } + + if ( + !initialNavigationRef.current.review && + !initialNavigationRef.current.contractDetails && + !initialNavigationRef.current.benefits + ) { + if (isNavigatingToReviewWhenEmploymentIsFinal || isNavigatingToReview) { + initialNavigationRef.current.review = true; + initializeStepValues(); + goToStep('review'); + } else if (isNavigatingToContractDetails) { + initialNavigationRef.current.contractDetails = true; + initializeStepValues(); + goToStep('contract_details'); + } else if (isNavigatingToBenefits) { + initialNavigationRef.current.benefits = true; + initializeStepValues(); + goToStep('benefits'); + } + } + }, [ goToStep, + initializeStepValues, + isLoading, + isNavigatingToBenefits, + isNavigatingToContractDetails, isNavigatingToReview, - selectCountryInitialValues, - setStepValues, - stepFields.basic_information, - stepFields.benefits, - stepFields.contract_details, - stepFields.select_country, + isNavigatingToReviewWhenEmploymentIsFinal, ]); const parseFormValues = (values: FieldValues) => { diff --git a/src/flows/useStepState.ts b/src/flows/useStepState.ts index 4adbabe9..dc694085 100644 --- a/src/flows/useStepState.ts +++ b/src/flows/useStepState.ts @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useCallback } from 'react'; import { FieldValues } from 'react-hook-form'; export type Step = { @@ -6,7 +6,7 @@ export type Step = { name: T; }; -type StepState = { +export type StepState = { currentStep: Step; totalSteps: number; values: @@ -32,7 +32,7 @@ export const useStepState = ( values: null, }); - function nextStep() { + const nextStep = useCallback(() => { const { index } = stepState.currentStep; const stepValues = Object.values>(steps); const nextStep = stepValues.find((step) => step.index === index + 1); @@ -49,11 +49,11 @@ export const useStepState = ( }, } as { [key in T]: Fields }, })); - setFieldValues({} as Fields); // Reset field values for the next step + setFieldValues({} as Fields); } - } + }, [fieldValues, stepState.currentStep, steps]); - function previousStep() { + const previousStep = useCallback(() => { const { index } = stepState.currentStep; const stepValues = Object.values>(steps); const previousStep = stepValues.find((step) => step.index === index - 1); @@ -70,26 +70,32 @@ export const useStepState = ( }, } as { [key in T]: Fields }, })); - setFieldValues({} as Fields); // Reset field values for the previous step + setFieldValues({} as Fields); } - } + }, [fieldValues, stepState.currentStep, steps]); - function goToStep(step: T) { - // to avoid going to a steps that hasn't been filled yet - if (stepState.values?.[step]) { - setStepState((previousState) => ({ - ...previousState, - currentStep: steps[step], - })); - } - } + const goToStep = useCallback( + (step: T) => { + setStepState((previousState) => { + // Check if we have values for this step + if (previousState.values?.[step]) { + return { + ...previousState, + currentStep: steps[step], + }; + } + return previousState; + }); + }, + [steps], + ); - function setStepValues(values: Record) { + const setStepValues = useCallback((values: Record) => { setStepState((previousState) => ({ ...previousState, values: values, })); - } + }, []); return { /** From 2f9cebdece56d32fd51b7d5d5bff5768e48dd710 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Tue, 24 Jun 2025 15:11:33 +0200 Subject: [PATCH 02/25] fix some tests --- .../Onboarding/tests/OnboardingFlow.test.tsx | 45 ++++--------------- 1 file changed, 8 insertions(+), 37 deletions(-) diff --git a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx index 5c0fa09d..fdd09cfe 100644 --- a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx @@ -491,37 +491,11 @@ describe('OnboardingFlow', () => { ); render( - , + , { wrapper }, ); await screen.findByText(/Step: Basic Information/i); - await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); - - const nextButton = screen.getByText(/Next Step/i); - expect(nextButton).toBeInTheDocument(); - - nextButton.click(); - - await screen.findByText(/Step: Contract Details/i); - - const backButton = screen.getByText(/Back/i); - expect(backButton).toBeInTheDocument(); - - backButton.click(); - - await screen.findByText(/Step: Basic Information/i); - - const employeePersonalEmail = screen.getByLabelText(/Personal email/i); - await waitFor(() => { - expect(employeePersonalEmail).toHaveValue( - employmentResponse.data.employment.personal_email, - ); - }); }); it('should select a country and advance to the next step', async () => { @@ -568,7 +542,7 @@ describe('OnboardingFlow', () => { ); render( , @@ -580,6 +554,8 @@ describe('OnboardingFlow', () => { await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); + await fillBasicInformation(); + const nextButton = screen.getByText(/Next Step/i); expect(nextButton).toBeInTheDocument(); @@ -594,15 +570,14 @@ describe('OnboardingFlow', () => { await screen.findByText(/Step: Basic Information/i); - const employeePersonalEmail = screen.getByLabelText(/Personal email/i); await waitFor(() => { - expect(employeePersonalEmail).toHaveValue( + expect(screen.getByLabelText(/Personal email/i)).toHaveValue( employmentResponse.data.employment.personal_email, ); }); }); - it('should submit the basic information step', async () => { + it.only('should submit the basic information step', async () => { mockRender.mockImplementation( ({ onboardingBag, components }: OnboardingRenderProps) => { const currentStepIndex = onboardingBag.stepState.currentStep.index; @@ -628,7 +603,7 @@ describe('OnboardingFlow', () => { ); render( , @@ -637,11 +612,7 @@ describe('OnboardingFlow', () => { }, ); - await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); - - await waitFor(() => { - expect(screen.getByLabelText(/Full name/i)).toBeInTheDocument(); - }); + await fillBasicInformation(); const nextButton = screen.getByText(/Next Step/i); expect(nextButton).toBeInTheDocument(); From 3d67e475fa11cc2f79f1a384d5329b16b3c1b2e9 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Tue, 24 Jun 2025 16:37:58 +0200 Subject: [PATCH 03/25] fix some test setup --- src/flows/Onboarding/hooks.tsx | 25 +- .../Onboarding/tests/OnboardingFlow.test.tsx | 8 +- .../tests/OnboardingInvite.test.tsx | 299 +++++++++++++++--- 3 files changed, 262 insertions(+), 70 deletions(-) diff --git a/src/flows/Onboarding/hooks.tsx b/src/flows/Onboarding/hooks.tsx index eb56e678..afc65f04 100644 --- a/src/flows/Onboarding/hooks.tsx +++ b/src/flows/Onboarding/hooks.tsx @@ -351,18 +351,6 @@ export const useOnboarding = ({ }); }; - const isBasicInformationDetailsEnabled = Boolean( - internalCountryCode && - (stepState.currentStep.name === 'basic_information' || - Boolean(employmentId)), - ); - - const isContractDetailsEnabled = Boolean( - internalCountryCode && - (stepState.currentStep.name === 'contract_details' || - Boolean(employmentId)), - ); - const { data: basicInformationForm, isLoading: isLoadingBasicInformationForm, @@ -371,7 +359,7 @@ export const useOnboarding = ({ options: { jsfModify: options?.jsfModify?.basic_information, queryOptions: { - enabled: isBasicInformationDetailsEnabled, + enabled: stepState.currentStep.name === 'basic_information', }, }, }); @@ -437,7 +425,7 @@ export const useOnboarding = ({ }, }, queryOptions: { - enabled: isContractDetailsEnabled, + enabled: stepState.currentStep.name === 'contract_details', }, }, }); @@ -595,6 +583,15 @@ export const useOnboarding = ({ isNavigatingToContractDetails || isNavigatingToBenefits; + console.log({ + isLoading, + isNavigatingToReviewWhenEmploymentIsFinal, + isNavigatingToReview, + isNavigatingToContractDetails, + isNavigatingToBenefits, + initialLoading, + }); + const initializeStepValues = useCallback(() => { fieldsMetaRef.current = { select_country: prettifyFormValues( diff --git a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx index fdd09cfe..9ed85626 100644 --- a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx @@ -38,6 +38,8 @@ import { import { NormalizedFieldError } from '@/src/lib/mutations'; import { fireEvent } from '@testing-library/react'; +const queryClient = new QueryClient(); + // Helper function to generate unique employment IDs for each test let employmentIdCounter = 0; const generateUniqueEmploymentId = () => { @@ -45,8 +47,6 @@ const generateUniqueEmploymentId = () => { return `test-employment-${employmentIdCounter}-${Date.now()}`; }; -const queryClient = new QueryClient(); - const wrapper = ({ children }: PropsWithChildren) => ( {children} @@ -89,7 +89,7 @@ function Review({ values }: { values: Record }) { ); } -describe('OnboardingFlow', () => { +describe.skip('OnboardingFlow', () => { const MultiStepFormWithCountry = ({ components, onboardingBag, @@ -577,7 +577,7 @@ describe('OnboardingFlow', () => { }); }); - it.only('should submit the basic information step', async () => { + it('should submit the basic information step', async () => { mockRender.mockImplementation( ({ onboardingBag, components }: OnboardingRenderProps) => { const currentStepIndex = onboardingBag.stepState.currentStep.index; diff --git a/src/flows/Onboarding/tests/OnboardingInvite.test.tsx b/src/flows/Onboarding/tests/OnboardingInvite.test.tsx index f64da57e..e2331f22 100644 --- a/src/flows/Onboarding/tests/OnboardingInvite.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingInvite.test.tsx @@ -24,6 +24,13 @@ import { http, HttpResponse } from 'msw'; import { PropsWithChildren } from 'react'; import { vi } from 'vitest'; +// Helper function to generate unique employment IDs for each test +let employmentIdCounter = 0; +const generateUniqueEmploymentId = () => { + employmentIdCounter++; + return `test-employment-${employmentIdCounter}-${Date.now()}`; +}; + const queryClient = new QueryClient(); const wrapper = ({ children }: PropsWithChildren) => ( @@ -75,7 +82,6 @@ const mockRender = vi.fn( const defaultProps = { companyId: '1234', - employmentId: '1234', countryCode: 'PRT', options: {}, render: mockRender, @@ -113,8 +119,19 @@ describe('OnboardingInvite', () => { return HttpResponse.json(contractDetailsSchema); }), - http.get('*/v1/employments/*', () => { - return HttpResponse.json(employmentResponse); + http.get('*/v1/employments/:id', ({ params }) => { + // Create a response with the actual employment ID from the request + const employmentId = params?.id; + return HttpResponse.json({ + ...employmentResponse, + data: { + ...employmentResponse.data, + employment: { + ...employmentResponse.data.employment, + id: employmentId, + }, + }, + }); }), http.get('*/v1/employments/*/benefit-offers/schema', () => { @@ -147,7 +164,13 @@ describe('OnboardingInvite', () => { }); it('should render the OnboardingInvite component with default "Invite Employee" text', async () => { - render(, { wrapper }); + render( + , + { wrapper }, + ); await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); await screen.findByText(/Step: Select Country/); @@ -172,7 +195,13 @@ describe('OnboardingInvite', () => { }), ); - render(, { wrapper }); + render( + , + { wrapper }, + ); await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); await screen.findByText(/Step: Select Country/); @@ -182,7 +211,13 @@ describe('OnboardingInvite', () => { }); it('should call onSubmit and onSuccess when invite is successful', async () => { - render(, { wrapper }); + render( + , + { wrapper }, + ); await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); const button = screen.getByText(/Invite Employee/i); fireEvent.click(button); @@ -204,7 +239,13 @@ describe('OnboardingInvite', () => { }), ); - render(, { wrapper }); + render( + , + { wrapper }, + ); await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); const button = screen.getByText(/Invite Employee/i); @@ -230,7 +271,13 @@ describe('OnboardingInvite', () => { }), ); - render(, { wrapper }); + render( + , + { wrapper }, + ); await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); const button = screen.getByText(/Invite Employee/i); @@ -274,7 +321,13 @@ describe('OnboardingInvite', () => { }), ); - render(, { wrapper }); + render( + , + { wrapper }, + ); await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); await screen.findByText(/Step: Select Country/); @@ -326,7 +379,13 @@ describe('OnboardingInvite', () => { }), ); - render(, { wrapper }); + render( + , + { wrapper }, + ); const button = await screen.findByTestId('onboarding-invite'); fireEvent.click(button); await waitFor(() => { @@ -370,7 +429,13 @@ describe('OnboardingInvite', () => { }), ); - render(, { wrapper }); + render( + , + { wrapper }, + ); const button = await screen.findByTestId('onboarding-invite'); fireEvent.click(button); await waitFor(() => { @@ -392,13 +457,15 @@ describe('OnboardingInvite', () => { }, }); }), - http.get('*/v1/employments/*', () => { + http.get('*/v1/employments/:id', ({ params }) => { + const employmentId = params?.id; return HttpResponse.json({ ...employmentResponse, data: { ...employmentResponse.data, employment: { ...employmentResponse.data.employment, + id: employmentId, status: 'created_reserve_paid', }, }, @@ -406,7 +473,15 @@ describe('OnboardingInvite', () => { }), ); - render(, { wrapper }); + render( + , + { wrapper }, + ); + + await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); const button = await screen.findByText(/Invite Employee/i); expect(button).toBeInTheDocument(); @@ -429,13 +504,15 @@ describe('OnboardingInvite', () => { }, }); }), - http.get('*/v1/employments/*', () => { + http.get('*/v1/employments/:id', ({ params }) => { + const employmentId = params?.id; return HttpResponse.json({ ...employmentResponse, data: { ...employmentResponse.data, employment: { ...employmentResponse.data.employment, + id: employmentId, status: 'created_reserve_paid', }, }, @@ -455,7 +532,13 @@ describe('OnboardingInvite', () => { }), ); - render(, { wrapper }); + render( + , + { wrapper }, + ); const button = await screen.findByText(/Invite Employee/i); expect(button).toBeInTheDocument(); @@ -491,7 +574,13 @@ describe('OnboardingInvite', () => { ); }); - render(, { wrapper }); + render( + , + { wrapper }, + ); const button = await screen.findByTestId('onboarding-invite'); expect(button).toBeDisabled(); @@ -505,7 +594,13 @@ describe('OnboardingInvite', () => { }), ); - render(, { wrapper }); + render( + , + { wrapper }, + ); const button = await screen.findByText(/Invite Employee/i); expect(button).toBeInTheDocument(); @@ -538,7 +633,13 @@ describe('OnboardingInvite', () => { }), ); - render(, { wrapper }); + render( + , + { wrapper }, + ); const button = await screen.findByText(/Create Reserve/i); expect(button).toBeInTheDocument(); @@ -575,13 +676,15 @@ describe('OnboardingInvite', () => { }, }); }), - http.get('*/v1/employments/*', () => { + http.get('*/v1/employments/:id', ({ params }) => { + const employmentId = params?.id; return HttpResponse.json({ ...employmentResponse, data: { ...employmentResponse.data, employment: { ...employmentResponse.data.employment, + id: employmentId, status, }, }, @@ -589,7 +692,15 @@ describe('OnboardingInvite', () => { }), ); - render(, { wrapper }); + render( + , + { wrapper }, + ); + + await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); const button = await screen.findByText(/Invite Employee/i); expect(button).toBeInTheDocument(); @@ -616,13 +727,15 @@ describe('OnboardingInvite', () => { }, }); }), - http.get('*/v1/employments/*', () => { + http.get('*/v1/employments/:id', ({ params }) => { + const employmentId = params?.id; return HttpResponse.json({ ...employmentResponse, data: { ...employmentResponse.data, employment: { ...employmentResponse.data.employment, + id: employmentId, status: 'created', }, }, @@ -635,7 +748,13 @@ describe('OnboardingInvite', () => { }), ); - render(, { wrapper }); + render( + , + { wrapper }, + ); const button = await screen.findByText(/Create Reserve/i); expect(button).toBeInTheDocument(); @@ -664,13 +783,15 @@ describe('OnboardingInvite', () => { }, }); }), - http.get('*/v1/employments/*', () => { + http.get('*/v1/employments/:id', ({ params }) => { + const employmentId = params?.id; return HttpResponse.json({ ...employmentResponse, data: { ...employmentResponse.data, employment: { ...employmentResponse.data.employment, + id: employmentId, status: 'created', }, }, @@ -683,7 +804,13 @@ describe('OnboardingInvite', () => { }), ); - render(, { wrapper }); + render( + , + { wrapper }, + ); const button = await screen.findByText(/Invite Employee/i); expect(button).toBeInTheDocument(); @@ -736,9 +863,15 @@ describe('OnboardingInvite', () => { }), ); - const { unmount } = render(, { - wrapper, - }); + const { unmount } = render( + , + { + wrapper, + }, + ); await screen.findByText('Custom Invite Button'); expect(mockRenderProp).toHaveBeenCalledWith({ employmentStatus: 'invited', @@ -763,7 +896,13 @@ describe('OnboardingInvite', () => { }), ); - render(, { wrapper }); + render( + , + { wrapper }, + ); await screen.findByText('Custom Reserve Button'); expect(mockRenderProp).toHaveBeenCalledWith({ employmentStatus: 'created_awaiting_reserve', @@ -799,13 +938,15 @@ describe('OnboardingInvite', () => { }, }); }), - http.get('*/v1/employments/*', () => { + http.get('*/v1/employments/:id', ({ params }) => { + const employmentId = params?.id; return HttpResponse.json({ ...employmentResponse, data: { ...employmentResponse.data, employment: { ...employmentResponse.data.employment, + id: employmentId, status: 'created_reserve_paid', }, }, @@ -813,7 +954,13 @@ describe('OnboardingInvite', () => { }), ); - render(, { wrapper }); + render( + , + { wrapper }, + ); await screen.findByText('Hidden Deposit Button'); expect(mockRenderProp).toHaveBeenCalledWith({ @@ -865,9 +1012,15 @@ describe('OnboardingInvite', () => { ); }); - render(, { - wrapper: customWrapper, - }); + render( + , + { + wrapper: customWrapper, + }, + ); const customButton = await screen.findByTestId('custom-button'); expect(customButton).toBeInTheDocument(); @@ -899,9 +1052,15 @@ describe('OnboardingInvite', () => { ); }); - render(, { - wrapper: customWrapper, - }); + render( + , + { + wrapper: customWrapper, + }, + ); const customButton = await screen.findByTestId('custom-button'); expect(customButton).toBeDisabled(); @@ -930,9 +1089,15 @@ describe('OnboardingInvite', () => { ); }); - render(, { - wrapper: customWrapper, - }); + render( + , + { + wrapper: customWrapper, + }, + ); const customButton = await screen.findByTestId('custom-button'); fireEvent.click(customButton); @@ -963,9 +1128,15 @@ describe('OnboardingInvite', () => { ); }); - render(, { - wrapper: customWrapper, - }); + render( + , + { + wrapper: customWrapper, + }, + ); const customButton = await screen.findByTestId('custom-button'); expect(customButton).not.toBeDisabled(); // Initially not disabled @@ -1017,9 +1188,15 @@ describe('OnboardingInvite', () => { }), ); - render(, { - wrapper: customWrapper, - }); + render( + , + { + wrapper: customWrapper, + }, + ); const customButton = await screen.findByTestId('custom-button'); await waitFor(() => { @@ -1046,7 +1223,13 @@ describe('OnboardingInvite', () => { ); }); - render(, { wrapper }); + render( + , + { wrapper }, + ); const button = await screen.findByTestId('onboarding-invite'); expect(button).toBeInTheDocument(); @@ -1074,9 +1257,15 @@ describe('OnboardingInvite', () => { ); }); - render(, { - wrapper: customWrapper, - }); + render( + , + { + wrapper: customWrapper, + }, + ); await screen.findByTestId('custom-button'); @@ -1116,9 +1305,15 @@ describe('OnboardingInvite', () => { ); }); - render(, { - wrapper: customWrapper, - }); + render( + , + { + wrapper: customWrapper, + }, + ); await screen.findByTestId('custom-button'); From 4dc38964e55d4dc6a3717192f7bc8ab1a994a2ee Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Tue, 24 Jun 2025 17:07:40 +0200 Subject: [PATCH 04/25] fix more tests --- src/flows/Onboarding/hooks.tsx | 26 +------------------ .../Onboarding/tests/OnboardingFlow.test.tsx | 2 +- 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/src/flows/Onboarding/hooks.tsx b/src/flows/Onboarding/hooks.tsx index afc65f04..ad19ccbe 100644 --- a/src/flows/Onboarding/hooks.tsx +++ b/src/flows/Onboarding/hooks.tsx @@ -65,8 +65,6 @@ const getLoadingStates = ({ isLoadingCompany, isLoadingCountries, employmentStatus, - employmentId, - currentStepName, }: { isLoadingBasicInformationForm: boolean; isLoadingContractDetailsForm: boolean; @@ -76,8 +74,6 @@ const getLoadingStates = ({ isLoadingCompany: boolean; isLoadingCountries: boolean; employmentStatus?: Employment['status']; - employmentId?: string; - currentStepName: string; }) => { const initialLoading = isLoadingBasicInformationForm || @@ -92,13 +88,7 @@ const getLoadingStates = ({ employmentStatus && reviewStepAllowedEmploymentStatus.includes(employmentStatus); - const shouldHandleReadOnlyEmployment = Boolean( - employmentId && isEmploymentReadOnly && currentStepName !== 'review', - ); - - const isLoading = initialLoading || shouldHandleReadOnlyEmployment; - - return { isLoading, isEmploymentReadOnly }; + return { isLoading: initialLoading, isEmploymentReadOnly }; }; const useNavigationConditions = ({ @@ -477,7 +467,6 @@ export const useOnboarding = ({ contract_details: employmentContractDetails = {}, status: employmentStatus, } = employment || {}; - const currentStepName = stepState.currentStep.name; const selectCountryInitialValues = useMemo( () => @@ -535,9 +524,7 @@ export const useOnboarding = ({ isLoadingBenefitOffers, isLoadingCompany, isLoadingCountries, - employmentId, employmentStatus: employmentStatus, - currentStepName: currentStepName, }), [ isLoadingBasicInformationForm, @@ -547,9 +534,7 @@ export const useOnboarding = ({ isLoadingBenefitOffers, isLoadingCompany, isLoadingCountries, - employmentId, employmentStatus, - currentStepName, ], ); @@ -583,15 +568,6 @@ export const useOnboarding = ({ isNavigatingToContractDetails || isNavigatingToBenefits; - console.log({ - isLoading, - isNavigatingToReviewWhenEmploymentIsFinal, - isNavigatingToReview, - isNavigatingToContractDetails, - isNavigatingToBenefits, - initialLoading, - }); - const initializeStepValues = useCallback(() => { fieldsMetaRef.current = { select_country: prettifyFormValues( diff --git a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx index 9ed85626..2c3135b5 100644 --- a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx @@ -89,7 +89,7 @@ function Review({ values }: { values: Record }) { ); } -describe.skip('OnboardingFlow', () => { +describe('OnboardingFlow', () => { const MultiStepFormWithCountry = ({ components, onboardingBag, From e150719034e3198bd992405105d4a5e5593616ed Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Tue, 24 Jun 2025 17:14:29 +0200 Subject: [PATCH 05/25] fix more tests --- src/flows/Onboarding/hooks.tsx | 20 ++++++++++++++++++- .../Onboarding/tests/OnboardingFlow.test.tsx | 2 +- .../tests/OnboardingInvite.test.tsx | 19 +++++------------- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/flows/Onboarding/hooks.tsx b/src/flows/Onboarding/hooks.tsx index ad19ccbe..16980ea9 100644 --- a/src/flows/Onboarding/hooks.tsx +++ b/src/flows/Onboarding/hooks.tsx @@ -88,7 +88,9 @@ const getLoadingStates = ({ employmentStatus && reviewStepAllowedEmploymentStatus.includes(employmentStatus); - return { isLoading: initialLoading, isEmploymentReadOnly }; + const isLoading = initialLoading; + + return { isLoading, isEmploymentReadOnly }; }; const useNavigationConditions = ({ @@ -568,6 +570,22 @@ export const useOnboarding = ({ isNavigatingToContractDetails || isNavigatingToBenefits; + console.log({ + isLoading, + isNavigatingToReviewWhenEmploymentIsFinal, + isNavigatingToReview, + isNavigatingToContractDetails, + isNavigatingToBenefits, + initialLoading, + isLoadingBasicInformationForm, + isLoadingContractDetailsForm, + isLoadingEmployment, + isLoadingBenefitsOffersSchema, + isLoadingBenefitOffers, + isLoadingCompany, + isLoadingCountries, + }); + const initializeStepValues = useCallback(() => { fieldsMetaRef.current = { select_country: prettifyFormValues( diff --git a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx index 2c3135b5..9ed85626 100644 --- a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx @@ -89,7 +89,7 @@ function Review({ values }: { values: Record }) { ); } -describe('OnboardingFlow', () => { +describe.skip('OnboardingFlow', () => { const MultiStepFormWithCountry = ({ components, onboardingBag, diff --git a/src/flows/Onboarding/tests/OnboardingInvite.test.tsx b/src/flows/Onboarding/tests/OnboardingInvite.test.tsx index e2331f22..da004760 100644 --- a/src/flows/Onboarding/tests/OnboardingInvite.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingInvite.test.tsx @@ -652,15 +652,9 @@ describe('OnboardingInvite', () => { }); }); - it('should render "Invite Employee" when creditRiskStatus is deposit_required but employment status indicates deposit handling', async () => { - // Test with various employment statuses that should show "Invite Employee" instead of "Create Reserve" - const employmentStatuses = [ - 'created_reserve_paid', - 'invited', - 'created_awaiting_reserve', - ]; - - for (const status of employmentStatuses) { + it.each(['created_reserve_paid', 'invited', 'created_awaiting_reserve'])( + 'should render "Invite Employee" when employment status is %s', + async (status) => { mockRender.mockClear(); server.use( @@ -707,11 +701,8 @@ describe('OnboardingInvite', () => { // Ensure it's not showing "Create Reserve" expect(screen.queryByText(/Create Reserve/i)).not.toBeInTheDocument(); - - // Clean up before next iteration - queryClient.clear(); - } - }); + }, + ); it('should call onSuccess with "created_awaiting_reserve" status when creating a reserve invoice', async () => { server.use( From 6edea0f9f83844ede73e402cf779f4618ec9490f Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Tue, 24 Jun 2025 17:17:39 +0200 Subject: [PATCH 06/25] remove logs --- src/flows/Onboarding/hooks.tsx | 16 ---------------- .../Onboarding/tests/OnboardingFlow.test.tsx | 2 +- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/flows/Onboarding/hooks.tsx b/src/flows/Onboarding/hooks.tsx index 16980ea9..1bfcdd45 100644 --- a/src/flows/Onboarding/hooks.tsx +++ b/src/flows/Onboarding/hooks.tsx @@ -570,22 +570,6 @@ export const useOnboarding = ({ isNavigatingToContractDetails || isNavigatingToBenefits; - console.log({ - isLoading, - isNavigatingToReviewWhenEmploymentIsFinal, - isNavigatingToReview, - isNavigatingToContractDetails, - isNavigatingToBenefits, - initialLoading, - isLoadingBasicInformationForm, - isLoadingContractDetailsForm, - isLoadingEmployment, - isLoadingBenefitsOffersSchema, - isLoadingBenefitOffers, - isLoadingCompany, - isLoadingCountries, - }); - const initializeStepValues = useCallback(() => { fieldsMetaRef.current = { select_country: prettifyFormValues( diff --git a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx index 9ed85626..2c3135b5 100644 --- a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx @@ -89,7 +89,7 @@ function Review({ values }: { values: Record }) { ); } -describe.skip('OnboardingFlow', () => { +describe('OnboardingFlow', () => { const MultiStepFormWithCountry = ({ components, onboardingBag, From fcb8bfba5dcd991ee87d1ead7af538a349cbb888 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Wed, 25 Jun 2025 06:28:23 +0200 Subject: [PATCH 07/25] fix tests --- src/flows/Onboarding/api.ts | 5 +- src/flows/Onboarding/hooks.tsx | 26 ++++- .../Onboarding/tests/OnboardingFlow.test.tsx | 109 ++++++++++-------- 3 files changed, 87 insertions(+), 53 deletions(-) diff --git a/src/flows/Onboarding/api.ts b/src/flows/Onboarding/api.ts index 78e2c332..3ad5f205 100644 --- a/src/flows/Onboarding/api.ts +++ b/src/flows/Onboarding/api.ts @@ -243,6 +243,7 @@ export const useJSONSchemaForm = ({ export const useBenefitOffersSchema = ( employmentId: string, + stepName: string, fieldValues: FieldValues, options: OnboardingFlowParams['options'], ) => { @@ -255,9 +256,9 @@ export const useBenefitOffersSchema = ( : {}; const { client } = useClient(); return useQuery({ - queryKey: ['benefit-offers-schema', employmentId], + queryKey: ['benefit-offers-schema', stepName], retry: false, - enabled: !!employmentId, + enabled: stepName === 'benefits', queryFn: async () => { const response = await getShowSchema({ client: client as Client, diff --git a/src/flows/Onboarding/hooks.tsx b/src/flows/Onboarding/hooks.tsx index 1bfcdd45..732a1395 100644 --- a/src/flows/Onboarding/hooks.tsx +++ b/src/flows/Onboarding/hooks.tsx @@ -84,13 +84,22 @@ const getLoadingStates = ({ isLoadingCompany || isLoadingCountries; + console.log({ + isLoadingBasicInformationForm, + isLoadingContractDetailsForm, + isLoadingEmployment, + isLoadingBenefitsOffersSchema, + isLoadingBenefitOffers, + isLoadingCompany, + isLoadingCountries, + initialLoading, + }); + const isEmploymentReadOnly = employmentStatus && reviewStepAllowedEmploymentStatus.includes(employmentStatus); - const isLoading = initialLoading; - - return { isLoading, isEmploymentReadOnly }; + return { isLoading: initialLoading, isEmploymentReadOnly }; }; const useNavigationConditions = ({ @@ -351,7 +360,9 @@ export const useOnboarding = ({ options: { jsfModify: options?.jsfModify?.basic_information, queryOptions: { - enabled: stepState.currentStep.name === 'basic_information', + enabled: + Boolean(!!internalEmploymentId && internalCountryCode) || + stepState.currentStep.name === 'basic_information', }, }, }); @@ -417,7 +428,9 @@ export const useOnboarding = ({ }, }, queryOptions: { - enabled: stepState.currentStep.name === 'contract_details', + enabled: + Boolean(!!internalEmploymentId && internalCountryCode) || + stepState.currentStep.name === 'contract_details', }, }, }); @@ -426,6 +439,7 @@ export const useOnboarding = ({ data: benefitOffersSchema, isLoading: isLoadingBenefitsOffersSchema, } = useBenefitOffersSchema( + stepState.currentStep.name, internalEmploymentId as string, fieldValues, options, @@ -607,7 +621,7 @@ export const useOnboarding = ({ ]); useEffect(() => { - if (isLoading) { + if (initialLoading) { return; } diff --git a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx index 2c3135b5..259de97a 100644 --- a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx @@ -455,6 +455,8 @@ describe('OnboardingFlow', () => { async function fillCountry(country: string) { await screen.findByText(/Step: Select Country/i); + await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); + await fillSelect('Country', country); const nextButton = screen.getByText(/Continue/i); @@ -563,6 +565,8 @@ describe('OnboardingFlow', () => { await screen.findByText(/Step: Contract Details/i); + await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); + const backButton = screen.getByText(/Back/i); expect(backButton).toBeInTheDocument(); @@ -572,7 +576,7 @@ describe('OnboardingFlow', () => { await waitFor(() => { expect(screen.getByLabelText(/Personal email/i)).toHaveValue( - employmentResponse.data.employment.personal_email, + 'john.doe@gmail.com', ); }); }); @@ -628,27 +632,38 @@ describe('OnboardingFlow', () => { id: undefined, name: undefined, }, - email: employmentResponse.data.employment.personal_email, + email: 'john.doe@gmail.com', has_seniority_date: 'no', - job_title: employmentResponse.data.employment.job_title, + job_title: 'Software Engineer', manager: { id: undefined, }, - tax_job_category: - employmentResponse.data.employment.basic_information.tax_job_category, - tax_servicing_countries: - employmentResponse.data.employment.basic_information - .tax_servicing_countries, - name: employmentResponse.data.employment.basic_information.name, - provisional_start_date: - employmentResponse.data.employment.provisional_start_date, - work_email: employmentResponse.data.employment.work_email, + name: 'John Doe', + provisional_start_date: '2025-05-15', + work_email: 'john.doe@remote.com', }); await screen.findByText(/Step: Contract Details/i); }); - it('should retrieve the basic information step based on an employmentId', async () => { + it('should retrieve the contract details step based on an employmentId', async () => { + server.use( + http.get('*/v1/employments/:id', ({ params }) => { + // Create a response with the actual employment ID from the request + const employmentId = params?.id; + return HttpResponse.json({ + ...employmentResponse, + data: { + ...employmentResponse.data, + employment: { + ...employmentResponse.data.employment, + id: employmentId, + contract_details: null, + }, + }, + }); + }), + ); render( { await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); - await fillCountry('Portugal'); - - await waitFor(() => { - expect(screen.getByLabelText(/Personal email/i)).toHaveValue( - employmentResponse.data.employment.personal_email, - ); - }); + await screen.findByText(/Step: Contract Details/i); }); it('should call the update employment endpoint when the user submits the form and the employmentId is present', async () => { + server.use( + http.get('*/v1/employments/:id', ({ params }) => { + // Create a response with the actual employment ID from the request + const employmentId = params?.id; + return HttpResponse.json({ + ...employmentResponse, + data: { + ...employmentResponse.data, + employment: { + ...employmentResponse.data.employment, + id: employmentId, + contract_details: null, + }, + }, + }); + }), + ); render( { }, ); await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); - await fillCountry('Portugal'); + await screen.findByText(/Step: Contract Details/i); + + const backButton = screen.getByText(/Back/i); + expect(backButton).toBeInTheDocument(); + backButton.click(); await waitFor(() => { expect(screen.getByLabelText(/Personal email/i)).toHaveValue( @@ -732,7 +762,12 @@ describe('OnboardingFlow', () => { await screen.findByText(/Step: Contract Details/i); }); - it('should fill the contract details step and go to the benefits step', async () => { + it.only('should fill the contract details step and go to the benefits step', async () => { + server.use( + http.get('*/v1/employments/*/benefit-offers', () => { + return HttpResponse.json({ data: [] }); + }), + ); render( { }, ); - await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); - - await fillCountry('Portugal'); - - await waitFor(() => { - expect(screen.getByLabelText(/Personal email/i)).toHaveValue( - employmentResponse.data.employment.personal_email, - ); - }); + await screen.findByText(/Step: Benefits/i); - let nextButton = screen.getByText(/Next Step/i); - expect(nextButton).toBeInTheDocument(); + const backButton = screen.getByText(/Back/i); + expect(backButton).toBeInTheDocument(); - nextButton.click(); + backButton.click(); await screen.findByText(/Step: Contract Details/i); - await waitFor(() => { - expect(screen.getByLabelText(/Role description/i)).toBeInTheDocument(); - }); - - nextButton = screen.getByText(/Next Step/i); + const nextButton = screen.getByText(/Next Step/i); expect(nextButton).toBeInTheDocument(); nextButton.click(); await waitFor(() => { - expect(mockOnSubmit).toHaveBeenCalledTimes(3); + expect(mockOnSubmit).toHaveBeenCalledTimes(1); }); - - // Get the second call to mockOnSubmit (index 1) - const contractDetailsSubmission = mockOnSubmit.mock.calls[2][0]; - // Assert the contract details submission - expect(contractDetailsSubmission).toEqual({ + expect(mockOnSubmit).toHaveBeenCalledWith({ annual_gross_salary: 2000000, annual_training_hours_ack: 'acknowledged', available_pto: 22, From ee004cce69b9048b8030ce6baab8f245881bbd6b Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Wed, 25 Jun 2025 06:30:36 +0200 Subject: [PATCH 08/25] remove logs --- src/flows/Onboarding/hooks.tsx | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/flows/Onboarding/hooks.tsx b/src/flows/Onboarding/hooks.tsx index 732a1395..3eaf3f87 100644 --- a/src/flows/Onboarding/hooks.tsx +++ b/src/flows/Onboarding/hooks.tsx @@ -84,17 +84,6 @@ const getLoadingStates = ({ isLoadingCompany || isLoadingCountries; - console.log({ - isLoadingBasicInformationForm, - isLoadingContractDetailsForm, - isLoadingEmployment, - isLoadingBenefitsOffersSchema, - isLoadingBenefitOffers, - isLoadingCompany, - isLoadingCountries, - initialLoading, - }); - const isEmploymentReadOnly = employmentStatus && reviewStepAllowedEmploymentStatus.includes(employmentStatus); @@ -439,8 +428,8 @@ export const useOnboarding = ({ data: benefitOffersSchema, isLoading: isLoadingBenefitsOffersSchema, } = useBenefitOffersSchema( - stepState.currentStep.name, internalEmploymentId as string, + stepState.currentStep.name, fieldValues, options, ); @@ -647,7 +636,7 @@ export const useOnboarding = ({ }, [ goToStep, initializeStepValues, - isLoading, + initialLoading, isNavigatingToBenefits, isNavigatingToContractDetails, isNavigatingToReview, From 370ebbfa97c51aac0fdabdd42674314578973f4f Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Wed, 25 Jun 2025 06:35:04 +0200 Subject: [PATCH 09/25] fix more tests --- src/flows/Onboarding/api.ts | 2 +- src/flows/Onboarding/tests/OnboardingFlow.test.tsx | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/flows/Onboarding/api.ts b/src/flows/Onboarding/api.ts index 3ad5f205..a142eed0 100644 --- a/src/flows/Onboarding/api.ts +++ b/src/flows/Onboarding/api.ts @@ -258,7 +258,7 @@ export const useBenefitOffersSchema = ( return useQuery({ queryKey: ['benefit-offers-schema', stepName], retry: false, - enabled: stepName === 'benefits', + enabled: !!employmentId || stepName === 'benefits', queryFn: async () => { const response = await getShowSchema({ client: client as Client, diff --git a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx index 259de97a..778a2ca1 100644 --- a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx @@ -762,7 +762,7 @@ describe('OnboardingFlow', () => { await screen.findByText(/Step: Contract Details/i); }); - it.only('should fill the contract details step and go to the benefits step', async () => { + it('should fill the contract details step and go to the benefits step', async () => { server.use( http.get('*/v1/employments/*/benefit-offers', () => { return HttpResponse.json({ data: [] }); @@ -780,6 +780,8 @@ describe('OnboardingFlow', () => { await screen.findByText(/Step: Benefits/i); + await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); + const backButton = screen.getByText(/Back/i); expect(backButton).toBeInTheDocument(); @@ -787,6 +789,8 @@ describe('OnboardingFlow', () => { await screen.findByText(/Step: Contract Details/i); + await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); + const nextButton = screen.getByText(/Next Step/i); expect(nextButton).toBeInTheDocument(); nextButton.click(); From 4987ee37ea6374ed0b733c2f9dc61de9aa69152a Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Wed, 25 Jun 2025 06:42:29 +0200 Subject: [PATCH 10/25] fix more tests --- .../Onboarding/tests/OnboardingFlow.test.tsx | 123 +++++------------- 1 file changed, 31 insertions(+), 92 deletions(-) diff --git a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx index 778a2ca1..8a314663 100644 --- a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx @@ -25,12 +25,10 @@ import { benefitOffersResponse, employmentResponse, benefitOffersUpdatedResponse, - inviteResponse, companyResponse, conversionFromEURToUSD, } from '@/src/flows/Onboarding/tests/fixtures'; import { - assertRadioValue, fillRadio, fillSelect, selectDayInCalendar, @@ -830,7 +828,13 @@ describe('OnboardingFlow', () => { await screen.findByText(/Step: Benefits/i); }); - it('should go to the third step and check that benefits are initalized correctly', async () => { + it('should verify that the benefits are submitted correctly', async () => { + server.use( + http.get('*/v1/employments/*/benefit-offers', () => { + // Return empty benefits so we can fill them + return HttpResponse.json({ data: [] }); + }), + ); render( { ); await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); - await fillCountry('Portugal'); - - await waitFor(() => { - expect(screen.getByLabelText(/Personal email/i)).toHaveValue( - employmentResponse.data.employment.personal_email, - ); - }); - - let nextButton = screen.getByText(/Next Step/i); - expect(nextButton).toBeInTheDocument(); - - nextButton.click(); - - await screen.findByText(/Step: Contract Details/i); - - await waitFor(() => { - expect(screen.getByLabelText(/Role description/i)).toBeInTheDocument(); - }); - - nextButton = screen.getByText(/Next Step/i); - expect(nextButton).toBeInTheDocument(); - nextButton.click(); - await screen.findByText(/Step: Benefits/i); - await assertRadioValue( + // Fill all three benefits + await fillRadio( '0e0293ae-eec6-4d0e-9176-51c46eed435e.value', 'Meal Card Standard 2025', ); - await assertRadioValue( + await fillRadio( 'baa1ce1d-39ea-4eec-acf0-88fc8a357f54.value', 'Basic Health Plan 2025', ); - await assertRadioValue( - '072e0edb-bfca-46e8-a449-9eed5cbaba33.value', - 'Life Insurance 50K', - ); - await fillRadio( '072e0edb-bfca-46e8-a449-9eed5cbaba33.value', "I don't want to offer this benefit.", ); - nextButton = screen.getByText(/Next Step/i); + const nextButton = screen.getByText(/Next Step/i); expect(nextButton).toBeInTheDocument(); nextButton.click(); await waitFor(() => { - expect(mockOnSubmit).toHaveBeenCalledTimes(4); + expect(mockOnSubmit).toHaveBeenCalledTimes(1); }); - const benefitsSubmission = mockOnSubmit.mock.calls[3][0]; - - // Assert the contract details submission - expect(benefitsSubmission).toEqual({ - '072e0edb-bfca-46e8-a449-9eed5cbaba33': { - filter: '73a134db-4743-4d81-a1ec-1887f2240c5c', - value: 'no', - }, + expect(mockOnSubmit).toHaveBeenCalledWith({ '0e0293ae-eec6-4d0e-9176-51c46eed435e': { value: '601d28b6-efde-4b8f-b9e2-e394792fc594', }, @@ -910,64 +880,33 @@ describe('OnboardingFlow', () => { filter: '866c0615-a810-429b-b480-3a4f6ca6157d', value: '45e47ffd-e1d9-4c5f-b367-ad717c30801b', }, - }); - }); - - it("should invite the employee when the user clicks on the 'Invite Employee' button", async () => { - server.use( - http.post('*/v1/employments/*/invite', () => { - return HttpResponse.json(inviteResponse); - }), - ); - render( - , - { - wrapper, + '072e0edb-bfca-46e8-a449-9eed5cbaba33': { + filter: '73a134db-4743-4d81-a1ec-1887f2240c5c', + value: 'no', }, - ); - - await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); - await fillCountry('Portugal'); - - let nextButton = screen.getByText(/Next Step/i); - expect(nextButton).toBeInTheDocument(); - - nextButton.click(); - - await screen.findByText(/Step: Contract Details/i); - - nextButton = screen.getByText(/Next Step/i); - expect(nextButton).toBeInTheDocument(); - nextButton.click(); - - await screen.findByText(/Step: Benefits/i); - - nextButton = screen.getByText(/Next Step/i); - expect(nextButton).toBeInTheDocument(); - nextButton.click(); - - await screen.findByText(/Step: Review/i); - - const inviteEmployeeButton = screen.getByText(/Invite Employee/i); - expect(inviteEmployeeButton).toBeInTheDocument(); - - inviteEmployeeButton.click(); - - // it should be called - await waitFor(() => { - expect(mockOnSuccess).toHaveBeenCalledTimes(4); }); - expect(mockOnSuccess.mock.calls[3][0]).toEqual(inviteResponse); + // Optionally verify we moved to the next step + await screen.findByText(/Step: Review/i); }); it('should call POST when submitting basic information', async () => { const postSpy = vi.fn(); server.use( + http.get('*/v1/employments/:id', () => { + return HttpResponse.json({ + ...employmentResponse, + data: { + ...employmentResponse.data, + employment: { + ...employmentResponse.data.employment, + basic_information: null, + contract_details: null, + }, + }, + }); + }), http.post('*/v1/employments', () => { postSpy(); return HttpResponse.json(employmentCreatedResponse); From c3f0c9629f7c401a0220e4dbfe35be8e144acfcc Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Wed, 25 Jun 2025 07:31:48 +0200 Subject: [PATCH 11/25] fix tests --- src/flows/Onboarding/tests/OnboardingFlow.test.tsx | 1 - src/flows/Onboarding/tests/OnboardingInvite.test.tsx | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx index 7811a558..f43de6ca 100644 --- a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx @@ -1,7 +1,6 @@ import { FormFieldsProvider } from '@/src/RemoteFlowsProvider'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { PropsWithChildren } from 'react'; -import { beforeEach, describe, it, vi } from 'vitest'; import { server } from '@/src/tests/server'; import { render, diff --git a/src/flows/Onboarding/tests/OnboardingInvite.test.tsx b/src/flows/Onboarding/tests/OnboardingInvite.test.tsx index da004760..0c9fe2b6 100644 --- a/src/flows/Onboarding/tests/OnboardingInvite.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingInvite.test.tsx @@ -173,7 +173,7 @@ describe('OnboardingInvite', () => { ); await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); - await screen.findByText(/Step: Select Country/); + await screen.findByText(/Step: Review/); const button = await screen.findByText(/Invite Employee/i); expect(button).toBeInTheDocument(); @@ -204,7 +204,7 @@ describe('OnboardingInvite', () => { ); await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); - await screen.findByText(/Step: Select Country/); + await screen.findByText(/Step: Review/); const button = await screen.findByText(/Create Reserve/i); expect(button).toBeInTheDocument(); @@ -330,7 +330,7 @@ describe('OnboardingInvite', () => { ); await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); - await screen.findByText(/Step: Select Country/); + await screen.findByText(/Step: Review/); const button = await screen.findByText(/Create Reserve/i); expect(button).toBeInTheDocument(); From 01b3b9d078c735ffceaa5f6abf579db439761f68 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Wed, 25 Jun 2025 07:35:52 +0200 Subject: [PATCH 12/25] correct endpoint call --- src/flows/Onboarding/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flows/Onboarding/api.ts b/src/flows/Onboarding/api.ts index a142eed0..98a1fea9 100644 --- a/src/flows/Onboarding/api.ts +++ b/src/flows/Onboarding/api.ts @@ -256,7 +256,7 @@ export const useBenefitOffersSchema = ( : {}; const { client } = useClient(); return useQuery({ - queryKey: ['benefit-offers-schema', stepName], + queryKey: ['benefit-offers-schema', employmentId, stepName], retry: false, enabled: !!employmentId || stepName === 'benefits', queryFn: async () => { From 6c674eff4fd5777c62a186d2316f84a5e2b0df4c Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Wed, 25 Jun 2025 07:53:42 +0200 Subject: [PATCH 13/25] fix tests --- src/flows/Onboarding/hooks.tsx | 4 +- .../Onboarding/tests/OnboardingFlow.test.tsx | 38 +++++++++---------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/flows/Onboarding/hooks.tsx b/src/flows/Onboarding/hooks.tsx index 3eaf3f87..475e7e3b 100644 --- a/src/flows/Onboarding/hooks.tsx +++ b/src/flows/Onboarding/hooks.tsx @@ -349,9 +349,7 @@ export const useOnboarding = ({ options: { jsfModify: options?.jsfModify?.basic_information, queryOptions: { - enabled: - Boolean(!!internalEmploymentId && internalCountryCode) || - stepState.currentStep.name === 'basic_information', + enabled: Boolean(internalCountryCode), }, }, }); diff --git a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx index f43de6ca..89a89ac2 100644 --- a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx @@ -1698,6 +1698,9 @@ describe('OnboardingFlow', () => { // Mock the employment endpoint to return data with a non-readonly status server.use( + http.get('*/v1/employments/*/benefit-offers', () => { + return HttpResponse.json({ data: [] }); + }), http.get(`*/v1/employments/${uniqueEmploymentId}`, () => { return HttpResponse.json({ ...employmentResponse, @@ -1762,32 +1765,27 @@ describe('OnboardingFlow', () => { ); // Wait for the basic information step to load - await screen.findByText(/Step: Basic Information/i); + await screen.findByText(/Step: Benefits/i); await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); - // Navigate to contract details step - let nextButton = screen.getByText(/Next Step/i); - expect(nextButton).toBeInTheDocument(); - nextButton.click(); - - // Wait for contract details step to load - await screen.findByText(/Step: Contract Details/i); - - // Wait for the form to be populated with existing data - await waitFor(() => { - expect(screen.getByLabelText(/Role description/i)).toBeInTheDocument(); - }); + // Fill all three benefits to make the form valid + await fillRadio( + '0e0293ae-eec6-4d0e-9176-51c46eed435e.value', + 'Meal Card Standard 2025', + ); - // Submit contract details to move to benefits step - nextButton = screen.getByText(/Next Step/i); - expect(nextButton).toBeInTheDocument(); - nextButton.click(); + await fillRadio( + 'baa1ce1d-39ea-4eec-acf0-88fc8a357f54.value', + 'Basic Health Plan 2025', + ); - // Wait for benefits step to load - await screen.findByText(/Step: Benefits/i); + await fillRadio( + '072e0edb-bfca-46e8-a449-9eed5cbaba33.value', + "I don't want to offer this benefit.", + ); - nextButton = screen.getByText(/Next Step/i); + const nextButton = screen.getByText(/Next Step/i); expect(nextButton).toBeInTheDocument(); nextButton.click(); From 7c4cdf3eb958e31a2a57de0c4ec3eb5e9400b6ae Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Sat, 28 Jun 2025 11:01:12 +0200 Subject: [PATCH 14/25] fix test --- .../Onboarding/tests/OnboardingFlow.test.tsx | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx index 89a89ac2..7d5b1792 100644 --- a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx @@ -1555,11 +1555,13 @@ describe('OnboardingFlow', () => { }); it('should handle 422 validation errors with field errors when updating employment in contract details step', async () => { - let patchCallCount = 0; const uniqueEmploymentId = generateUniqueEmploymentId(); // Mock the PATCH endpoint to return success first, then 422 error server.use( + http.get('*/v1/employments/*/benefit-offers', () => { + return HttpResponse.json({ data: [] }); + }), http.get(`*/v1/employments/${uniqueEmploymentId}`, () => { return HttpResponse.json({ ...employmentResponse, @@ -1574,22 +1576,14 @@ describe('OnboardingFlow', () => { }); }), http.patch('*/v1/employments/*', () => { - patchCallCount++; - - if (patchCallCount === 1) { - // First call - return success - return HttpResponse.json(employmentUpdatedResponse); - } else { - // Second call - return 422 error - return HttpResponse.json( - { - errors: { - annual_gross_salary: ['must be greater than 0'], - }, + return HttpResponse.json( + { + errors: { + annual_gross_salary: ['must be greater than 0'], }, - { status: 422 }, - ); - } + }, + { status: 422 }, + ); }), ); @@ -1637,14 +1631,14 @@ describe('OnboardingFlow', () => { ); // Wait for the basic information step to load - await screen.findByText(/Step: Basic Information/i); + await screen.findByText(/Step: Benefits/i); await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); // Navigate to contract details step - let nextButton = screen.getByText(/Next Step/i); - expect(nextButton).toBeInTheDocument(); - nextButton.click(); + const backButton = screen.getByText(/Back/i); + expect(backButton).toBeInTheDocument(); + backButton.click(); // Wait for contract details step to load await screen.findByText(/Step: Contract Details/i); @@ -1659,7 +1653,7 @@ describe('OnboardingFlow', () => { fireEvent.change(annualGrossSalaryInput, { target: { value: '' } }); fireEvent.change(annualGrossSalaryInput, { target: { value: '100000' } }); - nextButton = screen.getByText(/Next Step/i); + const nextButton = screen.getByText(/Next Step/i); expect(nextButton).toBeInTheDocument(); nextButton.click(); From 4d356cc8890b3e56e177228e355acb920ce0b474 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Sat, 28 Jun 2025 11:05:59 +0200 Subject: [PATCH 15/25] fix tests --- .../Onboarding/tests/OnboardingFlow.test.tsx | 47 +++++++++++++++---- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx index 7d5b1792..fc249ea1 100644 --- a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx @@ -1295,6 +1295,23 @@ describe('OnboardingFlow', () => { }); it('should override annual gross salary field labels and conversion properties', async () => { + const uniqueEmploymentId = generateUniqueEmploymentId(); + server.use( + http.get(`*/v1/employments/${uniqueEmploymentId}`, () => { + return HttpResponse.json({ + ...employmentResponse, + data: { + ...employmentResponse.data, + employment: { + ...employmentResponse.data.employment, + id: uniqueEmploymentId, + contract_details: null, + status: 'created', // Ensure it's not a readonly status + }, + }, + }); + }), + ); const customFieldLabel = 'Test label'; const customConversionLabel = 'Annual Gross Salary Conversion'; const customConversionDescription = @@ -1329,7 +1346,7 @@ describe('OnboardingFlow', () => { render( { }, ); - // Wait for loading to finish and form to be ready - await screen.findByText(/Step: Basic Information/i); - - const nextButton = screen.getByText(/Next Step/i); - expect(nextButton).toBeInTheDocument(); - - nextButton.click(); - await screen.findByText(/Step: Contract Details/i); // Verify the custom field label is displayed @@ -1389,6 +1398,24 @@ describe('OnboardingFlow', () => { }); it('should override the name field label in basic_information using jsfModify', async () => { + const uniqueEmploymentId = generateUniqueEmploymentId(); + server.use( + http.get(`*/v1/employments/${uniqueEmploymentId}`, () => { + return HttpResponse.json({ + ...employmentResponse, + data: { + ...employmentResponse.data, + employment: { + ...employmentResponse.data.employment, + id: uniqueEmploymentId, + contract_details: null, + basic_information: null, + status: 'created', // Ensure it's not a readonly status + }, + }, + }); + }), + ); const customNameLabel = 'Custom Full Name Label'; mockRender.mockImplementation( @@ -1420,7 +1447,7 @@ describe('OnboardingFlow', () => { render( Date: Sat, 28 Jun 2025 11:07:52 +0200 Subject: [PATCH 16/25] fix test --- .../Onboarding/tests/OnboardingFlow.test.tsx | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx index fc249ea1..255abfc1 100644 --- a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx @@ -1228,6 +1228,23 @@ describe('OnboardingFlow', () => { }); it('should override field labels using jsfModify options', async () => { + const uniqueEmploymentId = generateUniqueEmploymentId(); + server.use( + http.get(`*/v1/employments/${uniqueEmploymentId}`, () => { + return HttpResponse.json({ + ...employmentResponse, + data: { + ...employmentResponse.data, + employment: { + ...employmentResponse.data.employment, + id: uniqueEmploymentId, + contract_details: null, + status: 'created', // Ensure it's not a readonly status + }, + }, + }); + }), + ); const customSigningBonusLabel = 'Custom Signing Bonus Label'; mockRender.mockImplementation( @@ -1259,7 +1276,7 @@ describe('OnboardingFlow', () => { render( { }, ); - // Wait for loading to finish and form to be ready - await screen.findByText(/Step: Basic Information/i); - - const nextButton = screen.getByText(/Next Step/i); - expect(nextButton).toBeInTheDocument(); - - nextButton.click(); - await screen.findByText(/Step: Contract Details/i); // Verify that the custom label is displayed From 50b15b7679e23ba6fa8946b3f800d78996968204 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Sat, 28 Jun 2025 11:10:14 +0200 Subject: [PATCH 17/25] fix tests --- src/flows/Onboarding/tests/OnboardingFlow.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx index 255abfc1..398eddc2 100644 --- a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx @@ -1131,6 +1131,8 @@ describe('OnboardingFlow', () => { // Should automatically go to review step instead of starting from select country await screen.findByText(/Step: Review/i); + await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); + // Verify basic information data is displayed in the Review component expect(screen.getByText('name: Gabriel')).toBeInTheDocument(); From 5b8ad3ce88986c0b1ef65fe31a18033af319459d Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Sat, 28 Jun 2025 11:15:19 +0200 Subject: [PATCH 18/25] fix more tests --- .../Onboarding/tests/OnboardingFlow.test.tsx | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx index 398eddc2..25d2d389 100644 --- a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx @@ -739,7 +739,7 @@ describe('OnboardingFlow', () => { id: undefined, name: undefined, }, - emails: 'gabriel@gmail.com', + email: 'gabriel@gmail.com', has_seniority_date: 'no', job_title: employmentResponse.data.employment.job_title, manager: { @@ -961,6 +961,18 @@ describe('OnboardingFlow', () => { const patchSpy = vi.fn(); server.use( + http.get('*/v1/employments/:id', () => { + return HttpResponse.json({ + ...employmentResponse, + data: { + ...employmentResponse.data, + employment: { + ...employmentResponse.data.employment, + contract_details: null, + }, + }, + }); + }), http.patch('*/v1/employments/*', () => { patchSpy(); return HttpResponse.json(employmentUpdatedResponse); @@ -999,9 +1011,15 @@ describe('OnboardingFlow', () => { { wrapper }, ); - await screen.findByText(/Step: Basic Information/i); + await screen.findByText(/Step: Contract Details/i); await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); + const backButton = screen.getByText(/Back/i); + expect(backButton).toBeInTheDocument(); + backButton.click(); + + await screen.findByText(/Step: Basic Information/i); + const nextButton = screen.getByText(/Next Step/i); nextButton.click(); @@ -1070,6 +1088,8 @@ describe('OnboardingFlow', () => { // Should automatically go to review step instead of starting from select country await screen.findByText(/Step: Review/i); + await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); + // Verify basic information data is displayed in the Review component expect(screen.getByText('name: Gabriel')).toBeInTheDocument(); From 03db07945d2382c737d95787a612aaef08bbc093 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Sat, 28 Jun 2025 11:24:36 +0200 Subject: [PATCH 19/25] fix tests --- .../Onboarding/tests/OnboardingFlow.test.tsx | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx index 25d2d389..f4a415d4 100644 --- a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx @@ -452,8 +452,6 @@ describe('OnboardingFlow', () => { async function fillCountry(country: string) { await screen.findByText(/Step: Select Country/i); - await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); - await fillSelect('Country', country); const nextButton = screen.getByText(/Continue/i); @@ -504,6 +502,24 @@ describe('OnboardingFlow', () => { }); it('should render the seniority_date field when the user selects yes in the radio button', async () => { + server.use( + http.get('*/v1/employments/:id', ({ params }) => { + // Create a response with the actual employment ID from the request + const employmentId = params?.id; + return HttpResponse.json({ + ...employmentResponse, + data: { + ...employmentResponse.data, + employment: { + ...employmentResponse.data.employment, + id: employmentId, + contract_details: null, + basic_information: null, + }, + }, + }); + }), + ); render(, { wrapper }); await fillCountry('Portugal'); @@ -516,6 +532,24 @@ describe('OnboardingFlow', () => { }); it('should fill the first step, go to the second step and go back to the first step', async () => { + server.use( + http.get('*/v1/employments/:id', ({ params }) => { + // Create a response with the actual employment ID from the request + const employmentId = params?.id; + return HttpResponse.json({ + ...employmentResponse, + data: { + ...employmentResponse.data, + employment: { + ...employmentResponse.data.employment, + id: employmentId, + contract_details: null, + basic_information: null, + }, + }, + }); + }), + ); mockRender.mockImplementation( ({ onboardingBag, components }: OnboardingRenderProps) => { const currentStepIndex = onboardingBag.stepState.currentStep.index; @@ -551,8 +585,6 @@ describe('OnboardingFlow', () => { ); await screen.findByText(/Step: Basic Information/i); - await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); - await fillBasicInformation(); const nextButton = screen.getByText(/Next Step/i); From 9566ec13c13f24dd188a812189f9aa081ddee479 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Sun, 29 Jun 2025 18:26:36 +0200 Subject: [PATCH 20/25] fix loading --- src/flows/Onboarding/hooks.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/flows/Onboarding/hooks.tsx b/src/flows/Onboarding/hooks.tsx index 475e7e3b..6d4c85db 100644 --- a/src/flows/Onboarding/hooks.tsx +++ b/src/flows/Onboarding/hooks.tsx @@ -564,12 +564,22 @@ export const useOnboarding = ({ initialNavigationRef, }); + const shouldNavigateToReview = useMemo(() => { + return Boolean( + employment?.status && + reviewStepAllowedEmploymentStatus.includes(employment.status) && + !initialLoading && + stepState.currentStep.name !== 'review', + ); + }, [employment?.status, initialLoading, stepState.currentStep.name]); + const isLoading = initialLoading || isNavigatingToReviewWhenEmploymentIsFinal || isNavigatingToReview || isNavigatingToContractDetails || - isNavigatingToBenefits; + isNavigatingToBenefits || + shouldNavigateToReview; const initializeStepValues = useCallback(() => { fieldsMetaRef.current = { From 8ab1c1a49adab5d5ef97a07e05fadfe3935f378a Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Sun, 29 Jun 2025 19:02:46 +0200 Subject: [PATCH 21/25] fix loadings --- src/flows/Onboarding/hooks.tsx | 67 ++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/src/flows/Onboarding/hooks.tsx b/src/flows/Onboarding/hooks.tsx index 6d4c85db..776ff216 100644 --- a/src/flows/Onboarding/hooks.tsx +++ b/src/flows/Onboarding/hooks.tsx @@ -564,6 +564,8 @@ export const useOnboarding = ({ initialNavigationRef, }); + const hasCompletedInitialNavigation = useRef(false); + const shouldNavigateToReview = useMemo(() => { return Boolean( employment?.status && @@ -573,13 +575,27 @@ export const useOnboarding = ({ ); }, [employment?.status, initialLoading, stepState.currentStep.name]); - const isLoading = - initialLoading || - isNavigatingToReviewWhenEmploymentIsFinal || - isNavigatingToReview || - isNavigatingToContractDetails || - isNavigatingToBenefits || - shouldNavigateToReview; + const isNavigationLoading = useMemo(() => { + if (hasCompletedInitialNavigation.current) { + return false; + } + + return ( + isNavigatingToReviewWhenEmploymentIsFinal || + isNavigatingToReview || + isNavigatingToContractDetails || + isNavigatingToBenefits || + shouldNavigateToReview + ); + }, [ + isNavigatingToReviewWhenEmploymentIsFinal, + isNavigatingToReview, + isNavigatingToContractDetails, + isNavigatingToBenefits, + shouldNavigateToReview, + ]); + + const isLoading = initialLoading || isNavigationLoading; const initializeStepValues = useCallback(() => { fieldsMetaRef.current = { @@ -618,28 +634,25 @@ export const useOnboarding = ({ ]); useEffect(() => { - if (initialLoading) { + if (initialLoading || hasCompletedInitialNavigation.current) { return; } - if ( - !initialNavigationRef.current.review && - !initialNavigationRef.current.contractDetails && - !initialNavigationRef.current.benefits - ) { - if (isNavigatingToReviewWhenEmploymentIsFinal || isNavigatingToReview) { - initialNavigationRef.current.review = true; - initializeStepValues(); - goToStep('review'); - } else if (isNavigatingToContractDetails) { - initialNavigationRef.current.contractDetails = true; - initializeStepValues(); - goToStep('contract_details'); - } else if (isNavigatingToBenefits) { - initialNavigationRef.current.benefits = true; - initializeStepValues(); - goToStep('benefits'); - } + if (isNavigatingToReviewWhenEmploymentIsFinal || isNavigatingToReview) { + hasCompletedInitialNavigation.current = true; + initializeStepValues(); + goToStep('review'); + } else if (isNavigatingToContractDetails) { + hasCompletedInitialNavigation.current = true; + initializeStepValues(); + goToStep('contract_details'); + } else if (isNavigatingToBenefits) { + hasCompletedInitialNavigation.current = true; + initializeStepValues(); + goToStep('benefits'); + } else { + // No navigation needed, but mark as completed + hasCompletedInitialNavigation.current = true; } }, [ goToStep, @@ -706,7 +719,6 @@ export const useOnboarding = ({ }; try { const response = await createEmploymentMutationAsync(payload); - await refetchCompany(); setInternalEmploymentId( // @ts-expect-error the types from the response are not matching response.data?.data?.employment?.id, @@ -735,6 +747,7 @@ export const useOnboarding = ({ frequency: 'monthly', }, }; + return updateEmploymentMutationAsync({ employmentId: internalEmploymentId as string, ...payload, From 2961ca3c721461ff59bca5c200f0f11a2c5ec6ec Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Sun, 29 Jun 2025 19:06:02 +0200 Subject: [PATCH 22/25] remove initialNavigationRef --- src/flows/Onboarding/hooks.tsx | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/src/flows/Onboarding/hooks.tsx b/src/flows/Onboarding/hooks.tsx index 776ff216..1e778ce1 100644 --- a/src/flows/Onboarding/hooks.tsx +++ b/src/flows/Onboarding/hooks.tsx @@ -99,7 +99,6 @@ const useNavigationConditions = ({ stepState, isEmploymentReadOnly, benefitOffers, - initialNavigationRef, }: { employmentId?: string; employment?: Employment; @@ -108,11 +107,6 @@ const useNavigationConditions = ({ stepState: StepState; isEmploymentReadOnly?: boolean; benefitOffers: unknown; - initialNavigationRef: React.RefObject<{ - review: boolean; - contractDetails: boolean; - benefits: boolean; - }>; }) => { // Common conditions extracted as variables const hasBasicInformation = @@ -136,15 +130,13 @@ const useNavigationConditions = ({ hasLoadedEmployment && isEmploymentReadOnly && areSchemasLoaded && - stepState.currentStep.name !== 'review' && - !initialNavigationRef.current.review, + stepState.currentStep.name !== 'review', ); }, [ hasLoadedEmployment, isEmploymentReadOnly, areSchemasLoaded, stepState.currentStep.name, - initialNavigationRef, ]); const isNavigatingToReview = useMemo(() => { @@ -156,8 +148,7 @@ const useNavigationConditions = ({ hasBenefitsSchema && hasBenefitOffers && stepState.currentStep.name !== 'review' && - !isNavigatingToReviewWhenEmploymentIsFinal && - !initialNavigationRef.current.review, + !isNavigatingToReviewWhenEmploymentIsFinal, ); }, [ hasLoadedEmployment, @@ -168,7 +159,6 @@ const useNavigationConditions = ({ hasBenefitOffers, stepState.currentStep.name, isNavigatingToReviewWhenEmploymentIsFinal, - initialNavigationRef, ]); const isNavigatingToContractDetails = useMemo(() => { @@ -179,8 +169,7 @@ const useNavigationConditions = ({ areSchemasLoaded && stepState.currentStep.name !== 'contract_details' && !isNavigatingToReviewWhenEmploymentIsFinal && - !isNavigatingToReview && - !initialNavigationRef.current.contractDetails, + !isNavigatingToReview, ); }, [ hasLoadedEmployment, @@ -190,7 +179,6 @@ const useNavigationConditions = ({ stepState.currentStep.name, isNavigatingToReviewWhenEmploymentIsFinal, isNavigatingToReview, - initialNavigationRef, ]); const isNavigatingToBenefits = useMemo(() => { @@ -203,8 +191,7 @@ const useNavigationConditions = ({ stepState.currentStep.name !== 'benefits' && !isNavigatingToReviewWhenEmploymentIsFinal && !isNavigatingToReview && - !isNavigatingToContractDetails && - !initialNavigationRef.current.benefits, + !isNavigatingToContractDetails, ); }, [ hasLoadedEmployment, @@ -216,7 +203,6 @@ const useNavigationConditions = ({ isNavigatingToReviewWhenEmploymentIsFinal, isNavigatingToReview, isNavigatingToContractDetails, - initialNavigationRef, ]); return { @@ -541,13 +527,6 @@ export const useOnboarding = ({ ], ); - // Single ref to track all initial navigations - const initialNavigationRef = useRef({ - review: false, - contractDetails: false, - benefits: false, - }); - const { isNavigatingToReviewWhenEmploymentIsFinal, isNavigatingToReview, @@ -561,7 +540,6 @@ export const useOnboarding = ({ stepState, isEmploymentReadOnly, benefitOffers, - initialNavigationRef, }); const hasCompletedInitialNavigation = useRef(false); From 3ec12dc0f90bb4618f93c65292dd92003496974b Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Mon, 30 Jun 2025 08:51:43 +0200 Subject: [PATCH 23/25] add test --- .../Onboarding/tests/OnboardingFlow.test.tsx | 175 +++++++++++++++++- 1 file changed, 167 insertions(+), 8 deletions(-) diff --git a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx index f4a415d4..c2615d56 100644 --- a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx @@ -28,6 +28,7 @@ import { conversionFromEURToUSD, } from '@/src/flows/Onboarding/tests/fixtures'; import { + fillCheckbox, fillRadio, fillSelect, selectDayInCalendar, @@ -449,6 +450,106 @@ describe('OnboardingFlow', () => { } } + async function fillContractDetails() { + const newValues = { + hasSigningBonus: 'No', + offerOtherBonuses: 'No', + offerCommision: 'No', + offerEquityCompensation: 'No', + workingHoursExemption: 'No', + typeOfEmployment: 'Full-time', + experienceLevel: 'Level 2 - Entry Level', + paidTimeOff: 'Unlimited paid time off', + workAddress: "Same as the employee's residential address", + contractDuration: true, + salaryInstallmentsConfirmation: true, + workFromHomeAllowanceAck: true, + annualTrainingHoursAck: true, + annualGrossSalary: '20000', + probationLength: '40', + roleDescription: + 'A Product Manager is responsible for guiding the development and success of a product from concept to launch. They act as the bridge between business, design, and engineering teams, ensuring alignment with user needs and company goals. Product Managers conduct market research, define product requirements, prioritize features, and manage the product roadmap. They communicate clearly with stakeholders, analyze performance data, and make decisions to optimize user experience and business outcomes. Strategic thinking, problem-solving, and leadership are key traits. A Product Manager must balance customer desires, technical feasibility, and business viability to deliver valuable, innovative products in a competitive market.', + }; + + if (newValues?.annualGrossSalary) { + fireEvent.change(screen.getByLabelText(/Test Label/i), { + target: { value: newValues?.annualGrossSalary }, + }); + } + + if (newValues?.probationLength) { + fireEvent.change(screen.getByLabelText(/Probation period, in days/i), { + target: { value: newValues?.probationLength }, + }); + } + + if (newValues?.roleDescription) { + fireEvent.change(screen.getByLabelText(/Role description/i), { + target: { value: newValues?.roleDescription }, + }); + } + + if (newValues?.contractDuration) { + fillCheckbox('Contract duration'); + } + + if (newValues?.salaryInstallmentsConfirmation) { + fillCheckbox( + 'I confirm the annual gross salary includes 13th and 14th salaries', + ); + } + + if (newValues?.workFromHomeAllowanceAck) { + fillCheckbox("I acknowledge Portugal's work-from-home allowance"); + } + + if (newValues?.annualTrainingHoursAck) { + fillCheckbox("I acknowledge Portugal's annual training requirement"); + } + + if (newValues?.workAddress) { + await fillRadio('Local', newValues?.workAddress); + } + + if (newValues?.hasSigningBonus) { + await fillRadio('Offer a signing bonus?', newValues?.hasSigningBonus); + } + + if (newValues?.offerOtherBonuses) { + await fillRadio('Offer other bonuses?', newValues?.offerOtherBonuses); + } + + if (newValues?.paidTimeOff) { + await fillRadio('Paid time off policy', newValues?.paidTimeOff); + } + + if (newValues?.offerCommision) { + await fillRadio('Offer commission?', newValues?.offerCommision); + } + + if (newValues?.offerEquityCompensation) { + await fillRadio( + 'Will this employee receive equity?', + newValues?.offerEquityCompensation, + ); + } + + if (newValues?.workingHoursExemption) { + await fillRadio( + 'Will this employee need to work outside regular work hours?', + newValues?.workingHoursExemption, + ); + } + + if (newValues?.typeOfEmployment) { + await fillRadio('Type of employee', newValues?.typeOfEmployment); + } + + if (newValues?.experienceLevel) { + await fillRadio('Experience level', newValues?.experienceLevel); + } + } + async function fillCountry(country: string) { await screen.findByText(/Step: Select Country/i); @@ -806,28 +907,87 @@ describe('OnboardingFlow', () => { wrapper, }, ); + }); - await screen.findByText(/Step: Benefits/i); + it('should arrive in the benefits step if contract details are filled', async () => { + server.use( + http.get('*/v1/employments/*/benefit-offers', () => { + return HttpResponse.json({ data: [] }); + }), + http.get('*/v1/employments/:id', ({ params }) => { + const employmentId = params?.id; + return HttpResponse.json({ + ...employmentResponse, + data: { + ...employmentResponse.data, + employment: { + ...employmentResponse.data.employment, + id: employmentId, + contract_details: null, + }, + }, + }); + }), + ); - await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); + mockRender.mockImplementation( + ({ onboardingBag, components }: OnboardingRenderProps) => { + const currentStepIndex = onboardingBag.stepState.currentStep.index; - const backButton = screen.getByText(/Back/i); - expect(backButton).toBeInTheDocument(); + const steps: Record = { + [0]: 'Basic Information', + [1]: 'Contract Details', + [2]: 'Benefits', + [3]: 'Review', + }; - backButton.click(); + return ( + <> +

Step: {steps[currentStepIndex]}

+ + + ); + }, + ); + render( + , + { + wrapper, + }, + ); await screen.findByText(/Step: Contract Details/i); - await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); + await fillContractDetails(); + const nextButton = screen.getByText(/Next Step/i); expect(nextButton).toBeInTheDocument(); + nextButton.click(); await waitFor(() => { expect(mockOnSubmit).toHaveBeenCalledTimes(1); }); - // Assert the contract details submission + expect(mockOnSubmit).toHaveBeenCalledWith({ annual_gross_salary: 2000000, annual_training_hours_ack: 'acknowledged', @@ -855,7 +1015,6 @@ describe('OnboardingFlow', () => { working_hours_exemption: 'no', }); - // Verify we move to the next step (Benefits) await screen.findByText(/Step: Benefits/i); }); From cb71773d82e14e3901c00e4c93befc9b21a4dd54 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Mon, 30 Jun 2025 08:58:06 +0200 Subject: [PATCH 24/25] move utility --- src/flows/Onboarding/tests/OnboardingFlow.test.tsx | 8 +------- src/flows/Onboarding/tests/OnboardingInvite.test.tsx | 8 +------- src/flows/Onboarding/tests/utils.ts | 6 ++++++ 3 files changed, 8 insertions(+), 14 deletions(-) create mode 100644 src/flows/Onboarding/tests/utils.ts diff --git a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx index c2615d56..b7226e33 100644 --- a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx @@ -35,16 +35,10 @@ import { } from '@/src/tests/testHelpers'; import { NormalizedFieldError } from '@/src/lib/mutations'; import { fireEvent } from '@testing-library/react'; +import { generateUniqueEmploymentId } from '@/src/flows/Onboarding/tests/utils'; const queryClient = new QueryClient(); -// Helper function to generate unique employment IDs for each test -let employmentIdCounter = 0; -const generateUniqueEmploymentId = () => { - employmentIdCounter++; - return `test-employment-${employmentIdCounter}-${Date.now()}`; -}; - const wrapper = ({ children }: PropsWithChildren) => ( {children} diff --git a/src/flows/Onboarding/tests/OnboardingInvite.test.tsx b/src/flows/Onboarding/tests/OnboardingInvite.test.tsx index 0c9fe2b6..84236b8e 100644 --- a/src/flows/Onboarding/tests/OnboardingInvite.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingInvite.test.tsx @@ -10,6 +10,7 @@ import { contractDetailsSchema, employmentResponse, } from '@/src/flows/Onboarding/tests/fixtures'; +import { generateUniqueEmploymentId } from '@/src/flows/Onboarding/tests/utils'; import { FormFieldsProvider } from '@/src/RemoteFlowsProvider'; import { server } from '@/src/tests/server'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; @@ -24,13 +25,6 @@ import { http, HttpResponse } from 'msw'; import { PropsWithChildren } from 'react'; import { vi } from 'vitest'; -// Helper function to generate unique employment IDs for each test -let employmentIdCounter = 0; -const generateUniqueEmploymentId = () => { - employmentIdCounter++; - return `test-employment-${employmentIdCounter}-${Date.now()}`; -}; - const queryClient = new QueryClient(); const wrapper = ({ children }: PropsWithChildren) => ( diff --git a/src/flows/Onboarding/tests/utils.ts b/src/flows/Onboarding/tests/utils.ts new file mode 100644 index 00000000..6e464445 --- /dev/null +++ b/src/flows/Onboarding/tests/utils.ts @@ -0,0 +1,6 @@ +// Helper function to generate unique employment IDs for each test +let employmentIdCounter = 0; +export const generateUniqueEmploymentId = () => { + employmentIdCounter++; + return `test-employment-${employmentIdCounter}-${Date.now()}`; +}; From d626174a2289a9730553e4fdc27896dab88091a2 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Fri, 4 Jul 2025 11:17:00 +0200 Subject: [PATCH 25/25] fix conflicts --- .../Onboarding/tests/OnboardingFlow.test.tsx | 183 +----------------- .../tests/OnboardingInvite.test.tsx | 2 +- src/flows/Onboarding/tests/helpers.ts | 106 +++++++++- src/flows/Onboarding/tests/utils.ts | 6 - 4 files changed, 108 insertions(+), 189 deletions(-) delete mode 100644 src/flows/Onboarding/tests/utils.ts diff --git a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx index 44e4e51f..d4e45525 100644 --- a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx @@ -27,21 +27,14 @@ import { companyResponse, conversionFromEURToUSD, } from '@/src/flows/Onboarding/tests/fixtures'; -import { - fillCheckbox, - fillRadio, - fillSelect, -} from '@/src/tests/testHelpers'; +import { fillRadio, fillSelect } from '@/src/tests/testHelpers'; import { NormalizedFieldError } from '@/src/lib/mutations'; import { fireEvent } from '@testing-library/react'; -<<<<<<< HEAD -import { generateUniqueEmploymentId } from '@/src/flows/Onboarding/tests/utils'; -======= import { fillBasicInformation, + fillContractDetails, generateUniqueEmploymentId, } from '@/src/flows/Onboarding/tests/helpers'; ->>>>>>> main const queryClient = new QueryClient(); @@ -381,178 +374,6 @@ describe('OnboardingFlow', () => { mockRender.mockReset(); }); -<<<<<<< HEAD - async function fillBasicInformation( - values?: Partial<{ - fullName: string; - personalEmail: string; - workEmail: string; - jobTitle: string; - country: string; - jobCategory: string; - provisionalStartDate: string; - hasSeniorityDate: string; - }>, - ) { - const defaultValues = { - fullName: 'John Doe', - personalEmail: 'john.doe@gmail.com', - workEmail: 'john.doe@remote.com', - jobTitle: 'Software Engineer', - provisionalStartDate: '15', - hasSeniorityDate: 'No', - }; - - const newValues = { - ...defaultValues, - ...values, - }; - - await waitFor(() => { - expect(screen.getByLabelText(/Full name/i)).toBeInTheDocument(); - }); - - if (newValues?.fullName) { - fireEvent.change(screen.getByLabelText(/Full name/i), { - target: { value: newValues?.fullName }, - }); - } - - if (newValues?.personalEmail) { - fireEvent.change(screen.getByLabelText(/Personal email/i), { - target: { value: newValues?.personalEmail }, - }); - } - - if (newValues?.workEmail) { - fireEvent.change(screen.getByLabelText(/Work email/i), { - target: { value: newValues?.workEmail }, - }); - } - - if (newValues?.jobTitle) { - fireEvent.change(screen.getByLabelText(/Job title/i), { - target: { value: newValues?.jobTitle }, - }); - } - - if (newValues?.provisionalStartDate) { - await selectDayInCalendar( - newValues?.provisionalStartDate, - 'provisional_start_date', - ); - } - - if (newValues?.hasSeniorityDate) { - await fillRadio( - 'Does the employee have a seniority date?', - newValues?.hasSeniorityDate, - ); - } - } - - async function fillContractDetails() { - const newValues = { - hasSigningBonus: 'No', - offerOtherBonuses: 'No', - offerCommision: 'No', - offerEquityCompensation: 'No', - workingHoursExemption: 'No', - typeOfEmployment: 'Full-time', - experienceLevel: 'Level 2 - Entry Level', - paidTimeOff: 'Unlimited paid time off', - workAddress: "Same as the employee's residential address", - contractDuration: true, - salaryInstallmentsConfirmation: true, - workFromHomeAllowanceAck: true, - annualTrainingHoursAck: true, - annualGrossSalary: '20000', - probationLength: '40', - roleDescription: - 'A Product Manager is responsible for guiding the development and success of a product from concept to launch. They act as the bridge between business, design, and engineering teams, ensuring alignment with user needs and company goals. Product Managers conduct market research, define product requirements, prioritize features, and manage the product roadmap. They communicate clearly with stakeholders, analyze performance data, and make decisions to optimize user experience and business outcomes. Strategic thinking, problem-solving, and leadership are key traits. A Product Manager must balance customer desires, technical feasibility, and business viability to deliver valuable, innovative products in a competitive market.', - }; - - if (newValues?.annualGrossSalary) { - fireEvent.change(screen.getByLabelText(/Test Label/i), { - target: { value: newValues?.annualGrossSalary }, - }); - } - - if (newValues?.probationLength) { - fireEvent.change(screen.getByLabelText(/Probation period, in days/i), { - target: { value: newValues?.probationLength }, - }); - } - - if (newValues?.roleDescription) { - fireEvent.change(screen.getByLabelText(/Role description/i), { - target: { value: newValues?.roleDescription }, - }); - } - - if (newValues?.contractDuration) { - fillCheckbox('Contract duration'); - } - - if (newValues?.salaryInstallmentsConfirmation) { - fillCheckbox( - 'I confirm the annual gross salary includes 13th and 14th salaries', - ); - } - - if (newValues?.workFromHomeAllowanceAck) { - fillCheckbox("I acknowledge Portugal's work-from-home allowance"); - } - - if (newValues?.annualTrainingHoursAck) { - fillCheckbox("I acknowledge Portugal's annual training requirement"); - } - - if (newValues?.workAddress) { - await fillRadio('Local', newValues?.workAddress); - } - - if (newValues?.hasSigningBonus) { - await fillRadio('Offer a signing bonus?', newValues?.hasSigningBonus); - } - - if (newValues?.offerOtherBonuses) { - await fillRadio('Offer other bonuses?', newValues?.offerOtherBonuses); - } - - if (newValues?.paidTimeOff) { - await fillRadio('Paid time off policy', newValues?.paidTimeOff); - } - - if (newValues?.offerCommision) { - await fillRadio('Offer commission?', newValues?.offerCommision); - } - - if (newValues?.offerEquityCompensation) { - await fillRadio( - 'Will this employee receive equity?', - newValues?.offerEquityCompensation, - ); - } - - if (newValues?.workingHoursExemption) { - await fillRadio( - 'Will this employee need to work outside regular work hours?', - newValues?.workingHoursExemption, - ); - } - - if (newValues?.typeOfEmployment) { - await fillRadio('Type of employee', newValues?.typeOfEmployment); - } - - if (newValues?.experienceLevel) { - await fillRadio('Experience level', newValues?.experienceLevel); - } - } - -======= ->>>>>>> main async function fillCountry(country: string) { await screen.findByText(/Step: Select Country/i); diff --git a/src/flows/Onboarding/tests/OnboardingInvite.test.tsx b/src/flows/Onboarding/tests/OnboardingInvite.test.tsx index 84236b8e..92114622 100644 --- a/src/flows/Onboarding/tests/OnboardingInvite.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingInvite.test.tsx @@ -10,7 +10,7 @@ import { contractDetailsSchema, employmentResponse, } from '@/src/flows/Onboarding/tests/fixtures'; -import { generateUniqueEmploymentId } from '@/src/flows/Onboarding/tests/utils'; +import { generateUniqueEmploymentId } from '@/src/flows/Onboarding/tests/helpers'; import { FormFieldsProvider } from '@/src/RemoteFlowsProvider'; import { server } from '@/src/tests/server'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; diff --git a/src/flows/Onboarding/tests/helpers.ts b/src/flows/Onboarding/tests/helpers.ts index 4bb5f84e..62aeff3d 100644 --- a/src/flows/Onboarding/tests/helpers.ts +++ b/src/flows/Onboarding/tests/helpers.ts @@ -1,4 +1,8 @@ -import { fillRadio, selectDayInCalendar } from '@/src/tests/testHelpers'; +import { + fillCheckbox, + fillRadio, + selectDayInCalendar, +} from '@/src/tests/testHelpers'; import { fireEvent, waitFor, screen } from '@testing-library/react'; // Helper function to generate unique employment IDs for each test @@ -76,3 +80,103 @@ export async function fillBasicInformation( ); } } + +export async function fillContractDetails() { + const newValues = { + hasSigningBonus: 'No', + offerOtherBonuses: 'No', + offerCommision: 'No', + offerEquityCompensation: 'No', + workingHoursExemption: 'No', + typeOfEmployment: 'Full-time', + experienceLevel: 'Level 2 - Entry Level', + paidTimeOff: 'Unlimited paid time off', + workAddress: "Same as the employee's residential address", + contractDuration: true, + salaryInstallmentsConfirmation: true, + workFromHomeAllowanceAck: true, + annualTrainingHoursAck: true, + annualGrossSalary: '20000', + probationLength: '40', + roleDescription: + 'A Product Manager is responsible for guiding the development and success of a product from concept to launch. They act as the bridge between business, design, and engineering teams, ensuring alignment with user needs and company goals. Product Managers conduct market research, define product requirements, prioritize features, and manage the product roadmap. They communicate clearly with stakeholders, analyze performance data, and make decisions to optimize user experience and business outcomes. Strategic thinking, problem-solving, and leadership are key traits. A Product Manager must balance customer desires, technical feasibility, and business viability to deliver valuable, innovative products in a competitive market.', + }; + + if (newValues?.annualGrossSalary) { + fireEvent.change(screen.getByLabelText(/Test Label/i), { + target: { value: newValues?.annualGrossSalary }, + }); + } + + if (newValues?.probationLength) { + fireEvent.change(screen.getByLabelText(/Probation period, in days/i), { + target: { value: newValues?.probationLength }, + }); + } + + if (newValues?.roleDescription) { + fireEvent.change(screen.getByLabelText(/Role description/i), { + target: { value: newValues?.roleDescription }, + }); + } + + if (newValues?.contractDuration) { + fillCheckbox('Contract duration'); + } + + if (newValues?.salaryInstallmentsConfirmation) { + fillCheckbox( + 'I confirm the annual gross salary includes 13th and 14th salaries', + ); + } + + if (newValues?.workFromHomeAllowanceAck) { + fillCheckbox("I acknowledge Portugal's work-from-home allowance"); + } + + if (newValues?.annualTrainingHoursAck) { + fillCheckbox("I acknowledge Portugal's annual training requirement"); + } + + if (newValues?.workAddress) { + await fillRadio('Local', newValues?.workAddress); + } + + if (newValues?.hasSigningBonus) { + await fillRadio('Offer a signing bonus?', newValues?.hasSigningBonus); + } + + if (newValues?.offerOtherBonuses) { + await fillRadio('Offer other bonuses?', newValues?.offerOtherBonuses); + } + + if (newValues?.paidTimeOff) { + await fillRadio('Paid time off policy', newValues?.paidTimeOff); + } + + if (newValues?.offerCommision) { + await fillRadio('Offer commission?', newValues?.offerCommision); + } + + if (newValues?.offerEquityCompensation) { + await fillRadio( + 'Will this employee receive equity?', + newValues?.offerEquityCompensation, + ); + } + + if (newValues?.workingHoursExemption) { + await fillRadio( + 'Will this employee need to work outside regular work hours?', + newValues?.workingHoursExemption, + ); + } + + if (newValues?.typeOfEmployment) { + await fillRadio('Type of employee', newValues?.typeOfEmployment); + } + + if (newValues?.experienceLevel) { + await fillRadio('Experience level', newValues?.experienceLevel); + } +} diff --git a/src/flows/Onboarding/tests/utils.ts b/src/flows/Onboarding/tests/utils.ts deleted file mode 100644 index 6e464445..00000000 --- a/src/flows/Onboarding/tests/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Helper function to generate unique employment IDs for each test -let employmentIdCounter = 0; -export const generateUniqueEmploymentId = () => { - employmentIdCounter++; - return `test-employment-${employmentIdCounter}-${Date.now()}`; -};