From 120d0d1ac24daaddfbaeff7c7bd0b6b6f9c89e0e Mon Sep 17 00:00:00 2001 From: Blomios Date: Fri, 17 Jul 2026 10:40:26 +0200 Subject: [PATCH] fix(frontend): clavier texte et normalisation du code d'appairage (#75) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le code d'appairage est de l'hexadécimal majuscule (p. ex. 3F7A9C21), pas une suite de chiffres. Le champ appelait pourtant le pavé numérique des mobiles, rendant les lettres A-F non saisissables. Le champ passe donc en inputMode="text" avec autoCapitalize="characters", ce qui revient sur le choix inverse fait en #69 : ce ticket avait supposé un code purement numérique. Le placeholder et l'aide du champ décrivent désormais le format réel. La saisie est normalisée (majuscules, espaces retirés) avant pair() car le serveur compare le code strictement ; la dette correspondante côté backend est suivie en #76. Co-Authored-By: Claude Opus 4.8 --- .../src/features/web/PairingScreen.test.tsx | 77 +++++++++++++++++++ frontend/src/features/web/PairingScreen.tsx | 28 ++++--- 2 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 frontend/src/features/web/PairingScreen.test.tsx diff --git a/frontend/src/features/web/PairingScreen.test.tsx b/frontend/src/features/web/PairingScreen.test.tsx new file mode 100644 index 0000000..4aab76a --- /dev/null +++ b/frontend/src/features/web/PairingScreen.test.tsx @@ -0,0 +1,77 @@ +/** + * #75 — the pairing code is uppercase hex (`3F7A9C21`), not digits: + * - the field must summon the text keyboard, not the numeric keypad; + * - a lowercase entry must reach `session.pair()` uppercased and space-free, + * because the server compares the code strictly. + */ +import { describe, it, expect, vi } from "vitest"; +import { fireEvent, render, screen } from "@testing-library/react"; + +import { WebSession } from "@/adapters/http"; +import type { FetchLike } from "@/adapters/http/httpInvoker"; +import type { FlagStore } from "@/adapters/http/webSession"; +import { PairingScreen } from "./PairingScreen"; + +function memStore(): FlagStore { + const map = new Map(); + return { + getItem: (k) => map.get(k) ?? null, + setItem: (k, v) => void map.set(k, v), + removeItem: (k) => void map.delete(k), + }; +} + +const okFetch: FetchLike = async () => ({ + ok: true, + status: 200, + json: async () => ({ ok: true }), + text: async () => "{}", +}); + +function setup() { + const session = new WebSession({ baseUrl: "https://h", fetchImpl: okFetch, store: memStore() }); + const pair = vi.spyOn(session, "pair"); + const onPaired = vi.fn(); + render(); + return { pair, onPaired }; +} + +describe("PairingScreen code field", () => { + it("asks for the text keyboard, not the numeric keypad", () => { + setup(); + const field = screen.getByLabelText("Code d'appairage"); + + expect(field.getAttribute("inputmode")).toBe("text"); + expect(field.getAttribute("autocapitalize")).toBe("characters"); + expect(field.getAttribute("autocomplete")).toBe("one-time-code"); + }); + + it("describes the code without lying about its shape", () => { + setup(); + const field = screen.getByLabelText("Code d'appairage"); + + expect(field.getAttribute("placeholder")).toBe("p. ex. AB12CD34"); + expect(screen.getByText("8 caractères : chiffres et lettres A-F")).toBeTruthy(); + }); + + it("uppercases and strips spaces before pairing", async () => { + const { pair, onPaired } = setup(); + + fireEvent.change(screen.getByLabelText("Code d'appairage"), { + target: { value: " ab12cd34 " }, + }); + fireEvent.click(screen.getByRole("button", { name: "Appairer" })); + + await vi.waitFor(() => expect(onPaired).toHaveBeenCalled()); + expect(pair).toHaveBeenCalledWith("AB12CD34"); + }); + + it("keeps submit disabled for a blank code", () => { + setup(); + const submit = screen.getByRole("button", { name: "Appairer" }); + expect((submit as HTMLButtonElement).disabled).toBe(true); + + fireEvent.change(screen.getByLabelText("Code d'appairage"), { target: { value: " " } }); + expect((submit as HTMLButtonElement).disabled).toBe(true); + }); +}); diff --git a/frontend/src/features/web/PairingScreen.tsx b/frontend/src/features/web/PairingScreen.tsx index 32bb704..89edf46 100644 --- a/frontend/src/features/web/PairingScreen.tsx +++ b/frontend/src/features/web/PairingScreen.tsx @@ -31,6 +31,14 @@ function describe(e: unknown): string { return String(e); } +/** + * The server compares the code strictly against the uppercase hex it generated, + * so the UI uppercases (and drops stray spaces) before sending. + */ +function normalize(raw: string): string { + return raw.replace(/\s+/g, "").toUpperCase(); +} + export function PairingScreen({ session, onPaired }: PairingScreenProps) { const [code, setCode] = useState(""); const [error, setError] = useState(null); @@ -38,12 +46,12 @@ export function PairingScreen({ session, onPaired }: PairingScreenProps) { async function submit(e: FormEvent): Promise { e.preventDefault(); - const trimmed = code.trim(); - if (!trimmed || busy) return; + const normalized = normalize(code); + if (!normalized || busy) return; setBusy(true); setError(null); try { - await session.pair(trimmed); + await session.pair(normalized); onPaired(); } catch (err) { setError(describe(err)); @@ -64,20 +72,20 @@ export function PairingScreen({ session, onPaired }: PairingScreenProps) {

- + {({ id, describedBy }) => ( setCode(e.target.value)} - placeholder="p. ex. 4821-93" + placeholder="p. ex. AB12CD34" autoFocus autoComplete="one-time-code" - // #69 — phones: summon the numeric-ish keypad for a pairing code - // and keep the OS from "helpfully" capitalising/correcting it. - inputMode="numeric" - autoCapitalize="off" + // #75 — the code mixes digits and letters, so phones need the + // text keyboard; uppercase it on the way in, never autocorrect. + inputMode="text" + autoCapitalize="characters" autoCorrect="off" spellCheck={false} disabled={busy} @@ -92,7 +100,7 @@ export function PairingScreen({ session, onPaired }: PairingScreenProps) {

)} -