Skip to content

Commit 9607435

Browse files
test(mosaic): wait for state instead of sleeping in tests (#10452)
1 parent 419a305 commit 9607435

11 files changed

Lines changed: 173 additions & 116 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

‎packages/mosaic/src/blocks/confirmation/__tests__/confirmation.feature.test.tsx‎

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -297,25 +297,21 @@ describe('Confirmation shared by rows', () => {
297297

298298
function ReverifiedHost({
299299
action,
300-
cleanupMs,
301-
beforeRunMs,
300+
cleanup,
301+
beforeRun,
302302
}: {
303303
action: () => Promise<unknown>;
304-
cleanupMs?: number;
305-
beforeRunMs?: number;
304+
cleanup?: () => Promise<unknown>;
305+
beforeRun?: () => Promise<unknown>;
306306
}) {
307307
const [run, reverification] = useReverificationFlow(action);
308308
const controller = useConfirmController({
309309
onConfirm: async () => {
310-
if (beforeRunMs !== undefined) {
311-
await new Promise(resolve => setTimeout(resolve, beforeRunMs));
312-
}
310+
await beforeRun?.();
313311
try {
314312
return await run();
315313
} catch (error) {
316-
if (cleanupMs !== undefined) {
317-
await new Promise(resolve => setTimeout(resolve, cleanupMs));
318-
}
314+
await cleanup?.();
319315
throw error;
320316
}
321317
},
@@ -398,7 +394,9 @@ describe('Confirmation with reverification', () => {
398394

399395
it('closes from the error card without going back to the confirm step', async () => {
400396
serveFapi({ client: fapiClient([session]) });
401-
const { user } = await renderReverified({ action: interruptedOnce(), cleanupMs: 400 });
397+
const cleanupDone = deferred<void>();
398+
const cleanup = vi.fn(() => cleanupDone.promise);
399+
const { user } = await renderReverified({ action: interruptedOnce(), cleanup });
402400
holdRequests('post', VERIFY).fail('some_new_error', 'Nope');
403401

404402
await confirmIntoVerification(user);
@@ -408,42 +406,54 @@ describe('Confirmation with reverification', () => {
408406
const stopWatching = watchForConfirmStep();
409407
const buttons = screen.getAllByRole('button', { name: 'Close' });
410408
await user.click(buttons[buttons.length - 1]);
409+
await waitFor(() => expect(cleanup).toHaveBeenCalledOnce());
410+
cleanupDone.resolve();
411411
await waitFor(() => expect(screen.queryByRole('alertdialog')).toBeNull());
412412
expect(stopWatching()).toEqual([]);
413413
expect(screen.getByLabelText('Outcome')).toHaveTextContent('closed');
414414
});
415415

416-
for (const cleanupMs of [undefined, 400]) {
417-
it(`closes from the factor card's corner dismiss without going back to the confirm step (host cleans up after ${cleanupMs ?? 0}ms)`, async () => {
416+
for (const cleansUpLater of [false, true]) {
417+
it(`closes from the factor card's corner dismiss without going back to the confirm step (host cleans up ${cleansUpLater ? 'later' : 'at once'})`, async () => {
418418
serveFapi(passwordVerification);
419-
const { user } = await renderReverified({ action: interruptedOnce(), cleanupMs });
419+
const cleanupDone = deferred<void>();
420+
if (!cleansUpLater) {
421+
cleanupDone.resolve();
422+
}
423+
const cleanup = vi.fn(() => cleanupDone.promise);
424+
const { user } = await renderReverified({ action: interruptedOnce(), cleanup });
420425
await confirmIntoVerification(user);
421426

422427
expect(await screen.findByLabelText('Password')).toBeVisible();
423428
await untilConfirmStepIsGone();
424429
const stopWatching = watchForConfirmStep();
425430
await user.click(screen.getAllByRole('button', { name: 'Close' })[0]);
431+
await waitFor(() => expect(cleanup).toHaveBeenCalledOnce());
432+
cleanupDone.resolve();
426433
await waitFor(() => expect(screen.queryByRole('alertdialog')).toBeNull());
427434
expect(stopWatching()).toEqual([]);
428435
expect(screen.getByLabelText('Outcome')).toHaveTextContent('closed');
429436
});
430437
}
431438

432-
for (const settleMs of [undefined, 400]) {
433-
it(`does not go back to the confirm step after a successful verification (action settles after ${settleMs ?? 0}ms)`, async () => {
439+
for (const settlesLater of [false, true]) {
440+
it(`does not go back to the confirm step after a successful verification (action settles ${settlesLater ? 'later' : 'at once'})`, async () => {
434441
serveFapi(passwordVerification);
435-
const action = vi
436-
.fn()
437-
.mockResolvedValueOnce(reverificationError())
438-
.mockImplementation(() => new Promise(resolve => setTimeout(() => resolve({ done: true }), settleMs ?? 0)));
442+
const retry = deferred<{ done: true }>();
443+
if (!settlesLater) {
444+
retry.resolve({ done: true });
445+
}
446+
const action = vi.fn().mockResolvedValueOnce(reverificationError()).mockReturnValueOnce(retry.promise);
439447
const { user } = await renderReverified({ action });
440448
await confirmIntoVerification(user);
441449

442450
const field = await screen.findByLabelText('Password');
443451
await untilConfirmStepIsGone();
444452
const stopWatching = watchForConfirmStep();
445453
await user.type(field, 'hunter2{Enter}');
446-
await waitFor(() => expect(screen.queryByRole('alertdialog')).toBeNull(), { timeout: 3000 });
454+
await waitFor(() => expect(action).toHaveBeenCalledTimes(2));
455+
retry.resolve({ done: true });
456+
await waitFor(() => expect(screen.queryByRole('alertdialog')).toBeNull());
447457
expect(stopWatching()).toEqual([]);
448458
expect(screen.getByLabelText('Outcome')).toHaveTextContent('closed');
449459
});
@@ -476,7 +486,9 @@ describe('Confirmation with reverification', () => {
476486
.mockResolvedValueOnce(reverificationError())
477487
.mockResolvedValue({ done: true });
478488
serveFapi(passwordVerification);
479-
const { user } = await renderReverified({ action, beforeRunMs: 400 });
489+
const secondRun = deferred<void>();
490+
const beforeRun = vi.fn().mockResolvedValueOnce(undefined).mockReturnValueOnce(secondRun.promise);
491+
const { user } = await renderReverified({ action, beforeRun });
480492
await confirmIntoVerification(user);
481493
expect(await screen.findByLabelText('Password')).toBeVisible();
482494
await user.click(screen.getAllByRole('button', { name: 'Close' })[0]);
@@ -485,9 +497,10 @@ describe('Confirmation with reverification', () => {
485497
await openDialog(user, screen.getByRole('button', { name: 'Remove Google' }), { role: 'alertdialog', name: TITLE });
486498
const stopWatching = watchForConfirmStep();
487499
await user.click(confirmButton());
488-
await new Promise(resolve => setTimeout(resolve, 150));
500+
await waitFor(() => expect(beforeRun).toHaveBeenCalledTimes(2));
489501
expect(screen.queryByLabelText('Password')).toBeNull();
490-
expect(await screen.findByLabelText('Password', undefined, { timeout: 3000 })).toBeVisible();
502+
secondRun.resolve();
503+
expect(await screen.findByLabelText('Password')).toBeVisible();
491504
expect(stopWatching()).toEqual([]);
492505
});
493506
});

‎packages/mosaic/src/features/reverification/__tests__/reverification.feature.test.tsx‎

Lines changed: 53 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -377,40 +377,50 @@ describe('Reverification', () => {
377377
),
378378
);
379379

380-
const user = await startVerification();
381-
382-
// The error card stays on screen without the server's message
383-
expect(await screen.findByText(GENERIC_ERROR_TITLE)).toBeVisible();
384-
expect(screen.getByText('Please try again.')).toBeVisible();
385-
expect(screen.queryByText(NO_FACTORS_TITLE)).toBeNull();
386-
expect(screen.queryByText('Nope')).toBeNull();
387-
await settle();
388-
expect(screen.getByText(GENERIC_ERROR_TITLE)).toBeVisible();
389-
expect(outcome()).toBe('');
380+
vi.useFakeTimers({ shouldAdvanceTime: true });
381+
try {
382+
const user = await startVerification();
390383

391-
// Closing it rejects the action as cancelled
392-
await user.click(screen.getByRole('button', { name: 'Close' }));
393-
await waitFor(() => expect(outcome()).toBe('rejected: reverification_cancelled'));
394-
await untilClosed();
395-
expect(screen.queryByText(GENERIC_ERROR_TITLE)).toBeNull();
384+
// The error card stays on screen without the server's message
385+
expect(await screen.findByText(GENERIC_ERROR_TITLE)).toBeVisible();
386+
expect(screen.getByText('Please try again.')).toBeVisible();
387+
expect(screen.queryByText(NO_FACTORS_TITLE)).toBeNull();
388+
expect(screen.queryByText('Nope')).toBeNull();
389+
await act(() => vi.advanceTimersByTimeAsync(60_000));
390+
expect(screen.getByText(GENERIC_ERROR_TITLE)).toBeVisible();
391+
expect(outcome()).toBe('');
392+
393+
// Closing it rejects the action as cancelled
394+
await user.click(screen.getByRole('button', { name: 'Close' }));
395+
await waitFor(() => expect(outcome()).toBe('rejected: reverification_cancelled'));
396+
await untilClosed();
397+
expect(screen.queryByText(GENERIC_ERROR_TITLE)).toBeNull();
398+
} finally {
399+
vi.useRealTimers();
400+
}
396401
});
397402

398403
it('keeps showing the no factors card until it is dismissed when none of the factors are supported', async () => {
399404
await renderReverification(guardedAction(), verificationSeed({ firstFactors: [enterpriseSsoFactor] }));
400405

401-
const user = await startVerification();
402-
403-
expect(await screen.findByText(NO_FACTORS_TITLE)).toBeVisible();
404-
expect(screen.getByText(/No suitable authentication factor is configured/)).toBeVisible();
405-
expect(screen.queryByText(GENERIC_ERROR_TITLE)).toBeNull();
406-
await settle();
407-
expect(screen.getByText(NO_FACTORS_TITLE)).toBeVisible();
408-
expect(outcome()).toBe('');
406+
vi.useFakeTimers({ shouldAdvanceTime: true });
407+
try {
408+
const user = await startVerification();
409409

410-
await user.click(screen.getByRole('button', { name: 'Close' }));
411-
await waitFor(() => expect(outcome()).toBe('rejected: reverification_cancelled'));
412-
await untilClosed();
413-
expect(screen.queryByText(NO_FACTORS_TITLE)).toBeNull();
410+
expect(await screen.findByText(NO_FACTORS_TITLE)).toBeVisible();
411+
expect(screen.getByText(/No suitable authentication factor is configured/)).toBeVisible();
412+
expect(screen.queryByText(GENERIC_ERROR_TITLE)).toBeNull();
413+
await act(() => vi.advanceTimersByTimeAsync(60_000));
414+
expect(screen.getByText(NO_FACTORS_TITLE)).toBeVisible();
415+
expect(outcome()).toBe('');
416+
417+
await user.click(screen.getByRole('button', { name: 'Close' }));
418+
await waitFor(() => expect(outcome()).toBe('rejected: reverification_cancelled'));
419+
await untilClosed();
420+
expect(screen.queryByText(NO_FACTORS_TITLE)).toBeNull();
421+
} finally {
422+
vi.useRealTimers();
423+
}
414424
});
415425
});
416426

@@ -565,11 +575,12 @@ describe('Reverification', () => {
565575

566576
expect(screen.getByRole('button', { name: 'Continue' })).toBeDisabled();
567577
await user.type(passwordField(), '{Enter}');
568-
await settle();
569-
expect(attempt.requests).toHaveLength(0);
570578

571579
await user.type(passwordField(), PASSWORD);
572580
expect(screen.getByRole('button', { name: 'Continue' })).toBeEnabled();
581+
await user.keyboard('{Enter}');
582+
await waitFor(() => expect(attempt.requests).toHaveLength(1));
583+
expect(new URLSearchParams(await attempt.requests[0]?.text()).get('password')).toBe(PASSWORD);
573584
attempt.release();
574585
});
575586
});
@@ -639,8 +650,10 @@ describe('Reverification', () => {
639650
await pasteCode(user, CODE.slice(0, 3));
640651
expect(screen.getByRole('button', { name: 'Continue' })).toBeDisabled();
641652
await user.keyboard('{Enter}');
642-
await settle();
643-
expect(attempt.requests).toHaveLength(0);
653+
654+
await user.keyboard(CODE.slice(3));
655+
await waitFor(() => expect(attempt.requests).toHaveLength(1));
656+
expect(new URLSearchParams(await attempt.requests[0]?.text()).get('code')).toBe(CODE);
644657
attempt.release();
645658
});
646659

@@ -686,20 +699,21 @@ describe('Reverification', () => {
686699

687700
it('resends a code once the cooldown has passed', async () => {
688701
const { fapi } = await renderReverification(guardedAction(), verificationSeed({ firstFactors: [emailFactor] }));
689-
vi.useFakeTimers({ toFake: ['Date'] });
702+
vi.useFakeTimers({ toFake: ['Date', 'setInterval', 'clearInterval'] });
690703

691704
try {
692705
const user = await startVerification();
693706
expect(await screen.findByText(STEP.emailCode)).toBeVisible();
694-
await waitFor(() => expect(fapi.verification.firstFactorVerification?.strategy).toBe('email_code'));
707+
await vi.waitFor(() => expect(fapi.verification.firstFactorVerification?.strategy).toBe('email_code'));
695708

696709
// Resend is disabled and shows the countdown while the cooldown runs
697710
const resend = await screen.findByRole('button', { name: /Resend \(\d+\)/ });
698711
expect(resend).toBeDisabled();
699712

700713
// Resend is enabled again after the cooldown
701714
vi.setSystemTime(Date.now() + 30_000);
702-
await waitFor(() => expect(screen.getByRole('button', { name: /Resend/ })).toBeEnabled(), { timeout: 3000 });
715+
await act(() => vi.advanceTimersByTimeAsync(1000));
716+
expect(screen.getByRole('button', { name: /Resend/ })).toBeEnabled();
703717
expect(screen.getByRole('button', { name: /Resend/ }).textContent).not.toMatch(/\(\d+\)/);
704718

705719
// Resending sends a new code and clears the typed value
@@ -708,7 +722,7 @@ describe('Reverification', () => {
708722
expect(slotValues()).toEqual(['1', '2', '3', '', '', '']);
709723
const prepare = holdRequests('post', PREPARE_FIRST);
710724
await user.click(screen.getByRole('button', { name: /Resend/ }));
711-
await waitFor(() => expect(prepare.requests).toHaveLength(1));
725+
await vi.waitFor(() => expect(prepare.requests).toHaveLength(1));
712726
expect(slotValues()).toEqual(['', '', '', '', '', '']);
713727
expect(screen.getByRole('button', { name: /Resend \(\d+\)/ })).toBeDisabled();
714728

@@ -718,7 +732,7 @@ describe('Reverification', () => {
718732
await settle();
719733
expect(attempt.requests).toHaveLength(0);
720734
prepare.release();
721-
await waitFor(() => expect(attempt.requests).toHaveLength(1));
735+
await vi.waitFor(() => expect(attempt.requests).toHaveLength(1));
722736
attempt.release();
723737
await untilResolved();
724738
} finally {
@@ -817,11 +831,12 @@ describe('Reverification', () => {
817831

818832
expect(screen.getByRole('button', { name: 'Continue' })).toBeDisabled();
819833
await user.type(screen.getByLabelText('Backup code'), '{Enter}');
820-
await settle();
821-
expect(attempt.requests).toHaveLength(0);
822834

823835
await user.type(screen.getByLabelText('Backup code'), BACKUP_CODE);
824836
expect(screen.getByRole('button', { name: 'Continue' })).toBeEnabled();
837+
await user.keyboard('{Enter}');
838+
await waitFor(() => expect(attempt.requests).toHaveLength(1));
839+
expect(new URLSearchParams(await attempt.requests[0]?.text()).get('code')).toBe(BACKUP_CODE);
825840
attempt.release();
826841
});
827842
});

‎packages/mosaic/src/features/user-button/__tests__/user-button.feature.test.tsx‎

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -371,18 +371,24 @@ describe('UserButton', () => {
371371

372372
it('stays busy until the memberships have reloaded', async () => {
373373
await renderUserButton();
374-
const user = await open();
374+
await open();
375375
await screen.findByRole('button', { name: 'Accept' });
376376
const reload = holdRequests('get', '/v1/me/organization_memberships');
377+
vi.useFakeTimers({ shouldAdvanceTime: true });
377378

378-
await user.click(screen.getByRole('button', { name: 'Accept' }));
379-
await waitFor(() => expect(reload.requests).toHaveLength(1));
379+
try {
380+
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
381+
await user.click(screen.getByRole('button', { name: 'Accept' }));
382+
await waitFor(() => expect(reload.requests).toHaveLength(1));
380383

381-
await new Promise(resolve => setTimeout(resolve, 450));
382-
expect(waiting()).toBe(true);
384+
await act(() => vi.advanceTimersByTimeAsync(400));
385+
expect(waiting()).toBe(true);
383386

384-
reload.release();
385-
await waitFor(() => expect(waiting()).toBe(false));
387+
reload.release();
388+
await waitFor(() => expect(waiting()).toBe(false));
389+
} finally {
390+
vi.useRealTimers();
391+
}
386392
expect(popup()).toBeInTheDocument();
387393
});
388394

@@ -727,7 +733,7 @@ describe('UserButton', () => {
727733

728734
touch.fail('session_touch_failed');
729735

730-
await waitFor(() => expect(waiting()).toBe(false), { timeout: 2000 });
736+
await waitFor(() => expect(waiting()).toBe(false));
731737
expect(popup()).toBeInTheDocument();
732738
expect(signOut()).toBeEnabled();
733739
expect(log).toHaveBeenCalledWith(
@@ -738,28 +744,34 @@ describe('UserButton', () => {
738744

739745
it('still shows the action when reopened before it settles, and starts no second one', async () => {
740746
await renderUserButton();
741-
const user = await open();
747+
await open();
742748
await screen.findByRole('button', { name: 'Other' });
743749
const touch = holdRequests('post', '/v1/client/sessions/:id/touch');
750+
vi.useFakeTimers({ shouldAdvanceTime: true });
744751

745-
const menu = await openAccounts(user);
746-
await user.click(within(menu).getByRole('menuitem', { name: 'bob@example.com' }));
747-
expect(waiting()).toBe(true);
752+
try {
753+
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
754+
const menu = await openAccounts(user);
755+
await user.click(within(menu).getByRole('menuitem', { name: 'bob@example.com' }));
756+
expect(waiting()).toBe(true);
748757

749-
await user.click(trigger());
750-
await waitFor(() => expect(popup()).toBeNull());
758+
await user.click(trigger());
759+
await waitFor(() => expect(popup()).toBeNull());
751760

752-
await new Promise(resolve => setTimeout(resolve, 450));
753-
await user.click(trigger());
761+
await act(() => vi.advanceTimersByTimeAsync(400));
762+
await user.click(trigger());
754763

755-
expect(popup()).toBeInTheDocument();
756-
expect(waiting()).toBe(true);
764+
expect(popup()).toBeInTheDocument();
765+
expect(waiting()).toBe(true);
757766

758-
await user.click(screen.getByRole('button', { name: 'Other' }));
759-
expect(touch.requests).toHaveLength(1);
767+
await user.click(screen.getByRole('button', { name: 'Other' }));
768+
expect(touch.requests).toHaveLength(1);
760769

761-
touch.release();
762-
await waitFor(() => expect(waiting()).toBe(false));
770+
touch.release();
771+
await waitFor(() => expect(waiting()).toBe(false));
772+
} finally {
773+
vi.useRealTimers();
774+
}
763775
expect(popup()).toBeInTheDocument();
764776
});
765777

‎packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/__tests__/user-profile-enterprise-accounts.feature.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ describe('enterprise accounts', () => {
361361
await userEvent.setup().click(connect);
362362
await waitFor(() => expect(navigate).toHaveBeenCalledOnce());
363363
expect(connect).toHaveAttribute('aria-busy', 'true');
364-
await waitFor(() => expect(connect).not.toHaveAttribute('aria-busy', 'true'), { timeout: 2500 });
364+
await waitFor(() => expect(connect).not.toHaveAttribute('aria-busy', 'true'));
365365
expect(connect).toBeEnabled();
366366
});
367367

0 commit comments

Comments
 (0)