Some checks failed
Nouveau composant `components/ui/PasswordInput` : un `<input>` de mot de passe avec un bouton œil (`lucide-react` Eye/EyeOff) qui bascule le `type` entre "password" et "text". Visibilité en état local, toujours masquée au montage, jamais persistée. Bouton `type="button"` (ne soumet pas le formulaire) + `aria-label` traduit (`common.passwordInput.*`) + `aria-pressed`. Branché dans `LoginPage` et `SignupPage` à la place du `<input type="password">` nu. La règle `.auth-card button` de `auth-form.scss` est resserrée sur `button[type="submit"]` pour que le bouton œil n'hérite pas du traitement plein-largeur du bouton de connexion. Tests : `cypress/component/PasswordInput.cy.tsx` (7 cas, composant isolé) + `cypress/e2e/auth-form.cy.ts` (mode layout, l'œil sur /login et /signup). Closes #134 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
105 lines
3.7 KiB
TypeScript
105 lines
3.7 KiB
TypeScript
import { useState } from "react";
|
|
import "../../src/i18n/i18n";
|
|
import { PasswordInput } from "../../src/components/ui/PasswordInput";
|
|
|
|
// Mounts PasswordInput in isolation (no form, no router, no backend) — the
|
|
// same "generic component test" posture as CheckboxOption.cy.tsx. Needs
|
|
// `../../src/i18n/i18n` imported for its side effect: unlike Checkbox/Radio,
|
|
// this component calls `useTranslation()` for the eye button's aria-label,
|
|
// and with no <I18nextProvider> in the tree it falls back to that default
|
|
// instance (see TechStepCorrectionPopover.cy.tsx for the same note).
|
|
|
|
const SHOW_LABEL = "Afficher le mot de passe";
|
|
const HIDE_LABEL = "Masquer le mot de passe";
|
|
|
|
/**
|
|
* Controlled wrapper — PasswordInput holds no text state of its own, so a
|
|
* real caller (LoginPage, SignupPage) always owns the value. `onSpy` lets a
|
|
* test assert on what each change reports.
|
|
*/
|
|
function Wrapper({ onSpy }: { onSpy?: (value: string) => void }) {
|
|
const [value, setValue] = useState("");
|
|
return (
|
|
<PasswordInput
|
|
id="password"
|
|
value={value}
|
|
onChange={(next) => {
|
|
setValue(next);
|
|
onSpy?.(next);
|
|
}}
|
|
autoComplete="current-password"
|
|
/>
|
|
);
|
|
}
|
|
|
|
describe("PasswordInput", () => {
|
|
it("renders a masked password field by default", () => {
|
|
cy.mount(<Wrapper />);
|
|
cy.get("input#password").should("have.attr", "type", "password");
|
|
});
|
|
|
|
it("forwards id and autoComplete to the native input", () => {
|
|
cy.mount(<Wrapper />);
|
|
cy.get("input#password").should("have.attr", "autocomplete", "current-password");
|
|
});
|
|
|
|
it("reveals the text when the eye is clicked, then re-masks it on a second click", () => {
|
|
cy.mount(<Wrapper />);
|
|
cy.get("input#password").type("hunter2").should("have.attr", "type", "password");
|
|
|
|
cy.get("button.password-input__toggle").click();
|
|
cy.get("input#password").should("have.attr", "type", "text").and("have.value", "hunter2");
|
|
|
|
cy.get("button.password-input__toggle").click();
|
|
cy.get("input#password").should("have.attr", "type", "password").and("have.value", "hunter2");
|
|
});
|
|
|
|
it("labels the toggle with the action the next click performs, and exposes state via aria-pressed", () => {
|
|
cy.mount(<Wrapper />);
|
|
cy.get("button.password-input__toggle")
|
|
.should("have.attr", "aria-label", SHOW_LABEL)
|
|
.and("have.attr", "aria-pressed", "false");
|
|
|
|
cy.get("button.password-input__toggle").click();
|
|
cy.get("button.password-input__toggle")
|
|
.should("have.attr", "aria-label", HIDE_LABEL)
|
|
.and("have.attr", "aria-pressed", "true");
|
|
});
|
|
|
|
it("is a controlled input — it shows the parent's value and reports every change", () => {
|
|
const onSpy = cy.stub().as("onSpy");
|
|
cy.mount(<Wrapper onSpy={onSpy} />);
|
|
|
|
cy.get("input#password").type("abc");
|
|
cy.get("input#password").should("have.value", "abc");
|
|
cy.get("@onSpy").should("have.been.calledWith", "abc");
|
|
});
|
|
|
|
it("does not submit a surrounding form when the eye is clicked — the toggle is a plain button", () => {
|
|
const onSubmit = cy.stub().as("onSubmit");
|
|
cy.mount(
|
|
<form
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
onSubmit();
|
|
}}
|
|
>
|
|
<Wrapper />
|
|
<button type="submit">Go</button>
|
|
</form>,
|
|
);
|
|
|
|
cy.get("button.password-input__toggle").click();
|
|
cy.get("@onSubmit").should("not.have.been.called");
|
|
});
|
|
|
|
it("starts hidden again on a fresh mount — visibility is never persisted", () => {
|
|
cy.mount(<Wrapper />);
|
|
cy.get("button.password-input__toggle").click();
|
|
cy.get("input#password").should("have.attr", "type", "text");
|
|
|
|
// A brand-new mount, as if the visitor navigated back to the page later.
|
|
cy.mount(<Wrapper />);
|
|
cy.get("input#password").should("have.attr", "type", "password");
|
|
});
|
|
});
|