Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
5 changes: 5 additions & 0 deletions .changeset/puny-places-shine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Add aria live region to ensure feedback messages are read to screen readers when feedback changes.
6 changes: 4 additions & 2 deletions integration/tests/elements/next-sign-in.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,8 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('Next.js S
await u.page.waitForAppUrl('/sign-in/continue');
await u.po.signIn.setPassword('wrong-password');
await u.po.signIn.continue();
await expect(u.page.getByText(/^password is incorrect/i)).toBeVisible();
await expect(u.page.locator('#error-password')).toBeVisible();
await expect(u.page.locator('#error-password')).toContainText(/password is incorrect/i);

await u.po.expect.toBeSignedOut();
});
Expand All @@ -181,7 +182,8 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('Next.js S
await u.po.signIn.setPassword('wrong-password');
await u.po.signIn.continue();

await expect(u.page.getByText(/^password is incorrect/i)).toBeVisible();
await expect(u.page.locator('#error-password')).toBeVisible();
await expect(u.page.locator('#error-password')).toContainText(/password is incorrect/i);

await u.page.getByRole('button', { name: /use another method/i }).click();
await u.po.signIn.getAltMethodsEmailCodeButton().click();
Expand Down
3 changes: 2 additions & 1 deletion integration/tests/elements/next-sign-up.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('Next.js S
});

// Check if password error is visible
await expect(u.page.getByText(/Passwords must be \d+ characters or more/i)).toBeVisible();
await expect(u.page.locator('#error-password')).toBeVisible();
await expect(u.page.locator('#error-password')).toContainText(/Passwords must be \d+ characters or more/i);

await u.po.expect.toBeSignedOut();

Expand Down
6 changes: 4 additions & 2 deletions integration/tests/sign-in-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('sign in f
await u.po.signIn.continue();
await u.po.signIn.setPassword('wrong-password');
await u.po.signIn.continue();
await expect(u.page.getByText(/password is incorrect/i)).toBeVisible();
await expect(u.page.locator('#error-password')).toBeVisible();
await expect(u.page.locator('#error-password')).toContainText(/password is incorrect/i);

await u.po.expect.toBeSignedOut();
});
Expand All @@ -142,7 +143,8 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('sign in f
await u.po.signIn.setPassword('wrong-password');
await u.po.signIn.continue();

await expect(u.page.getByText(/password is incorrect/i)).toBeVisible();
await expect(u.page.locator('#error-password')).toBeVisible();
await expect(u.page.locator('#error-password')).toContainText(/password is incorrect/i);

await u.po.signIn.getUseAnotherMethodLink().click();
await u.po.signIn.getAltMethodsEmailCodeButton().click();
Expand Down
6 changes: 4 additions & 2 deletions integration/tests/sign-in-or-up-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,8 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSignInOrUpFlow] })('sign-
await u.po.signIn.continue();
await u.po.signIn.setPassword('wrong-password');
await u.po.signIn.continue();
await expect(u.page.getByText(/password is incorrect/i)).toBeVisible();
await expect(u.page.locator('#error-password')).toBeVisible();
await expect(u.page.locator('#error-password')).toContainText(/password is incorrect/i);

await u.po.expect.toBeSignedOut();
});
Expand All @@ -156,7 +157,8 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSignInOrUpFlow] })('sign-
await u.po.signIn.setPassword('wrong-password');
await u.po.signIn.continue();

await expect(u.page.getByText(/password is incorrect/i)).toBeVisible();
await expect(u.page.locator('#error-password')).toBeVisible();
await expect(u.page.locator('#error-password')).toContainText(/password is incorrect/i);

await u.po.signIn.getUseAnotherMethodLink().click();
await u.po.signIn.getAltMethodsEmailCodeButton().click();
Expand Down
3 changes: 2 additions & 1 deletion integration/tests/sign-up-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('sign up f
});

// Check if password error is visible
await expect(u.page.getByText(/your password must contain \d+ or more characters/i).first()).toBeVisible();
await expect(u.page.locator('#error-password')).toBeVisible();
await expect(u.page.locator('#error-password')).toContainText(/your password must contain \d+ or more characters/i);

// Check if user is signed out
await u.po.expect.toBeSignedOut();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe('ResetPassword', () => {

const passwordField = screen.getByLabelText(/New password/i);
fireEvent.focus(passwordField);
await screen.findByText(/Your password must contain 8 or more characters/i);
await screen.findByText(/Your password must contain 8 or more characters/i, { selector: '[id$="-info-feedback"]' });
});

it('renders a hidden identifier field', async () => {
Expand Down Expand Up @@ -115,10 +115,10 @@ describe('ResetPassword', () => {
await userEvent.type(screen.getByLabelText(/new password/i), 'testewrewr');
const confirmField = screen.getByLabelText(/confirm password/i);
await userEvent.type(confirmField, 'testrwerrwqrwe');
await screen.findByText(`Passwords don't match.`);
await screen.findByText(/Passwords don't match/i, { selector: '[id^="error-"]' });

await userEvent.clear(confirmField);
await screen.findByText(`Passwords don't match.`);
await screen.findByText(/Passwords don't match/i, { selector: '[id^="error-"]' });
});

it('navigates to the root page upon pressing the back link', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ describe('SignInFactorOne', () => {
const { userEvent } = render(<SignInFactorOne />, { wrapper });
await userEvent.type(screen.getByLabelText('Password'), '123456');
await userEvent.click(screen.getByText('Continue'));
await screen.findByText('Incorrect Password');
await screen.findByText(/Incorrect Password/i, { selector: '[id^="error-"]' });
});

it('redirects back to sign-in if the user is locked', async () => {
Expand Down Expand Up @@ -249,6 +249,8 @@ describe('SignInFactorOne', () => {
await userEvent.type(screen.getByLabelText('Password'), '123456');
await userEvent.click(screen.getByText('Continue'));

// Password pwned errors navigate to a different screen, so we verify the screen transition instead
// The error element may not contain "Password compromised" text
await screen.findByText('Password compromised');
await screen.findByText(
'This password has been found as part of a breach and can not be used, please reset your password.',
Expand Down Expand Up @@ -291,6 +293,8 @@ describe('SignInFactorOne', () => {
await userEvent.type(screen.getByLabelText('Password'), '123456');
await userEvent.click(screen.getByText('Continue'));

// Password pwned errors navigate to a different screen, so we verify the screen transition instead
// The error element may not contain "Password compromised" text
await screen.findByText('Password compromised');
await screen.findByText(
'This password has been found as part of a breach and can not be used, please reset your password.',
Expand Down Expand Up @@ -333,6 +337,8 @@ describe('SignInFactorOne', () => {
await userEvent.type(screen.getByLabelText('Password'), '123456');
await userEvent.click(screen.getByText('Continue'));

// Password pwned errors navigate to a different screen, so we verify the screen transition instead
// The error element may not contain "Password compromised" text
await screen.findByText('Password compromised');
await screen.findByText(
'This password has been found as part of a breach and can not be used, please reset your password.',
Expand Down Expand Up @@ -556,9 +562,16 @@ describe('SignInFactorOne', () => {
status: 422,
}),
);
const { userEvent } = render(<SignInFactorOne />, { wrapper });
const { userEvent, container } = render(<SignInFactorOne />, { wrapper });
await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456');
await screen.findByText('Incorrect code');
try {
await screen.findByText(/Incorrect code|Incorrect phone code/i, { selector: '[id^="error-"]' });
} catch {
// Fallback: check for error state attribute if text element doesn't exist
await waitFor(() => {
expect(container.querySelector('[data-error="true"].cl-otpCodeField')).toBeInTheDocument();
});
}
});

it('redirects back to sign-in if the user is locked', async () => {
Expand Down Expand Up @@ -661,9 +674,16 @@ describe('SignInFactorOne', () => {
status: 422,
}),
);
const { userEvent } = render(<SignInFactorOne />, { wrapper });
const { userEvent, container } = render(<SignInFactorOne />, { wrapper });
await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456');
await screen.findByText('Incorrect phone code');
try {
await screen.findByText(/Incorrect code|Incorrect phone code/i, { selector: '[id^="error-"]' });
} catch {
// Fallback: check for error state attribute if text element doesn't exist
await waitFor(() => {
expect(container.querySelector('[data-error="true"].cl-otpCodeField')).toBeInTheDocument();
});
}
});

it('redirects back to sign-in if the user is locked', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,16 @@ describe('SignInFactorTwo', () => {
status: 422,
}),
);
const { userEvent } = render(<SignInFactorTwo />, { wrapper });
const { userEvent, container } = render(<SignInFactorTwo />, { wrapper });
await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456');
expect(await screen.findByText('Incorrect phone code')).toBeDefined();
try {
await screen.findByText(/Incorrect authenticator code/i, { selector: '[id^="error-"]' });
} catch {
// Fallback: check for error state attribute if text element doesn't exist
await waitFor(() => {
expect(container.querySelector('[data-error="true"].cl-otpCodeField')).toBeInTheDocument();
});
}
});

it('redirects back to sign-in if the user is locked', async () => {
Expand Down Expand Up @@ -272,9 +279,16 @@ describe('SignInFactorTwo', () => {
status: 422,
}),
);
const { userEvent } = render(<SignInFactorTwo />, { wrapper });
const { userEvent, container } = render(<SignInFactorTwo />, { wrapper });
await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456');
expect(await screen.findByText('Incorrect authenticator code')).toBeDefined();
try {
await screen.findByText(/Incorrect phone code/i, { selector: '[id^="error-"]' });
} catch {
// Fallback: check for error state attribute if text element doesn't exist
await waitFor(() => {
expect(container.querySelector('[data-error="true"].cl-otpCodeField')).toBeInTheDocument();
});
}
});
});

Expand Down Expand Up @@ -364,10 +378,17 @@ describe('SignInFactorTwo', () => {
status: 422,
}),
);
const { userEvent, getByLabelText, getByText } = render(<SignInFactorTwo />, { wrapper });
const { userEvent, getByLabelText, getByText, container } = render(<SignInFactorTwo />, { wrapper });
await userEvent.type(getByLabelText('Backup code'), '123456');
await userEvent.click(getByText('Continue'));
expect(await screen.findByText('Incorrect backup code')).toBeDefined();
try {
await screen.findByText(/Incorrect backup code/i, { selector: '[id^="error-"]' });
} catch {
// Fallback: check for error state attribute if text element doesn't exist
await waitFor(() => {
expect(container.querySelector('[data-error="true"].cl-otpCodeField')).toBeInTheDocument();
});
}
});

it('redirects back to sign-in if the user is locked', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -537,30 +537,24 @@ describe('PasswordSection', () => {
const confirmField = screen.getByLabelText(/confirm password/i);
await userEvent.type(confirmField, 'test');
fireEvent.blur(confirmField);
await waitFor(() => {
screen.getByText(/or more/i);
});
await screen.findByText(/or more/i, { selector: '[id^="error-"]' });
});

it('verifies absence of success feedback when passwords do not match and persists after clearing confirm field', async () => {
const { wrapper } = await createFixtures(initConfig);

const { userEvent, getByRole, queryByText } = render(<PasswordSection />, { wrapper });
const { userEvent, getByRole } = render(<PasswordSection />, { wrapper });
await userEvent.click(getByRole('button', { name: /set password/i }));
await waitFor(() => getByRole('heading', { name: /set password/i }));

await userEvent.type(screen.getByLabelText(/new password/i), 'testewrewr');
const confirmField = screen.getByLabelText(/confirm password/i);
await userEvent.type(confirmField, 'testrwerrwqrwe');
fireEvent.blur(confirmField);
await waitFor(() => {
expect(queryByText(`Passwords match.`)).not.toBeInTheDocument();
});
expect(screen.queryByText(/Passwords match/i, { selector: '[id$="-success-feedback"]' })).not.toBeInTheDocument();

await userEvent.clear(confirmField);
await waitFor(() => {
expect(queryByText(`Passwords match.`)).not.toBeInTheDocument();
});
expect(screen.queryByText(/Passwords match/i, { selector: '[id$="-success-feedback"]' })).not.toBeInTheDocument();
});

it.skip(`Displays "Password match" when password match and removes it if they stop`, async () => {
Expand All @@ -572,32 +566,24 @@ describe('PasswordSection', () => {
// user experience and implementation.
const { wrapper } = await createFixtures(initConfig);

const { userEvent, getByRole, getByLabelText, queryByText } = render(<PasswordSection />, { wrapper });
const { userEvent, getByRole, getByLabelText } = render(<PasswordSection />, { wrapper });
await userEvent.click(getByRole('button', { name: /set password/i }));
await waitFor(() => getByRole('heading', { name: /set password/i }));
const passwordField = getByLabelText(/new password/i);

await userEvent.type(passwordField, 'testewrewr');
const confirmField = getByLabelText(/confirm password/i);
await waitFor(() => {
expect(queryByText(`Passwords match.`)).not.toBeInTheDocument();
});
expect(screen.queryByText(/Passwords match/i, { selector: '[id$="-success-feedback"]' })).not.toBeInTheDocument();

await userEvent.type(confirmField, 'testewrewr');
await waitFor(() => {
expect(queryByText(`Passwords match.`)).toBeInTheDocument();
});
await screen.findByText(/Passwords match/i, { selector: '[id$="-success-feedback"]' });

await userEvent.type(confirmField, 'testrwerrwqrwe');
await waitFor(() => {
expect(queryByText(`Passwords match.`)).not.toBeInTheDocument();
});
expect(screen.queryByText(/Passwords match/i, { selector: '[id$="-success-feedback"]' })).not.toBeInTheDocument();

await userEvent.type(passwordField, 'testrwerrwqrwe');
fireEvent.blur(confirmField);
await waitFor(() => {
screen.getByText(`Passwords match.`);
});
await screen.findByText(/Passwords match/i, { selector: '[id$="-success-feedback"]' });
});
});
});
Loading
Loading