Public: anyone can read this; the crew can change it.
A test runs the toolbar pill's script and proves it draws one freshness line under "Preview of pull #N" for a merged, closed, deploying or behind preview, and nothing for an open, current one #9141
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/9140-a-test-runs-the-toolbar-pill-s-script-an → main 1 commit fork
2 files
changed +105
−5
src/apps/toolbar.ts +13 −4
| @@ -15,6 +15,14 @@ import { staticResponse } from "../web/asset.ts"; | ||
| 15 | 15 | import { sha256 } from "../kernel/hash.ts"; |
| 16 | 16 | import { NOTE_EMPTY, NOTE_FAILED } from "./note_words.ts"; |
| 17 | 17 | |
| 18 | /** What the panel says about a preview's freshness (#9111): the pill's words, in one table. */ | |
| 19 | export const FRESHNESS_WORDS = { | |
| 20 | merged: "Merged: this preview shows the pull as it landed", | |
| 21 | closed: "Closed without landing", | |
| 22 | deploying: "A newer push is deploying", | |
| 23 | behind: "Shows an older push than the pull's newest", | |
| 24 | } as const; | |
| 25 | ||
| 18 | 26 | /** The client, as plain source: no `eval`, no inline handlers. */ |
| 19 | 27 | export const toolbarJs = `(function () { |
| 20 | 28 | if (window.__forgeToolbar) return; |
| @@ -89,10 +97,11 @@ export const toolbarJs = `(function () { | ||
| 89 | 97 | } else if (d.pull) line("Preview of pull #" + d.pull); |
| 90 | 98 | if (d.title) line(d.title); |
| 91 | 99 | // Whether this is the pull as it stands now; an open, current preview says nothing. |
| 92 | var freshness = d.state === "merged" ? "Merged: this preview shows the pull as it landed" | |
| 93 | : d.state === "closed" ? "Closed without landing" | |
| 94 | : d.deploying ? "A newer push is deploying" | |
| 95 | : d.behind ? "Shows an older push than the pull's newest" : ""; | |
| 100 | var W = ${JSON.stringify(FRESHNESS_WORDS)}; | |
| 101 | var freshness = d.state === "merged" ? W.merged | |
| 102 | : d.state === "closed" ? W.closed | |
| 103 | : d.deploying ? W.deploying | |
| 104 | : d.behind ? W.behind : ""; | |
| 96 | 105 | if (freshness) line(freshness); |
| 97 | 106 | var link = document.createElement("a"); |
| 98 | 107 | link.textContent = "Home"; |
test/apps-pill-freshness.test.ts +92 −1
| @@ -9,7 +9,7 @@ import { join } from "node:path"; | ||
| 9 | 9 | import { createApp, type App } from "../src/app.ts"; |
| 10 | 10 | import { newApp } from "../src/actions/apps.ts"; |
| 11 | 11 | import { pillWhere, proxyApp, signGrant, type AppSocket, type AppTarget } from "../src/apps/proxy.ts"; |
| 12 | import { toolbarJs, toolbarTag } from "../src/apps/toolbar.ts"; | |
| 12 | import { FRESHNESS_WORDS, toolbarJs, toolbarTag } from "../src/apps/toolbar.ts"; | |
| 13 | 13 | import { AppsRuntime, type EnvStatus } from "../src/apps/runtime.ts"; |
| 14 | 14 | import { getAppByRepo } from "../src/core/apps.ts"; |
| 15 | 15 | import { createIssue } from "../src/core/issues.ts"; |
| @@ -203,3 +203,94 @@ describe("the pill's freshness, through the proxy", () => { | ||
| 203 | 203 | live = HEAD; |
| 204 | 204 | }); |
| 205 | 205 | }); |
| 206 | ||
| 207 | describe("the pill's script draws the freshness line", () => { | |
| 208 | /** A stand-in element: it keeps what the script sets and who it holds, and nothing more. */ | |
| 209 | class El { | |
| 210 | children: El[] = []; | |
| 211 | attrs: Record<string, string> = {}; | |
| 212 | style: Record<string, unknown> = {}; | |
| 213 | dataset: Record<string, string> = {}; | |
| 214 | textContent = ""; | |
| 215 | [k: string]: unknown; | |
| 216 | constructor(public tag: string) {} | |
| 217 | setAttribute(k: string, v: string) { this.attrs[k] = v; } | |
| 218 | removeAttribute() {} | |
| 219 | appendChild(c: El) { this.children.push(c); return c; } | |
| 220 | append(...c: El[]) { this.children.push(...c); } | |
| 221 | prepend(c: El) { this.children.unshift(c); } | |
| 222 | addEventListener() {} | |
| 223 | removeEventListener() {} | |
| 224 | attachShadow() { return new El("#shadow"); } | |
| 225 | focus() {} | |
| 226 | getBoundingClientRect() { return { left: 0, top: 0, width: 0, height: 0 }; } | |
| 227 | } | |
| 228 | ||
| 229 | /** Run the script on a page whose tag carries `data`; return the panel's lines, top to bottom. */ | |
| 230 | function panelLines(data: Record<string, string>): string[] { | |
| 231 | const made: El[] = []; | |
| 232 | const me = new El("script"); | |
| 233 | me.dataset = { forge: "http://forge.test", ...data }; | |
| 234 | const doc = { | |
| 235 | currentScript: me, | |
| 236 | readyState: "complete", | |
| 237 | documentElement: new El("html"), | |
| 238 | body: new El("body"), | |
| 239 | head: new El("head"), | |
| 240 | createElement: (t: string) => { const e = new El(t); made.push(e); return e; }, | |
| 241 | addEventListener() {}, | |
| 242 | removeEventListener() {}, | |
| 243 | }; | |
| 244 | const win: Record<string, unknown> = { | |
| 245 | innerWidth: 1000, innerHeight: 800, scrollX: 0, scrollY: 0, | |
| 246 | localStorage: { getItem: () => null, setItem() {} }, | |
| 247 | addEventListener() {}, removeEventListener() {}, | |
| 248 | }; | |
| 249 | const loc = { href: "http://site.test/", origin: "http://site.test", pathname: "/", search: "" }; | |
| 250 | const never = () => new Promise<never>(() => {}); | |
| 251 | new Function("window", "document", "location", "fetch", "getComputedStyle", "requestAnimationFrame", "setTimeout", "setInterval", | |
| 252 | toolbarJs)(win, doc, loc, never, () => ({}), () => 0, () => 0, () => 0); | |
| 253 | const panel = made.find((e) => e.attrs["role"] === "region"); | |
| 254 | if (!panel) throw new Error("no panel"); | |
| 255 | return panel.children.map((c) => c.textContent).filter((t) => t !== ""); | |
| 256 | } | |
| 257 | ||
| 258 | const base = { app: "site", repo: "alice/site", env: "pull-7", pull: "7", title: "Add a thing" }; | |
| 259 | const words = Object.values(FRESHNESS_WORDS); | |
| 260 | /** The lines after the title, if the panel has one freshness line among the pull's own. */ | |
| 261 | const after = (lines: string[]) => lines.slice(lines.indexOf("Add a thing") + 1).filter((l) => words.includes(l as never)); | |
| 262 | ||
| 263 | test("merged, closed, deploying and behind each draw one line, right after the pull and its title", () => { | |
| 264 | const cases: [Record<string, string>, string][] = [ | |
| 265 | [{ state: "merged" }, FRESHNESS_WORDS.merged], | |
| 266 | [{ state: "closed" }, FRESHNESS_WORDS.closed], | |
| 267 | [{ state: "open", deploying: "1" }, FRESHNESS_WORDS.deploying], | |
| 268 | [{ state: "open", behind: "1" }, FRESHNESS_WORDS.behind], | |
| 269 | ]; | |
| 270 | for (const [extra, said] of cases) { | |
| 271 | const lines = panelLines({ ...base, ...extra }); | |
| 272 | expect(lines.slice(0, 4)).toEqual(["site", "Preview of pull #7", "Add a thing", said]); | |
| 273 | expect(lines.filter((l) => words.includes(l as never))).toEqual([said]); | |
| 274 | } | |
| 275 | }); | |
| 276 | ||
| 277 | test("the line follows the pull when there is no title", () => { | |
| 278 | const lines = panelLines({ ...base, title: "", state: "merged" }); | |
| 279 | expect(lines.slice(0, 3)).toEqual(["site", "Preview of pull #7", FRESHNESS_WORDS.merged]); | |
| 280 | }); | |
| 281 | ||
| 282 | test("merged and closed win over deploying and behind; deploying wins over behind", () => { | |
| 283 | const both = { deploying: "1", behind: "1" }; | |
| 284 | expect(after(panelLines({ ...base, state: "merged", ...both }))).toEqual([FRESHNESS_WORDS.merged]); | |
| 285 | expect(after(panelLines({ ...base, state: "closed", ...both }))).toEqual([FRESHNESS_WORDS.closed]); | |
| 286 | expect(after(panelLines({ ...base, state: "open", ...both }))).toEqual([FRESHNESS_WORDS.deploying]); | |
| 287 | }); | |
| 288 | ||
| 289 | test("an open, current preview draws none; so does production; so does a viewer told nothing", () => { | |
| 290 | expect(panelLines({ ...base, state: "open" }).filter((l) => words.includes(l as never))).toEqual([]); | |
| 291 | expect(panelLines({ ...base }).filter((l) => words.includes(l as never))).toEqual([]); | |
| 292 | const prod = panelLines({ app: "site", repo: "alice/site", env: "production" }); | |
| 293 | expect(prod.slice(0, 2)).toEqual(["site", "Production"]); | |
| 294 | expect(prod.filter((l) => words.includes(l as never))).toEqual([]); | |
| 295 | }); | |
| 296 | }); |
new above
offline