Something went wrong. Try again.
Web frontend and supporting services for lance.blue
Something went wrong. Try again.
4.9 kB · 124 lines
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125/** * The sign-in form is the one form on the site a stranger has to get through, * and the way it goes wrong is silent. * * It minted `handle-N` ids so that "labels need an id to point at" — and no * label was ever written. Nothing pointed at the id, so the field's only * accessible name was its placeholder: a name that disappears the moment * anyone types, and that asks a voice-control user to say * "yourname.bsky.social" to reach the box. Lighthouse scores the page 100 * either way, because a placeholder does technically produce a name. * * The dialog around it had the matching hole: `<dialog>` with no * aria-labelledby is announced as "dialog" and nothing else, at the one moment * a player needs telling what took over the screen. * * Both are structural. Nothing errors, both render, and the only way to see * either is to read the accessibility tree. So this reads the source instead. * * Run with `npm test`. */import { test } from "node:test";import assert from "node:assert/strict";import { readFile } from "node:fs/promises";import { fileURLToPath } from "node:url";
const src = fileURLToPath(new URL("../src/", import.meta.url));const form = await readFile(`${src}signin-form.ts`, "utf8");const modal = await readFile(`${src}signin-modal.ts`, "utf8");
// An id earns its keep by being pointed at, not by being set. `id` was set on// the input from the day it was minted, so "is it referenced?" was always yes// and the missing label still went unnoticed for as long as it did.const POINTERS = ["htmlFor", '"aria-describedby"', '"aria-labelledby"'];
test("every id the form mints is pointed at, not just set", () => { // The bare `id` the input takes, plus `const blurbId`, `const headingId`. assert.match(form, /const id = `handle-/, "the handle field's id moved"); const minted = [ "id", ...[...form.matchAll(/const (\w+Id) =/g)].map((m) => m[1]), ];
const both = `${form}\n${modal}`; for (const name of minted) { const pointedAt = POINTERS.some((attr) => new RegExp(`${attr}: ${name}\\b`).test(both), ); assert.ok( pointedAt, `${name} is minted but nothing points at it with ${POINTERS.join(" / ")} — ` + `which is how the handle field went without a label`, ); }});
test("the handle field is named by a label, not by its placeholder", () => { assert.match( form, /el\("label",\s*\{[^}]*htmlFor: id/, "no <label> points at the handle field; its name falls back to the placeholder", ); assert.match( form, /"aria-describedby": blurbId/, "the line explaining what a handle is is not tied to the field", ); // The placeholder is a fine example of the format and a bad name. Keeping it // is deliberate; relying on it is the bug. assert.match(form, /placeholder: "yourname\.bsky\.social"/);});
test("the dialog is named by the form's heading", () => { assert.match( form, /el\("h2",\s*\{ id: headingId \}/, "the heading lost the id the dialog names itself with", ); assert.match( form, /return \{[^}]*\bheadingId\b[^}]*\}/, "signInForm no longer hands its heading id back", ); assert.match( modal, /"aria-labelledby": headingId/, "the dialog is unnamed; a modal with no name announces only 'dialog'", );});
test("the dialog answers Escape itself, because the browser's own will not", () => { // <actor-typeahead> wraps the handle field and preventDefaults every Escape // it sees, and a prevented keydown never becomes the dialog's `cancel`. The // field is focused the moment the dialog opens, so relying on the browser // meant Escape did nothing from the one control a player was already in. // // Nothing about that is visible from the source of this repo alone — it // reads as a plain <dialog>, and plain <dialog> closes on Escape — so the // handler is the sort of thing a tidy-up removes. // The call spans several lines and its own body contains a `);`, so it is // read to the closing paren at this call's indent rather than by a lazy // regex — which stops inside the handler. const start = modal.indexOf('dialog.addEventListener(\n "keydown"'); assert.notEqual( start, -1, "the dialog no longer handles Escape, and the typeahead eats it", ); const end = modal.indexOf("\n );", start); assert.notEqual(end, -1, "the keydown listener has no end"); const listener = modal.slice(start, end);
assert.match( listener, /event\.key !== "Escape"/, "the keydown handler is not about Escape any more", ); // Capture. In the bubble phase the typeahead has already had the key. assert.match( listener, /\n {4}true,\s*$/, "the Escape handler is not in the capture phase, so it runs after the " + "typeahead has already prevented the key", );});