Public: anyone can read this; the crew can change it.
The pictures the forge took of an environment speak the home page's language on the pull page #9967
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/9959-the-pictures-the-forge-took-of-an-enviro → main 2 commits fork
2 files
changed +84
−9
src/web/pages/looks.ts +11 −9
| @@ -34,7 +34,7 @@ import { mayFetchLook as mayFetchLookFor } from "../../looks/readable.ts"; | ||
| 34 | 34 | import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts"; |
| 35 | 35 | import { loadRepo, repoBase } from "../chrome.ts"; |
| 36 | 36 | import { html, when, type Html } from "../html.ts"; |
| 37 | import { card, disclosure, icon, sha, time } from "../ui.ts"; | |
| 37 | import { disclosure, sha, time } from "../ui.ts"; | |
| 38 | 38 | |
| 39 | 39 | /** The newest picture at one width, when there is one. */ |
| 40 | 40 | export const pictured = ( |
| @@ -115,7 +115,7 @@ export const mayFetchLook = ( | ||
| 115 | 115 | ): boolean => mayFetchLookFor(ctx.app, repo, record, look, ctx.auth.user); |
| 116 | 116 | |
| 117 | 117 | /** |
| 118 | * The Look card on a pull: phone and desktop side by side, the commit | |
| 118 | * The Look block on a pull: phone and desktop side by side, the commit | |
| 119 | 119 | * they are of, and a word when the head has moved since — or, honestly, |
| 120 | 120 | * why there is no picture. More than one page in `app.looks` gets one |
| 121 | 121 | * group per page, each labelled by its path, since each can be at a |
| @@ -132,7 +132,7 @@ export function lookCard( | ||
| 132 | 132 | const base = repoBase(repo); |
| 133 | 133 | const env = previewEnv(pull.number); |
| 134 | 134 | // What this viewer may see: all of it for a writer; for a reader, only a |
| 135 | // phone or desktop picture taken as nobody (a public app's), and no card | |
| 135 | // phone or desktop picture taken as nobody (a public app's), and no block | |
| 136 | 136 | // without one. |
| 137 | 137 | const writer = canSeeOwnerFacts(ctx.app.db, repo, ctx.auth.user); |
| 138 | 138 | const every = looksForEnv(ctx.app.db, record.id, env, "any").filter( |
| @@ -146,15 +146,18 @@ export function lookCard( | ||
| 146 | 146 | if (!writer && looks.length === 0) return html``; |
| 147 | 147 | const attached = looks.filter(isAttached).reverse(); |
| 148 | 148 | const taken = looks.filter((l) => !isAttached(l)); |
| 149 | const head = html`${icon("eye")} | |
| 150 | <h2>Look</h2>`; | |
| 149 | // A block under an h2, the home page's language — not a card. | |
| 150 | const block = (body: Html): Html => | |
| 151 | html`<section class="settings-block" id="look" data-look-block> | |
| 152 | <h2>Look</h2> | |
| 153 | ${body} | |
| 154 | </section>`; | |
| 151 | 155 | if (taken.length === 0 && attached.length === 0) { |
| 152 | 156 | if (pull.state !== "open" || pull.mergedAt !== null) return html``; |
| 153 | return card( | |
| 157 | return block( | |
| 154 | 158 | html`<p class="muted small"> |
| 155 | 159 | No picture yet. One is taken when the preview comes up green. |
| 156 | 160 | </p>`, |
| 157 | { head }, | |
| 158 | 161 | ); |
| 159 | 162 | } |
| 160 | 163 | // Home first, then the rest in the order the manifest is likeliest to |
| @@ -300,13 +303,12 @@ export function lookCard( | ||
| 300 | 303 | const summary = line === undefined |
| 301 | 304 | ? html`` |
| 302 | 305 | : html`<p class="muted small" data-pairs>${line}</p>`; |
| 303 | return card( | |
| 306 | return block( | |
| 304 | 307 | html`<div class="stack"> |
| 305 | 308 | ${summary}${pages.map(group)}${when(attached.length > 0, () => |
| 306 | 309 | html`<div class="looks">${attached.map(person)}</div>`, |
| 307 | 310 | )} |
| 308 | 311 | </div>`, |
| 309 | { head }, | |
| 310 | 312 | ); |
| 311 | 313 | } |
| 312 | 314 |
test/looks-language.test.ts added +73
| @@ -0,0 +1,73 @@ | ||
| 1 | /** The look under a pull's checks speaks the home page's language: a block under an h2, not a card; pictures still need read access. */ | |
| 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 3 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 4 | import { tmpdir } from "node:os"; | |
| 5 | import { join } from "node:path"; | |
| 6 | import { openPull } from "../src/actions/pulls.ts"; | |
| 7 | import { commitToBranch, createRepo } from "../src/actions/repos.ts"; | |
| 8 | import { createApp, type App } from "../src/app.ts"; | |
| 9 | import { previewEnv } from "../src/apps/data.ts"; | |
| 10 | import { createApp as insertApp } from "../src/core/apps.ts"; | |
| 11 | import { recordLook } from "../src/core/looks.ts"; | |
| 12 | import { createUser } from "../src/core/users.ts"; | |
| 13 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 14 | import { quietLogs } from "./helpers/globals.ts"; | |
| 15 | ||
| 16 | quietLogs(); | |
| 17 | ||
| 18 | const HEAD = "c".repeat(40); | |
| 19 | let dataDir = ""; | |
| 20 | let app: App, servers: Servers; | |
| 21 | let publicPull = 0, privatePull = 0; | |
| 22 | ||
| 23 | beforeAll(async () => { | |
| 24 | dataDir = await mkdtemp(join(tmpdir(), "forge-looks-lang-")); | |
| 25 | app = createApp({ dataDir, port: 0 }); | |
| 26 | const made = await createUser(app.db, { | |
| 27 | origin: "seed", trusted: true, username: "alice", email: "[email protected]", | |
| 28 | password: "a-good-passphrase", adminIfFirst: true, | |
| 29 | }); | |
| 30 | if (!made.ok) throw new Error(made.error); | |
| 31 | const setup = async (name: string, visibility: "public" | "private") => { | |
| 32 | const r = await createRepo(app, made.value, { name, visibility }); | |
| 33 | if (!r.ok) throw new Error(String(r.error)); | |
| 34 | await commitToBranch(app, made.value, r.value, { message: "seed", files: [{ path: "README.md", content: "# g\n" }] }); | |
| 35 | await commitToBranch(app, made.value, r.value, { branch: "one", base: "main", message: "one", files: [{ path: "one.md", content: "x\n" }] }); | |
| 36 | const o = await openPull(app, made.value, r.value, { title: "One", body: "", baseBranch: "main", headBranch: "one" }); | |
| 37 | if (!o.ok) throw new Error(String(o.error)); | |
| 38 | const a = insertApp(app.db, { repoId: r.value.id, slug: name, access: visibility === "public" ? "public" : "private" }); | |
| 39 | if (!a.ok) throw new Error(a.error); | |
| 40 | recordLook(app.db, { | |
| 41 | appId: a.value.id, env: previewEnv(o.value.number), commitOid: HEAD, | |
| 42 | path: "looks/x.png", takenAs: "nobody", kind: "phone", theme: "light", | |
| 43 | }); | |
| 44 | return o.value.number; | |
| 45 | }; | |
| 46 | publicPull = await setup("garden", "public"); | |
| 47 | privatePull = await setup("vault", "private"); | |
| 48 | servers = await serve(app); | |
| 49 | }, 30_000); | |
| 50 | ||
| 51 | afterAll(async () => { | |
| 52 | servers.stop(); | |
| 53 | app.close(); | |
| 54 | await rm(dataDir, { recursive: true, force: true }); | |
| 55 | }); | |
| 56 | ||
| 57 | const main = (page: string) => page.slice(page.indexOf("<main"), page.indexOf("</main>")); | |
| 58 | ||
| 59 | test("the look on a pull is a block under an h2, no card, pictures intact", async () => { | |
| 60 | const m = main(await (await fetch(`${servers.url}/alice/garden/pulls/${publicPull}`)).text()); | |
| 61 | expect(m).toMatch(/<section class="settings-block" id="look"[^>]*>\s*<h2>Look<\/h2>/); | |
| 62 | const look = m.slice(m.indexOf('id="look"')); | |
| 63 | expect(look).toContain(`/${HEAD}/phone.png`); | |
| 64 | expect(look).toContain('data-slot="phone"'); | |
| 65 | expect(look.slice(0, look.indexOf("</section>"))).not.toContain('class="card'); | |
| 66 | }); | |
| 67 | ||
| 68 | test("a private repository's picture is a 404 to an outsider, and its pull page too", async () => { | |
| 69 | const url = `${servers.url}/alice/vault/looks/${previewEnv(privatePull)}/${HEAD}/phone.png`; | |
| 70 | expect((await fetch(url)).status).toBe(404); | |
| 71 | const page = await fetch(`${servers.url}/alice/vault/pulls/${privatePull}`); | |
| 72 | expect(page.status).toBe(404); | |
| 73 | }); |