Skip to content

Commit 08f6ce5

Browse files
authored
refactor(onboarding-invite) - move business logic internally (#794)
* refactor(onboarding-invite) - move business logic internally * add tests
1 parent aa870d0 commit 08f6ce5

3 files changed

Lines changed: 152 additions & 34 deletions

File tree

example/src/ReviewContractorOnboardingStep.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,6 @@ export const ReviewContractorOnboardingStep = ({
156156
</BackButton>
157157
<OnboardingInvite
158158
className='submit-button'
159-
disabled={!onboardingBag.canInvite}
160159
render={() => {
161160
return 'Invite Contractor';
162161
}}

src/flows/ContractorOnboarding/components/OnboardingInvite.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,13 @@ export function OnboardingInvite({
5252
const data = await employmentInviteMutationAsync({
5353
employment_id: contractorOnboardingBag.employmentId,
5454
});
55-
await onSuccess?.({
56-
data: data as SuccessResponse,
57-
employmentStatus: 'invited',
58-
});
59-
contractorOnboardingBag.refetchEmployment();
55+
if (data) {
56+
await onSuccess?.({
57+
data: data as SuccessResponse,
58+
employmentStatus: 'invited',
59+
});
60+
contractorOnboardingBag.refetchEmployment();
61+
}
6062
}
6163
} catch (error: unknown) {
6264
if (isStructuredError(error)) {
@@ -76,10 +78,16 @@ export function OnboardingInvite({
7678
throw new Error(`Button component not found`);
7779
}
7880

81+
const disabled = Boolean(
82+
employmentInviteMutation.isPending ||
83+
!contractorOnboardingBag.canInvite ||
84+
props.disabled,
85+
);
86+
7987
return (
8088
<CustomButton
8189
{...props}
82-
disabled={employmentInviteMutation.isPending || props.disabled}
90+
disabled={disabled}
8391
onClick={(evt) => {
8492
handleSubmit();
8593
props.onClick?.(evt);

src/flows/ContractorOnboarding/tests/OnboardingInvite.test.tsx

Lines changed: 138 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -178,29 +178,39 @@ describe('ContractorOnboarding - OnboardingInvite', () => {
178178
});
179179

180180
it('should refetch employment after inviting the contractor', async () => {
181-
const refetchEmploymentMock = vi.fn();
182-
mockRender.mockImplementation(({ contractorOnboardingBag, components }) => {
183-
contractorOnboardingBag.refetchEmployment = refetchEmploymentMock;
184-
const { OnboardingInvite } = components;
185-
return (
186-
<OnboardingInvite
187-
data-testid='onboarding-invite'
188-
onSuccess={mockSuccess}
189-
onError={mockError}
190-
onSubmit={mockSubmit}
191-
render={() => 'Invite Contractor'}
192-
/>
193-
);
194-
});
181+
const getEmploymentSpy = vi.fn();
195182

183+
server.use(
184+
http.get('*/v1/employments/:id', ({ request }) => {
185+
getEmploymentSpy(request.url);
186+
return HttpResponse.json(mockContractorEmploymentResponse);
187+
}),
188+
);
196189
render(<ContractorOnboardingFlow {...defaultProps} />, {
197190
wrapper: TestProviders,
198191
});
199-
const button = await screen.findByTestId('onboarding-invite');
192+
193+
// CRITICAL: Wait for initial load to complete
194+
await waitForElementToBeRemoved(() => screen.getByTestId('spinner'));
195+
196+
// Verify initial employment fetch happened
197+
expect(getEmploymentSpy).toHaveBeenCalledTimes(1);
198+
199+
// Find and click the button
200+
const button = screen.getByText(/Invite Contractor/i);
200201
fireEvent.click(button);
202+
// Wait for the invite to complete and verify refetch happened
203+
await waitFor(() => {
204+
expect(mockSubmit).toHaveBeenCalled();
205+
});
201206

202207
await waitFor(() => {
203-
expect(refetchEmploymentMock).toHaveBeenCalled();
208+
expect(mockSuccess).toHaveBeenCalled();
209+
});
210+
211+
// Verify employment was fetched again (refetch happened)
212+
await waitFor(() => {
213+
expect(getEmploymentSpy).toHaveBeenCalledTimes(2);
204214
});
205215
});
206216

@@ -273,6 +283,86 @@ describe('ContractorOnboarding - OnboardingInvite', () => {
273283
expect(button).toBeInTheDocument();
274284
expect(button).toHaveTextContent('Default Button');
275285
});
286+
287+
it('should disable button when employment status is "invited"', async () => {
288+
server.use(
289+
http.get('*/v1/employments/*', () => {
290+
return HttpResponse.json({
291+
...mockContractorEmploymentResponse,
292+
data: {
293+
...mockContractorEmploymentResponse.data,
294+
employment: {
295+
...mockContractorEmploymentResponse.data.employment,
296+
status: 'invited',
297+
},
298+
},
299+
});
300+
}),
301+
);
302+
303+
mockRender.mockImplementation(({ components }) => {
304+
const { OnboardingInvite } = components;
305+
return (
306+
<OnboardingInvite
307+
data-testid='onboarding-invite'
308+
onSuccess={mockSuccess}
309+
onError={mockError}
310+
onSubmit={mockSubmit}
311+
render={() => 'Invite Contractor'}
312+
/>
313+
);
314+
});
315+
316+
render(<ContractorOnboardingFlow {...defaultProps} />, {
317+
wrapper: TestProviders,
318+
});
319+
320+
const button = await screen.findByTestId('onboarding-invite');
321+
322+
await waitFor(() => {
323+
expect(button).toBeDisabled();
324+
});
325+
});
326+
327+
it('should disable button when employment status is "created_awaiting_reserve"', async () => {
328+
server.use(
329+
http.get('*/v1/employments/*', () => {
330+
return HttpResponse.json({
331+
...mockContractorEmploymentResponse,
332+
data: {
333+
...mockContractorEmploymentResponse.data,
334+
employment: {
335+
...mockContractorEmploymentResponse.data.employment,
336+
status: 'created_awaiting_reserve',
337+
},
338+
},
339+
});
340+
}),
341+
);
342+
343+
mockRender.mockImplementation(({ components }) => {
344+
const { OnboardingInvite } = components;
345+
return (
346+
<OnboardingInvite
347+
data-testid='onboarding-invite'
348+
onSuccess={mockSuccess}
349+
onError={mockError}
350+
onSubmit={mockSubmit}
351+
render={() => 'Invite Contractor'}
352+
/>
353+
);
354+
});
355+
356+
render(<ContractorOnboardingFlow {...defaultProps} />, {
357+
wrapper: TestProviders,
358+
});
359+
360+
const button = await screen.findByTestId('onboarding-invite');
361+
362+
await waitFor(() => {
363+
expect(button).toBeDisabled();
364+
});
365+
});
276366
});
277367

278368
describe('render prop functionality', () => {
@@ -352,16 +442,17 @@ describe('ContractorOnboarding - OnboardingInvite', () => {
352442
expect(customButton).toBeInTheDocument();
353443
expect(customButton).toHaveTextContent('Custom Button Label');
354444

355-
// Verify custom button received correct props
356-
expect(MockCustomButton).toHaveBeenCalledWith(
357-
expect.objectContaining({
358-
className: 'test-class',
359-
disabled: false,
360-
onClick: expect.any(Function),
361-
children: 'Custom Button Label',
362-
}),
363-
{},
364-
);
445+
await waitFor(() => {
446+
expect(MockCustomButton).toHaveBeenCalledWith(
447+
expect.objectContaining({
448+
className: 'test-class',
449+
disabled: false,
450+
onClick: expect.any(Function),
451+
children: 'Custom Button Label',
452+
}),
453+
{},
454+
);
455+
});
365456
});
366457

367458
it('should apply disabled state correctly to custom button', async () => {
@@ -418,7 +509,9 @@ describe('ContractorOnboarding - OnboardingInvite', () => {
418509
});
419510

420511
const customButton = await screen.findByTestId('custom-button');
421-
expect(customButton).not.toBeDisabled(); // Initially not disabled
512+
await waitFor(() => {
513+
expect(customButton).not.toBeDisabled();
514+
});
422515

423516
fireEvent.click(customButton);
424517

@@ -453,6 +546,15 @@ describe('ContractorOnboarding - OnboardingInvite', () => {
453546
wrapper: customWrapper,
454547
});
455548

549+
await waitFor(() => {
550+
expect(MockCustomButton).toHaveBeenCalledWith(
551+
expect.objectContaining({
552+
disabled: false,
553+
}),
554+
expect.anything(),
555+
);
556+
});
557+
456558
const customButton = await screen.findByTestId('custom-button');
457559
fireEvent.click(customButton);
458560

@@ -492,6 +594,15 @@ describe('ContractorOnboarding - OnboardingInvite', () => {
492594

493595
await screen.findByTestId('custom-button');
494596

597+
await waitFor(() => {
598+
expect(MockCustomButton).toHaveBeenCalledWith(
599+
expect.objectContaining({
600+
disabled: false,
601+
}),
602+
expect.anything(),
603+
);
604+
});
605+
495606
expect(MockCustomButton).toHaveBeenCalledWith(
496607
expect.objectContaining({
497608
'data-testid': 'custom-button',

0 commit comments

Comments
 (0)