import type { EmailAddressJSON, EnterpriseAccountConnectionJSON } from '@clerk/shared/types';
import { act, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';
import {
demandReverificationOnce,
type FakeFapiSeed,
holdRequests,
serveFapi,
VERIFICATION_CODE,
verifyEmailOutOfBand,
} from '../../../../__tests__/feature/fake-fapi';
import {
fapiClient,
fapiEmailAddress,
fapiEnterpriseAccount,
fapiEnvironment,
fapiSession,
fapiUser,
fapiVerification,
} from '../../../../__tests__/feature/fapi';
import { openMenu } from '../../../../__tests__/feature/menu';
import { renderWithClerk } from '../../../../__tests__/feature/render';
import { UserProfileEmailSection } from '../user-profile-email-section';
const VERIFIED = fapiVerification('email_code', { status: 'verified' });
const PRIMARY = fapiEmailAddress({ id: 'idn_primary', email_address: 'alice@example.com', verification: VERIFIED });
function signedInWithEmails(emails: EmailAddressJSON[], environment = fapiEnvironment()): FakeFapiSeed {
const user = fapiUser({
id: 'user_1',
first_name: 'Alice',
last_name: 'Smith',
username: 'alicesmith',
email_addresses: emails,
primary_email_address_id: emails[0]?.id ?? null,
});
return { environment, client: fapiClient([fapiSession({ id: 'sess_1', user })]) };
}
const verifiesByLink = fapiEnvironment({ attributes: { email_address: { verifications: ['email_link'] } } });
async function renderSection(seed: FakeFapiSeed) {
const fapi = serveFapi(seed);
const view = await renderWithClerk();
return { ...view, fapi, actor: userEvent.setup() };
}
type Actor = ReturnType;
const emailRow = () => screen.getByRole('group', { name: 'Email' });
const emailsListed = () =>
within(emailRow())
.queryAllByText(/@/)
.map(node => node.textContent);
async function manageEmail(actor: Actor, label: string, action: string) {
await openMenu(actor, within(emailRow()).getByRole('button', { name: `Manage ${label}` }));
await actor.click(screen.getByRole('menuitem', { name: action }));
}
async function enterCode(actor: Actor, code: string) {
const input = await screen.findByRole('textbox', { name: 'Verification code' });
await waitFor(() => expect(input).not.toHaveAttribute('aria-disabled'));
await actor.click(input);
await actor.keyboard(code);
}
describe('the user profile email section', () => {
it('adds an address and verifies the code it was sent', async () => {
const { actor } = await renderSection(signedInWithEmails([]));
expect(emailRow()).toHaveTextContent('No email addresses added');
await actor.click(screen.getByRole('button', { name: 'Add email' }));
await actor.type(screen.getByRole('textbox', { name: 'Email' }), 'new@example.com');
await actor.click(screen.getByRole('button', { name: 'Continue' }));
await enterCode(actor, VERIFICATION_CODE);
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
expect(emailsListed()).toEqual(['new@example.com']);
expect(emailRow()).not.toHaveTextContent('Unverified');
});
it('holds the code field until the code has been sent', async () => {
const { actor } = await renderSection(signedInWithEmails([]));
const prepare = holdRequests('post', '/v1/me/email_addresses/:id/prepare_verification');
await actor.click(screen.getByRole('button', { name: 'Add email' }));
await actor.type(screen.getByRole('textbox', { name: 'Email' }), 'new@example.com');
await actor.click(screen.getByRole('button', { name: 'Continue' }));
await waitFor(() => expect(prepare.requests).toHaveLength(1));
expect(await screen.findByRole('textbox', { name: 'Verification code' })).toHaveAttribute('aria-disabled', 'true');
prepare.release();
await enterCode(actor, VERIFICATION_CODE);
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
expect(emailsListed()).toEqual(['new@example.com']);
});
it('verifies an address left unverified', async () => {
const pending = fapiEmailAddress({ id: 'idn_pending', email_address: 'pending@example.com' });
const { actor } = await renderSection(signedInWithEmails([PRIMARY, pending]));
expect(within(emailRow()).getByText('pending@example.com').parentElement).toHaveTextContent('Unverified');
await manageEmail(actor, 'pending@example.com', 'Verify');
await enterCode(actor, VERIFICATION_CODE);
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
await waitFor(() =>
expect(within(emailRow()).getByText('pending@example.com').parentElement).not.toHaveTextContent('Unverified'),
);
});
it('holds the menu open with a spinner on Verify until the code is sent', async () => {
const pending = fapiEmailAddress({ id: 'idn_pending', email_address: 'pending@example.com' });
const { actor } = await renderSection(signedInWithEmails([PRIMARY, pending]));
const prepare = holdRequests('post', '/v1/me/email_addresses/:id/prepare_verification');
try {
await manageEmail(actor, 'pending@example.com', 'Verify');
await waitFor(() => expect(prepare.requests).toHaveLength(1));
expect(screen.getByRole('menu')).toBeVisible();
expect(screen.getByRole('menuitem', { name: 'Verify' })).toHaveAttribute('aria-busy', 'true');
expect(await screen.findByRole('progressbar', { name: 'Verify' })).toBeVisible();
expect(screen.queryByRole('dialog')).toBeNull();
} finally {
prepare.release();
}
await enterCode(actor, VERIFICATION_CODE);
expect(screen.queryByRole('menu')).toBeNull();
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
});
it('waits for the emailed link when the instance verifies by link, then closes once it is opened', async () => {
const pending = fapiEmailAddress({ id: 'idn_pending', email_address: 'pending@example.com' });
const { actor, fapi } = await renderSection(signedInWithEmails([PRIMARY, pending], verifiesByLink));
const poll = holdRequests('get', '/v1/me/email_addresses/:id');
await manageEmail(actor, 'pending@example.com', 'Verify');
const dialog = await screen.findByRole('dialog', { name: 'Verify your email' });
await waitFor(() => expect(dialog).toHaveTextContent('Open the link we sent to pending@example.com'));
expect(screen.queryByRole('textbox', { name: 'Verification code' })).not.toBeInTheDocument();
await waitFor(() => expect(poll.requests).toHaveLength(1));
verifyEmailOutOfBand(fapi, 'idn_pending');
poll.release();
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
});
it('points the emailed link at the user profile on the host origin', async () => {
const pending = fapiEmailAddress({ id: 'idn_pending', email_address: 'pending@example.com' });
const { actor } = await renderSection(signedInWithEmails([PRIMARY, pending], verifiesByLink));
const prepare = holdRequests('post', '/v1/me/email_addresses/:id/prepare_verification');
await manageEmail(actor, 'pending@example.com', 'Verify');
await waitFor(() => expect(prepare.requests).toHaveLength(1));
const [request] = prepare.requests;
if (!request) {
throw new Error('expected a prepare_verification request');
}
const body = new URLSearchParams(await request.text());
expect(body.get('strategy')).toBe('email_link');
expect(body.get('redirect_url')).toBe(new URL('/user-profile#/verify', window.location.origin).href);
prepare.release();
});
it('leaves the address unverified when the link dialog is dismissed', async () => {
const pending = fapiEmailAddress({ id: 'idn_pending', email_address: 'pending@example.com' });
const { actor } = await renderSection(signedInWithEmails([PRIMARY, pending], verifiesByLink));
await manageEmail(actor, 'pending@example.com', 'Verify');
const dialog = await screen.findByRole('dialog', { name: 'Verify your email' });
await actor.click(within(dialog).getByRole('button', { name: 'Cancel' }));
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
expect(within(emailRow()).getByText('pending@example.com').parentElement).toHaveTextContent('Unverified');
});
it('stops checking for the opened link once the link dialog is dismissed', async () => {
const pending = fapiEmailAddress({ id: 'idn_pending', email_address: 'pending@example.com' });
const { actor } = await renderSection(signedInWithEmails([PRIMARY, pending], verifiesByLink));
const poll = holdRequests('get', '/v1/me/email_addresses/:id');
await manageEmail(actor, 'pending@example.com', 'Verify');
const dialog = await screen.findByRole('dialog', { name: 'Verify your email' });
await waitFor(() => expect(poll.requests).toHaveLength(1));
poll.release();
await act(() => new Promise(resolve => setTimeout(resolve, 100)));
await actor.click(within(dialog).getByRole('button', { name: 'Cancel' }));
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
await act(() => new Promise(resolve => setTimeout(resolve, 1500)));
expect(poll.requests).toHaveLength(1);
});
it('sends the user to the identity provider for an address that matches an SSO connection, even when the instance verifies by link', async () => {
const sso = fapiEmailAddress({
id: 'idn_sso',
email_address: 'alice@acme.co',
matches_sso_connection: true,
});
const { actor, clerk } = await renderSection(signedInWithEmails([PRIMARY, sso], verifiesByLink));
const windowNavigate = vi.spyOn(clerk, '__internal_windowNavigate').mockImplementation(() => {});
const prepare = holdRequests('post', '/v1/me/email_addresses/:id/prepare_verification');
await manageEmail(actor, 'alice@acme.co', 'Verify');
const dialog = await screen.findByRole('dialog', { name: 'Verify your email' });
expect(dialog).toHaveTextContent('acme.co');
expect(screen.queryByRole('textbox', { name: 'Verification code' })).not.toBeInTheDocument();
await actor.click(within(dialog).getByRole('button', { name: 'Connect' }));
await waitFor(() => expect(prepare.requests).toHaveLength(1));
const body = new URLSearchParams(await prepare.requests[0]?.text());
expect(body.get('strategy')).toBe('enterprise_sso');
expect(body.get('redirect_url')).toBe(window.location.href);
prepare.release();
await waitFor(() => expect(windowNavigate).toHaveBeenCalledWith(new URL('https://idp.acme.co/sso')));
});
it('does not offer to add the username as an address', async () => {
const { actor } = await renderSection(
signedInWithEmails([], fapiEnvironment({ attributes: { username: { enabled: true } } })),
);
await actor.click(screen.getByRole('button', { name: 'Add email' }));
await actor.type(screen.getByRole('textbox', { name: 'Email' }), 'alicesmith');
expect(screen.getByRole('button', { name: 'Continue' })).toBeDisabled();
});
it('is left out when the instance does not collect email addresses', async () => {
await renderSection(
signedInWithEmails(
[],
fapiEnvironment({
attributes: { email_address: { enabled: false, used_for_first_factor: false, first_factors: [] } },
}),
),
);
expect(screen.queryByRole('group', { name: 'Email' })).not.toBeInTheDocument();
});
it('is left out when it is immutable and has nothing to list', async () => {
await renderSection(
signedInWithEmails([], fapiEnvironment({ attributes: { email_address: { enabled: true, immutable: true } } })),
);
expect(screen.queryByRole('group', { name: 'Email' })).not.toBeInTheDocument();
});
it('lists addresses but does not let the user remove them when it is immutable', async () => {
const secondary = fapiEmailAddress({
id: 'idn_secondary',
email_address: 'work@example.com',
verification: VERIFIED,
});
const { actor } = await renderSection(
signedInWithEmails(
[PRIMARY, secondary],
fapiEnvironment({ attributes: { email_address: { enabled: true, immutable: true } } }),
),
);
expect(emailsListed()).toEqual(['alice@example.com', 'work@example.com']);
expect(screen.queryByRole('button', { name: 'Add email' })).not.toBeInTheDocument();
await openMenu(actor, within(emailRow()).getByRole('button', { name: 'Manage work@example.com' }));
expect(screen.getByRole('menuitem', { name: 'Set as primary' })).toBeInTheDocument();
expect(screen.queryByRole('menuitem', { name: 'Remove email' })).not.toBeInTheDocument();
});
});
describe('the email section for a user signed in through an enterprise connection', () => {
function signedInThroughSso(connection: Partial): FakeFapiSeed {
const user = fapiUser({
id: 'user_1',
email_addresses: [fapiEmailAddress({ id: 'idn_primary', email_address: 'alice@acme.co' })],
enterprise_accounts: [
fapiEnterpriseAccount({ id: 'eac_1', email_address: 'alice@acme.co' }, { name: 'Acme Corp', ...connection }),
],
});
return {
environment: fapiEnvironment({
user_settings: {
enterprise_sso: { enabled: true, self_serve_sso: false, self_serve_directory_sync: false },
},
}),
client: fapiClient([fapiSession({ id: 'sess_1', user })]),
};
}
it('stops the user adding an address when the connection disables additional identifications', async () => {
await renderSection(signedInThroughSso({ disable_additional_identifications: true }));
expect(await screen.findByText('alice@acme.co')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Add email' })).not.toBeInTheDocument();
});
it('still lets the user add an address when the connection allows it', async () => {
await renderSection(signedInThroughSso({ disable_additional_identifications: false }));
expect(await screen.findByRole('button', { name: 'Add email' })).toBeInTheDocument();
});
});
describe('email reverification', () => {
it('sets a verified address as primary without a challenge when none is required', async () => {
const second = fapiEmailAddress({ id: 'idn_second', email_address: 'bob@example.com', verification: VERIFIED });
const { actor, fapi } = await renderSection(signedInWithEmails([PRIMARY, second]));
await manageEmail(actor, 'bob@example.com', 'Set as primary');
await waitFor(() => expect(fapi.client.sessions[0]?.user?.primary_email_address_id).toBe('idn_second'));
expect(screen.queryByRole('dialog')).toBeNull();
});
it('verifies the session before setting an address as primary and returns focus to its menu', async () => {
const second = fapiEmailAddress({ id: 'idn_second', email_address: 'bob@example.com', verification: VERIFIED });
const { actor, fapi } = await renderSection({
...signedInWithEmails([PRIMARY, second]),
verification: { secrets: { password: 'hunter2' } },
});
demandReverificationOnce('/v1/me');
await manageEmail(actor, 'bob@example.com', 'Set as primary');
expect(await screen.findByText('Enter your current password to continue')).toBeVisible();
await waitFor(() => expect(screen.getByLabelText('Password')).toHaveFocus());
await actor.keyboard('hunter2{Enter}');
await waitFor(() => expect(fapi.client.sessions[0]?.user?.primary_email_address_id).toBe('idn_second'));
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
expect(within(emailRow()).getByRole('button', { name: 'Manage bob@example.com' })).toHaveFocus();
});
it('lets the user cancel verification quietly and retry the primary action', async () => {
const second = fapiEmailAddress({ id: 'idn_second', email_address: 'bob@example.com', verification: VERIFIED });
const { actor, fapi } = await renderSection({
...signedInWithEmails([PRIMARY, second]),
verification: { secrets: { password: 'hunter2' } },
});
demandReverificationOnce('/v1/me');
await manageEmail(actor, 'bob@example.com', 'Set as primary');
await screen.findByText('Enter your current password to continue');
const dialog = screen.getByRole('dialog');
await actor.click(within(dialog).getByRole('button', { name: 'Close' }));
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
expect(fapi.client.sessions[0]?.user?.primary_email_address_id).toBe('idn_primary');
expect(within(emailRow()).queryByRole('alert')).toBeNull();
expect(within(emailRow()).getByRole('button', { name: 'Manage bob@example.com' })).toHaveFocus();
await manageEmail(actor, 'bob@example.com', 'Set as primary');
await waitFor(() => expect(fapi.client.sessions[0]?.user?.primary_email_address_id).toBe('idn_second'));
});
it('keeps the verification dialog open while retrying and reports an ordinary retry error', async () => {
const second = fapiEmailAddress({ id: 'idn_second', email_address: 'bob@example.com', verification: VERIFIED });
const { actor, fapi } = await renderSection({
...signedInWithEmails([PRIMARY, second]),
verification: { secrets: { password: 'hunter2' } },
});
demandReverificationOnce('/v1/me');
await manageEmail(actor, 'bob@example.com', 'Set as primary');
await screen.findByText('Enter your current password to continue');
const dialog = screen.getByRole('dialog');
const replay = holdRequests('post', '/v1/me');
await waitFor(() => expect(screen.getByLabelText('Password')).toHaveFocus());
await actor.keyboard('hunter2{Enter}');
await waitFor(() => expect(replay.requests).toHaveLength(1));
expect(within(dialog).queryByRole('button', { name: 'Close' })).toBeNull();
await actor.keyboard('{Escape}');
expect(dialog).toBeVisible();
expect(fapi.client.sessions[0]?.user?.primary_email_address_id).toBe('idn_primary');
replay.fail('action_blocked', 'Unable to set primary address.');
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
await waitFor(() =>
expect(within(emailRow()).getByRole('alert')).toHaveTextContent("This action couldn't be completed."),
);
});
it('waits for a verification factor before opening the dialog', async () => {
const second = fapiEmailAddress({ id: 'idn_second', email_address: 'bob@example.com', verification: VERIFIED });
const { actor } = await renderSection({
...signedInWithEmails([PRIMARY, second]),
verification: { secrets: { password: 'hunter2' } },
});
demandReverificationOnce('/v1/me');
const start = holdRequests('post', '/v1/client/sessions/:id/verify');
await manageEmail(actor, 'bob@example.com', 'Set as primary');
await waitFor(() => expect(start.requests).toHaveLength(1));
expect(screen.queryByRole('dialog')).toBeNull();
start.release();
expect(await screen.findByText('Enter your current password to continue')).toBeVisible();
});
it('focuses Close when the dialog opens on a verification that could not start', async () => {
const second = fapiEmailAddress({ id: 'idn_second', email_address: 'bob@example.com', verification: VERIFIED });
const { actor } = await renderSection({
...signedInWithEmails([PRIMARY, second]),
verification: { secrets: { password: 'hunter2' } },
});
demandReverificationOnce('/v1/me');
const start = holdRequests('post', '/v1/client/sessions/:id/verify');
await manageEmail(actor, 'bob@example.com', 'Set as primary');
await waitFor(() => expect(start.requests).toHaveLength(1));
start.fail('verification_failed', 'Unable to start verification.');
const dialog = await screen.findByRole('dialog');
await waitFor(() => expect(within(dialog).getAllByRole('button', { name: 'Close' })[0]).toHaveFocus());
});
const challengedSeed = (environment = fapiEnvironment()): FakeFapiSeed => ({
...signedInWithEmails([], environment),
verification: { secrets: { password: 'hunter2' }, firstFactors: [{ strategy: 'password' }] },
});
async function beginAdd(actor: Actor, address = 'new@example.com') {
await actor.click(screen.getByRole('button', { name: 'Add email' }));
await actor.type(screen.getByRole('textbox', { name: 'Email' }), address);
await actor.click(screen.getByRole('button', { name: 'Continue' }));
}
it('confirms it is the user before an email address is added, then verifies its code', async () => {
const { actor, fapi } = await renderSection(challengedSeed());
demandReverificationOnce('/v1/me/email_addresses');
await beginAdd(actor);
expect(await screen.findByText('Enter your current password to continue')).toBeVisible();
expect(fapi.client.sessions[0]?.user.email_addresses).toHaveLength(0);
await waitFor(() => expect(screen.getByLabelText('Password')).toHaveFocus());
await actor.keyboard('hunter2{Enter}');
await enterCode(actor, VERIFICATION_CODE);
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
expect(emailsListed()).toEqual(['new@example.com']);
});
it('focuses the session verification code before adding an email address', async () => {
const { actor, fapi } = await renderSection({
...signedInWithEmails([PRIMARY]),
verification: {
firstFactors: [
{ strategy: 'email_code', email_address_id: PRIMARY.id, safe_identifier: PRIMARY.email_address },
],
secrets: { email_code: VERIFICATION_CODE },
},
});
demandReverificationOnce('/v1/me/email_addresses');
await beginAdd(actor);
const code = await screen.findByRole('textbox', { name: 'Verification code' });
await waitFor(() => expect(code).toHaveFocus());
await waitFor(() => expect(fapi.verification.firstFactorVerification?.strategy).toBe('email_code'));
await actor.keyboard(VERIFICATION_CODE);
await screen.findByRole('dialog', { name: 'Verify your email' });
await enterCode(actor, VERIFICATION_CODE);
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
expect(emailsListed()).toEqual(['alice@example.com', 'new@example.com']);
});
it('closes quietly when confirmation is dismissed and starts clean on reopen', async () => {
const { actor, fapi } = await renderSection(challengedSeed());
demandReverificationOnce('/v1/me/email_addresses');
await beginAdd(actor);
await screen.findByPlaceholderText('Enter your password');
await actor.keyboard('{Escape}');
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
expect(fapi.client.sessions[0]?.user.email_addresses).toHaveLength(0);
await actor.click(screen.getByRole('button', { name: 'Add email' }));
expect(screen.getByRole('textbox', { name: 'Email' })).toHaveValue('');
await actor.type(screen.getByRole('textbox', { name: 'Email' }), 'retry@example.com');
await actor.click(screen.getByRole('button', { name: 'Continue' }));
await enterCode(actor, VERIFICATION_CODE);
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
expect(emailsListed()).toEqual(['retry@example.com']);
});
it('continues to the emailed link after confirming the session', async () => {
const { actor, fapi } = await renderSection(challengedSeed(verifiesByLink));
const poll = holdRequests('get', '/v1/me/email_addresses/:id');
demandReverificationOnce('/v1/me/email_addresses');
await beginAdd(actor);
await actor.type(await screen.findByPlaceholderText('Enter your password'), 'hunter2{Enter}');
const dialog = await screen.findByRole('dialog', { name: 'Verify your email' });
await waitFor(() => expect(dialog).toHaveTextContent('Open the link we sent to new@example.com'));
expect(screen.queryByRole('textbox', { name: 'Verification code' })).not.toBeInTheDocument();
await waitFor(() => expect(poll.requests).toHaveLength(1));
const added = fapi.client.sessions[0]?.user.email_addresses[0];
if (!added) {
throw new Error('Expected the new email address');
}
verifyEmailOutOfBand(fapi, added.id);
poll.release();
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
expect(emailsListed()).toEqual(['new@example.com']);
});
it('keeps an initial create pending through duplicate submit and dismissal attempts', async () => {
const { actor } = await renderSection(signedInWithEmails([]));
const create = holdRequests('post', '/v1/me/email_addresses');
await beginAdd(actor);
await waitFor(() => expect(create.requests).toHaveLength(1));
expect(screen.getByRole('textbox', { name: 'Email' })).toHaveAttribute('aria-disabled', 'true');
await actor.keyboard('{Escape}{Enter}');
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(create.requests).toHaveLength(1);
create.release();
await enterCode(actor, VERIFICATION_CODE);
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
expect(emailsListed()).toEqual(['new@example.com']);
});
it('allows cancellation while the session challenge loads', async () => {
const { actor, fapi } = await renderSection(challengedSeed());
const start = holdRequests('post', '/v1/client/sessions/sess_1/verify');
demandReverificationOnce('/v1/me/email_addresses');
await beginAdd(actor);
await waitFor(() => expect(start.requests).toHaveLength(1));
await actor.keyboard('{Escape}');
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
expect(fapi.client.sessions[0]?.user.email_addresses).toHaveLength(0);
start.release();
});
it('keeps the retried create locked and returns the email field error without a duplicate', async () => {
const { actor, fapi } = await renderSection(challengedSeed());
const retry = holdRequests('post', '/v1/me/email_addresses');
demandReverificationOnce('/v1/me/email_addresses');
await beginAdd(actor);
await actor.type(await screen.findByPlaceholderText('Enter your password'), 'hunter2{Enter}');
await waitFor(() => expect(retry.requests).toHaveLength(1));
await actor.keyboard('{Escape}{Enter}');
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(retry.requests).toHaveLength(1);
retry.fail('form_param_invalid', 'That email is taken.', 'email_address');
const email = await screen.findByRole('textbox', { name: 'Email' });
expect(email).toHaveValue('new@example.com');
await waitFor(() => expect(email).toHaveAccessibleDescription('That email is taken.'));
expect(fapi.client.sessions[0]?.user.email_addresses).toHaveLength(0);
});
it('focuses the email field when returning to the form after verifying', async () => {
const { actor } = await renderSection(challengedSeed());
const retry = holdRequests('post', '/v1/me/email_addresses');
demandReverificationOnce('/v1/me/email_addresses');
await beginAdd(actor);
await actor.type(await screen.findByPlaceholderText('Enter your password'), 'hunter2{Enter}');
await waitFor(() => expect(retry.requests).toHaveLength(1));
retry.fail('form_param_invalid', 'That email is taken.', 'email_address');
const email = await screen.findByRole('textbox', { name: 'Email' });
await waitFor(() => expect(email).toHaveFocus());
});
it('cancels an in-progress verification when the active session changes', async () => {
const seed = challengedSeed();
const { actor, fapi, clerk } = await renderSection({
...seed,
client: fapiClient([
...seed.client.sessions,
fapiSession({ id: 'sess_2', user: fapiUser({ id: 'user_2', username: 'bob' }) }),
]),
});
demandReverificationOnce('/v1/me/email_addresses');
await beginAdd(actor);
await screen.findByPlaceholderText('Enter your password');
await act(() => clerk.setActive({ session: 'sess_2' }));
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
expect(fapi.client.sessions.map(session => session.user.email_addresses)).toEqual([[], []]);
});
it('closes the host from the challenge error card', async () => {
const { actor, fapi } = await renderSection(challengedSeed());
holdRequests('post', '/v1/client/sessions/sess_1/verify').fail('some_new_error', 'Nope');
demandReverificationOnce('/v1/me/email_addresses');
await beginAdd(actor);
expect(await screen.findByText('Something went wrong')).toBeVisible();
const buttons = screen.getAllByRole('button', { name: 'Close' });
const close = buttons.at(-1);
if (!close) {
throw new Error('Expected a close button');
}
await actor.click(close);
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
expect(fapi.client.sessions[0]?.user.email_addresses).toHaveLength(0);
});
});
describe('Removing an address', () => {
it('verifies the session when the removal demands it, then removes the address', async () => {
const second = fapiEmailAddress({ id: 'idn_second', email_address: 'bob@example.com', verification: VERIFIED });
const { actor } = await renderSection({
...signedInWithEmails([PRIMARY, second]),
verification: { secrets: { password: 'hunter2' } },
});
demandReverificationOnce('/v1/me/email_addresses/idn_second');
await manageEmail(actor, 'bob@example.com', 'Remove email');
const dialog = await screen.findByRole('alertdialog', { name: 'Remove email address?' });
await actor.click(within(dialog).getByRole('button', { name: 'Remove' }));
expect(await screen.findByText('Enter your current password to continue')).toBeVisible();
await actor.type(screen.getByLabelText('Password'), 'hunter2{Enter}');
await waitFor(() => expect(emailsListed()).toEqual(['alice@example.com']));
expect(screen.queryByRole('alertdialog')).toBeNull();
});
});