Public: anyone can read this; the crew can change it.
Every forge preview takes a phone look of the sample repository's Settings scrolled to "Move to another owner", with its measured width #10272
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/10194-every-forge-preview-takes-a-phone-look-o → main 3 commits fork
7 files
changed +66
−5
src/core/looks.ts +1 −1
| @@ -70,7 +70,7 @@ export const DEFAULT_PAGE = "/"; | ||
| 70 | 70 | * column is truth. |
| 71 | 71 | */ |
| 72 | 72 | const pageSlug = (page: string): string => |
| 73 | page === DEFAULT_PAGE ? "" : page.slice(1).replace(/\//g, "-"); | |
| 73 | page === DEFAULT_PAGE ? "" : page.slice(1).replace(/[/#]/g, "-"); | |
| 74 | 74 | |
| 75 | 75 | /** |
| 76 | 76 | * The fragment one picture is named by, on disk and in its URL: the page |
src/spawn/preview-looks.ts +10
| @@ -48,6 +48,15 @@ export const PREVIEW_MOVED = "moved"; | ||
| 48 | 48 | export const PREVIEW_MOVED_OWNER = "preview-ada"; |
| 49 | 49 | export const PREVIEW_MOVED_PAGE = `/${PREVIEW_MOVED_OWNER}/${PREVIEW_MOVED}`; |
| 50 | 50 | |
| 51 | /** | |
| 52 | * The sample's Settings scrolled to "Move to another owner" by the fragment | |
| 53 | * (#10194): the owner field, button and refusal sentences must fit a 390 px | |
| 54 | * phone. The seed refuses to finish unless the page answers 200 and holds | |
| 55 | * `id="move"`. The path is the page's own; no route is added. | |
| 56 | */ | |
| 57 | export const PREVIEW_MOVE_PAGE = `${PREVIEW_REPO_HOME}/settings`; | |
| 58 | export const PREVIEW_MOVE_LOOK = `${PREVIEW_MOVE_PAGE}#move`; | |
| 59 | ||
| 51 | 60 | /** The Iterations page of the seeded fork family (#9182). */ |
| 52 | 61 | export const PREVIEW_ITERATIONS_PAGE = `${PREVIEW_REPO_HOME}/iterations`; |
| 53 | 62 | /** |
| @@ -142,6 +151,7 @@ export const PREVIEW_OWNER_LOOKS: readonly OwnerLook[] = [ | ||
| 142 | 151 | PREVIEW_REPO_HOME, |
| 143 | 152 | PREVIEW_APP_PAGE, |
| 144 | 153 | PREVIEW_MOVED_PAGE, |
| 154 | PREVIEW_MOVE_LOOK, | |
| 145 | 155 | PREVIEW_PULL_PAGE, |
| 146 | 156 | PREVIEW_PULL_FILES, |
| 147 | 157 | PREVIEW_ITERATIONS_PAGE, |
src/spawn/preview-seed.ts +32
| @@ -62,6 +62,7 @@ import { | ||
| 62 | 62 | PREVIEW_MOVED, |
| 63 | 63 | PREVIEW_MOVED_OWNER, |
| 64 | 64 | PREVIEW_MOVED_PAGE, |
| 65 | PREVIEW_MOVE_PAGE, | |
| 65 | 66 | PREVIEW_REPO, |
| 66 | 67 | PREVIEW_SETTINGS_PAGES, |
| 67 | 68 | } from "./preview-looks.ts"; |
| @@ -112,6 +113,33 @@ async function seedMoved(app: App, admin: User): Promise<Result<void, string>> { | ||
| 112 | 113 | return ok(); |
| 113 | 114 | } |
| 114 | 115 | |
| 116 | /** | |
| 117 | * The sample's Settings, asked for as the admin through `ask`, must answer | |
| 118 | * 200 and hold `id="move"`: the section its phone look scrolls to (#10194). | |
| 119 | * `ask` is the forge's own handler; a test hands in one that answers otherwise. | |
| 120 | */ | |
| 121 | export async function checkMoveSection( | |
| 122 | ask: (path: string) => Promise<{ readonly status: number; text(): Promise<string> }>, | |
| 123 | ): Promise<Result<void, string>> { | |
| 124 | const res = await ask(PREVIEW_MOVE_PAGE); | |
| 125 | if (res.status !== 200) return err(`${PREVIEW_MOVE_PAGE} answers ${String(res.status)}, not 200`); | |
| 126 | if (!(await res.text()).includes('id="move"')) return err(`${PREVIEW_MOVE_PAGE} holds no id="move"`); | |
| 127 | return ok(); | |
| 128 | } | |
| 129 | ||
| 130 | async function seedMoveSection(app: App, admin: User): Promise<Result<void, string>> { | |
| 131 | const http = forgeHttp(app); | |
| 132 | const session = createSession(app.db, admin.id); | |
| 133 | try { | |
| 134 | return await checkMoveSection((path) => | |
| 135 | http.handle(new Request(`http://localhost${path}`, { headers: { cookie: `forge_session=${session.token}` } })), | |
| 136 | ); | |
| 137 | } finally { | |
| 138 | destroySession(app.db, session.token); | |
| 139 | http.close(); | |
| 140 | } | |
| 141 | } | |
| 142 | ||
| 115 | 143 | /** The seeded wave's version and headline, which the owner's /waves look shows. */ |
| 116 | 144 | export const PREVIEW_WAVE = "0.1.0"; |
| 117 | 145 | export const PREVIEW_WAVE_HEADLINE = "The sample says hello"; |
| @@ -170,6 +198,8 @@ export async function seedPreview( | ||
| 170 | 198 | if (getUserSetting(app.db, admin.id, SEEDED) !== undefined) { |
| 171 | 199 | const settings = await seedSettingsPages(app, admin); |
| 172 | 200 | if (!settings.ok) return settings; |
| 201 | const moveSection = await seedMoveSection(app, admin); | |
| 202 | if (!moveSection.ok) return moveSection; | |
| 173 | 203 | const files = await seedConfigFiles(app, admin); |
| 174 | 204 | return files.ok ? ok({ seeded: false }) : files; |
| 175 | 205 | } |
| @@ -178,6 +208,8 @@ export async function seedPreview( | ||
| 178 | 208 | setUserSetting(app.db, admin.id, SEEDED, new Date().toISOString()); |
| 179 | 209 | const settings = await seedSettingsPages(app, admin); |
| 180 | 210 | if (!settings.ok) return settings; |
| 211 | const moveSection = await seedMoveSection(app, admin); | |
| 212 | if (!moveSection.ok) return moveSection; | |
| 181 | 213 | const files = await seedConfigFiles(app, admin); |
| 182 | 214 | return files.ok ? ok({ seeded: true }) : files; |
| 183 | 215 | } |
src/web/pages/settings.ts +1 −1
| @@ -416,7 +416,7 @@ export async function settingsPage( | ||
| 416 | 416 | </div> |
| 417 | 417 | </form> |
| 418 | 418 | </section>`; |
| 419 | const move = html`<section class="settings-block"> | |
| 419 | const move = html`<section class="settings-block" id="move"> | |
| 420 | 420 | <h2>Move to another owner</h2> |
| 421 | 421 | <form method="get" action="/-/confirm"> |
| 422 | 422 | <input type="hidden" name="to" value="${base}/settings/transfer" /> |
src/web/style/components.ts +2 −2
| @@ -127,8 +127,8 @@ select.input { | ||
| 127 | 127 | .field > label { |
| 128 | 128 | display: block; font-size: 13px; font-weight: 550; margin: 0 0 6px; color: var(--fg); |
| 129 | 129 | } |
| 130 | .field .hint { display: block; font-size: 12.5px; color: var(--muted-fg); margin-top: 6px; } | |
| 131 | .field .error { display: block; font-size: 12.5px; color: var(--danger); margin-top: 6px; font-weight: 500; } | |
| 130 | .field .hint { display: block; font-size: 12.5px; color: var(--muted-fg); margin-top: 6px; overflow-wrap: anywhere; } | |
| 131 | .field .error { display: block; font-size: 12.5px; color: var(--danger); margin-top: 6px; font-weight: 500; overflow-wrap: anywhere; } | |
| 132 | 132 | .field[data-invalid] > label { color: var(--danger); } |
| 133 | 133 | /* A group of checks is one field: the legend reads as a label, no frame. */ |
| 134 | 134 | fieldset.field { border: 0; padding: 0; min-width: 0; } |
test/looks.test.ts +1
| @@ -723,6 +723,7 @@ describe("the look", () => { | ||
| 723 | 723 | const dark = recordLook(app.db, { ...base, theme: "dark", path: "looks/x-dark.png" }); |
| 724 | 724 | expect(dark.id).not.toBe(old.id); |
| 725 | 725 | expect(dark.theme).toBe("dark"); |
| 726 | expect(lookSlot("/o/r/settings#move", "phone")).toBe("o-r-settings-move-phone"); | |
| 726 | 727 | expect(lookSlot("/", "phone")).toBe("phone"); |
| 727 | 728 | expect(lookSlot("/", "phone", "dark")).toBe("phone-dark"); |
| 728 | 729 | expect(lookSlot("/board", "phone", "dark")).toBe("board-phone-dark"); |
test/preview-moved.test.ts +19 −1
| @@ -9,7 +9,7 @@ import { createSession } from "../src/core/sessions.ts"; | ||
| 9 | 9 | import { recentMove } from "../src/core/transfer.ts"; |
| 10 | 10 | import { getRepo } from "../src/core/repos.ts"; |
| 11 | 11 | import { PREVIEW_ADMIN, PREVIEW_MOVED, PREVIEW_MOVED_PAGE, PREVIEW_OWNER_LOOKS } from "../src/spawn/preview-looks.ts"; |
| 12 | import { previewAdmin, seedPreview } from "../src/spawn/preview-seed.ts"; | |
| 12 | import { checkMoveSection, previewAdmin, seedPreview } from "../src/spawn/preview-seed.ts"; | |
| 13 | 13 | |
| 14 | 14 | let dir: string; |
| 15 | 15 | let app: App; |
| @@ -42,3 +42,21 @@ test("its head is looked at on desktop and phone", () => { | ||
| 42 | 42 | for (const kind of ["desktop", "phone"] as const) |
| 43 | 43 | expect(PREVIEW_OWNER_LOOKS).toContainEqual({ page: PREVIEW_MOVED_PAGE, kind }); |
| 44 | 44 | }); |
| 45 | ||
| 46 | test("its Settings is looked at on desktop and phone, by the fragment that lands on the move section", async () => { | |
| 47 | for (const kind of ["desktop", "phone"] as const) | |
| 48 | expect(PREVIEW_OWNER_LOOKS).toContainEqual({ page: "/preview-admin/sample/settings#move", kind }); | |
| 49 | const cookie = `forge_session=${createSession(app.db, previewAdmin(app)!.id).token}`; | |
| 50 | const res = await fetch(`${servers.url}/preview-admin/sample/settings`, { headers: { cookie } }); | |
| 51 | expect(res.status).toBe(200); | |
| 52 | expect(await res.text()).toContain('id="move"'); | |
| 53 | }); | |
| 54 | ||
| 55 | test("the seed refuses a Settings that is not 200 or lacks the move section", async () => { | |
| 56 | const answer = (status: number, body: string) => async () => ({ status, text: async () => body }); | |
| 57 | expect((await checkMoveSection(answer(200, '<section id="move">'))).ok).toBe(true); | |
| 58 | const missing = await checkMoveSection(answer(404, "")); | |
| 59 | expect(!missing.ok && missing.error).toContain("404"); | |
| 60 | const bare = await checkMoveSection(answer(200, "<h2>Move to another owner</h2>")); | |
| 61 | expect(!bare.ok && bare.error).toContain('id="move"'); | |
| 62 | }); |