Repository navigation
test(onboarding): cover the job title eligibility blur dedup #1408
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
gabrielseco
merged 20 commits into
found-1869-hook-up-the-form-onblur-events-to-trigger-the-job
from
found-1869-job-title-eligibility-tests
Oct 8, 2026
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
130d062
test(onboarding): cover the job title eligibility blur dedup
gabrielseco 836274c
fix(onboarding): give the dedup test time for a broken fetch to land
gabrielseco 56b8bad
docs(onboarding): explain the inverted waitFor in the dedup test
gabrielseco c438891
docs(onboarding): reword the waitFor comment in plain language
gabrielseco e13fb13
test(onboarding): cover retrying the job title eligibility check afte…
gabrielseco 57bd08a
fix(onboarding): retry job title eligibility check after a failed req…
gabrielseco 1fb6fda
chore(example): rename vite config to .mts to silence native config l…
gabrielseco 522c18f
fix(scripts): send checkbox const value, not [true], in seed-onboardi…
gabrielseco 8f20c58
feat(scripts): seed onboarding directly against a gateway env (#1371)
gabrielseco 603325a
1.57.0 (#1407)
gabrielseco 9e454ff
docs: surface the inline-comment convention in CLAUDE.md (#1405)
gabrielseco 908261f
docs: document import order convention (#1404)
gabrielseco 3f0a584
chore(deps): update dependency @remoteoss/remote-json-schema-form-kit…
renovate[bot] 30d7c12
fix(onboarding): block invite while employment is in job_title_review…
gabrielseco a238d4b
chore: ignore CLAUDE.local.md (#1400)
gabrielseco d85505d
chore(deps-dev): update dependency dotenv to v18.0.1 (#1410)
renovate[bot] b4c8db3
chore(deps-dev): update dependency tsx to v4.23.15 (#1411)
renovate[bot] ca023c9
Merge branch 'found-1869-hook-up-the-form-onblur-events-to-trigger-th…
gabrielseco 5071433
Merge branch 'found-1869-hook-up-the-form-onblur-events-to-trigger-th…
gabrielseco 1cca2cd
Merge branch 'found-1869-hook-up-the-form-onblur-events-to-trigger-th…
gabrielseco File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
207 changes: 207 additions & 0 deletions
207
src/flows/Onboarding/tests/OnboardingFlowJobTitleEligibility.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| import { act, render, screen, waitFor } from '@testing-library/react'; | ||
| import userEvent from '@testing-library/user-event'; | ||
| import { http, HttpResponse } from 'msw'; | ||
| import { OnboardingFlow } from '@/src/flows/Onboarding/OnboardingFlow'; | ||
| import { | ||
| contractDetailsSchemaV1JobTitleEligibility, | ||
| employmentDefaultResponse, | ||
| } from '@/src/flows/Onboarding/tests/fixtures'; | ||
| import { OnboardingRenderProps } from '@/src/flows/Onboarding/types'; | ||
| import { fillRadio, queryClient, TestProviders } from '@/src/tests/testHelpers'; | ||
| import { server } from '@/src/tests/server'; | ||
| import { $TSFixMe } from '@/src/types/remoteFlows'; | ||
|
|
||
| describe('OnboardingFlow - job title eligibility check on blur', () => { | ||
| let latestOnboardingBag: $TSFixMe; | ||
|
|
||
| const mockRender = vi.fn( | ||
| ({ onboardingBag, components }: OnboardingRenderProps) => { | ||
| latestOnboardingBag = onboardingBag; | ||
| const { ContractDetailsStep } = components; | ||
|
|
||
| if (onboardingBag.stepState.currentStep.name !== 'contract_details') { | ||
| return null; | ||
| } | ||
|
|
||
| return <ContractDetailsStep />; | ||
| }, | ||
| ); | ||
|
|
||
| const jobTitleEligibilityCheckSpy = vi.fn(); | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| mockRender.mockReset(); | ||
| queryClient.clear(); | ||
|
|
||
| server.use( | ||
| http.get('*/v1/employments/:id', ({ params }) => { | ||
| return HttpResponse.json({ | ||
| ...employmentDefaultResponse, | ||
| data: { | ||
| ...employmentDefaultResponse.data, | ||
| employment: { | ||
| ...employmentDefaultResponse.data.employment, | ||
| id: params?.id, | ||
| country: { | ||
| code: 'ITA', | ||
| name: 'Italy', | ||
| alpha_2_code: 'IT', | ||
| supported_json_schemas: ['employment_basic_information'], | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
| }), | ||
| http.get('*/v1/countries/ITA/employment_basic_information*', () => { | ||
| return HttpResponse.json({ | ||
| data: { properties: { name: { type: 'string', title: 'Name' } } }, | ||
| }); | ||
| }), | ||
| http.get('*/v1/countries/ITA/contract_details*', () => { | ||
| return HttpResponse.json(contractDetailsSchemaV1JobTitleEligibility); | ||
| }), | ||
| http.post( | ||
| '*/v2/employments/:id/job-title-eligibility-check', | ||
| async ({ request }) => { | ||
| jobTitleEligibilityCheckSpy(await request.json()); | ||
| return HttpResponse.json({ | ||
| data: { | ||
| job_title_eligibility_check: { | ||
| slug: 'job-title-eligibility-check-slug', | ||
| result: 'yes', | ||
| }, | ||
| }, | ||
| }); | ||
| }, | ||
| ), | ||
| ); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| vi.restoreAllMocks(); | ||
| }); | ||
|
|
||
| const renderContractDetailsStep = async () => { | ||
| render( | ||
| <OnboardingFlow | ||
| companyId='test-company-id' | ||
| employmentId='test-employment-id' | ||
| skipSteps={['select_country']} | ||
| options={{ features: ['job_title_eligibility'] }} | ||
| render={mockRender} | ||
| />, | ||
| { wrapper: TestProviders }, | ||
| ); | ||
|
|
||
| await waitFor(() => expect(latestOnboardingBag.isLoading).toBe(false)); | ||
|
|
||
| act(() => { | ||
| latestOnboardingBag.goTo('contract_details'); | ||
| }); | ||
|
|
||
| await screen.findByLabelText(/Role description/i); | ||
| }; | ||
|
|
||
| const fillRoleFields = async () => { | ||
| const user = userEvent.setup(); | ||
| const roleDescription = screen.getByLabelText(/Role description/i); | ||
|
|
||
| await user.clear(roleDescription); | ||
| await user.type(roleDescription, 'Backend engineer responsibilities'); | ||
| await fillRadio('Will this role require working onsite', 'no'); | ||
| await fillRadio('Does this role require a professional license', 'no'); | ||
| // Blur the last field touched so the values captured on blur include | ||
| // all three role fields already committed. | ||
| await user.tab(); | ||
| }; | ||
|
|
||
| it('calls the job title eligibility check once the role fields are filled and blurred', async () => { | ||
| await renderContractDetailsStep(); | ||
|
|
||
| await fillRoleFields(); | ||
|
|
||
| await waitFor(() => | ||
| expect(jobTitleEligibilityCheckSpy).toHaveBeenCalledTimes(1), | ||
| ); | ||
| expect(jobTitleEligibilityCheckSpy).toHaveBeenCalledWith({ | ||
| job_title: 'pm', | ||
| role_description: 'Backend engineer responsibilities', | ||
| role_is_onsite: 'no', | ||
| role_requires_license: 'no', | ||
| }); | ||
| }); | ||
|
|
||
| it('does not call the check again when a later blur carries the same values', async () => { | ||
| await renderContractDetailsStep(); | ||
|
|
||
| await fillRoleFields(); | ||
| await waitFor(() => | ||
| expect(jobTitleEligibilityCheckSpy).toHaveBeenCalledTimes(1), | ||
| ); | ||
|
|
||
| const user = userEvent.setup(); | ||
| const roleDescription = screen.getByLabelText(/Role description/i); | ||
| await user.click(roleDescription); | ||
| await user.tab(); | ||
|
|
||
| // user.tab() doesn't wait for checkJobTitleEligibility - the onBlur handler | ||
| // doesn't await it, so a duplicate request (if the dedup were ever broken) | ||
| // could still be in flight right now. We wait up to 200ms to give it a | ||
| // chance to show up before checking that it didn't. waitFor normally throws | ||
| // if its condition never comes true, but here that's the outcome we want, | ||
| // so we swallow it with .catch and let the assertion below do the real check. | ||
| await waitFor( | ||
| () => { | ||
| expect(jobTitleEligibilityCheckSpy.mock.calls.length).toBeGreaterThan( | ||
| 1, | ||
| ); | ||
| }, | ||
| { timeout: 200 }, | ||
| ).catch(() => undefined); | ||
|
|
||
| expect(jobTitleEligibilityCheckSpy).toHaveBeenCalledTimes(1); | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| }); | ||
|
|
||
| it('retries the check on a later blur when the previous request failed', async () => { | ||
| vi.spyOn(console, 'error').mockImplementation(() => {}); | ||
| let failing = true; | ||
| const successSpy = vi.fn(); | ||
| server.use( | ||
| http.post('*/v2/employments/:id/job-title-eligibility-check', () => { | ||
| if (failing) { | ||
| return HttpResponse.json( | ||
| { message: 'Internal error' }, | ||
| { status: 500 }, | ||
| ); | ||
| } | ||
| successSpy(); | ||
| return HttpResponse.json({ | ||
| data: { | ||
| job_title_eligibility_check: { | ||
| slug: 'job-title-eligibility-check-slug', | ||
| result: 'yes', | ||
| }, | ||
| }, | ||
| }); | ||
| }), | ||
| ); | ||
|
|
||
| await renderContractDetailsStep(); | ||
|
|
||
| await fillRoleFields(); | ||
| await waitFor(() => | ||
| expect(console.error).toHaveBeenCalledWith( | ||
| 'Failed to fetch job title eligibility check', | ||
| ), | ||
| ); | ||
| expect(successSpy).not.toHaveBeenCalled(); | ||
|
|
||
| failing = false; | ||
| const user = userEvent.setup(); | ||
| await user.click(screen.getByLabelText(/Role description/i)); | ||
| await user.tab(); | ||
|
|
||
| await waitFor(() => expect(successSpy).toHaveBeenCalledTimes(1)); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
66 changes: 66 additions & 0 deletions
66
src/flows/Onboarding/tests/fixtures/contractDetails/v1-job-title-eligibility.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| /** | ||
| * Trimmed down jsf v1 contract details schema exposing only the fields the | ||
| * job title eligibility check cares about: role_description, role_is_onsite, | ||
| * role_requires_license, and the additional_job_title_eligibility_check_slug | ||
| * field whose presence gates the check. | ||
| */ | ||
| export const contractDetailsSchemaV1JobTitleEligibility = { | ||
| data: { | ||
| additionalProperties: false, | ||
| properties: { | ||
| role_description: { | ||
| title: 'Role description', | ||
| type: 'string', | ||
| 'x-jsf-presentation': { | ||
| inputType: 'textarea', | ||
| }, | ||
| }, | ||
| role_is_onsite: { | ||
| oneOf: [ | ||
| { | ||
| const: 'yes', | ||
| title: 'Yes', | ||
| }, | ||
| { | ||
| const: 'no', | ||
| title: 'No', | ||
| }, | ||
| ], | ||
| title: 'Will this role require working onsite?', | ||
| type: 'string', | ||
| 'x-jsf-presentation': { | ||
| direction: 'row', | ||
| inputType: 'radio', | ||
| }, | ||
| }, | ||
| role_requires_license: { | ||
| oneOf: [ | ||
| { | ||
| const: 'yes', | ||
| title: 'Yes', | ||
| }, | ||
| { | ||
| const: 'no', | ||
| title: 'No', | ||
| }, | ||
| ], | ||
| title: 'Does this role require a professional license?', | ||
| type: 'string', | ||
| 'x-jsf-presentation': { | ||
| direction: 'row', | ||
| inputType: 'radio', | ||
| }, | ||
| }, | ||
| additional_job_title_eligibility_check_slug: { | ||
| type: ['string', 'null'], | ||
| 'x-jsf-presentation': { | ||
| inputType: 'hidden', | ||
| }, | ||
| }, | ||
| }, | ||
| type: 'object', | ||
| 'x-rmt-meta': { | ||
| jsfVersion: '1', | ||
| }, | ||
| }, | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.