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) {

)} -