From 17a48179ca3ac71606f026d94c6c6a0198af2e6c Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Mon, 28 Sep 2026 10:50:42 +0200 Subject: [PATCH 1/2] fix(onboarding): retry job title eligibility check after a failed request check() recorded the new params before the request finished and only fired the request when params changed. With retry: false and staleTime: Infinity, a failed first attempt was never retried on later blurs unless a role field was edited. Always call queryClient.query when params are complete: a successful result is served from cache (no request), an errored/missing result refetches, and in-flight requests are deduped by the query cache. Co-Authored-By: Claude Opus 5.5 --- .../hooks/useJobTitleEligibilityCheck.tsx | 5 +- .../useJobTitleEligibilityCheck.test.tsx | 101 ++++++++++++++++++ 2 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 src/flows/Onboarding/tests/useJobTitleEligibilityCheck.test.tsx diff --git a/src/flows/Onboarding/hooks/useJobTitleEligibilityCheck.tsx b/src/flows/Onboarding/hooks/useJobTitleEligibilityCheck.tsx index e2c96b63f..eac4d3b98 100644 --- a/src/flows/Onboarding/hooks/useJobTitleEligibilityCheck.tsx +++ b/src/flows/Onboarding/hooks/useJobTitleEligibilityCheck.tsx @@ -103,11 +103,10 @@ export const useJobTitleEligibilityCheck = ({ validation?.formErrors, jobTitle, ); - const paramsChanged = !equal(paramsRef.current, nextParams); - if (paramsChanged) { + if (!equal(paramsRef.current, nextParams)) { setParams(nextParams); } - if (nextParams && paramsChanged) { + if (nextParams) { await queryClient .query(getOptions(nextParams)) .catch(() => diff --git a/src/flows/Onboarding/tests/useJobTitleEligibilityCheck.test.tsx b/src/flows/Onboarding/tests/useJobTitleEligibilityCheck.test.tsx new file mode 100644 index 000000000..27be35031 --- /dev/null +++ b/src/flows/Onboarding/tests/useJobTitleEligibilityCheck.test.tsx @@ -0,0 +1,101 @@ +import { act, renderHook } from '@testing-library/react'; +import { http, HttpResponse } from 'msw'; +import { JobTitleEligibilityCheckResponse } from '@/src/client'; +import { useJobTitleEligibilityCheck } from '@/src/flows/Onboarding/hooks/useJobTitleEligibilityCheck'; +import { server } from '@/src/tests/server'; +import { queryClient, TestProviders } from '@/src/tests/testHelpers'; +import { JSFFields } from '@/src/types/remoteFlows'; + +const contractDetailsFields = [ + { name: 'additional_job_title_eligibility_check_slug', isVisible: false }, + { name: 'role_description', isVisible: true }, +] as unknown as JSFFields; + +const values = { role_description: 'Builds things' }; + +const eligibilityCheckResponse: JobTitleEligibilityCheckResponse = { + data: { + job_title_eligibility_check: { + check_id: 'check-id', + verdict: 'eligible', + }, + }, +}; + +const renderCheck = () => + renderHook( + () => + useJobTitleEligibilityCheck({ + enabled: true, + employmentId: 'test-employment-id', + currentStepName: 'contract_details', + contractDetailsFields, + fallbackJobTitle: 'Engineer', + submittedJobTitle: undefined, + parseFormValues: async (formValues) => formValues, + handleValidation: async () => null, + }), + { wrapper: TestProviders }, + ); + +describe('useJobTitleEligibilityCheck', () => { + let requestCount: number; + + beforeEach(() => { + queryClient.clear(); + requestCount = 0; + vi.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('retries the check on the next blur after a failed request with unchanged params', async () => { + let failing = true; + let successCount = 0; + server.use( + http.post( + '*/v2/employments/:employmentId/job-title-eligibility-check', + () => { + if (failing) { + return HttpResponse.json( + { message: 'Internal error' }, + { status: 500 }, + ); + } + successCount += 1; + return HttpResponse.json(eligibilityCheckResponse); + }, + ), + ); + + const { result } = renderCheck(); + + await act(() => result.current.check(values)); + expect(successCount).toBe(0); + + failing = false; + await act(() => result.current.check(values)); + expect(successCount).toBe(1); + }); + + it('does not re-request a successful check when params are unchanged', async () => { + server.use( + http.post( + '*/v2/employments/:employmentId/job-title-eligibility-check', + () => { + requestCount += 1; + return HttpResponse.json(eligibilityCheckResponse); + }, + ), + ); + + const { result } = renderCheck(); + + await act(() => result.current.check(values)); + await act(() => result.current.check(values)); + + expect(requestCount).toBe(1); + }); +}); From cc4cf81a4bd766b8fba0a8a8a280cd28ea020e24 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Mon, 28 Sep 2026 10:54:49 +0200 Subject: [PATCH 2/2] test(onboarding): move the retry-after-failure test to #1408 Co-Authored-By: Claude Opus 5.5 --- .../useJobTitleEligibilityCheck.test.tsx | 101 ------------------ 1 file changed, 101 deletions(-) delete mode 100644 src/flows/Onboarding/tests/useJobTitleEligibilityCheck.test.tsx diff --git a/src/flows/Onboarding/tests/useJobTitleEligibilityCheck.test.tsx b/src/flows/Onboarding/tests/useJobTitleEligibilityCheck.test.tsx deleted file mode 100644 index 27be35031..000000000 --- a/src/flows/Onboarding/tests/useJobTitleEligibilityCheck.test.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { act, renderHook } from '@testing-library/react'; -import { http, HttpResponse } from 'msw'; -import { JobTitleEligibilityCheckResponse } from '@/src/client'; -import { useJobTitleEligibilityCheck } from '@/src/flows/Onboarding/hooks/useJobTitleEligibilityCheck'; -import { server } from '@/src/tests/server'; -import { queryClient, TestProviders } from '@/src/tests/testHelpers'; -import { JSFFields } from '@/src/types/remoteFlows'; - -const contractDetailsFields = [ - { name: 'additional_job_title_eligibility_check_slug', isVisible: false }, - { name: 'role_description', isVisible: true }, -] as unknown as JSFFields; - -const values = { role_description: 'Builds things' }; - -const eligibilityCheckResponse: JobTitleEligibilityCheckResponse = { - data: { - job_title_eligibility_check: { - check_id: 'check-id', - verdict: 'eligible', - }, - }, -}; - -const renderCheck = () => - renderHook( - () => - useJobTitleEligibilityCheck({ - enabled: true, - employmentId: 'test-employment-id', - currentStepName: 'contract_details', - contractDetailsFields, - fallbackJobTitle: 'Engineer', - submittedJobTitle: undefined, - parseFormValues: async (formValues) => formValues, - handleValidation: async () => null, - }), - { wrapper: TestProviders }, - ); - -describe('useJobTitleEligibilityCheck', () => { - let requestCount: number; - - beforeEach(() => { - queryClient.clear(); - requestCount = 0; - vi.spyOn(console, 'error').mockImplementation(() => {}); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - it('retries the check on the next blur after a failed request with unchanged params', async () => { - let failing = true; - let successCount = 0; - server.use( - http.post( - '*/v2/employments/:employmentId/job-title-eligibility-check', - () => { - if (failing) { - return HttpResponse.json( - { message: 'Internal error' }, - { status: 500 }, - ); - } - successCount += 1; - return HttpResponse.json(eligibilityCheckResponse); - }, - ), - ); - - const { result } = renderCheck(); - - await act(() => result.current.check(values)); - expect(successCount).toBe(0); - - failing = false; - await act(() => result.current.check(values)); - expect(successCount).toBe(1); - }); - - it('does not re-request a successful check when params are unchanged', async () => { - server.use( - http.post( - '*/v2/employments/:employmentId/job-title-eligibility-check', - () => { - requestCount += 1; - return HttpResponse.json(eligibilityCheckResponse); - }, - ), - ); - - const { result } = renderCheck(); - - await act(() => result.current.check(values)); - await act(() => result.current.check(values)); - - expect(requestCount).toBe(1); - }); -});