Public: anyone can read this; the crew can change it.

The layout's unused meta-refresh option is gone, and a test keeps every page off meta refresh #3210

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/3195-the-layout-s-unused-meta-refresh-option → main 1 commit fork

  • Merged. mason/3195-the-layout-s-unused-meta-refresh-option was merged into main as 5d7e275.

Review

Sign in to review this change.
4 files changed +48 −54
docs/STUDIO.md +4 −4
@@ -408,10 +408,10 @@ so a card that is answered goes and a new one arrives without a reload.
408408
409409 ### Live regions
410410
411 A page with a script never reloads itself: the `meta refresh` some pages
412 carry is the no-script fallback, written inside `<noscript>` so a browser
413 running the script never even schedules it (removing the tag afterwards
414 would not cancel a refresh the parser had already set). Every live region
411 A page never reloads itself: no page carries a `meta refresh`
412 (`test/refresh-meta.test.ts` renders every page in `app.looks` to hold
413 it), since a refresh is scheduled the moment the parser meets the tag and
414 removing the tag afterwards would not cancel it. Every live region
415415 listens to one `EventSource` (`/events`,
416416 everything the viewer may see) and works like this. (The stream also carries
417417 `run.log` frames, `{ run, lines, truncated }`, a run's new log lines to those
src/web/layout.ts −31
@@ -105,20 +105,6 @@ export interface PageOptions {
105105 turnstile?: boolean;
106106 /** Extra palette rows for this page's context (a repo's own tabs). */
107107 palette?: PaletteItem[];
108 /**
109 * The JavaScript-free half of a page that changes on its own: a
110 * `<meta http-equiv="refresh">`, optionally to somewhere else. Only a page
111 * whose content is genuinely still moving should set it — a refresh a
112 * reader did not ask for steals their scroll position and their selection.
113 *
114 * A plain refresh (no `url`) is written inside `<noscript>`: a browser
115 * running the page's script never parses it, so the script's stream is
116 * the only thing moving the page. Removing the element afterwards would
117 * not do — a refresh is scheduled the moment the parser meets the tag,
118 * and the timer outlives the element. A refresh that leads somewhere
119 * (`url`) is a redirect and stays in force either way.
120 */
121 refresh?: { seconds: number; url?: string };
122108 /**
123109 * No topbar, no footer, no page width: a page that is the whole window
124110 * (the board). The overlays stay, so the palette is still one chord away.
@@ -528,22 +514,6 @@ function pageDescription(options: PageOptions): string {
528514 return line.length <= 200 ? line : `${line.slice(0, 197).trimEnd()}…`;
529515 }
530516
531 /**
532 * The `<meta http-equiv="refresh">` for `PageOptions.refresh`. A redirect
533 * (`url`) is a bare tag; a reload is wrapped in `<noscript>`, so only a
534 * browser with no script ever schedules it.
535 */
536 export function refreshMeta(r: { seconds: number; url?: string }): Html {
537 if (r.url !== undefined)
538 return html`<meta
539 http-equiv="refresh"
540 content="${r.seconds}; url=${r.url}"
541 />`;
542 return html`<noscript
543 ><meta http-equiv="refresh" content="${String(r.seconds)}"
544 /></noscript>`;
545 }
546
547517 export function page(options: PageOptions, status = 200): Response {
548518 // A nonce lets exactly this page's <style> and <script> run while
549519 // everything else stays forbidden; it is random, or bound to the body when
@@ -635,7 +605,6 @@ export function page(options: PageOptions, status = 200): Response {
635605 href="${f.href}"
636606 />`,
637607 )}
638 ${when(options.refresh, refreshMeta)}
639608 <meta name="color-scheme" content="light dark" />
640609 ${theme
641610 ? html`<meta name="theme-color" content="${metaThemeColor(theme)}" />`
test/no-polling.test.ts −1
@@ -10,7 +10,6 @@ import { Glob } from "bun";
1010 /** file -> the patterns it may carry, and why */
1111 const ALLOWED: Record<string, { allow: string[]; why: string }> = {
1212 "src/web/sw.ts": { allow: ['http-equiv="refresh"'], why: "the restart page: the forge is down, so there is no /events to hear" },
13 "src/web/layout.ts": { allow: ['http-equiv="refresh"'], why: "defines the refresh option and its meta tag; callers are listed here" },
1413 "src/web/client.ts": { allow: ["data-live-every", "setInterval"], why: "the data-live-every mechanism itself, and clocks that only redraw text" },
1514 };
1615
test/refresh-meta.test.ts +44 −18
@@ -1,28 +1,54 @@
11 /**
2 * A page that changes on its own carries a `meta refresh` for browsers with
3 * no script. A browser running the page's script must never schedule it: a
4 * refresh is set the moment the parser meets the tag, and removing the tag
5 * afterwards does not cancel it — which is how a live run page threw away
6 * the reader's scroll every five seconds. So the reload is written inside
7 * `<noscript>`, and only a redirect (`url`) stands on its own.
2 * No forge page carries a `meta refresh`. A refresh is set the moment the
3 * parser meets the tag, and removing the tag afterwards does not cancel it
4 * — which is how a live run page threw away the reader's scroll every five
5 * seconds. Every page now follows the event stream instead (#3175, #3179),
6 * and the layout no longer offers the option (#3195).
87 */
9 import { describe, expect, test } from "bun:test";
10 import { refreshMeta } from "../src/web/layout.ts";
8 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
9 import { mkdtemp, rm } from "node:fs/promises";
10 import { tmpdir } from "node:os";
11 import { join } from "node:path";
12 import { createApp, type App } from "../src/app.ts";
13 import { serve, type Servers } from "../src/http/server.ts";
14 import { adminCookie } from "./helpers/admin.ts";
1115
12 describe("the refresh meta", () => {
13 test("a reload is for browsers without a script only", () => {
14 const tag = String(refreshMeta({ seconds: 5 })).replace(/\s+/g, " ");
15 expect(tag).toMatch(
16 /^<noscript ?><meta http-equiv="refresh" content="5" ?\/><\/noscript>$/,
17 );
16 const looks: string[] = (await Bun.file(new URL("../forge.json", import.meta.url)).json()).app.looks;
17
18 describe("no page refreshes itself", () => {
19 let dataDir: string;
20 let app: App;
21 let servers: Servers;
22 let cookie = "";
23
24 beforeAll(async () => {
25 dataDir = await mkdtemp(join(tmpdir(), "forge-refresh-meta-"));
26 app = createApp({ dataDir, port: 0 });
27 servers = await serve(app);
28 cookie = await adminCookie(app, { username: "ada", email: "[email protected]", password: "refresh-password" });
1829 });
1930
20 test("a redirect stays in force with or without a script", () => {
21 const tag = String(refreshMeta({ seconds: 1, url: "/trevato/forge" }));
22 expect(tag).toContain('content="1; url=/trevato/forge"');
23 expect(tag).not.toContain("noscript");
31 afterAll(async () => {
32 servers.stop();
33 app.close();
34 await rm(dataDir, { recursive: true, force: true });
2435 });
2536
37 test("forge.json names pages to look at", () => {
38 expect(looks.length).toBeGreaterThan(0);
39 });
40
41 for (const path of looks) {
42 test(`${path}, signed in or not, has no http-equiv="refresh"`, async () => {
43 for (const headers of [{ cookie }, {}]) {
44 const res = await fetch(`${servers.url}${path}`, { headers, redirect: "manual" });
45 const body = await res.text();
46 expect(res.status).toBeLessThan(500);
47 expect(body).not.toMatch(/http-equiv=["']?refresh/i);
48 }
49 });
50 }
51
2652 test("the page's script no longer reaches for the tag it cannot cancel", async () => {
2753 const client = await Bun.file(
2854 new URL("../src/web/client.ts", import.meta.url),

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs