fix(auth): nome do botão de senha em texto sr-only para não colidir com getByLabel(/senha/i)

O aria-label "Mostrar senha" fazia getByLabel(/senha/i) e getByLabel("Senha")
do Playwright acharem o campo E o botão (strict mode violation no .fill()),
e 45 specs e2e entram no app assim. O nome acessível dos três botões novos
passa a vir de um <span className="sr-only"> dentro do botão: o leitor de
tela anuncia o mesmo, getByRole("button", { name }) segue achando o botão, e
a busca por rótulo volta a achar só o campo.

Caso novo em PasswordVisibility.test.tsx vigia a regressão; sabotado contra
o LoginForm anterior: 1 failed | 3 passed.

Refs: #1871

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
melgarafael
2026-09-28 16:59:01 -03:00
co-authored by Claude Opus 5.5
parent 1df99a181b
commit b282ef0d6d
3 changed files with 17 additions and 5 deletions
+2 -1
View File
@@ -89,10 +89,11 @@ export function LoginForm({ next }: { next?: string }) {
<button
type="button"
className="absolute inset-y-0 right-0 flex w-11 items-center justify-center rounded-r-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-accent-500 focus-visible:outline-hidden focus-visible:ring-inset"
aria-label={t(showPassword ? "Ocultar senha" : "Mostrar senha")}
aria-pressed={showPassword}
onClick={() => setShowPassword((visible) => !visible)}
>
{/* Nome em sr-only, não aria-label: getByLabel(/senha/i) casa aria-label e acharia o botão junto do campo. */}
<span className="sr-only">{t(showPassword ? "Ocultar senha" : "Mostrar senha")}</span>
{showPassword ? <EyeSlash size={20} aria-hidden /> : <Eye size={20} aria-hidden />}
</button>
</div>
@@ -28,6 +28,12 @@ describe("visibilidade de senha no acesso", () => {
expect(password).toHaveAttribute("type", "password");
});
// As specs e2e entram com getByLabel(/senha/i); nome do botão em aria-label o faria casar junto do campo.
it("o botão não entra na busca por rótulo de senha", () => {
render(<LoginForm />);
expect(screen.getAllByLabelText(/senha/i)).toEqual([screen.getByLabelText("Senha")]);
});
it.each(["cadastro", "convite"])(
"mostra cada senha separadamente no %s e informa a força",
(modo) => {
+9 -4
View File
@@ -229,10 +229,11 @@ export function SignupForm({ convite }: { convite?: ConviteDoSignup }) {
<button
type="button"
className="absolute inset-y-0 right-0 flex w-11 items-center justify-center rounded-r-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-accent-500 focus-visible:outline-hidden focus-visible:ring-inset"
aria-label={t(showPassword ? "Ocultar senha" : "Mostrar senha")}
aria-pressed={showPassword}
onClick={() => setShowPassword((visible) => !visible)}
>
{/* Nome em sr-only, não aria-label: getByLabel(/senha/i) casa aria-label e acharia o botão junto do campo. */}
<span className="sr-only">{t(showPassword ? "Ocultar senha" : "Mostrar senha")}</span>
{showPassword ? <EyeSlash size={20} aria-hidden /> : <Eye size={20} aria-hidden />}
</button>
</div>
@@ -255,12 +256,16 @@ export function SignupForm({ convite }: { convite?: ConviteDoSignup }) {
<button
type="button"
className="absolute inset-y-0 right-0 flex w-11 items-center justify-center rounded-r-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-accent-500 focus-visible:outline-hidden focus-visible:ring-inset"
aria-label={t(
showPasswordConfirm ? "Ocultar confirmação da senha" : "Mostrar confirmação da senha",
)}
aria-pressed={showPasswordConfirm}
onClick={() => setShowPasswordConfirm((visible) => !visible)}
>
<span className="sr-only">
{t(
showPasswordConfirm
? "Ocultar confirmação da senha"
: "Mostrar confirmação da senha",
)}
</span>
{showPasswordConfirm ? (
<EyeSlash size={20} aria-hidden />
) : (