Something went wrong. Try again.
Web frontend and supporting services for lance.blue
Something went wrong. Try again.
8.6 kB · 223 lines
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224/** * The card that opens when you point at another player. * * Structural, like the lobby screen's own checks: there is no DOM under * `npm test`, and none of what these assert fails at runtime. What they hold * in place is the part that is only wrong in ways a person would have to * notice — a card that reports the caller's own numbers under somebody * else's name, a listener that outlives the screen that added it, and a * slow read landing in a card that has moved on to another player. * * Run with `npm test`. */import { test } from "node:test";import assert from "node:assert/strict";import { readFile } from "node:fs/promises";import { join } from "node:path";import { fileURLToPath } from "node:url";
const web = fileURLToPath(new URL("../", import.meta.url));const source = await readFile(join(web, "src/screens/pilot-hover.ts"), "utf8");const lobby = await readFile(join(web, "src/screens/lobby.ts"), "utf8");const styles = await readFile(join(web, "src/styles.css"), "utf8");
test("the card reads nothing that is scoped to the caller's own session", () => { // pilot.ts counts the caller's matches through a session-scoped route. // Showing that figure under another player's name would be a lie of the // worst kind here: plausible, specific, and about somebody else. assert.doesNotMatch( source, /listMatches|from "\.\.\/api"/, "the hover card imports the session-scoped API - every figure on it " + "must be readable about a player who is not the one looking", ); assert.match( source, /camo\/repo/, "the camo count is gone - it was the one real figure the card had, and " + "it is real because a player's camo lives in their own repository", );});
test("a figure nothing records yet is an em dash that says why", () => { assert.match( source, /const UNRECORDED = "No match result is recorded yet\.";/, "the placeholder reason moved or was renamed - it must match the one " + "pilot.ts gives for the same two figures", ); const placeholder = source.match(/function dash\(\)[\s\S]*?\n\}/); assert.ok(placeholder, "the placeholder figure moved or was renamed"); assert.match(placeholder[0], /—/, "the placeholder is no longer an em dash"); assert.match( placeholder[0], /title: UNRECORDED/, "the placeholder no longer says why it is a dash", );});
test("a read that lands late may not write into a card about somebody else", () => { const open = source.match(/function open\([\s\S]*?\n \}/); assert.ok(open, "open() moved or was renamed"); assert.match( open[0], /const mine = \+\+opening;/, "the open counter is gone - a pointer moving down a roster starts one " + "read per name it crosses", ); const guards = [...open[0].matchAll(/if \(mine !== opening\) return;/g)]; assert.equal( guards.length, 2, "every asynchronous write into the card needs the guard: expected one " + "for the profile read and one for the camo count", );});
test("Escape closes it, and the listener does not outlive the card being open", () => { assert.match( source, /document\.addEventListener\("keydown", onKey\)/, "the Escape listener moved or was renamed", ); assert.match( source, /function close\(\): void \{[\s\S]*?document\.removeEventListener\("keydown", onKey\)/, "closing no longer removes the Escape listener - a screen visited five " + "times would leave five listeners on the document", );});
test("moving the page under the card closes it rather than stranding it", () => { // It is placed once, from where the trigger was standing. A scroll leaves // it pointing at nothing - and a scroll inside the chat panel does not // bubble, hence the capture flag. assert.match( source, /window\.addEventListener\("scroll", close, true\)/, "the card no longer closes on scroll, or no longer listens in the " + "capture phase, which is the only way it hears the panel's own scroll", ); assert.match( source, /window\.addEventListener\("resize", close\)/, "the card no longer closes on resize", ); assert.match( source, /function close\(\): void \{[\s\S]*?window\.removeEventListener\("scroll", close, true\)/, "closing no longer removes the scroll listener", );});
test("the lobby closes the card when the screen goes", () => { const teardown = lobby.match(/onTeardown\(\(\) => \{[\s\S]*?\n \}\);/); assert.ok(teardown, "the lobby screen's teardown moved or was restructured"); assert.match( teardown[0], /hover\.close\(\)/, "a card left open would be a card about somebody from a screen that is " + "no longer on the page", );});
test("a name that repeats is not a tab stop; a name that does not is", () => { assert.match( lobby, /hover\.attach\(who, \(\) => pilotFacts\(line\.did, line\.handle\), \{\s*focusable: false,?\s*\}\)/, "chat speakers are back in the tab order - the same four people are " + "named on every line, and the roster is one stop each", ); assert.match( lobby, /hover\.attach\(chip, \(\) => pilotFacts\(watcher\.did, watcher\.handle\)\)/, "the roster chips are no longer focusable, which leaves a keyboard no " + "way to open a card at all", );});
test("what a player is to this lobby is read when the card opens, not when it is wired", () => { // A chat line said ten minutes ago still points at where that player // ended up, because the facts are a callback rather than a snapshot. const facts = lobby.match( /function pilotFacts\(did: string, handle: string \| null\): PilotFacts \{[\s\S]*?\n \}/, ); assert.ok(facts, "pilotFacts moved or was renamed"); assert.match( facts[0], /seats\.find/, "the note no longer reads the current seat map", ); assert.match( facts[0], /readyDids\.includes\(did\)/, "the note no longer says whether they are ready", );});
test("a closed card is closed in the stylesheet as well as in the DOM", () => { // `.pilot-hover` sets `display: flex`, and a class rule beats the browser's // own rule for [hidden]. Without this the card is painted from the moment a // lobby is built - empty, with nobody pointing at anything - and close() // cannot take it off the screen either. assert.match( styles, /\.pilot-hover\[hidden\] \{\s*display: none;\s*\}/, "the card's [hidden] rule is gone - `hidden = true` in pilot-hover.ts " + "does nothing without it, because `display: flex` overrides it", );});
test("the pointer rests on a name before the card opens; the keyboard does not wait", () => { const attach = source.match( /attach\(trigger, facts, options\) \{[\s\S]*?\n \},/, ); assert.ok(attach, "attach() moved or was restructured"); assert.match( attach[0], /"mouseenter",[\s\S]*?setTimeout\(show, OPEN_DELAY_MS\)/, "the pointer opens the card immediately again - a pointer on its way to " + "a button crosses four names and would open four cards", ); assert.match( attach[0], /addEventListener\("focus", show\)/, "focus waits now too, which makes a keyboard user hold still on a " + "trigger they already chose deliberately", );});
test("a pending open does not survive the card closing", () => { assert.match( source, /function close\(\): void \{[\s\S]*?stopResting\(\)/, "closing no longer cancels a pending open - a name crossed on the way " + "past would open a card after the pointer had left it", );});
test("the card is announced as describing the trigger, and only while it is open", () => { assert.match( source, /trigger\.setAttribute\("aria-describedby", card\.id\)/, "the trigger no longer points at the card, which leaves a screen reader " + "nothing to read when focus opens one", ); assert.match( source, /function close\(\): void \{[\s\S]*?described\.removeAttribute\("aria-describedby"\)/, "the link outlives the open card - a screen reader would announce a card " + "that is no longer on the screen", );});
test("a card opened low on the screen still fits on it", () => { const place = source.match(/function place\([\s\S]*?\n \}/); assert.ok(place, "place() moved or was renamed"); assert.match( place[0], /window\.innerHeight - height - margin/, "the card is no longer clamped to the bottom of the viewport - opened " + "from a name near the foot of a tall roster it runs off the edge", );});