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

Merged opened this

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

Merged

scout/forge:scout/9959-the-pictures-the-forge-took-of-an-enviro → main 2 commits fork

  • Merged. scout/9959-the-pictures-the-forge-took-of-an-enviro was merged into main as 006eab4.

Review

Sign in to review this change.
2 files changed +84 −9
src/web/pages/looks.ts +11 −9
@@ -34,7 +34,7 @@ import { mayFetchLook as mayFetchLookFor } from "../../looks/readable.ts";
3434 import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts";
3535 import { loadRepo, repoBase } from "../chrome.ts";
3636 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";
3838
3939 /** The newest picture at one width, when there is one. */
4040 export const pictured = (
@@ -115,7 +115,7 @@ export const mayFetchLook = (
115115 ): boolean => mayFetchLookFor(ctx.app, repo, record, look, ctx.auth.user);
116116
117117 /**
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
119119 * they are of, and a word when the head has moved since — or, honestly,
120120 * why there is no picture. More than one page in `app.looks` gets one
121121 * group per page, each labelled by its path, since each can be at a
@@ -132,7 +132,7 @@ export function lookCard(
132132 const base = repoBase(repo);
133133 const env = previewEnv(pull.number);
134134 // 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
136136 // without one.
137137 const writer = canSeeOwnerFacts(ctx.app.db, repo, ctx.auth.user);
138138 const every = looksForEnv(ctx.app.db, record.id, env, "any").filter(
@@ -146,15 +146,18 @@ export function lookCard(
146146 if (!writer && looks.length === 0) return html``;
147147 const attached = looks.filter(isAttached).reverse();
148148 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>`;
151155 if (taken.length === 0 && attached.length === 0) {
152156 if (pull.state !== "open" || pull.mergedAt !== null) return html``;
153 return card(
157 return block(
154158 html`<p class="muted small">
155159 No picture yet. One is taken when the preview comes up green.
156160 </p>`,
157 { head },
158161 );
159162 }
160163 // Home first, then the rest in the order the manifest is likeliest to
@@ -300,13 +303,12 @@ export function lookCard(
300303 const summary = line === undefined
301304 ? html``
302305 : html`<p class="muted small" data-pairs>${line}</p>`;
303 return card(
306 return block(
304307 html`<div class="stack">
305308 ${summary}${pages.map(group)}${when(attached.length > 0, () =>
306309 html`<div class="looks">${attached.map(person)}</div>`,
307310 )}
308311 </div>`,
309 { head },
310312 );
311313 }
312314
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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs