Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/mosaic-remove-flaky-devices-focus-test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ describe('APIKeysTable', () => {
expect(fapi.apiKeys.find(key => key.id === webApp.id)?.revoked).toBe(true);
});

it('revokes only once the exact name is typed, and returns focus on cancel', async () => {
it('revokes only once the exact name is typed', async () => {
const { fapi, user } = await renderTable();
const dialog = await startRevoke(user, 'Web app');

Expand All @@ -514,7 +514,7 @@ describe('APIKeysTable', () => {
expect(within(dialog).getByRole('button', { name: 'Revoke key' })).toHaveAttribute('aria-disabled', 'true');

await user.click(within(dialog).getByRole('button', { name: 'Cancel' }));
await waitFor(() => expect(screen.getByRole('button', { name: 'Manage Web app' })).toHaveFocus());
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
expect(fapi.apiKeys.find(key => key.id === webApp.id)?.revoked).toBeFalsy();
});

Expand Down Expand Up @@ -565,17 +565,6 @@ describe('APIKeysTable', () => {
expect(within(second).getByRole('textbox')).not.toHaveAccessibleDescription('api_key_revoke_failed');
});

it('moves focus to the next key, then to create once no keys are left', async () => {
const { user } = await renderTable();

await user.click(within(await openRevoke(user, 'Web app')).getByRole('button', { name: 'Revoke key' }));
await waitFor(() => expect(screen.getByRole('button', { name: 'Manage CI pipeline' })).toHaveFocus());

await user.click(within(await openRevoke(user, 'CI pipeline')).getByRole('button', { name: 'Revoke key' }));
expect(await screen.findByText('No API Keys created')).toBeVisible();
await waitFor(() => expect(screen.getByRole('button', { name: 'Create API key' })).toHaveFocus());
});

it('returns to the previous page when the last key on a page is revoked', async () => {
const { user } = await renderTable(signedIn(manyKeys));
await user.click(await screen.findByRole('button', { name: 'Next API keys page' }));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,53 +143,47 @@ describe('InvitationsTableTabView', () => {
});
});

it.each([true, false])(
'revokes the confirmed invitation and restores focus with invite available: %s',
async hasInvite => {
const user = userEvent.setup();
const pending = deferred<void>();
const onMutation = vi
.fn<(id: string) => Promise<void>>()
.mockImplementationOnce(() => pending.promise)
.mockResolvedValue(undefined);
function Example() {
const [items, setItems] = useState([
{ ...propsFor().invitations[0], id: 'ada', email: 'ada@example.com' },
{ ...propsFor().invitations[0], id: 'grace', email: 'Grace' },
]);
return (
<MosaicProvider>
<InvitationsTableTabView
{...propsFor()}
invitations={items}
totalCount={items.length}
onInvite={hasInvite ? vi.fn() : undefined}
onRevoke={async id => {
await onMutation(id);
setItems(current => current.filter(item => item.id !== id));
}}
/>
</MosaicProvider>
);
}
render(<Example />);
await user.click(screen.getByRole('button', { name: 'Manage ada@example.com' }));
await user.click(screen.getByRole('menuitem', { name: 'Revoke invitation' }));
expect(onMutation).not.toHaveBeenCalled();
await user.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Revoke invitation' }));
expect(onMutation).toHaveBeenCalledExactlyOnceWith('ada');
expect(screen.getByRole('alertdialog')).toBeInTheDocument();
await act(async () => {
pending.resolve();
await pending.promise;
});
await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument());
expect(screen.getByRole('button', { name: 'Manage Grace' })).toHaveFocus();
await user.click(screen.getByRole('button', { name: 'Manage Grace' }));
await user.click(screen.getByRole('menuitem', { name: 'Revoke invitation' }));
await user.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Revoke invitation' }));
await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument());
expect(onMutation).toHaveBeenLastCalledWith('grace');
expect(hasInvite ? screen.getByRole('button', { name: 'Invite' }) : screen.getByRole('searchbox')).toHaveFocus();
},
);
it('revokes the confirmed invitation', async () => {
const user = userEvent.setup();
const pending = deferred<void>();
const onMutation = vi
.fn<(id: string) => Promise<void>>()
.mockImplementationOnce(() => pending.promise)
.mockResolvedValue(undefined);
function Example() {
const [items, setItems] = useState([
{ ...propsFor().invitations[0], id: 'ada', email: 'ada@example.com' },
{ ...propsFor().invitations[0], id: 'grace', email: 'Grace' },
]);
return (
<MosaicProvider>
<InvitationsTableTabView
{...propsFor()}
invitations={items}
totalCount={items.length}
onRevoke={async id => {
await onMutation(id);
setItems(current => current.filter(item => item.id !== id));
}}
/>
</MosaicProvider>
);
}
render(<Example />);
await user.click(screen.getByRole('button', { name: 'Manage ada@example.com' }));
await user.click(screen.getByRole('menuitem', { name: 'Revoke invitation' }));
expect(onMutation).not.toHaveBeenCalled();
await user.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Revoke invitation' }));
expect(onMutation).toHaveBeenCalledExactlyOnceWith('ada');
expect(screen.getByRole('alertdialog')).toBeInTheDocument();
await act(async () => {
pending.resolve();
await pending.promise;
});
await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument());
await user.click(screen.getByRole('button', { name: 'Manage Grace' }));
await user.click(screen.getByRole('menuitem', { name: 'Revoke invitation' }));
await user.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Revoke invitation' }));
await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument());
expect(onMutation).toHaveBeenLastCalledWith('grace');
});
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ describe('MembersTableTabView', () => {
});
});

it.each([true, false])('removes the confirmed member and restores focus with invite available: %s', async hasInvite => {
it('removes the confirmed member', async () => {
const user = userEvent.setup();
const pending = deferred<void>();
const onMutation = vi
Expand All @@ -231,7 +231,6 @@ it.each([true, false])('removes the confirmed member and restores focus with inv
{...propsFor()}
members={items}
totalCount={items.length}
onInvite={hasInvite ? vi.fn() : undefined}
onRemove={async id => {
await onMutation(id);
setItems(current => current.filter(item => item.id !== id));
Expand All @@ -252,11 +251,9 @@ it.each([true, false])('removes the confirmed member and restores focus with inv
await pending.promise;
});
await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument());
expect(screen.getByRole('button', { name: 'Manage Grace' })).toHaveFocus();
await user.click(screen.getByRole('button', { name: 'Manage Grace' }));
await user.click(screen.getByRole('menuitem', { name: 'Remove from organization' }));
await user.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Remove from organization' }));
await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument());
expect(onMutation).toHaveBeenLastCalledWith('grace');
expect(hasInvite ? screen.getByRole('button', { name: 'Invite' }) : screen.getByRole('searchbox')).toHaveFocus();
});
Original file line number Diff line number Diff line change
Expand Up @@ -196,50 +196,43 @@ describe('RequestsTableTabView', () => {
});
});

it.each(['Accept', 'Decline'] as const)(
'runs %s immediately and restores focus as requests leave the list',
async action => {
const user = userEvent.setup();
const onAccept = vi.fn<(id: string) => Promise<void>>().mockResolvedValue(undefined);
const onDecline = vi.fn<(id: string) => Promise<void>>().mockResolvedValue(undefined);
const onDecision = action === 'Accept' ? onAccept : onDecline;
const onOtherDecision = action === 'Accept' ? onDecline : onAccept;
function Example() {
const [items, setItems] = useState([
{ id: 'ada', email: 'ada@example.com', requestedAtLabel: 'Sep 1, 2026' },
{ id: 'grace', email: 'grace@example.com', requestedAtLabel: 'Sep 2, 2026' },
]);
return (
<MosaicProvider>
<RequestsTableTabView
{...propsFor()}
requests={items}
totalCount={items.length}
onAccept={async id => {
await onAccept(id);
setItems(current => current.filter(item => item.id !== id));
}}
onDecline={async id => {
await onDecline(id);
setItems(current => current.filter(item => item.id !== id));
}}
/>
</MosaicProvider>
);
}
render(<Example />);
for (const id of ['ada', 'grace']) {
await user.click(screen.getByRole('button', { name: `${action} ${id}@example.com` }));
expect(onDecision).toHaveBeenLastCalledWith(id);
expect(onOtherDecision).not.toHaveBeenCalled();
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument();
await waitFor(() =>
expect(
id === 'ada'
? screen.getByRole('button', { name: `${action} grace@example.com` })
: screen.getByRole('searchbox'),
).toHaveFocus(),
);
}
},
);
it.each(['Accept', 'Decline'] as const)('runs %s immediately as requests leave the list', async action => {
const user = userEvent.setup();
const onAccept = vi.fn<(id: string) => Promise<void>>().mockResolvedValue(undefined);
const onDecline = vi.fn<(id: string) => Promise<void>>().mockResolvedValue(undefined);
const onDecision = action === 'Accept' ? onAccept : onDecline;
const onOtherDecision = action === 'Accept' ? onDecline : onAccept;
function Example() {
const [items, setItems] = useState([
{ id: 'ada', email: 'ada@example.com', requestedAtLabel: 'Sep 1, 2026' },
{ id: 'grace', email: 'grace@example.com', requestedAtLabel: 'Sep 2, 2026' },
]);
return (
<MosaicProvider>
<RequestsTableTabView
{...propsFor()}
requests={items}
totalCount={items.length}
onAccept={async id => {
await onAccept(id);
setItems(current => current.filter(item => item.id !== id));
}}
onDecline={async id => {
await onDecline(id);
setItems(current => current.filter(item => item.id !== id));
}}
/>
</MosaicProvider>
);
}
render(<Example />);
for (const id of ['ada', 'grace']) {
await user.click(screen.getByRole('button', { name: `${action} ${id}@example.com` }));
expect(onDecision).toHaveBeenLastCalledWith(id);
expect(onOtherDecision).not.toHaveBeenCalled();
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument();
await waitFor(() =>
expect(screen.queryByRole('button', { name: `${action} ${id}@example.com` })).not.toBeInTheDocument(),
);
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,6 @@ describe('OrganizationProfileGeneralPanel', () => {
expect(within(dialog).getByText('Name')).toHaveAttribute('data-visually-hidden', '');
await user.click(within(dialog).getByRole('button', { name: 'Cancel' }));
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
await waitFor(() => expect(trigger).toHaveFocus());
});

it('requires a nonblank changed name before saving', async () => {
Expand Down Expand Up @@ -218,7 +217,6 @@ describe('OrganizationProfileGeneralPanel', () => {
await user.type(field, '-draft');
await user.click(within(dialog).getByRole('button', { name: 'Cancel' }));
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
await waitFor(() => expect(trigger).toHaveFocus());
await user.click(trigger);
expect(within(screen.getByRole('dialog')).getByRole('textbox', { name: 'Slug' })).toHaveValue('acme');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,33 @@ import { MosaicProvider } from '../../../mosaic-provider';
import { UserProfileProfilePanelView } from '../user-profile-profile-panel.view';

describe('profile add email', () => {
it.each([false, true])(
'owns the dialog and returns focus with multiple accounts = %s',
async allowMultipleAccounts => {
const user = userEvent.setup();
const onSend = vi.fn(() => Promise.resolve());
const onVerify = vi.fn(() => Promise.resolve());
render(
<MosaicProvider>
<UserProfileProfilePanelView
allowMultipleAccounts={allowMultipleAccounts}
name='Test'
username='test'
emails={[]}
phones={[]}
onSendEmailCode={onSend}
onVerifyEmailCode={onVerify}
/>
</MosaicProvider>,
);
const trigger = screen.getByRole('button', { name: 'Add email' });
await user.click(trigger);
expect(screen.getByRole('dialog', { name: 'Add email' })).toBeInTheDocument();
await user.type(screen.getByRole('textbox', { name: 'Email' }), 'new@example.com');
await user.click(screen.getByRole('button', { name: 'Send code' }));
const codeInput = await screen.findByRole('textbox', { name: 'Verification code' });
await waitFor(() => expect(codeInput).toHaveFocus());
await user.keyboard('123456');
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
expect(onSend).toHaveBeenCalledExactlyOnceWith('new@example.com');
expect(onVerify).toHaveBeenCalledExactlyOnceWith('new@example.com', '123456');
await waitFor(() => expect(trigger).toHaveFocus());
},
);
it.each([false, true])('owns the dialog with multiple accounts = %s', async allowMultipleAccounts => {
const user = userEvent.setup();
const onSend = vi.fn(() => Promise.resolve());
const onVerify = vi.fn(() => Promise.resolve());
render(
<MosaicProvider>
<UserProfileProfilePanelView
allowMultipleAccounts={allowMultipleAccounts}
name='Test'
username='test'
emails={[]}
phones={[]}
onSendEmailCode={onSend}
onVerifyEmailCode={onVerify}
/>
</MosaicProvider>,
);
const trigger = screen.getByRole('button', { name: 'Add email' });
await user.click(trigger);
expect(screen.getByRole('dialog', { name: 'Add email' })).toBeInTheDocument();
await user.type(screen.getByRole('textbox', { name: 'Email' }), 'new@example.com');
await user.click(screen.getByRole('button', { name: 'Send code' }));
const codeInput = await screen.findByRole('textbox', { name: 'Verification code' });
await waitFor(() => expect(codeInput).toHaveFocus());
await user.keyboard('123456');
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
expect(onSend).toHaveBeenCalledExactlyOnceWith('new@example.com');
expect(onVerify).toHaveBeenCalledExactlyOnceWith('new@example.com', '123456');
});
});
Loading
Loading