phase: 04_controller_page_responsive
**Final verification pass** (all three task reports PASS; tasks already in `complete/`). One real defect found and fixed.
**Commands / results.** `bash tests/run_tests.sh` → `RESULT: all suites passed`, **5 104** checks (page **220**). `SANITIZE=1 bash tests/run_tests.sh` → passed. `bash .agents/validate.sh` → `validation OK`; bridge/console/rollback `SUCCESS`; `page bytes: 59262`; `README/AGENTS claims: OK`; `repo hygiene: OK`. Real-browser leg (headless Chromium, served assembled bytes): no horizontal scroll at 360/390/768/1440, ARM first-in-`main` and sticky, log height-capped, all 5 folds openable, all 3 changed page statements execute.
**Fix.** The folded notes' `<summary>` — the only route to five pinned safety sentences — rendered **17 px** on coarse pointers, under the page's own 44 px thumb floor, while every byte-level assertion stayed green. `src/web_page.h`: `button.k,.pill,input,summary{min-height:44px}` + `summary{padding:13px 0}`; `tests/test_page.js` +1 check (219→220); mutation-tested (reverting the CSS fails host *and* browser legs, then restored byte-for-byte).
**Criteria.** 1 ✓ browser-measured (physical phone still unobserved). 2 ✓ 220 checks + prose untouched. 3 ✓ page 59 286→**59 262 B** (−24 vs phase start), README = gate figure. 4 ✓ one `test_page` row at 220, intro 5 104 = measured. 5 ✓ gate green; phases 01–03 assertions unchanged apart from the re-pinned byte constant.
**Docs.** Re-pinned 59 262 (README, `config.h`, `test_conn.cpp`), totals 5 103→5 104, README Tests row 220; re-measured footprint rows (flash 1 257 901 / 1 178 141 / 1 157 593, TLS +6 176/+100 308); fixed stale "48 KB"/"~50 KB" (×4 src comments, ×2 README) and the 51 626-byte page-delivery row; added **bug 14** with root cause and renamed *Thirteen*→*Fourteen bugs* in README + AGENTS.md's two stale references; added the Phase 04 browser+device row to `.agents/HW_VERIFICATION.md`.
**Deviations.** Device leg stays open: live board serves 51 626 B (pre-phase image); no reflash attempted, per the standing no-unattended-reflash rule — no byte-equality claim made. `ruff check tools tests`: 29 findings, identical at HEAD and `--isolated`, `E9,F` clean, no repo lint config or gate step → left untouched. `.agents/validate.sh` is **untracked and has never been committed** (`git log --all --` empty) though README/AGENTS/PLAN cite it 13× — harness needs `git add` it; I did not stage anything.
This commit is contained in:
+354
-3
@@ -9,8 +9,12 @@
|
||||
// that matter, which are what the log is allowed to contain, which characters may
|
||||
// leave the tab, and when a run of pasted text stops.
|
||||
//
|
||||
// It is a behaviour test, not a rendering test: no CSS, no layout, no real keyboard.
|
||||
// tests/run_tests.sh skips it when node is not installed.
|
||||
// It is not a rendering test -- nothing here lays out a page. But it is the only
|
||||
// place the served bytes can be seen without a browser, so the CSS a phone is the
|
||||
// only real judge of (breakpoint, sticky ARM bar, thumb floors) is asserted at the
|
||||
// byte level, the safety sentences are asserted as text read out of the markup,
|
||||
// and the script itself is executed. Behaviour plus those pins is what a raw
|
||||
// string gets. tests/run_tests.sh skips this file when node is not installed.
|
||||
"use strict";
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
@@ -183,7 +187,9 @@ if (from < 0 || to < 0) {
|
||||
const pageScript = served.slice(from + "<script>".length, to);
|
||||
|
||||
const context = vm.createContext(sandbox);
|
||||
vm.runInContext(pageScript + expose, context);
|
||||
// filename so a throw inside the page names web_page.js, and so a V8 coverage
|
||||
// run (NODE_V8_COVERAGE=… node tests/test_page.js) can pick the page script out.
|
||||
vm.runInContext(pageScript + expose, context, { filename: "web_page.js" });
|
||||
const x = sandbox.__x;
|
||||
const logEl = x.logEl;
|
||||
|
||||
@@ -739,6 +745,342 @@ checkEq(!!x.inPageField({ target: null }), false, "a missing target does not thr
|
||||
"coming back is one deliberate click away, never zero");
|
||||
x.setArmed(false, "harness: done");
|
||||
|
||||
console.log("arming and the S= state frames");
|
||||
// Invariants 1 and 2, measured as bytes on the wire: disarmed forwards nothing,
|
||||
// arming is a REQUEST the board answers (the page never flips its own state),
|
||||
// and every keystroke restates the WHOLE state -- S=<mods>:<usages> -- so the
|
||||
// next frame corrects any lost one. The safety section below pins what the page
|
||||
// says; this pins what it sends, which is the half a restructure of the markup
|
||||
// could silently have moved.
|
||||
const sframes = () => sentFrames.filter(f => f.startsWith("S="));
|
||||
const stateWire = []; // every S= frame emitted this section
|
||||
const key = (type, code, extra) => {
|
||||
const before = sentFrames.length;
|
||||
docListeners.fire(type, Object.assign(
|
||||
{ code, target: { tagName: "BODY" }, preventDefault() {} }, extra || {}));
|
||||
for (const f of sentFrames.slice(before)) if (f.startsWith("S=")) stateWire.push(f);
|
||||
};
|
||||
|
||||
sandbox.location.protocol = "https:";
|
||||
byId("token").value = "hunter2";
|
||||
x.authed = false; x.armed = false;
|
||||
FakeWebSocket.instances.length = 0;
|
||||
x.connect();
|
||||
await wait(5); // onopen: T + H go out, authed stays false
|
||||
sentFrames.length = 0;
|
||||
byId("arm").fire("click");
|
||||
checkEq(sentFrames.length, 0,
|
||||
"an ARM click before the board has authenticated this socket asks for nothing");
|
||||
const live = FakeWebSocket.instances[FakeWebSocket.instances.length - 1];
|
||||
live.onmessage({ data: "OK" });
|
||||
checkEq(x.authed, true, "the board's OK is what links the page");
|
||||
|
||||
sentFrames.length = 0;
|
||||
byId("arm").fire("click");
|
||||
checkEq(sentFrames.join(","), "A1",
|
||||
"the ARM click sends A1 and nothing else -- one frame, one command");
|
||||
checkEq(x.armed, false,
|
||||
"and the page does not arm itself on its own say-so -- no optimistic flip, " +
|
||||
"the status heartbeat confirms or it did not happen");
|
||||
x.onFrame('S{"armed":1,"usb":1,"typing":0}');
|
||||
checkEq(x.armed, true, "the board's status frame is what arms the page");
|
||||
|
||||
sentFrames.length = 0;
|
||||
byId("arm").fire("click"); // click again while armed
|
||||
checkEq(sentFrames.join(","), "A0,R",
|
||||
"clicking again asks to disarm (A0) and releases every key (R) -- still no local flip");
|
||||
x.onFrame('S{"armed":0,"usb":1,"typing":0}');
|
||||
checkEq(x.armed, false, "and the disarm lands when the board says so");
|
||||
|
||||
sentFrames.length = 0;
|
||||
key("keydown", "KeyA");
|
||||
key("keyup", "KeyA");
|
||||
checkEq(sentFrames.length, 0,
|
||||
"while disarmed a whole press forwards nothing -- not even a restatement");
|
||||
|
||||
x.onFrame('S{"armed":1,"usb":1,"typing":0}'); // the board's own button arms it
|
||||
checkEq(x.armed, true, "a board-side arm is mirrored here without any click");
|
||||
|
||||
sentFrames.length = 0;
|
||||
key("keydown", "KeyA");
|
||||
checkEq(sframes().join(","), "S=00:04",
|
||||
"a press while armed restates the whole state: no modifiers, one usage (KeyA = 0x04)");
|
||||
key("keydown", "KeyA", { repeat: true });
|
||||
checkEq(sframes().length, 1,
|
||||
"autorepeat is not a state change -- the held state already says the key is down");
|
||||
key("keydown", "KeyB");
|
||||
checkEq(sframes().slice(-1)[0], "S=00:04,05",
|
||||
"the next restatement carries BOTH usages in press order, not an event for B alone");
|
||||
key("keyup", "KeyA");
|
||||
checkEq(sframes().slice(-1)[0], "S=00:05",
|
||||
"a release is a full restatement too: what is still held, nothing about A");
|
||||
key("keydown", "ShiftLeft");
|
||||
checkEq(sframes().slice(-1)[0], "S=02:05",
|
||||
"a modifier enters the mods byte (Shift = 0x02) and the held usage survives it");
|
||||
key("keyup", "ShiftLeft");
|
||||
checkEq(sframes().slice(-1)[0], "S=00:05", "and leaves it the same way");
|
||||
key("keyup", "KeyB");
|
||||
checkEq(sframes().slice(-1)[0], "S=00:",
|
||||
"the EMPTY restatement travels -- keys-up is a state, not an absence of event");
|
||||
|
||||
sentFrames.length = 0;
|
||||
key("keydown", "KeyA", { target: { tagName: "TEXTAREA" } });
|
||||
checkEq(sframes().length, 0,
|
||||
"a keystroke in the page's own text box belongs to the page, not to the target");
|
||||
key("keydown", "ShiftLeft");
|
||||
checkEq(sframes().slice(-1)[0], "S=02:", "set-up: Shift went down outside the field");
|
||||
key("keyup", "ShiftLeft", { target: { tagName: "TEXTAREA" } });
|
||||
checkEq(sframes().slice(-1)[0], "S=00:",
|
||||
"a modifier released OVER the page's field is still let go -- releasing Shift " +
|
||||
"above the text box must not strand it held on the target");
|
||||
|
||||
key("keydown", "KeyA"); // hold a key, then panic
|
||||
sentFrames.length = 0;
|
||||
key("keydown", "Escape", { ctrlKey: true, altKey: true });
|
||||
checkEq(sentFrames.join(","), "R,A0",
|
||||
"Ctrl+Alt+Esc releases every key (R) before disarming (A0), in that order");
|
||||
checkEq(x.armed, false, "and the page lands disarmed with the keys reset");
|
||||
|
||||
check(stateWire.length > 8 &&
|
||||
stateWire.every(f => /^S=[0-9a-f]{2}:(?:[0-9a-f]{2}(?:,[0-9a-f]{2})*)?$/.test(f)),
|
||||
"every state frame this section emitted is the wire form the firmware parses " +
|
||||
"(S=<hex mods>:<hex usages>) -- test_proto owns that parser");
|
||||
|
||||
console.log("responsive layout");
|
||||
// Byte-level layout claims. This suite is the only place the page can be seen
|
||||
// without a browser, and missing CSS fails silently -- it just renders the
|
||||
// laptop layout onto a phone. Each check names the mechanism it pins, so a
|
||||
// green run says the mechanism is in the served bytes, not that somebody felt
|
||||
// good about a breakpoint.
|
||||
function mediaBlock(query) {
|
||||
const at = served.indexOf("@media " + query);
|
||||
if (at < 0) return null;
|
||||
const start = served.indexOf("{", at);
|
||||
let depth = 0;
|
||||
for (let i = start; i < served.length; i++) {
|
||||
if (served[i] === "{") depth++;
|
||||
else if (served[i] === "}" && --depth === 0) return served.slice(start, i + 1);
|
||||
}
|
||||
return null; // unbalanced braces: report as absent
|
||||
}
|
||||
|
||||
const viewportMeta =
|
||||
(served.match(/<meta name="viewport" content="([^"]*)"/) || [])[1] || "";
|
||||
check(viewportMeta.includes("width=device-width"),
|
||||
"the viewport sizes to the device instead of a fake desktop canvas");
|
||||
check(viewportMeta.includes("viewport-fit=cover"),
|
||||
"the viewport meta carries viewport-fit=cover -- without it env(safe-area-inset-*) " +
|
||||
"is always 0 and the home indicator/notch sits on top of the page");
|
||||
check(!/maximum-scale|user-scalable=no/.test(viewportMeta),
|
||||
"and it does not disable pinch zoom -- a thumb floor must not be paid for with zoom");
|
||||
|
||||
const narrow = mediaBlock("(max-width:720px)");
|
||||
check(narrow !== null, "the one 720 px breakpoint exists");
|
||||
check(narrow !== null && /\.grid\s*\{[^}]*grid-template-columns\s*:\s*1fr/.test(narrow),
|
||||
"at the breakpoint the card grid is forced to one column (the base rule keeps " +
|
||||
"auto-fit/minmax(240px,1fr) for the laptop)");
|
||||
check(narrow !== null && /main\s*\{[^}]*display\s*:\s*flex[^}]*flex-direction\s*:\s*column/.test(narrow),
|
||||
"and main becomes a flex column there -- position:sticky on a grid item is pinned " +
|
||||
"inside its own grid area and never sticks, so this display switch is what makes " +
|
||||
"the ARM bar sticky for real");
|
||||
check(narrow !== null &&
|
||||
/input\[type=password\]\s*,\s*input\[type=text\]\s*,\s*textarea\s*\{[^}]*font-size\s*:\s*16px/.test(narrow),
|
||||
"inputs and the text box step to 16 px below the breakpoint -- iOS Safari zooms " +
|
||||
"the page on focusing a smaller field and never zooms back out");
|
||||
check(narrow !== null && /#log\s*\{[^}]*max-height\s*:/.test(narrow),
|
||||
"the log caps its own height on a phone instead of pushing the ARM bar off screen");
|
||||
|
||||
const coarse = mediaBlock("(pointer:coarse)");
|
||||
check(coarse !== null, "a coarse-pointer query exists for the thumb floor");
|
||||
check(coarse !== null &&
|
||||
/button\.k[^{}]*\.pill[^{}]*input[^{}]*summary\s*\{[^}]*min-height\s*:\s*44px/.test(coarse),
|
||||
"buttons, pills, inputs AND the folded notes' summary get a 44 px height floor on " +
|
||||
"touch devices; the fine-pointer page is untouched");
|
||||
check(coarse !== null && /min-width\s*:\s*44px/.test(coarse),
|
||||
"and narrow targets (chord buttons, checkboxes) get the matching width floor");
|
||||
// Why summary is in that list rather than a nice-to-have: task 02 moved five safety
|
||||
// sentences behind a <details>, which made the summary the only way to read them on a
|
||||
// phone. Its natural height is 17 px -- a tap target a thumb does not hit, in a build
|
||||
// whose byte-level layout assertions would otherwise still all have been green. The
|
||||
// rendered height is measured in a real browser at 360 px (.agents/HW_VERIFICATION.md);
|
||||
// this pins that the rule the measurement depends on is still in the served bytes.
|
||||
check(coarse !== null && /summary\s*\{[^}]*min-height\s*:\s*44px/.test(coarse) &&
|
||||
/summary\s*\{[^}]*padding\s*:/.test(coarse),
|
||||
"the fold's summary is a thumb-sized target, not the 17 px line of text it is by " +
|
||||
"default -- it is the only route to the folded safety notes");
|
||||
|
||||
check(/\.arm\s*\{[^}]*position\s*:\s*sticky[^}]*top\s*:\s*0[^}]*z-index/.test(served),
|
||||
"the ARM bar is sticky at the top of the scroll, above the cards -- the " +
|
||||
"armed/disarmed state must survive any scroll position");
|
||||
check(/#log\s*\{[^}]*overscroll-behavior\s*:\s*contain/.test(served),
|
||||
"scrolling the log does not scroll the page out from under the ARM bar");
|
||||
check(/padding-bottom\s*:\s*calc\(14px \+ env\(safe-area-inset-bottom\)\)/.test(served),
|
||||
"main's bottom padding carries the safe-area inset, so the last card clears the " +
|
||||
"home indicator (viewport-fit=cover above is what makes env() mean anything)");
|
||||
check(/button\.k\s*\{[^}]*touch-action\s*:\s*manipulation/.test(served),
|
||||
"controls set touch-action:manipulation -- no 300 ms double-tap-to-zoom between presses");
|
||||
check(/button\.k\s*\{[^}]*user-select\s*:\s*none/.test(served) &&
|
||||
/\.mousepad\s*\{[^}]*user-select\s*:\s*none/.test(served),
|
||||
"pressing a control or the pad does not start a text selection");
|
||||
check(/<main>\s*<button class="arm" id="arm"/.test(served),
|
||||
"the ARM bar is still the first element in main -- on every screen it is the " +
|
||||
"first thing below the header, no horizontal scroll assumed");
|
||||
const narrowKv = narrow !== null && /\.kv\s*\{[^}]*display\s*:\s*flex[^}]*flex-wrap/.test(narrow);
|
||||
check(narrowKv,
|
||||
"the State card's counters collapse to a wrapped summary at the breakpoint " +
|
||||
"instead of eight stacked rows -- they stay visible because they are the " +
|
||||
"'is it typing' evidence, only shorter");
|
||||
check(narrow !== null && /\.rowhead\s*\{[^}]*gap\s*:\s*\d+px/.test(narrow),
|
||||
"and the Log card's option row tightens so the card is two lines on a phone: " +
|
||||
"the options, then the log");
|
||||
|
||||
console.log("the safety statements that must never be tidied away");
|
||||
// Every long hint on this page exists because somebody got hurt without it, and a
|
||||
// page that reads as a wall of text invites exactly the edit that removes one. So
|
||||
// each statement is pinned by the words the operator has to read, BEFORE the prose
|
||||
// is folded -- the same assertions have to pass on the untrimmed page (they prove
|
||||
// the sentences were there) and on the trimmed one (they prove the fold kept them).
|
||||
//
|
||||
// They match text, not markup: tags stripped, entities decoded, whitespace
|
||||
// collapsed, case folded. That is what lets a sentence survive a rewrap and
|
||||
// survive moving inside a <details>, while still failing the moment it is deleted.
|
||||
// The matcher reads the served markup string rather than the shim's element tree --
|
||||
// <details> needs no shim, and a safety note one tap away is still on the page as
|
||||
// long as it is in the bytes the board serves. Anything matched here from the
|
||||
// script alone would also match a comment, so the script's safety statements are
|
||||
// pinned against `pageScript` explicitly instead.
|
||||
const decode = s => s
|
||||
.replace(/&#x([0-9a-f]+);/gi, (_, h) => String.fromCodePoint(parseInt(h, 16)))
|
||||
.replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d)))
|
||||
.replace(/—/g, "\u2014").replace(/–/g, "\u2013")
|
||||
.replace(/’|‘/g, "'").replace(/”|“/g, '"')
|
||||
.replace(/·/g, "\u00b7").replace(/ /g, " ")
|
||||
.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"')
|
||||
.replace(/&/g, "&");
|
||||
const pageTextOf = html => decode(html
|
||||
.replace(/<script[\s\S]*?<\/script>/gi, " ") // pinned against pageScript instead
|
||||
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
||||
.replace(/<!--[\s\S]*?-->/g, " ")
|
||||
.replace(/<[^>]+>/g, " ")).replace(/\s+/g, " ").trim().toLowerCase();
|
||||
const PAGE = pageTextOf(served);
|
||||
const says = (...phrases) => phrases.every(p => PAGE.includes(p));
|
||||
|
||||
// (1) the arming banner -- the one paragraph that must never sit behind a tap.
|
||||
const bannerHtml =
|
||||
(served.match(/<div class="banner" id="banner">([\s\S]*?)<\/div>/) || [])[1] || "";
|
||||
const bannerText = pageTextOf(bannerHtml);
|
||||
check(bannerText.includes("every key"),
|
||||
"the arming banner says EVERY key this tab presses goes to the target");
|
||||
check(/\benter\b/.test(bannerText),
|
||||
"and it names Enter by name -- the key that commits, the one nobody expects to travel");
|
||||
check(bannerText.includes("the machine the board is plugged into"),
|
||||
"and it says which machine receives them (not the one you are typing on)");
|
||||
check(/only on a machine you own|explicitly allowed to type on/.test(bannerText),
|
||||
"and it keeps the permission rule: a machine you own or may type on");
|
||||
const mainAt = served.indexOf("<main");
|
||||
const firstDetails = served.indexOf("<details", mainAt);
|
||||
const bannerAt = served.indexOf('id="banner"', mainAt);
|
||||
check(bannerAt > 0 && (firstDetails < 0 || bannerAt < firstDetails),
|
||||
"the arming banner is not inside or after a <details> -- no safety warning about " +
|
||||
"arming is ever one tap away from being unseen (holds with no folds at all)");
|
||||
|
||||
// (2) disarmed is the default, and what puts it back.
|
||||
check(says("nothing is forwarded while disarmed"),
|
||||
"the page still states the default: disarmed, nothing forwarded");
|
||||
check(says("reload", "focus loss", "socket loss"),
|
||||
"and still names what disarms you: reload, focus loss, socket loss");
|
||||
|
||||
// (3) the pointer: Esc is the rollback, and it releases.
|
||||
check(says("esc frees the cursor and lets go of every button"),
|
||||
"the mouse note still says Esc frees the cursor AND lets go of every button");
|
||||
check(says("never reaches the target"),
|
||||
"and that Esc belongs to the browser, so the pointer's rollback cannot be typed at the target");
|
||||
check(says("cannot leave one held down"),
|
||||
"and that restating the button byte every frame is what stops a lost frame sticking one");
|
||||
|
||||
// (4) a run of pasted text is cancelable, out loud.
|
||||
check(says("abandons the rest of a run"),
|
||||
"the send-text note still says stop typing / Ctrl+Alt+Esc abandons the rest of a run");
|
||||
|
||||
// (5) characters are positions.
|
||||
check(says("characters are positions", "us layout"),
|
||||
"the layout caveat is still on the page: positions, so the target must be on a US layout");
|
||||
|
||||
// (6) nothing stored, scheduled or replayed.
|
||||
check(says("no history", "nothing is saved"),
|
||||
"the send-text note still says nothing is stored, kept or typed later");
|
||||
check(says("nothing scheduled"),
|
||||
"and the mouse note still says nothing is scheduled (no macro, no saved gesture)");
|
||||
|
||||
// (7) the default-passphrase shout is the board's to raise, never static text: a
|
||||
// sentence in the markup would shout on a board that has a real passphrase and go
|
||||
// quiet on one that does not. It replaces the arming banner's own text, so it is
|
||||
// unmissable and it cannot be folded away with the hints.
|
||||
check(/if \(s\.dflt\)[\s\S]{0,400}default passphrase/i.test(pageScript),
|
||||
"the default-passphrase shout exists and is driven by the board's dflt flag");
|
||||
check(/if \(s\.dflt\)[\s\S]{0,160}\$\("banner"\)\.innerHTML/.test(pageScript),
|
||||
"and it lands on the arming banner, the one place that is never behind a tap");
|
||||
check(!PAGE.includes("default passphrase"),
|
||||
"and no static default-passphrase sentence hides in the markup -- the board raises it or nobody does");
|
||||
|
||||
// (8) a takeover cannot arm (phase 03). Guarded here so a later prose fold on the
|
||||
// Connection card cannot drop the sentence that says so.
|
||||
check(says("cannot arm it by doing so", "every handover lands disarmed"),
|
||||
"the Connection card still says taking the bridge cannot arm it -- every handover lands disarmed");
|
||||
|
||||
// (9) the clear-loaded-page warning (phase 02) -- hidden until true, but present.
|
||||
check(says("this page arrived in the clear"),
|
||||
"the clear-page warning is still in the served bytes");
|
||||
check(says("reload over"), "and it names the fix rather than just the fear");
|
||||
|
||||
// (10) Ctrl+Alt+Del is the one chord that can reboot a target, and its warning is
|
||||
// said at press time (the tooltip is a tooltip, not a manual, so this is where the
|
||||
// sentence has to live).
|
||||
check(/virtual terminal this REBOOTS the target/.test(pageScript),
|
||||
"the Ctrl+Alt+Del reboot warning is still said when the confirm is armed");
|
||||
|
||||
console.log("the folded notes are folded, not deleted");
|
||||
// Task 02 moved five paragraphs behind <details>. A fold is only safe while
|
||||
// there IS something behind it, so each one is checked structurally: the count
|
||||
// is pinned (deleting a whole note is a content decision, not a tidy-up, and it
|
||||
// must fail a suite), every fold names what is behind the tap, and every fold
|
||||
// still carries sentences. The safety checks above prove WHICH sentences survive;
|
||||
// these prove the containers did not hollow out.
|
||||
const folds = [...served.matchAll(/<details\b[^>]*>([\s\S]*?)<\/details>/gi)]
|
||||
.map(m => m[0]);
|
||||
checkEq(folds.length, 5,
|
||||
"the page carries exactly the five folded notes (chords, passphrase, mouse, " +
|
||||
"run, shortcuts) -- one fewer is a deleted note, not a tidied one");
|
||||
const summaries = folds.map(f =>
|
||||
(f.match(/<summary>([\s\S]*?)<\/summary>/i) || [])[1] || "");
|
||||
check(summaries.every(t => t.trim().length > 0),
|
||||
"every fold opens with a summary that names what is behind the tap -- a bare " +
|
||||
"<details> renders as 'Details', content one guess away");
|
||||
for (const want of ["why these are buttons", "what the passphrase does and does not buy",
|
||||
"how a forwarded mouse behaves", "what a run is and is not",
|
||||
"shortcuts while armed"]) {
|
||||
check(summaries.join(" | ").toLowerCase().includes(want),
|
||||
`the folded note "${want}" is still one of the page's five`);
|
||||
}
|
||||
folds.forEach((f, i) => {
|
||||
const body = pageTextOf(f.replace(/<summary>[\s\S]*?<\/summary>/i, " "));
|
||||
check(body.length >= 120,
|
||||
`fold ${i + 1} (${summaries[i] || "?"}) still carries its sentences `
|
||||
+ `(${body.length} chars) -- a folded note is not a deleted one`);
|
||||
});
|
||||
|
||||
console.log("the page's constants are the board's constants");
|
||||
// Hand-copied numbers, so they are checked against config.h here too: three readers
|
||||
// of the same arithmetic (here, test_proto.cpp, test_relayctl.py), none of them
|
||||
// trusting the page to have kept its own copy honest.
|
||||
const configText = fs.readFileSync(path.join(__dirname, "..", "src", "config.h"), "utf8");
|
||||
const configNum = name =>
|
||||
Number((configText.match(new RegExp("\\b" + name + "\\s*=\\s*(\\d+)")) || [])[1]);
|
||||
checkEq(x.TEXT_FRAME_MAX, configNum("kTextFrameMax"),
|
||||
"TEXT_FRAME_MAX is still config.h's kTextFrameMax -- drift here is an E8 mid-paste");
|
||||
checkEq(x.TEXT_JOB_MAX, configNum("kTextJobCap"),
|
||||
"TEXT_JOB_MAX is still config.h's kTextJobCap");
|
||||
|
||||
console.log("element ids");
|
||||
const markup = served;
|
||||
const present = new Set();
|
||||
@@ -747,6 +1089,15 @@ checkEq(!!x.inPageField({ target: null }), false, "a missing target does not thr
|
||||
const missing = [...requestedIds].filter(id => !present.has(id)).sort();
|
||||
checkEq(missing.join(","), "",
|
||||
"every element the script reached for exists in the page");
|
||||
// Prove this check bites. The shim hands out an element for ANY id, so a
|
||||
// mistyped id is invisible everywhere except right here -- which means a broken
|
||||
// matcher would also read as green. Hand it an id that cannot be in the markup
|
||||
// and require the same comparison to name exactly that one.
|
||||
byId("zzCoverSelfTest__notAnElement");
|
||||
const control = [...requestedIds].filter(id => !present.has(id));
|
||||
checkEq(control.join(","), "zzCoverSelfTest__notAnElement",
|
||||
"and the id-coverage comparison itself works: an id missing from the markup " +
|
||||
"is named by it (the shim would never have noticed)");
|
||||
|
||||
console.log(`page ${checks} checks, ${failures} failures`);
|
||||
process.exit(failures ? 1 : 0);
|
||||
|
||||
Reference in New Issue
Block a user