batchCooking/apps/web/cypress/component/PasswordInput.cy.tsx
Nicolas 24bd2239fc
Some checks failed
CI / test (push) Failing after 1m0s
CI / e2e (push) Failing after 1m43s
CI / lint (push) Successful in 2m19s
CI / build (push) Successful in 2m28s
CI / intent-service-test (push) Successful in 4m22s
CI / build-android (push) Has been skipped
feat(auth): bouton afficher / masquer le mot de passe (#134)
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>
2026-09-09 19:33:29 +02:00

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");
});
});