open-standards/joule-code/joule-code-rs/crates/joule-moq-web/web/room/markup.test.mjs
Transaction Science 984d95b273 room page: close the co-performance section, and test the page's markup
Train B's merge of #424 (co-performance in the room) with #413 (the browser
sink, 173981739) left `<section id="co">` without its `</section>`: the page
opened five sections and closed four. The parser then nested the binding table
(`#binding-debug`) and the Log inside the hidden `#co`, so in a room without
co-performance neither ever showed.

- index.html: the `</section>` after the `#co-lead` controls.
- web/room/markup.test.mjs (run by e2e/run.sh): every element the page opens is
  closed in order; the Log, the binding table and the co-watch section are not
  inside `#co`; each mode's controls are inside its own section.
- e2e: in the service scenario (a room without `[co]`), `#log`, `#binding-debug`
  and `#cowatch` are not descendants of `#co`, `#co` is hidden and the Log shows;
  in the cowatch scenario, the co-watch section and the Log show outside `#co`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 06:50:13 -04:00

57 lines
2.7 KiB
JavaScript

// The room page's markup (`node --test web/room/markup.test.mjs`): every element it opens is
// closed, in order, so no section nests inside another by accident; and each mode's section
// holds its own controls and nothing of another's. A section left open nests everything after it,
// so a section the page hides for one mode would hide the Log and the binding table with it.
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import { fileURLToPath } from "node:url";
const html = fs.readFileSync(fileURLToPath(new URL("./index.html", import.meta.url)), "utf8");
// HTML's void elements, which have no end tag.
const VOID = new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track", "wbr"]);
// The elements of `src` by `id`, each with the ids of the elements it is inside, outermost first;
// and every tag that does not balance.
function parse(src) {
const body = src.replace(/<!--[\s\S]*?-->/g, "").replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, "").replace(/<!doctype[^>]*>/i, "");
const stack = [];
const ids = new Map();
const errors = [];
const tag = /<(\/?)([a-zA-Z][a-zA-Z0-9-]*)\b([^>]*)>/g;
let m;
while ((m = tag.exec(body))) {
const [, close, raw, attrs] = m;
const name = raw.toLowerCase();
if (close) {
const top = stack.pop();
if (!top || top.name !== name) errors.push(`</${name}> closes <${top ? top.name : "nothing"}>`);
continue;
}
if (VOID.has(name) || /\/\s*$/.test(attrs)) continue;
const id = (attrs.match(/\bid="([^"]*)"/) || [])[1];
if (id) ids.set(id, stack.map((e) => e.id).filter(Boolean));
stack.push({ name, id });
}
for (const e of stack) errors.push(`<${e.name}${e.id ? ` id="${e.id}"` : ""}> is never closed`);
return { ids, errors };
}
const { ids, errors } = parse(html);
test("every element the room page opens is closed, in order", () => {
assert.deepEqual(errors, []);
});
test("the Log, the binding table and the co-watch section are outside the co-performance section", () => {
for (const id of ["log", "binding-debug", "cowatch", "cowatch-media", "cw-sync"]) {
assert.ok(ids.has(id), `#${id} is on the page`);
assert.ok(!ids.get(id).includes("co"), `#${id} is inside ${ids.get(id).map((x) => `#${x}`).join(" > ")}`);
}
});
test("each mode's controls are inside its own section", () => {
for (const id of ["co-lead", "co-input", "co-turns", "co-fidelity"]) assert.ok(ids.get(id).includes("co"), `#${id} is inside #co`);
for (const id of ["cw-play", "cw-pause", "cw-rate", "cw-sync"]) assert.ok(ids.get(id).includes("cowatch") && !ids.get(id).includes("co"), `#${id} is inside #cowatch alone`);
});