From 2e3f58c11013c7825a5e8da94234de182995e0ab Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 31 Oct 2025 10:51:53 -0400 Subject: [PATCH 01/11] fix(clerk-js): Ensure errors are being read to screen readers --- .../clerk-js/src/ui/elements/FormControl.tsx | 79 +++++++++++-------- .../ui/elements/__tests__/PlainInput.test.tsx | 33 +++++--- 2 files changed, 68 insertions(+), 44 deletions(-) diff --git a/packages/clerk-js/src/ui/elements/FormControl.tsx b/packages/clerk-js/src/ui/elements/FormControl.tsx index d94887dfddd..ac4e3e99a19 100644 --- a/packages/clerk-js/src/ui/elements/FormControl.tsx +++ b/packages/clerk-js/src/ui/elements/FormControl.tsx @@ -9,12 +9,13 @@ import { FormInfoText, FormSuccessText, FormWarningText, + Span, useAppearance, } from '../customizables'; import type { ElementDescriptor } from '../customizables/elementDescriptors'; import { usePrefersReducedMotion } from '../hooks'; import type { ThemableCssProp } from '../styledSystem'; -import { animations } from '../styledSystem'; +import { animations, common } from '../styledSystem'; import type { FeedbackType, useFormControlFeedback } from '../utils/useFormControl'; function useFormTextAnimation() { @@ -161,38 +162,48 @@ export const FormFeedback = (props: FormFeedbackProps) => { const InfoComponentB = FormInfoComponent[feedbacks.b?.feedbackType || 'info']; return ( - - ({ - visibility: feedbacks.a?.shouldEnter ? 'visible' : 'hidden', - }), - getFormTextAnimation(!!feedbacks.a?.shouldEnter, { inDelay: true }), - ]} - localizationKey={titleize(feedbacks.a?.feedback)} - aria-live={feedbacks.a?.shouldEnter ? 'polite' : 'off'} - /> - ({ - visibility: feedbacks.b?.shouldEnter ? 'visible' : 'hidden', - }), - getFormTextAnimation(!!feedbacks.b?.shouldEnter, { inDelay: true }), - ]} - localizationKey={titleize(feedbacks.b?.feedback)} - aria-live={feedbacks.b?.shouldEnter ? 'polite' : 'off'} - /> - + <> + {/* Screen reader only live region that updates when feedback changes */} + + {feedback ? titleize(feedback) : ''} + + + ({ + visibility: feedbacks.a?.shouldEnter ? 'visible' : 'hidden', + }), + getFormTextAnimation(!!feedbacks.a?.shouldEnter, { inDelay: true }), + ]} + localizationKey={titleize(feedbacks.a?.feedback)} + /> + ({ + visibility: feedbacks.b?.shouldEnter ? 'visible' : 'hidden', + }), + getFormTextAnimation(!!feedbacks.b?.shouldEnter, { inDelay: true }), + ]} + localizationKey={titleize(feedbacks.b?.feedback)} + /> + + ); }; diff --git a/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx b/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx index 6adaec9b8f2..5eaa5332c71 100644 --- a/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx +++ b/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx @@ -222,7 +222,7 @@ describe('PlainInput', () => { expect(successElement).toHaveTextContent(/Some Success/i); }); - it('aria-live attribute is correctly applied', async () => { + it('screen reader only live region announces feedback changes', async () => { const { wrapper } = await createFixtures(); const { Field } = createField('firstname', 'init value', { type: 'text', @@ -236,21 +236,34 @@ describe('PlainInput', () => { await userEvent.click(getByRole('button', { name: /set error/i })); expect(await findByText(/Some Error/i)).toBeInTheDocument(); - // Verify the visible error message has aria-live="polite" - const errorElement = container.querySelector('#error-firstname'); - expect(errorElement).toHaveAttribute('aria-live', 'polite'); + // Verify there's a screen-reader-only aria-live region with the error content + const ariaLiveRegions = container.querySelectorAll('[aria-live="polite"]'); + expect(ariaLiveRegions.length).toBeGreaterThanOrEqual(1); + + // Find the screen reader only region (it will have the visually hidden styles) + const srOnlyRegion = Array.from(ariaLiveRegions).find(el => { + const style = window.getComputedStyle(el); + return style.position === 'absolute' && style.width === '1px'; + }); + expect(srOnlyRegion).toBeDefined(); + expect(srOnlyRegion).toHaveTextContent(/Some Error/i); // Transition to success await userEvent.click(getByRole('button', { name: /set success/i })); expect(await findByText(/Some Success/i)).toBeInTheDocument(); - // Verify the visible success message has aria-live="polite" + // Verify the screen reader only region updated its content + expect(srOnlyRegion).toHaveTextContent(/Some Success/i); + + // Verify the visible error/success elements exist with proper IDs for aria-describedby + const errorElement = container.querySelector('#error-firstname'); const successElement = container.querySelector('#firstname-success-feedback'); - expect(successElement).toHaveAttribute('aria-live', 'polite'); - // The previous error message should now have aria-live="off" (though it might still exist in DOM but hidden) - // Verify exactly one element has aria-live="polite" at a time - const allAriaLivePolite = container.querySelectorAll('[aria-live="polite"]'); - expect(allAriaLivePolite.length).toBe(1); + // One should be visible, the other hidden (for animation) + const errorVisible = errorElement && window.getComputedStyle(errorElement).visibility === 'visible'; + const successVisible = successElement && window.getComputedStyle(successElement).visibility === 'visible'; + + // At least one should be visible (might be both during transition) + expect(errorVisible || successVisible).toBe(true); }); }); From ab795fb28cd0acf76afa82a92545408c45c4a311 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 31 Oct 2025 11:10:12 -0400 Subject: [PATCH 02/11] add changeset --- .changeset/puny-places-shine.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/puny-places-shine.md diff --git a/.changeset/puny-places-shine.md b/.changeset/puny-places-shine.md new file mode 100644 index 00000000000..9f221c753b6 --- /dev/null +++ b/.changeset/puny-places-shine.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Add aria live region to ensure feedback messages are read to screen readers when feedback changes. From b3ea687014e574c013bf1ed75787bc901b430d6e Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 4 Nov 2025 10:06:13 -0500 Subject: [PATCH 03/11] assert empty aria region --- .../ui/elements/__tests__/PlainInput.test.tsx | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx b/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx index 5eaa5332c71..595e56c4b30 100644 --- a/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx +++ b/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx @@ -139,7 +139,7 @@ describe('PlainInput', () => { await userEvent.click(getByRole('button', { name: /set error/i })); - expect(await findByText(/Some Error/i)).toBeInTheDocument(); + expect(await findByText(/Some Error/i, { selector: '#error-firstname' })).toBeInTheDocument(); const input = getByLabelText(/some label/i); expect(input).toHaveAttribute('aria-invalid', 'true'); @@ -163,7 +163,7 @@ describe('PlainInput', () => { const { findByLabelText, findByText } = render(, { wrapper }); fireEvent.focus(await findByLabelText(/some label/i)); - expect(await findByText(/some info/i)).toBeInTheDocument(); + expect(await findByText(/some info/i, { selector: '#firstname-info-feedback' })).toBeInTheDocument(); }); it('with success feedback and aria-describedby', async () => { @@ -178,7 +178,7 @@ describe('PlainInput', () => { await userEvent.click(getByRole('button', { name: /set success/i })); - expect(await findByText(/Some Success/i)).toBeInTheDocument(); + expect(await findByText(/Some Success/i, { selector: '#firstname-success-feedback' })).toBeInTheDocument(); const input = getByLabelText(/some label/i); expect(input).toHaveAttribute('aria-invalid', 'false'); @@ -202,7 +202,7 @@ describe('PlainInput', () => { // Start with error await userEvent.click(getByRole('button', { name: /set error/i })); - expect(await findByText(/Some Error/i)).toBeInTheDocument(); + expect(await findByText(/Some Error/i, { selector: '#error-firstname' })).toBeInTheDocument(); let input = getByLabelText(/some label/i); expect(input).toHaveAttribute('aria-invalid', 'true'); @@ -210,7 +210,7 @@ describe('PlainInput', () => { // Transition to success await userEvent.click(getByRole('button', { name: /set success/i })); - expect(await findByText(/Some Success/i)).toBeInTheDocument(); + expect(await findByText(/Some Success/i, { selector: '#firstname-success-feedback' })).toBeInTheDocument(); input = getByLabelText(/some label/i); expect(input).toHaveAttribute('aria-invalid', 'false'); @@ -232,9 +232,24 @@ describe('PlainInput', () => { const { getByRole, findByText, container } = render(, { wrapper }); + // Pre-state: aria-live region exists and is empty + const preRegions = container.querySelectorAll('[aria-live="polite"]'); + expect(preRegions.length).toBeGreaterThanOrEqual(1); + const preSrOnly = Array.from(preRegions).find(el => { + const style = window.getComputedStyle(el); + return style.position === 'absolute' && style.width === '1px'; + }); + expect(preSrOnly).toBeDefined(); + expect(preSrOnly?.textContent ?? '').toMatch(/^\s*$/); + + // Input is not in error and not described yet + const inputEl = container.querySelector('input#firstname-field'); + expect(inputEl).toHaveAttribute('aria-invalid', 'false'); + expect(inputEl).not.toHaveAttribute('aria-describedby'); + // Set error feedback await userEvent.click(getByRole('button', { name: /set error/i })); - expect(await findByText(/Some Error/i)).toBeInTheDocument(); + expect(await findByText(/Some Error/i, { selector: '#error-firstname' })).toBeInTheDocument(); // Verify there's a screen-reader-only aria-live region with the error content const ariaLiveRegions = container.querySelectorAll('[aria-live="polite"]'); @@ -250,7 +265,7 @@ describe('PlainInput', () => { // Transition to success await userEvent.click(getByRole('button', { name: /set success/i })); - expect(await findByText(/Some Success/i)).toBeInTheDocument(); + expect(await findByText(/Some Success/i, { selector: '#firstname-success-feedback' })).toBeInTheDocument(); // Verify the screen reader only region updated its content expect(srOnlyRegion).toHaveTextContent(/Some Success/i); From d770878afdea083faaa85f84795eeb4a0efe2c3a Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 5 Nov 2025 08:30:02 -0500 Subject: [PATCH 04/11] update tests selectors --- integration/tests/sign-in-flow.test.ts | 4 ++-- .../SignIn/__tests__/ResetPassword.test.tsx | 8 +++++--- .../SignIn/__tests__/SignInFactorOne.test.tsx | 12 ++++++------ .../SignIn/__tests__/SignInFactorTwo.test.tsx | 6 +++--- .../UserProfile/__tests__/PasswordSection.test.tsx | 2 +- .../src/ui/elements/__tests__/RadioGroup.test.tsx | 2 +- 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/integration/tests/sign-in-flow.test.ts b/integration/tests/sign-in-flow.test.ts index 20326de4204..68a80bbf844 100644 --- a/integration/tests/sign-in-flow.test.ts +++ b/integration/tests/sign-in-flow.test.ts @@ -128,7 +128,7 @@ 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 u.po.expect.toBeSignedOut(); }); @@ -142,7 +142,7 @@ 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 u.po.signIn.getUseAnotherMethodLink().click(); await u.po.signIn.getAltMethodsEmailCodeButton().click(); diff --git a/packages/clerk-js/src/ui/components/SignIn/__tests__/ResetPassword.test.tsx b/packages/clerk-js/src/ui/components/SignIn/__tests__/ResetPassword.test.tsx index f5778d8c918..5f880ac4030 100644 --- a/packages/clerk-js/src/ui/components/SignIn/__tests__/ResetPassword.test.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/__tests__/ResetPassword.test.tsx @@ -37,7 +37,9 @@ 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: '#newPassword-info-feedback', + }); }); it('renders a hidden identifier field', async () => { @@ -115,10 +117,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.`, { selector: '#error-confirmPassword' }); await userEvent.clear(confirmField); - await screen.findByText(`Passwords don't match.`); + await screen.findByText(`Passwords don't match.`, { selector: '#error-confirmPassword' }); }); it('navigates to the root page upon pressing the back link', async () => { diff --git a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx index d784f16ad3a..f8598a3b29d 100644 --- a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx @@ -186,7 +186,7 @@ describe('SignInFactorOne', () => { const { userEvent } = render(, { wrapper }); await userEvent.type(screen.getByLabelText('Password'), '123456'); await userEvent.click(screen.getByText('Continue')); - await screen.findByText('Incorrect Password'); + await screen.findByText('Incorrect Password', { selector: '#error-password' }); }); it('redirects back to sign-in if the user is locked', async () => { @@ -249,7 +249,7 @@ describe('SignInFactorOne', () => { await userEvent.type(screen.getByLabelText('Password'), '123456'); await userEvent.click(screen.getByText('Continue')); - await screen.findByText('Password compromised'); + await screen.findByText('Password compromised', { selector: '#error-password' }); await screen.findByText( 'This password has been found as part of a breach and can not be used, please reset your password.', ); @@ -291,7 +291,7 @@ describe('SignInFactorOne', () => { await userEvent.type(screen.getByLabelText('Password'), '123456'); await userEvent.click(screen.getByText('Continue')); - await screen.findByText('Password compromised'); + await screen.findByText('Password compromised', { selector: '#error-password' }); await screen.findByText( 'This password has been found as part of a breach and can not be used, please reset your password.', ); @@ -333,7 +333,7 @@ describe('SignInFactorOne', () => { await userEvent.type(screen.getByLabelText('Password'), '123456'); await userEvent.click(screen.getByText('Continue')); - await screen.findByText('Password compromised'); + await screen.findByText('Password compromised', { selector: '#error-password' }); await screen.findByText( 'This password has been found as part of a breach and can not be used, please reset your password.', ); @@ -558,7 +558,7 @@ describe('SignInFactorOne', () => { ); const { userEvent } = render(, { wrapper }); await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456'); - await screen.findByText('Incorrect code'); + await screen.findByText('Incorrect code', { selector: '#error-code' }); }); it('redirects back to sign-in if the user is locked', async () => { @@ -663,7 +663,7 @@ describe('SignInFactorOne', () => { ); const { userEvent } = render(, { wrapper }); await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456'); - await screen.findByText('Incorrect phone code'); + await screen.findByText('Incorrect phone code', { selector: '#error-code' }); }); it('redirects back to sign-in if the user is locked', async () => { diff --git a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorTwo.test.tsx b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorTwo.test.tsx index 019e6324047..193cf5f50d8 100644 --- a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorTwo.test.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorTwo.test.tsx @@ -185,7 +185,7 @@ describe('SignInFactorTwo', () => { ); const { userEvent } = render(, { wrapper }); await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456'); - expect(await screen.findByText('Incorrect phone code')).toBeDefined(); + expect(await screen.findByText('Incorrect phone code', { selector: '#error-code' })).toBeDefined(); }); it('redirects back to sign-in if the user is locked', async () => { @@ -274,7 +274,7 @@ describe('SignInFactorTwo', () => { ); const { userEvent } = render(, { wrapper }); await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456'); - expect(await screen.findByText('Incorrect authenticator code')).toBeDefined(); + expect(await screen.findByText('Incorrect authenticator code', { selector: '#error-code' })).toBeDefined(); }); }); @@ -367,7 +367,7 @@ describe('SignInFactorTwo', () => { const { userEvent, getByLabelText, getByText } = render(, { wrapper }); await userEvent.type(getByLabelText('Backup code'), '123456'); await userEvent.click(getByText('Continue')); - expect(await screen.findByText('Incorrect backup code')).toBeDefined(); + expect(await screen.findByText('Incorrect backup code', { selector: '#error-code' })).toBeDefined(); }); it('redirects back to sign-in if the user is locked', async () => { diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx index f23eaff6692..cd633d93719 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx @@ -538,7 +538,7 @@ describe('PasswordSection', () => { await userEvent.type(confirmField, 'test'); fireEvent.blur(confirmField); await waitFor(() => { - screen.getByText(/or more/i); + screen.getByText(/or more/i, { selector: '#error-newPassword' }); }); }); diff --git a/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx b/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx index ad56cae4364..3bb8ab46055 100644 --- a/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx +++ b/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx @@ -168,7 +168,7 @@ describe('RadioGroup', () => { ); await userEvent.click(getByRole('button', { name: /set error/i })); - expect(await findByText(/Some Error/i)).toBeInTheDocument(); + expect(await findByText(/Some Error/i, { selector: '#error-some-radio' })).toBeInTheDocument(); const radios = getAllByRole('radio'); radios.forEach(radio => { From ab5466eb82c85011fa8475a7fea4a594b2c9b628 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 5 Nov 2025 15:53:01 -0500 Subject: [PATCH 05/11] update selectors --- .../tests/elements/next-sign-in.test.ts | 6 ++-- .../tests/elements/next-sign-up.test.ts | 3 +- integration/tests/sign-in-flow.test.ts | 2 ++ integration/tests/sign-in-or-up-flow.test.ts | 6 ++-- integration/tests/sign-up-flow.test.ts | 3 +- .../SignIn/__tests__/ResetPassword.test.tsx | 8 ++--- .../SignIn/__tests__/SignInFactorOne.test.tsx | 36 ++++++++++++++----- .../SignIn/__tests__/SignInFactorTwo.test.tsx | 33 +++++++++++++---- .../__tests__/PasswordSection.test.tsx | 32 +++++------------ .../ui/elements/__tests__/PlainInput.test.tsx | 26 +++++++------- .../ui/elements/__tests__/RadioGroup.test.tsx | 10 +++--- 11 files changed, 99 insertions(+), 66 deletions(-) diff --git a/integration/tests/elements/next-sign-in.test.ts b/integration/tests/elements/next-sign-in.test.ts index 6534f28d3e5..cd1bbf3e376 100644 --- a/integration/tests/elements/next-sign-in.test.ts +++ b/integration/tests/elements/next-sign-in.test.ts @@ -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')).toHaveText(/password is incorrect/i); await u.po.expect.toBeSignedOut(); }); @@ -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')).toHaveText(/password is incorrect/i); await u.page.getByRole('button', { name: /use another method/i }).click(); await u.po.signIn.getAltMethodsEmailCodeButton().click(); diff --git a/integration/tests/elements/next-sign-up.test.ts b/integration/tests/elements/next-sign-up.test.ts index 70f7d42dd25..00dd3fb573c 100644 --- a/integration/tests/elements/next-sign-up.test.ts +++ b/integration/tests/elements/next-sign-up.test.ts @@ -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')).toHaveText(/Passwords must be \d+ characters or more/i); await u.po.expect.toBeSignedOut(); diff --git a/integration/tests/sign-in-flow.test.ts b/integration/tests/sign-in-flow.test.ts index 68a80bbf844..49b1078eea1 100644 --- a/integration/tests/sign-in-flow.test.ts +++ b/integration/tests/sign-in-flow.test.ts @@ -129,6 +129,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('sign in f await u.po.signIn.setPassword('wrong-password'); await u.po.signIn.continue(); await expect(u.page.locator('#error-password')).toBeVisible(); + await expect(u.page.locator('#error-password')).toHaveText(/password is incorrect/i); await u.po.expect.toBeSignedOut(); }); @@ -143,6 +144,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('sign in f await u.po.signIn.continue(); await expect(u.page.locator('#error-password')).toBeVisible(); + await expect(u.page.locator('#error-password')).toHaveText(/password is incorrect/i); await u.po.signIn.getUseAnotherMethodLink().click(); await u.po.signIn.getAltMethodsEmailCodeButton().click(); diff --git a/integration/tests/sign-in-or-up-flow.test.ts b/integration/tests/sign-in-or-up-flow.test.ts index c814201be65..da59e32def3 100644 --- a/integration/tests/sign-in-or-up-flow.test.ts +++ b/integration/tests/sign-in-or-up-flow.test.ts @@ -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')).toHaveText(/password is incorrect/i); await u.po.expect.toBeSignedOut(); }); @@ -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')).toHaveText(/password is incorrect/i); await u.po.signIn.getUseAnotherMethodLink().click(); await u.po.signIn.getAltMethodsEmailCodeButton().click(); diff --git a/integration/tests/sign-up-flow.test.ts b/integration/tests/sign-up-flow.test.ts index af9df350f4c..0ac0c483dfb 100644 --- a/integration/tests/sign-up-flow.test.ts +++ b/integration/tests/sign-up-flow.test.ts @@ -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')).toHaveText(/your password must contain \d+ or more characters/i); // Check if user is signed out await u.po.expect.toBeSignedOut(); diff --git a/packages/clerk-js/src/ui/components/SignIn/__tests__/ResetPassword.test.tsx b/packages/clerk-js/src/ui/components/SignIn/__tests__/ResetPassword.test.tsx index 5f880ac4030..7f9c93e99e3 100644 --- a/packages/clerk-js/src/ui/components/SignIn/__tests__/ResetPassword.test.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/__tests__/ResetPassword.test.tsx @@ -37,9 +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, { - selector: '#newPassword-info-feedback', - }); + await screen.findByText(/Your password must contain 8 or more characters/i, { selector: '[id$="-info-feedback"]' }); }); it('renders a hidden identifier field', async () => { @@ -117,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.`, { selector: '#error-confirmPassword' }); + await screen.findByText(/Passwords don't match/i, { selector: '[id^="error-"]' }); await userEvent.clear(confirmField); - await screen.findByText(`Passwords don't match.`, { selector: '#error-confirmPassword' }); + await screen.findByText(/Passwords don't match/i, { selector: '[id^="error-"]' }); }); it('navigates to the root page upon pressing the back link', async () => { diff --git a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx index f8598a3b29d..fe2a2763001 100644 --- a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx @@ -186,7 +186,7 @@ describe('SignInFactorOne', () => { const { userEvent } = render(, { wrapper }); await userEvent.type(screen.getByLabelText('Password'), '123456'); await userEvent.click(screen.getByText('Continue')); - await screen.findByText('Incorrect Password', { selector: '#error-password' }); + await screen.findByText(/Incorrect Password/i, { selector: '[id^="error-"]' }); }); it('redirects back to sign-in if the user is locked', async () => { @@ -249,7 +249,9 @@ describe('SignInFactorOne', () => { await userEvent.type(screen.getByLabelText('Password'), '123456'); await userEvent.click(screen.getByText('Continue')); - await screen.findByText('Password compromised', { selector: '#error-password' }); + // 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.', ); @@ -291,7 +293,9 @@ describe('SignInFactorOne', () => { await userEvent.type(screen.getByLabelText('Password'), '123456'); await userEvent.click(screen.getByText('Continue')); - await screen.findByText('Password compromised', { selector: '#error-password' }); + // 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.', ); @@ -333,7 +337,9 @@ describe('SignInFactorOne', () => { await userEvent.type(screen.getByLabelText('Password'), '123456'); await userEvent.click(screen.getByText('Continue')); - await screen.findByText('Password compromised', { selector: '#error-password' }); + // 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.', ); @@ -556,9 +562,16 @@ describe('SignInFactorOne', () => { status: 422, }), ); - const { userEvent } = render(, { wrapper }); + const { userEvent, container } = render(, { wrapper }); await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456'); - await screen.findByText('Incorrect code', { selector: '#error-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 () => { @@ -661,9 +674,16 @@ describe('SignInFactorOne', () => { status: 422, }), ); - const { userEvent } = render(, { wrapper }); + const { userEvent, container } = render(, { wrapper }); await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456'); - await screen.findByText('Incorrect phone code', { selector: '#error-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 () => { diff --git a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorTwo.test.tsx b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorTwo.test.tsx index 193cf5f50d8..5e86cfc9a37 100644 --- a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorTwo.test.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorTwo.test.tsx @@ -183,9 +183,16 @@ describe('SignInFactorTwo', () => { status: 422, }), ); - const { userEvent } = render(, { wrapper }); + const { userEvent, container } = render(, { wrapper }); await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456'); - expect(await screen.findByText('Incorrect phone code', { selector: '#error-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 () => { @@ -272,9 +279,16 @@ describe('SignInFactorTwo', () => { status: 422, }), ); - const { userEvent } = render(, { wrapper }); + const { userEvent, container } = render(, { wrapper }); await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456'); - expect(await screen.findByText('Incorrect authenticator code', { selector: '#error-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(); + }); + } }); }); @@ -364,10 +378,17 @@ describe('SignInFactorTwo', () => { status: 422, }), ); - const { userEvent, getByLabelText, getByText } = render(, { wrapper }); + const { userEvent, getByLabelText, getByText, container } = render(, { wrapper }); await userEvent.type(getByLabelText('Backup code'), '123456'); await userEvent.click(getByText('Continue')); - expect(await screen.findByText('Incorrect backup code', { selector: '#error-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 () => { diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx index cd633d93719..da20219098b 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx @@ -537,15 +537,13 @@ describe('PasswordSection', () => { const confirmField = screen.getByLabelText(/confirm password/i); await userEvent.type(confirmField, 'test'); fireEvent.blur(confirmField); - await waitFor(() => { - screen.getByText(/or more/i, { selector: '#error-newPassword' }); - }); + 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(, { wrapper }); + const { userEvent, getByRole } = render(, { wrapper }); await userEvent.click(getByRole('button', { name: /set password/i })); await waitFor(() => getByRole('heading', { name: /set password/i })); @@ -553,14 +551,10 @@ describe('PasswordSection', () => { 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 () => { @@ -572,32 +566,24 @@ describe('PasswordSection', () => { // user experience and implementation. const { wrapper } = await createFixtures(initConfig); - const { userEvent, getByRole, getByLabelText, queryByText } = render(, { wrapper }); + const { userEvent, getByRole, getByLabelText } = render(, { 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"]' }); }); }); }); diff --git a/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx b/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx index 595e56c4b30..f7bd4143951 100644 --- a/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx +++ b/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, expect, it } from 'vitest'; @@ -135,11 +135,11 @@ describe('PlainInput', () => { placeholder: 'some placeholder', }); - const { getByRole, getByLabelText, findByText, container } = render(, { wrapper }); + const { getByRole, getByLabelText, container } = render(, { wrapper }); await userEvent.click(getByRole('button', { name: /set error/i })); - expect(await findByText(/Some Error/i, { selector: '#error-firstname' })).toBeInTheDocument(); + await screen.findByText(/Some Error/i, { selector: '[id^="error-"]' }); const input = getByLabelText(/some label/i); expect(input).toHaveAttribute('aria-invalid', 'true'); @@ -160,10 +160,10 @@ describe('PlainInput', () => { infoText: 'some info', }); - const { findByLabelText, findByText } = render(, { wrapper }); + const { findByLabelText } = render(, { wrapper }); fireEvent.focus(await findByLabelText(/some label/i)); - expect(await findByText(/some info/i, { selector: '#firstname-info-feedback' })).toBeInTheDocument(); + await screen.findByText(/some info/i, { selector: '[id$="-info-feedback"]' }); }); it('with success feedback and aria-describedby', async () => { @@ -174,11 +174,11 @@ describe('PlainInput', () => { placeholder: 'some placeholder', }); - const { getByRole, getByLabelText, findByText, container } = render(, { wrapper }); + const { getByRole, getByLabelText, container } = render(, { wrapper }); await userEvent.click(getByRole('button', { name: /set success/i })); - expect(await findByText(/Some Success/i, { selector: '#firstname-success-feedback' })).toBeInTheDocument(); + await screen.findByText(/Some Success/i, { selector: '[id$="-success-feedback"]' }); const input = getByLabelText(/some label/i); expect(input).toHaveAttribute('aria-invalid', 'false'); @@ -198,11 +198,11 @@ describe('PlainInput', () => { placeholder: 'some placeholder', }); - const { getByRole, getByLabelText, findByText, container } = render(, { wrapper }); + const { getByRole, getByLabelText, container } = render(, { wrapper }); // Start with error await userEvent.click(getByRole('button', { name: /set error/i })); - expect(await findByText(/Some Error/i, { selector: '#error-firstname' })).toBeInTheDocument(); + await screen.findByText(/Some Error/i, { selector: '[id^="error-"]' }); let input = getByLabelText(/some label/i); expect(input).toHaveAttribute('aria-invalid', 'true'); @@ -210,7 +210,7 @@ describe('PlainInput', () => { // Transition to success await userEvent.click(getByRole('button', { name: /set success/i })); - expect(await findByText(/Some Success/i, { selector: '#firstname-success-feedback' })).toBeInTheDocument(); + await screen.findByText(/Some Success/i, { selector: '[id$="-success-feedback"]' }); input = getByLabelText(/some label/i); expect(input).toHaveAttribute('aria-invalid', 'false'); @@ -230,7 +230,7 @@ describe('PlainInput', () => { placeholder: 'some placeholder', }); - const { getByRole, findByText, container } = render(, { wrapper }); + const { getByRole, container } = render(, { wrapper }); // Pre-state: aria-live region exists and is empty const preRegions = container.querySelectorAll('[aria-live="polite"]'); @@ -249,7 +249,7 @@ describe('PlainInput', () => { // Set error feedback await userEvent.click(getByRole('button', { name: /set error/i })); - expect(await findByText(/Some Error/i, { selector: '#error-firstname' })).toBeInTheDocument(); + await screen.findByText(/Some Error/i, { selector: '[id^="error-"]' }); // Verify there's a screen-reader-only aria-live region with the error content const ariaLiveRegions = container.querySelectorAll('[aria-live="polite"]'); @@ -265,7 +265,7 @@ describe('PlainInput', () => { // Transition to success await userEvent.click(getByRole('button', { name: /set success/i })); - expect(await findByText(/Some Success/i, { selector: '#firstname-success-feedback' })).toBeInTheDocument(); + await screen.findByText(/Some Success/i, { selector: '[id$="-success-feedback"]' }); // Verify the screen reader only region updated its content expect(srOnlyRegion).toHaveTextContent(/Some Success/i); diff --git a/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx b/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx index 3bb8ab46055..08223adda49 100644 --- a/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx +++ b/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, expect, it } from 'vitest'; @@ -157,7 +157,7 @@ describe('RadioGroup', () => { type: 'radio', }); - const { getAllByRole, getByRole, findByText } = render( + const { getAllByRole, getByRole } = render( { ); await userEvent.click(getByRole('button', { name: /set error/i })); - expect(await findByText(/Some Error/i, { selector: '#error-some-radio' })).toBeInTheDocument(); + await screen.findByText(/Some Error/i, { selector: '[id^="error-"]' }); const radios = getAllByRole('radio'); radios.forEach(radio => { @@ -188,9 +188,9 @@ describe('RadioGroup', () => { infoText: 'some info', }); - const { findByLabelText, findByText } = render(, { wrapper }); + const { findByLabelText } = render(, { wrapper }); fireEvent.focus(await findByLabelText(/One/i)); - expect(await findByText(/some info/i)).toBeInTheDocument(); + await screen.findByText(/some info/i, { selector: '[id$="-info-feedback"]' }); }); }); From 31b7ffdf9e7a4a6eeac0679d7c14e7bfc8a120e5 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 5 Nov 2025 16:14:47 -0500 Subject: [PATCH 06/11] update selectors --- integration/tests/elements/next-sign-in.test.ts | 4 ++-- integration/tests/elements/next-sign-up.test.ts | 2 +- integration/tests/sign-in-flow.test.ts | 4 ++-- integration/tests/sign-in-or-up-flow.test.ts | 4 ++-- integration/tests/sign-up-flow.test.ts | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/integration/tests/elements/next-sign-in.test.ts b/integration/tests/elements/next-sign-in.test.ts index cd1bbf3e376..1b78b264bf9 100644 --- a/integration/tests/elements/next-sign-in.test.ts +++ b/integration/tests/elements/next-sign-in.test.ts @@ -167,7 +167,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('Next.js S await u.po.signIn.setPassword('wrong-password'); await u.po.signIn.continue(); await expect(u.page.locator('#error-password')).toBeVisible(); - await expect(u.page.locator('#error-password')).toHaveText(/password is incorrect/i); + await expect(u.page.locator('#error-password')).toContainText(/password is incorrect/i); await u.po.expect.toBeSignedOut(); }); @@ -183,7 +183,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('Next.js S await u.po.signIn.continue(); await expect(u.page.locator('#error-password')).toBeVisible(); - await expect(u.page.locator('#error-password')).toHaveText(/password is incorrect/i); + 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(); diff --git a/integration/tests/elements/next-sign-up.test.ts b/integration/tests/elements/next-sign-up.test.ts index 00dd3fb573c..ae53131e1ac 100644 --- a/integration/tests/elements/next-sign-up.test.ts +++ b/integration/tests/elements/next-sign-up.test.ts @@ -80,7 +80,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('Next.js S // Check if password error is visible await expect(u.page.locator('#error-password')).toBeVisible(); - await expect(u.page.locator('#error-password')).toHaveText(/Passwords must be \d+ characters or more/i); + await expect(u.page.locator('#error-password')).toContainText(/Passwords must be \d+ characters or more/i); await u.po.expect.toBeSignedOut(); diff --git a/integration/tests/sign-in-flow.test.ts b/integration/tests/sign-in-flow.test.ts index 49b1078eea1..e918c371ad2 100644 --- a/integration/tests/sign-in-flow.test.ts +++ b/integration/tests/sign-in-flow.test.ts @@ -129,7 +129,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('sign in f await u.po.signIn.setPassword('wrong-password'); await u.po.signIn.continue(); await expect(u.page.locator('#error-password')).toBeVisible(); - await expect(u.page.locator('#error-password')).toHaveText(/password is incorrect/i); + await expect(u.page.locator('#error-password')).toContainText(/password is incorrect/i); await u.po.expect.toBeSignedOut(); }); @@ -144,7 +144,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('sign in f await u.po.signIn.continue(); await expect(u.page.locator('#error-password')).toBeVisible(); - await expect(u.page.locator('#error-password')).toHaveText(/password is incorrect/i); + await expect(u.page.locator('#error-password')).toContainText(/password is incorrect/i); await u.po.signIn.getUseAnotherMethodLink().click(); await u.po.signIn.getAltMethodsEmailCodeButton().click(); diff --git a/integration/tests/sign-in-or-up-flow.test.ts b/integration/tests/sign-in-or-up-flow.test.ts index da59e32def3..b259b822a45 100644 --- a/integration/tests/sign-in-or-up-flow.test.ts +++ b/integration/tests/sign-in-or-up-flow.test.ts @@ -143,7 +143,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSignInOrUpFlow] })('sign- await u.po.signIn.setPassword('wrong-password'); await u.po.signIn.continue(); await expect(u.page.locator('#error-password')).toBeVisible(); - await expect(u.page.locator('#error-password')).toHaveText(/password is incorrect/i); + await expect(u.page.locator('#error-password')).toContainText(/password is incorrect/i); await u.po.expect.toBeSignedOut(); }); @@ -158,7 +158,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSignInOrUpFlow] })('sign- await u.po.signIn.continue(); await expect(u.page.locator('#error-password')).toBeVisible(); - await expect(u.page.locator('#error-password')).toHaveText(/password is incorrect/i); + await expect(u.page.locator('#error-password')).toContainText(/password is incorrect/i); await u.po.signIn.getUseAnotherMethodLink().click(); await u.po.signIn.getAltMethodsEmailCodeButton().click(); diff --git a/integration/tests/sign-up-flow.test.ts b/integration/tests/sign-up-flow.test.ts index 0ac0c483dfb..ff5e71e28a9 100644 --- a/integration/tests/sign-up-flow.test.ts +++ b/integration/tests/sign-up-flow.test.ts @@ -55,7 +55,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('sign up f // Check if password error is visible await expect(u.page.locator('#error-password')).toBeVisible(); - await expect(u.page.locator('#error-password')).toHaveText(/your password must contain \d+ or more characters/i); + 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(); From 06c3611db837957af6b101bc21244cc6aaee2963 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 5 Nov 2025 16:22:55 -0500 Subject: [PATCH 07/11] revert --- .../tests/elements/next-sign-in.test.ts | 6 ++-- .../tests/elements/next-sign-up.test.ts | 3 +- integration/tests/sign-in-flow.test.ts | 6 ++-- integration/tests/sign-in-or-up-flow.test.ts | 6 ++-- integration/tests/sign-up-flow.test.ts | 3 +- .../SignIn/__tests__/ResetPassword.test.tsx | 6 ++-- .../SignIn/__tests__/SignInFactorOne.test.tsx | 30 +++-------------- .../SignIn/__tests__/SignInFactorTwo.test.tsx | 33 ++++--------------- .../__tests__/PasswordSection.test.tsx | 32 +++++++++++++----- .../ui/elements/__tests__/PlainInput.test.tsx | 32 +++++++++++++----- .../ui/elements/__tests__/RadioGroup.test.tsx | 12 ++++--- 11 files changed, 76 insertions(+), 93 deletions(-) diff --git a/integration/tests/elements/next-sign-in.test.ts b/integration/tests/elements/next-sign-in.test.ts index 1b78b264bf9..6534f28d3e5 100644 --- a/integration/tests/elements/next-sign-in.test.ts +++ b/integration/tests/elements/next-sign-in.test.ts @@ -166,8 +166,7 @@ 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.locator('#error-password')).toBeVisible(); - await expect(u.page.locator('#error-password')).toContainText(/password is incorrect/i); + await expect(u.page.getByText(/^password is incorrect/i)).toBeVisible(); await u.po.expect.toBeSignedOut(); }); @@ -182,8 +181,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('Next.js S await u.po.signIn.setPassword('wrong-password'); await u.po.signIn.continue(); - await expect(u.page.locator('#error-password')).toBeVisible(); - await expect(u.page.locator('#error-password')).toContainText(/password is incorrect/i); + await expect(u.page.getByText(/^password is incorrect/i)).toBeVisible(); await u.page.getByRole('button', { name: /use another method/i }).click(); await u.po.signIn.getAltMethodsEmailCodeButton().click(); diff --git a/integration/tests/elements/next-sign-up.test.ts b/integration/tests/elements/next-sign-up.test.ts index ae53131e1ac..70f7d42dd25 100644 --- a/integration/tests/elements/next-sign-up.test.ts +++ b/integration/tests/elements/next-sign-up.test.ts @@ -79,8 +79,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('Next.js S }); // Check if password error is visible - 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 expect(u.page.getByText(/Passwords must be \d+ characters or more/i)).toBeVisible(); await u.po.expect.toBeSignedOut(); diff --git a/integration/tests/sign-in-flow.test.ts b/integration/tests/sign-in-flow.test.ts index e918c371ad2..20326de4204 100644 --- a/integration/tests/sign-in-flow.test.ts +++ b/integration/tests/sign-in-flow.test.ts @@ -128,8 +128,7 @@ 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.locator('#error-password')).toBeVisible(); - await expect(u.page.locator('#error-password')).toContainText(/password is incorrect/i); + await expect(u.page.getByText(/password is incorrect/i)).toBeVisible(); await u.po.expect.toBeSignedOut(); }); @@ -143,8 +142,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('sign in f await u.po.signIn.setPassword('wrong-password'); await u.po.signIn.continue(); - await expect(u.page.locator('#error-password')).toBeVisible(); - await expect(u.page.locator('#error-password')).toContainText(/password is incorrect/i); + await expect(u.page.getByText(/password is incorrect/i)).toBeVisible(); await u.po.signIn.getUseAnotherMethodLink().click(); await u.po.signIn.getAltMethodsEmailCodeButton().click(); diff --git a/integration/tests/sign-in-or-up-flow.test.ts b/integration/tests/sign-in-or-up-flow.test.ts index b259b822a45..c814201be65 100644 --- a/integration/tests/sign-in-or-up-flow.test.ts +++ b/integration/tests/sign-in-or-up-flow.test.ts @@ -142,8 +142,7 @@ 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.locator('#error-password')).toBeVisible(); - await expect(u.page.locator('#error-password')).toContainText(/password is incorrect/i); + await expect(u.page.getByText(/password is incorrect/i)).toBeVisible(); await u.po.expect.toBeSignedOut(); }); @@ -157,8 +156,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSignInOrUpFlow] })('sign- await u.po.signIn.setPassword('wrong-password'); await u.po.signIn.continue(); - await expect(u.page.locator('#error-password')).toBeVisible(); - await expect(u.page.locator('#error-password')).toContainText(/password is incorrect/i); + await expect(u.page.getByText(/password is incorrect/i)).toBeVisible(); await u.po.signIn.getUseAnotherMethodLink().click(); await u.po.signIn.getAltMethodsEmailCodeButton().click(); diff --git a/integration/tests/sign-up-flow.test.ts b/integration/tests/sign-up-flow.test.ts index ff5e71e28a9..af9df350f4c 100644 --- a/integration/tests/sign-up-flow.test.ts +++ b/integration/tests/sign-up-flow.test.ts @@ -54,8 +54,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('sign up f }); // Check if password error is visible - 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); + await expect(u.page.getByText(/your password must contain \d+ or more characters/i).first()).toBeVisible(); // Check if user is signed out await u.po.expect.toBeSignedOut(); diff --git a/packages/clerk-js/src/ui/components/SignIn/__tests__/ResetPassword.test.tsx b/packages/clerk-js/src/ui/components/SignIn/__tests__/ResetPassword.test.tsx index 7f9c93e99e3..f5778d8c918 100644 --- a/packages/clerk-js/src/ui/components/SignIn/__tests__/ResetPassword.test.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/__tests__/ResetPassword.test.tsx @@ -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, { selector: '[id$="-info-feedback"]' }); + await screen.findByText(/Your password must contain 8 or more characters/i); }); it('renders a hidden identifier field', async () => { @@ -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/i, { selector: '[id^="error-"]' }); + await screen.findByText(`Passwords don't match.`); await userEvent.clear(confirmField); - await screen.findByText(/Passwords don't match/i, { selector: '[id^="error-"]' }); + await screen.findByText(`Passwords don't match.`); }); it('navigates to the root page upon pressing the back link', async () => { diff --git a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx index fe2a2763001..8fa3afb7849 100644 --- a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx @@ -186,7 +186,7 @@ describe('SignInFactorOne', () => { const { userEvent } = render(, { wrapper }); await userEvent.type(screen.getByLabelText('Password'), '123456'); await userEvent.click(screen.getByText('Continue')); - await screen.findByText(/Incorrect Password/i, { selector: '[id^="error-"]' }); + await screen.findByText('Incorrect Password'); }); it('redirects back to sign-in if the user is locked', async () => { @@ -249,8 +249,6 @@ 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.', @@ -293,8 +291,6 @@ 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.', @@ -337,8 +333,6 @@ 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.', @@ -562,16 +556,9 @@ describe('SignInFactorOne', () => { status: 422, }), ); - const { userEvent, container } = render(, { wrapper }); + const { userEvent } = render(, { wrapper }); await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456'); - 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(); - }); - } + await screen.findByText('Incorrect code'); }); it('redirects back to sign-in if the user is locked', async () => { @@ -674,16 +661,9 @@ describe('SignInFactorOne', () => { status: 422, }), ); - const { userEvent, container } = render(, { wrapper }); + const { userEvent } = render(, { wrapper }); await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456'); - 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(); - }); - } + await screen.findByText('Incorrect code'); }); it('redirects back to sign-in if the user is locked', async () => { diff --git a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorTwo.test.tsx b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorTwo.test.tsx index 5e86cfc9a37..98c1f6ee2d7 100644 --- a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorTwo.test.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorTwo.test.tsx @@ -183,16 +183,9 @@ describe('SignInFactorTwo', () => { status: 422, }), ); - const { userEvent, container } = render(, { wrapper }); + const { userEvent } = render(, { wrapper }); await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456'); - 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(); - }); - } + expect(await screen.findByText('Incorrect authenticator code')).toBeDefined(); }); it('redirects back to sign-in if the user is locked', async () => { @@ -279,16 +272,9 @@ describe('SignInFactorTwo', () => { status: 422, }), ); - const { userEvent, container } = render(, { wrapper }); + const { userEvent } = render(, { wrapper }); await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456'); - 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(); - }); - } + expect(await screen.findByText('Incorrect phone code')).toBeDefined(); }); }); @@ -378,17 +364,10 @@ describe('SignInFactorTwo', () => { status: 422, }), ); - const { userEvent, getByLabelText, getByText, container } = render(, { wrapper }); + const { userEvent, getByLabelText, getByText } = render(, { wrapper }); await userEvent.type(getByLabelText('Backup code'), '123456'); await userEvent.click(getByText('Continue')); - 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(); - }); - } + expect(await screen.findByText('Incorrect backup code')).toBeDefined(); }); it('redirects back to sign-in if the user is locked', async () => { diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx index da20219098b..f23eaff6692 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx @@ -537,13 +537,15 @@ describe('PasswordSection', () => { const confirmField = screen.getByLabelText(/confirm password/i); await userEvent.type(confirmField, 'test'); fireEvent.blur(confirmField); - await screen.findByText(/or more/i, { selector: '[id^="error-"]' }); + await waitFor(() => { + screen.getByText(/or more/i); + }); }); 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 } = render(, { wrapper }); + const { userEvent, getByRole, queryByText } = render(, { wrapper }); await userEvent.click(getByRole('button', { name: /set password/i })); await waitFor(() => getByRole('heading', { name: /set password/i })); @@ -551,10 +553,14 @@ describe('PasswordSection', () => { const confirmField = screen.getByLabelText(/confirm password/i); await userEvent.type(confirmField, 'testrwerrwqrwe'); fireEvent.blur(confirmField); - expect(screen.queryByText(/Passwords match/i, { selector: '[id$="-success-feedback"]' })).not.toBeInTheDocument(); + await waitFor(() => { + expect(queryByText(`Passwords match.`)).not.toBeInTheDocument(); + }); await userEvent.clear(confirmField); - expect(screen.queryByText(/Passwords match/i, { selector: '[id$="-success-feedback"]' })).not.toBeInTheDocument(); + await waitFor(() => { + expect(queryByText(`Passwords match.`)).not.toBeInTheDocument(); + }); }); it.skip(`Displays "Password match" when password match and removes it if they stop`, async () => { @@ -566,24 +572,32 @@ describe('PasswordSection', () => { // user experience and implementation. const { wrapper } = await createFixtures(initConfig); - const { userEvent, getByRole, getByLabelText } = render(, { wrapper }); + const { userEvent, getByRole, getByLabelText, queryByText } = render(, { 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); - expect(screen.queryByText(/Passwords match/i, { selector: '[id$="-success-feedback"]' })).not.toBeInTheDocument(); + await waitFor(() => { + expect(queryByText(`Passwords match.`)).not.toBeInTheDocument(); + }); await userEvent.type(confirmField, 'testewrewr'); - await screen.findByText(/Passwords match/i, { selector: '[id$="-success-feedback"]' }); + await waitFor(() => { + expect(queryByText(`Passwords match.`)).toBeInTheDocument(); + }); await userEvent.type(confirmField, 'testrwerrwqrwe'); - expect(screen.queryByText(/Passwords match/i, { selector: '[id$="-success-feedback"]' })).not.toBeInTheDocument(); + await waitFor(() => { + expect(queryByText(`Passwords match.`)).not.toBeInTheDocument(); + }); await userEvent.type(passwordField, 'testrwerrwqrwe'); fireEvent.blur(confirmField); - await screen.findByText(/Passwords match/i, { selector: '[id$="-success-feedback"]' }); + await waitFor(() => { + screen.getByText(`Passwords match.`); + }); }); }); }); diff --git a/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx b/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx index f7bd4143951..9c7235c38d5 100644 --- a/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx +++ b/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, expect, it } from 'vitest'; @@ -139,7 +139,9 @@ describe('PlainInput', () => { await userEvent.click(getByRole('button', { name: /set error/i })); - await screen.findByText(/Some Error/i, { selector: '[id^="error-"]' }); + await waitFor(() => { + expect(container.querySelector('#error-firstname')).toBeInTheDocument(); + }); const input = getByLabelText(/some label/i); expect(input).toHaveAttribute('aria-invalid', 'true'); @@ -160,10 +162,12 @@ describe('PlainInput', () => { infoText: 'some info', }); - const { findByLabelText } = render(, { wrapper }); + const { findByLabelText, container } = render(, { wrapper }); fireEvent.focus(await findByLabelText(/some label/i)); - await screen.findByText(/some info/i, { selector: '[id$="-info-feedback"]' }); + await waitFor(() => { + expect(container.querySelector('#firstname-info-feedback')).toBeInTheDocument(); + }); }); it('with success feedback and aria-describedby', async () => { @@ -178,7 +182,9 @@ describe('PlainInput', () => { await userEvent.click(getByRole('button', { name: /set success/i })); - await screen.findByText(/Some Success/i, { selector: '[id$="-success-feedback"]' }); + await waitFor(() => { + expect(container.querySelector('#firstname-success-feedback')).toBeInTheDocument(); + }); const input = getByLabelText(/some label/i); expect(input).toHaveAttribute('aria-invalid', 'false'); @@ -202,7 +208,9 @@ describe('PlainInput', () => { // Start with error await userEvent.click(getByRole('button', { name: /set error/i })); - await screen.findByText(/Some Error/i, { selector: '[id^="error-"]' }); + await waitFor(() => { + expect(container.querySelector('#error-firstname')).toBeInTheDocument(); + }); let input = getByLabelText(/some label/i); expect(input).toHaveAttribute('aria-invalid', 'true'); @@ -210,7 +218,9 @@ describe('PlainInput', () => { // Transition to success await userEvent.click(getByRole('button', { name: /set success/i })); - await screen.findByText(/Some Success/i, { selector: '[id$="-success-feedback"]' }); + await waitFor(() => { + expect(container.querySelector('#firstname-success-feedback')).toBeInTheDocument(); + }); input = getByLabelText(/some label/i); expect(input).toHaveAttribute('aria-invalid', 'false'); @@ -249,7 +259,9 @@ describe('PlainInput', () => { // Set error feedback await userEvent.click(getByRole('button', { name: /set error/i })); - await screen.findByText(/Some Error/i, { selector: '[id^="error-"]' }); + await waitFor(() => { + expect(container.querySelector('#error-firstname')).toBeInTheDocument(); + }); // Verify there's a screen-reader-only aria-live region with the error content const ariaLiveRegions = container.querySelectorAll('[aria-live="polite"]'); @@ -265,7 +277,9 @@ describe('PlainInput', () => { // Transition to success await userEvent.click(getByRole('button', { name: /set success/i })); - await screen.findByText(/Some Success/i, { selector: '[id$="-success-feedback"]' }); + await waitFor(() => { + expect(container.querySelector('#firstname-success-feedback')).toBeInTheDocument(); + }); // Verify the screen reader only region updated its content expect(srOnlyRegion).toHaveTextContent(/Some Success/i); diff --git a/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx b/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx index 08223adda49..660f4c8d655 100644 --- a/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx +++ b/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx @@ -157,7 +157,7 @@ describe('RadioGroup', () => { type: 'radio', }); - const { getAllByRole, getByRole } = render( + const { getAllByRole, getByRole, container } = render( { ); await userEvent.click(getByRole('button', { name: /set error/i })); - await screen.findByText(/Some Error/i, { selector: '[id^="error-"]' }); + await waitFor(() => { + expect(container.querySelector('#error-some-radio')).toBeInTheDocument(); + }); const radios = getAllByRole('radio'); radios.forEach(radio => { @@ -188,9 +190,11 @@ describe('RadioGroup', () => { infoText: 'some info', }); - const { findByLabelText } = render(, { wrapper }); + const { findByLabelText, container } = render(, { wrapper }); fireEvent.focus(await findByLabelText(/One/i)); - await screen.findByText(/some info/i, { selector: '[id$="-info-feedback"]' }); + await waitFor(() => { + expect(container.querySelector('#some-radio-info-feedback')).toBeInTheDocument(); + }); }); }); From 49f238af0ba05b0a3342ee74e3138214e7bac44c Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 5 Nov 2025 16:26:29 -0500 Subject: [PATCH 08/11] revert --- .../SignIn/__tests__/SignInFactorOne.test.tsx | 2 +- .../SignIn/__tests__/SignInFactorTwo.test.tsx | 4 +- .../ui/elements/__tests__/PlainInput.test.tsx | 88 +++++-------------- .../ui/elements/__tests__/RadioGroup.test.tsx | 14 ++- 4 files changed, 31 insertions(+), 77 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx index 8fa3afb7849..d784f16ad3a 100644 --- a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx @@ -663,7 +663,7 @@ describe('SignInFactorOne', () => { ); const { userEvent } = render(, { wrapper }); await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456'); - await screen.findByText('Incorrect code'); + await screen.findByText('Incorrect phone code'); }); it('redirects back to sign-in if the user is locked', async () => { diff --git a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorTwo.test.tsx b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorTwo.test.tsx index 98c1f6ee2d7..019e6324047 100644 --- a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorTwo.test.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorTwo.test.tsx @@ -185,7 +185,7 @@ describe('SignInFactorTwo', () => { ); const { userEvent } = render(, { wrapper }); await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456'); - expect(await screen.findByText('Incorrect authenticator code')).toBeDefined(); + expect(await screen.findByText('Incorrect phone code')).toBeDefined(); }); it('redirects back to sign-in if the user is locked', async () => { @@ -274,7 +274,7 @@ describe('SignInFactorTwo', () => { ); const { userEvent } = render(, { wrapper }); await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456'); - expect(await screen.findByText('Incorrect phone code')).toBeDefined(); + expect(await screen.findByText('Incorrect authenticator code')).toBeDefined(); }); }); diff --git a/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx b/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx index 9c7235c38d5..6adaec9b8f2 100644 --- a/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx +++ b/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { fireEvent, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, expect, it } from 'vitest'; @@ -135,13 +135,11 @@ describe('PlainInput', () => { placeholder: 'some placeholder', }); - const { getByRole, getByLabelText, container } = render(, { wrapper }); + const { getByRole, getByLabelText, findByText, container } = render(, { wrapper }); await userEvent.click(getByRole('button', { name: /set error/i })); - await waitFor(() => { - expect(container.querySelector('#error-firstname')).toBeInTheDocument(); - }); + expect(await findByText(/Some Error/i)).toBeInTheDocument(); const input = getByLabelText(/some label/i); expect(input).toHaveAttribute('aria-invalid', 'true'); @@ -162,12 +160,10 @@ describe('PlainInput', () => { infoText: 'some info', }); - const { findByLabelText, container } = render(, { wrapper }); + const { findByLabelText, findByText } = render(, { wrapper }); fireEvent.focus(await findByLabelText(/some label/i)); - await waitFor(() => { - expect(container.querySelector('#firstname-info-feedback')).toBeInTheDocument(); - }); + expect(await findByText(/some info/i)).toBeInTheDocument(); }); it('with success feedback and aria-describedby', async () => { @@ -178,13 +174,11 @@ describe('PlainInput', () => { placeholder: 'some placeholder', }); - const { getByRole, getByLabelText, container } = render(, { wrapper }); + const { getByRole, getByLabelText, findByText, container } = render(, { wrapper }); await userEvent.click(getByRole('button', { name: /set success/i })); - await waitFor(() => { - expect(container.querySelector('#firstname-success-feedback')).toBeInTheDocument(); - }); + expect(await findByText(/Some Success/i)).toBeInTheDocument(); const input = getByLabelText(/some label/i); expect(input).toHaveAttribute('aria-invalid', 'false'); @@ -204,13 +198,11 @@ describe('PlainInput', () => { placeholder: 'some placeholder', }); - const { getByRole, getByLabelText, container } = render(, { wrapper }); + const { getByRole, getByLabelText, findByText, container } = render(, { wrapper }); // Start with error await userEvent.click(getByRole('button', { name: /set error/i })); - await waitFor(() => { - expect(container.querySelector('#error-firstname')).toBeInTheDocument(); - }); + expect(await findByText(/Some Error/i)).toBeInTheDocument(); let input = getByLabelText(/some label/i); expect(input).toHaveAttribute('aria-invalid', 'true'); @@ -218,9 +210,7 @@ describe('PlainInput', () => { // Transition to success await userEvent.click(getByRole('button', { name: /set success/i })); - await waitFor(() => { - expect(container.querySelector('#firstname-success-feedback')).toBeInTheDocument(); - }); + expect(await findByText(/Some Success/i)).toBeInTheDocument(); input = getByLabelText(/some label/i); expect(input).toHaveAttribute('aria-invalid', 'false'); @@ -232,7 +222,7 @@ describe('PlainInput', () => { expect(successElement).toHaveTextContent(/Some Success/i); }); - it('screen reader only live region announces feedback changes', async () => { + it('aria-live attribute is correctly applied', async () => { const { wrapper } = await createFixtures(); const { Field } = createField('firstname', 'init value', { type: 'text', @@ -240,59 +230,27 @@ describe('PlainInput', () => { placeholder: 'some placeholder', }); - const { getByRole, container } = render(, { wrapper }); - - // Pre-state: aria-live region exists and is empty - const preRegions = container.querySelectorAll('[aria-live="polite"]'); - expect(preRegions.length).toBeGreaterThanOrEqual(1); - const preSrOnly = Array.from(preRegions).find(el => { - const style = window.getComputedStyle(el); - return style.position === 'absolute' && style.width === '1px'; - }); - expect(preSrOnly).toBeDefined(); - expect(preSrOnly?.textContent ?? '').toMatch(/^\s*$/); - - // Input is not in error and not described yet - const inputEl = container.querySelector('input#firstname-field'); - expect(inputEl).toHaveAttribute('aria-invalid', 'false'); - expect(inputEl).not.toHaveAttribute('aria-describedby'); + const { getByRole, findByText, container } = render(, { wrapper }); // Set error feedback await userEvent.click(getByRole('button', { name: /set error/i })); - await waitFor(() => { - expect(container.querySelector('#error-firstname')).toBeInTheDocument(); - }); - - // Verify there's a screen-reader-only aria-live region with the error content - const ariaLiveRegions = container.querySelectorAll('[aria-live="polite"]'); - expect(ariaLiveRegions.length).toBeGreaterThanOrEqual(1); + expect(await findByText(/Some Error/i)).toBeInTheDocument(); - // Find the screen reader only region (it will have the visually hidden styles) - const srOnlyRegion = Array.from(ariaLiveRegions).find(el => { - const style = window.getComputedStyle(el); - return style.position === 'absolute' && style.width === '1px'; - }); - expect(srOnlyRegion).toBeDefined(); - expect(srOnlyRegion).toHaveTextContent(/Some Error/i); + // Verify the visible error message has aria-live="polite" + const errorElement = container.querySelector('#error-firstname'); + expect(errorElement).toHaveAttribute('aria-live', 'polite'); // Transition to success await userEvent.click(getByRole('button', { name: /set success/i })); - await waitFor(() => { - expect(container.querySelector('#firstname-success-feedback')).toBeInTheDocument(); - }); - - // Verify the screen reader only region updated its content - expect(srOnlyRegion).toHaveTextContent(/Some Success/i); + expect(await findByText(/Some Success/i)).toBeInTheDocument(); - // Verify the visible error/success elements exist with proper IDs for aria-describedby - const errorElement = container.querySelector('#error-firstname'); + // Verify the visible success message has aria-live="polite" const successElement = container.querySelector('#firstname-success-feedback'); + expect(successElement).toHaveAttribute('aria-live', 'polite'); - // One should be visible, the other hidden (for animation) - const errorVisible = errorElement && window.getComputedStyle(errorElement).visibility === 'visible'; - const successVisible = successElement && window.getComputedStyle(successElement).visibility === 'visible'; - - // At least one should be visible (might be both during transition) - expect(errorVisible || successVisible).toBe(true); + // The previous error message should now have aria-live="off" (though it might still exist in DOM but hidden) + // Verify exactly one element has aria-live="polite" at a time + const allAriaLivePolite = container.querySelectorAll('[aria-live="polite"]'); + expect(allAriaLivePolite.length).toBe(1); }); }); diff --git a/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx b/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx index 660f4c8d655..ad56cae4364 100644 --- a/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx +++ b/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { fireEvent, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, expect, it } from 'vitest'; @@ -157,7 +157,7 @@ describe('RadioGroup', () => { type: 'radio', }); - const { getAllByRole, getByRole, container } = render( + const { getAllByRole, getByRole, findByText } = render( { ); await userEvent.click(getByRole('button', { name: /set error/i })); - await waitFor(() => { - expect(container.querySelector('#error-some-radio')).toBeInTheDocument(); - }); + expect(await findByText(/Some Error/i)).toBeInTheDocument(); const radios = getAllByRole('radio'); radios.forEach(radio => { @@ -190,11 +188,9 @@ describe('RadioGroup', () => { infoText: 'some info', }); - const { findByLabelText, container } = render(, { wrapper }); + const { findByLabelText, findByText } = render(, { wrapper }); fireEvent.focus(await findByLabelText(/One/i)); - await waitFor(() => { - expect(container.querySelector('#some-radio-info-feedback')).toBeInTheDocument(); - }); + expect(await findByText(/some info/i)).toBeInTheDocument(); }); }); From 97a97b8d090a1d62c0ff2e62da33350d41bbccc9 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 5 Nov 2025 18:13:29 -0500 Subject: [PATCH 09/11] update tests --- .../tests/elements/next-sign-in.test.ts | 6 +- .../tests/elements/next-sign-up.test.ts | 3 +- integration/tests/sign-in-flow.test.ts | 6 +- integration/tests/sign-in-or-up-flow.test.ts | 6 +- integration/tests/sign-up-flow.test.ts | 5 +- .../SignIn/__tests__/ResetPassword.test.tsx | 9 ++- .../SignIn/__tests__/SignInFactorOne.test.tsx | 9 ++- .../SignIn/__tests__/SignInFactorTwo.test.tsx | 6 +- .../SignUp/__tests__/SignUpContinue.test.tsx | 10 ++- .../__tests__/PasswordSection.test.tsx | 2 +- .../clerk-js/src/ui/elements/FormControl.tsx | 2 + .../ui/elements/__tests__/PlainInput.test.tsx | 68 +++++++++++-------- .../ui/elements/__tests__/RadioGroup.test.tsx | 10 +-- 13 files changed, 86 insertions(+), 56 deletions(-) diff --git a/integration/tests/elements/next-sign-in.test.ts b/integration/tests/elements/next-sign-in.test.ts index 6534f28d3e5..baae97368d1 100644 --- a/integration/tests/elements/next-sign-in.test.ts +++ b/integration/tests/elements/next-sign-in.test.ts @@ -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.getByTestId('form-feedback-error')).toBeVisible(); + await expect(u.page.getByTestId('form-feedback-error')).toHaveText(/^password is incorrect/i); await u.po.expect.toBeSignedOut(); }); @@ -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.getByTestId('form-feedback-error')).toBeVisible(); + await expect(u.page.getByTestId('form-feedback-error')).toHaveText(/^password is incorrect/i); await u.page.getByRole('button', { name: /use another method/i }).click(); await u.po.signIn.getAltMethodsEmailCodeButton().click(); diff --git a/integration/tests/elements/next-sign-up.test.ts b/integration/tests/elements/next-sign-up.test.ts index 70f7d42dd25..8a017c20566 100644 --- a/integration/tests/elements/next-sign-up.test.ts +++ b/integration/tests/elements/next-sign-up.test.ts @@ -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.getByTestId('form-feedback-error')).toBeVisible(); + await expect(u.page.getByTestId('form-feedback-error')).toHaveText(/Passwords must be \d+ characters or more/i); await u.po.expect.toBeSignedOut(); diff --git a/integration/tests/sign-in-flow.test.ts b/integration/tests/sign-in-flow.test.ts index 20326de4204..0d16c45e327 100644 --- a/integration/tests/sign-in-flow.test.ts +++ b/integration/tests/sign-in-flow.test.ts @@ -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.getByTestId('form-feedback-error')).toBeVisible(); + await expect(u.page.getByTestId('form-feedback-error')).toHaveText(/password is incorrect/i); await u.po.expect.toBeSignedOut(); }); @@ -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.getByTestId('form-feedback-error')).toBeVisible(); + await expect(u.page.getByTestId('form-feedback-error')).toHaveText(/password is incorrect/i); await u.po.signIn.getUseAnotherMethodLink().click(); await u.po.signIn.getAltMethodsEmailCodeButton().click(); diff --git a/integration/tests/sign-in-or-up-flow.test.ts b/integration/tests/sign-in-or-up-flow.test.ts index c814201be65..bb743f17ac3 100644 --- a/integration/tests/sign-in-or-up-flow.test.ts +++ b/integration/tests/sign-in-or-up-flow.test.ts @@ -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.getByTestId('form-feedback-error')).toBeVisible(); + await expect(u.page.getByTestId('form-feedback-error')).toHaveText(/password is incorrect/i); await u.po.expect.toBeSignedOut(); }); @@ -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.getByTestId('form-feedback-error')).toBeVisible(); + await expect(u.page.getByTestId('form-feedback-error')).toHaveText(/password is incorrect/i); await u.po.signIn.getUseAnotherMethodLink().click(); await u.po.signIn.getAltMethodsEmailCodeButton().click(); diff --git a/integration/tests/sign-up-flow.test.ts b/integration/tests/sign-up-flow.test.ts index af9df350f4c..d9ba89d6e51 100644 --- a/integration/tests/sign-up-flow.test.ts +++ b/integration/tests/sign-up-flow.test.ts @@ -54,7 +54,10 @@ 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.getByTestId('form-feedback-error').first()).toBeVisible(); + await expect(u.page.getByTestId('form-feedback-error').first()).toHaveText( + /your password must contain \d+ or more characters/i, + ); // Check if user is signed out await u.po.expect.toBeSignedOut(); diff --git a/packages/clerk-js/src/ui/components/SignIn/__tests__/ResetPassword.test.tsx b/packages/clerk-js/src/ui/components/SignIn/__tests__/ResetPassword.test.tsx index f5778d8c918..72aa83a13b5 100644 --- a/packages/clerk-js/src/ui/components/SignIn/__tests__/ResetPassword.test.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/__tests__/ResetPassword.test.tsx @@ -37,7 +37,8 @@ describe('ResetPassword', () => { const passwordField = screen.getByLabelText(/New password/i); fireEvent.focus(passwordField); - await screen.findByText(/Your password must contain 8 or more characters/i); + const infoElement = await screen.findByTestId('form-feedback-info'); + expect(infoElement).toHaveTextContent(/Your password must contain 8 or more characters/i); }); it('renders a hidden identifier field', async () => { @@ -115,10 +116,12 @@ 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.`); + const errorElement = await screen.findByTestId('form-feedback-error'); + expect(errorElement).toHaveTextContent(/Passwords don't match/i); await userEvent.clear(confirmField); - await screen.findByText(`Passwords don't match.`); + const errorElementAfterClear = await screen.findByTestId('form-feedback-error'); + expect(errorElementAfterClear).toHaveTextContent(/Passwords don't match/i); }); it('navigates to the root page upon pressing the back link', async () => { diff --git a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx index d784f16ad3a..d610e2d9150 100644 --- a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx @@ -186,7 +186,8 @@ describe('SignInFactorOne', () => { const { userEvent } = render(, { wrapper }); await userEvent.type(screen.getByLabelText('Password'), '123456'); await userEvent.click(screen.getByText('Continue')); - await screen.findByText('Incorrect Password'); + const errorElement = await screen.findByTestId('form-feedback-error'); + expect(errorElement).toHaveTextContent(/Incorrect Password/i); }); it('redirects back to sign-in if the user is locked', async () => { @@ -558,7 +559,8 @@ describe('SignInFactorOne', () => { ); const { userEvent } = render(, { wrapper }); await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456'); - await screen.findByText('Incorrect code'); + const errorElement = await screen.findByTestId('form-feedback-error'); + expect(errorElement).toHaveTextContent(/Incorrect code/i); }); it('redirects back to sign-in if the user is locked', async () => { @@ -663,7 +665,8 @@ describe('SignInFactorOne', () => { ); const { userEvent } = render(, { wrapper }); await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456'); - await screen.findByText('Incorrect phone code'); + const errorElement = await screen.findByTestId('form-feedback-error'); + expect(errorElement).toHaveTextContent(/Incorrect phone code/i); }); it('redirects back to sign-in if the user is locked', async () => { diff --git a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorTwo.test.tsx b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorTwo.test.tsx index 019e6324047..3d944954df9 100644 --- a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorTwo.test.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorTwo.test.tsx @@ -185,7 +185,7 @@ describe('SignInFactorTwo', () => { ); const { userEvent } = render(, { wrapper }); await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456'); - expect(await screen.findByText('Incorrect phone code')).toBeDefined(); + expect(await screen.findByTestId('form-feedback-error')).toHaveTextContent(/Incorrect phone code/i); }); it('redirects back to sign-in if the user is locked', async () => { @@ -274,7 +274,7 @@ describe('SignInFactorTwo', () => { ); const { userEvent } = render(, { wrapper }); await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456'); - expect(await screen.findByText('Incorrect authenticator code')).toBeDefined(); + expect(await screen.findByTestId('form-feedback-error')).toHaveTextContent(/Incorrect authenticator code/i); }); }); @@ -367,7 +367,7 @@ describe('SignInFactorTwo', () => { const { userEvent, getByLabelText, getByText } = render(, { wrapper }); await userEvent.type(getByLabelText('Backup code'), '123456'); await userEvent.click(getByText('Continue')); - expect(await screen.findByText('Incorrect backup code')).toBeDefined(); + expect(await screen.findByTestId('form-feedback-error')).toHaveTextContent(/Incorrect backup code/i); }); it('redirects back to sign-in if the user is locked', async () => { diff --git a/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpContinue.test.tsx b/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpContinue.test.tsx index ca78577f8c3..96495d93cd3 100644 --- a/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpContinue.test.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpContinue.test.tsx @@ -168,9 +168,8 @@ describe('SignUpContinue', () => { await userEvent.click(button); await waitFor(() => expect(fixtures.signUp.update).toHaveBeenCalled()); - await waitFor(() => - expect(screen.queryByText(/^Your username must be between 4 and 40 characters long./i)).toBeInTheDocument(), - ); + const errorElement = await screen.findByTestId('form-feedback-error'); + expect(errorElement).toHaveTextContent(/Your username must be between 4 and 40 characters long/i); }); it('renders error for existing username', async () => { @@ -203,9 +202,8 @@ describe('SignUpContinue', () => { await userEvent.click(button); await waitFor(() => expect(fixtures.signUp.update).toHaveBeenCalled()); - await waitFor(() => - expect(screen.queryByText(/^This username is taken. Please try another./i)).toBeInTheDocument(), - ); + const errorElement = await screen.findByTestId('form-feedback-error'); + expect(errorElement).toHaveTextContent(/This username is taken. Please try another/i); }); describe('Sign in Link', () => { diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx index f23eaff6692..bfe8e96d82c 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx @@ -538,7 +538,7 @@ describe('PasswordSection', () => { await userEvent.type(confirmField, 'test'); fireEvent.blur(confirmField); await waitFor(() => { - screen.getByText(/or more/i); + expect(screen.getByTestId('form-feedback-error')).toHaveTextContent(/or more/i); }); }); diff --git a/packages/clerk-js/src/ui/elements/FormControl.tsx b/packages/clerk-js/src/ui/elements/FormControl.tsx index ac4e3e99a19..045c1aa77e6 100644 --- a/packages/clerk-js/src/ui/elements/FormControl.tsx +++ b/packages/clerk-js/src/ui/elements/FormControl.tsx @@ -183,6 +183,7 @@ export const FormFeedback = (props: FormFeedbackProps) => { > ({ @@ -194,6 +195,7 @@ export const FormFeedback = (props: FormFeedbackProps) => { /> ({ diff --git a/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx b/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx index 6adaec9b8f2..f5bb873a981 100644 --- a/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx +++ b/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render } from '@testing-library/react'; +import { fireEvent, render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, expect, it } from 'vitest'; @@ -135,11 +135,12 @@ describe('PlainInput', () => { placeholder: 'some placeholder', }); - const { getByRole, getByLabelText, findByText, container } = render(, { wrapper }); + const { getByRole, getByLabelText, findByTestId, container } = render(, { wrapper }); await userEvent.click(getByRole('button', { name: /set error/i })); - expect(await findByText(/Some Error/i)).toBeInTheDocument(); + expect(await findByTestId('form-feedback-error')).toBeInTheDocument(); + expect(await findByTestId('form-feedback-error')).toHaveTextContent(/Some Error/i); const input = getByLabelText(/some label/i); expect(input).toHaveAttribute('aria-invalid', 'true'); @@ -160,10 +161,11 @@ describe('PlainInput', () => { infoText: 'some info', }); - const { findByLabelText, findByText } = render(, { wrapper }); + const { findByLabelText, findByTestId } = render(, { wrapper }); fireEvent.focus(await findByLabelText(/some label/i)); - expect(await findByText(/some info/i)).toBeInTheDocument(); + expect(await findByTestId('form-feedback-info')).toBeInTheDocument(); + expect(await findByTestId('form-feedback-info')).toHaveTextContent(/some info/i); }); it('with success feedback and aria-describedby', async () => { @@ -174,11 +176,12 @@ describe('PlainInput', () => { placeholder: 'some placeholder', }); - const { getByRole, getByLabelText, findByText, container } = render(, { wrapper }); + const { getByRole, getByLabelText, findByTestId, container } = render(, { wrapper }); await userEvent.click(getByRole('button', { name: /set success/i })); - expect(await findByText(/Some Success/i)).toBeInTheDocument(); + expect(await findByTestId('form-feedback-success')).toBeInTheDocument(); + expect(await findByTestId('form-feedback-success')).toHaveTextContent(/Some Success/i); const input = getByLabelText(/some label/i); expect(input).toHaveAttribute('aria-invalid', 'false'); @@ -198,11 +201,12 @@ describe('PlainInput', () => { placeholder: 'some placeholder', }); - const { getByRole, getByLabelText, findByText, container } = render(, { wrapper }); + const { getByRole, getByLabelText, findByTestId, container } = render(, { wrapper }); // Start with error await userEvent.click(getByRole('button', { name: /set error/i })); - expect(await findByText(/Some Error/i)).toBeInTheDocument(); + expect(await findByTestId('form-feedback-error')).toBeInTheDocument(); + expect(await findByTestId('form-feedback-error')).toHaveTextContent(/Some Error/i); let input = getByLabelText(/some label/i); expect(input).toHaveAttribute('aria-invalid', 'true'); @@ -210,7 +214,8 @@ describe('PlainInput', () => { // Transition to success await userEvent.click(getByRole('button', { name: /set success/i })); - expect(await findByText(/Some Success/i)).toBeInTheDocument(); + expect(await findByTestId('form-feedback-success')).toBeInTheDocument(); + expect(await findByTestId('form-feedback-success')).toHaveTextContent(/Some Success/i); input = getByLabelText(/some label/i); expect(input).toHaveAttribute('aria-invalid', 'false'); @@ -222,7 +227,7 @@ describe('PlainInput', () => { expect(successElement).toHaveTextContent(/Some Success/i); }); - it('aria-live attribute is correctly applied', async () => { + it('renders the aria-live region for screen reader support', async () => { const { wrapper } = await createFixtures(); const { Field } = createField('firstname', 'init value', { type: 'text', @@ -230,27 +235,34 @@ describe('PlainInput', () => { placeholder: 'some placeholder', }); - const { getByRole, findByText, container } = render(, { wrapper }); + const { getByRole, container } = render(, { wrapper }); - // Set error feedback - await userEvent.click(getByRole('button', { name: /set error/i })); - expect(await findByText(/Some Error/i)).toBeInTheDocument(); + const ariaLiveRegion = container.querySelector('[aria-live="polite"]'); + expect(ariaLiveRegion).toBeInTheDocument(); - // Verify the visible error message has aria-live="polite" - const errorElement = container.querySelector('#error-firstname'); - expect(errorElement).toHaveAttribute('aria-live', 'polite'); + expect(ariaLiveRegion).toHaveAttribute('aria-live', 'polite'); + expect(ariaLiveRegion).toHaveAttribute('aria-atomic', 'true'); - // Transition to success - await userEvent.click(getByRole('button', { name: /set success/i })); - expect(await findByText(/Some Success/i)).toBeInTheDocument(); + // It is visually hidden + expect(ariaLiveRegion).toHaveStyle({ + position: 'absolute', + width: '1px', + height: '1px', + overflow: 'hidden', + }); - // Verify the visible success message has aria-live="polite" - const successElement = container.querySelector('#firstname-success-feedback'); - expect(successElement).toHaveAttribute('aria-live', 'polite'); + expect(ariaLiveRegion).toHaveTextContent(''); - // The previous error message should now have aria-live="off" (though it might still exist in DOM but hidden) - // Verify exactly one element has aria-live="polite" at a time - const allAriaLivePolite = container.querySelectorAll('[aria-live="polite"]'); - expect(allAriaLivePolite.length).toBe(1); + await userEvent.click(getByRole('button', { name: /set error/i })); + await waitFor(() => { + const ariaLiveRegion = container.querySelector('[aria-live="polite"]'); + expect(ariaLiveRegion?.textContent).toMatch(/Some Error/i); + }); + + await userEvent.click(getByRole('button', { name: /set success/i })); + await waitFor(() => { + const ariaLiveRegion = container.querySelector('[aria-live="polite"]'); + expect(ariaLiveRegion?.textContent).toMatch(/Some Success/i); + }); }); }); diff --git a/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx b/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx index ad56cae4364..09ff09b7d26 100644 --- a/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx +++ b/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx @@ -157,7 +157,7 @@ describe('RadioGroup', () => { type: 'radio', }); - const { getAllByRole, getByRole, findByText } = render( + const { getAllByRole, getByRole, findByTestId } = render( { ); await userEvent.click(getByRole('button', { name: /set error/i })); - expect(await findByText(/Some Error/i)).toBeInTheDocument(); + expect(await findByTestId('form-feedback-error')).toBeInTheDocument(); + expect(await findByTestId('form-feedback-error')).toHaveTextContent(/Some Error/i); const radios = getAllByRole('radio'); radios.forEach(radio => { @@ -188,9 +189,10 @@ describe('RadioGroup', () => { infoText: 'some info', }); - const { findByLabelText, findByText } = render(, { wrapper }); + const { findByLabelText, findByTestId } = render(, { wrapper }); fireEvent.focus(await findByLabelText(/One/i)); - expect(await findByText(/some info/i)).toBeInTheDocument(); + expect(await findByTestId('form-feedback-info')).toBeInTheDocument(); + expect(await findByTestId('form-feedback-info')).toHaveTextContent(/some info/i); }); }); From a6552ad5b4318aaa42da2bd7165dc7f61c8d8208 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 5 Nov 2025 18:23:50 -0500 Subject: [PATCH 10/11] revert elements changes --- integration/tests/elements/next-sign-in.test.ts | 6 ++---- integration/tests/elements/next-sign-up.test.ts | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/integration/tests/elements/next-sign-in.test.ts b/integration/tests/elements/next-sign-in.test.ts index baae97368d1..6534f28d3e5 100644 --- a/integration/tests/elements/next-sign-in.test.ts +++ b/integration/tests/elements/next-sign-in.test.ts @@ -166,8 +166,7 @@ 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.getByTestId('form-feedback-error')).toBeVisible(); - await expect(u.page.getByTestId('form-feedback-error')).toHaveText(/^password is incorrect/i); + await expect(u.page.getByText(/^password is incorrect/i)).toBeVisible(); await u.po.expect.toBeSignedOut(); }); @@ -182,8 +181,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('Next.js S await u.po.signIn.setPassword('wrong-password'); await u.po.signIn.continue(); - await expect(u.page.getByTestId('form-feedback-error')).toBeVisible(); - await expect(u.page.getByTestId('form-feedback-error')).toHaveText(/^password is incorrect/i); + await expect(u.page.getByText(/^password is incorrect/i)).toBeVisible(); await u.page.getByRole('button', { name: /use another method/i }).click(); await u.po.signIn.getAltMethodsEmailCodeButton().click(); diff --git a/integration/tests/elements/next-sign-up.test.ts b/integration/tests/elements/next-sign-up.test.ts index 8a017c20566..70f7d42dd25 100644 --- a/integration/tests/elements/next-sign-up.test.ts +++ b/integration/tests/elements/next-sign-up.test.ts @@ -79,8 +79,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('Next.js S }); // Check if password error is visible - await expect(u.page.getByTestId('form-feedback-error')).toBeVisible(); - await expect(u.page.getByTestId('form-feedback-error')).toHaveText(/Passwords must be \d+ characters or more/i); + await expect(u.page.getByText(/Passwords must be \d+ characters or more/i)).toBeVisible(); await u.po.expect.toBeSignedOut(); From e8083e0b1fe7b80381cacd44c8fd5bd2c5b14da0 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 5 Nov 2025 18:25:07 -0500 Subject: [PATCH 11/11] fix test --- integration/tests/sign-in-or-up-restricted-mode.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/integration/tests/sign-in-or-up-restricted-mode.test.ts b/integration/tests/sign-in-or-up-restricted-mode.test.ts index 0fcab87af2e..66ed92f0f4f 100644 --- a/integration/tests/sign-in-or-up-restricted-mode.test.ts +++ b/integration/tests/sign-in-or-up-restricted-mode.test.ts @@ -33,6 +33,7 @@ test.describe('sign-in-or-up restricted mode @nextjs', () => { await expect(u.page.getByText(/continue to/i)).toBeHidden(); await u.po.signIn.getIdentifierInput().fill(fakeUser.email); await u.po.signIn.continue(); - await expect(u.page.getByText(/Couldn't find your account\./i)).toBeVisible(); + await expect(u.page.getByTestId('form-feedback-error')).toBeVisible(); + await expect(u.page.getByTestId('form-feedback-error')).toHaveText(/Couldn't find your account\./i); }); });