Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit fb68a6bd3d626acf5e6537e68b657044e61bfaae
parent 7d3b71d05ba7b088b740c45c38462287ad3f1444
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Wed, 23 Sep 2026 09:02:58 -0400

e2e: pin the preview rules that are decided before a job exists

No spec starts a deploy. Every external binary the suite touches has a fake in
e2e/fixtures/bin and wrangler has none, because until previews there was no
reason for a spec to reach a deploy — so these assert the half that is settled
before any job and prove it by watching the sidecars not appear. A valid
preview is shown to reach the action by the refusal it gets there (no
Cloudflare project), which is the last gate before wrangler and distinguishable
from both "unknown key" and a branch complaint.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
Meditor/e2e/ops-api.spec.ts | 125++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Aeditor/e2e/site-publish-preview.spec.ts | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 234 insertions(+), 1 deletion(-)

diff --git a/editor/e2e/ops-api.spec.ts b/editor/e2e/ops-api.spec.ts @@ -50,7 +50,8 @@ type OpsResponse = { queued?: string[]; jobIds?: string[]; skipped?: { slug?: string; siteId?: string; reason: string }[]; - jobs?: { siteId: string; jobId: string }[]; + jobs?: { siteId: string; jobId: string; previewUrl?: string }[]; + previewUrl?: string; }; async function ops( @@ -768,6 +769,128 @@ test("build-site and build-deploy each take siteId or siteIds, and refuse both o } }); +// --- preview deploys ---------------------------------------------------------- +// +// NOTHING HERE STARTS A DEPLOY JOB, and that is deliberate. Every external +// binary this suite touches is a fake in e2e/fixtures/bin (yt-dlp, whisper, +// ffmpeg, …); wrangler has no fake, because until previews existed no spec had +// any reason to reach a deploy. So these assert the half that is decided +// BEFORE a job exists — which is the whole of what the preview rules are — and +// prove it by watching the job sidecars not appear. +test("deploy-site and build-deploy refuse a preview name that is not one, before any job", async ({ + request, +}) => { + await resetData("title-filter-channel"); + await settings(); + await writeSite("previewsite", { cloudflareProject: "proj" }); + + for (const action of ["deploy-site", "build-deploy"]) { + // "main" is the production branch: deploying there is not a preview, it is + // the live site, and the refusal says so rather than shipping it. + const before = await listJobIds(); + const main = await ops(request, action, { + siteId: "previewsite", + preview: "main", + }); + expect(main.status, action).toBe(400); + expect(main.body.error, action).toContain("production branch"); + expect(await listJobIds(), action).toEqual(before); + + // Uppercase is refused rather than lowercased FOR the caller: silently + // rewriting "Main" into "main" would deploy to production. + const upper = await ops(request, action, { + siteId: "previewsite", + preview: "Main", + }); + expect(upper.status, action).toBe(400); + expect(upper.body.error, action).toContain("not a valid preview branch name"); + expect(upper.body.error, action).not.toContain("production branch"); + + const spaced = await ops(request, action, { + siteId: "previewsite", + preview: "bad name", + }); + expect(spaced.status, action).toBe(400); + expect(spaced.body.error, action).toContain("not a valid preview branch name"); + + const tooLong = await ops(request, action, { + siteId: "previewsite", + preview: "a".repeat(29), + }); + expect(tooLong.status, action).toBe(400); + expect(tooLong.body.error, action).toContain("at most 28 characters"); + + expect(await listJobIds(), action).toEqual(before); + } +}); + +test("a valid preview is accepted and reaches the action, on both deploy routes", async ({ + request, +}) => { + await resetData("title-filter-channel"); + await settings(); + // NO Cloudflare project, on purpose: the action's own refusal is the last + // gate before wrangler, so a 400 saying THAT — rather than "unknown key: + // preview" or a branch complaint — proves the name passed validation and the + // route got all the way to the action, without a deploy ever starting. + await writeSite("noproj", {}); + + for (const action of ["deploy-site", "build-deploy"]) { + const before = await listJobIds(); + const { status, body } = await ops(request, action, { + siteId: "noproj", + preview: "tags-exclude", + }); + expect(status, action).toBe(400); + expect(body.error, action).toContain("no Cloudflare Pages project"); + expect(body.error, action).not.toContain("unknown key"); + expect(await listJobIds(), action).toEqual(before); + } +}); + +test("deploy-site takes siteId or siteIds, and has no all", async ({ + request, +}) => { + await resetData("title-filter-channel"); + await settings(); + + const both = await ops(request, "deploy-site", { siteId: "a", siteIds: ["a"] }); + expect(both.status).toBe(400); + expect(both.body.error).toContain("not both"); + + const neither = await ops(request, "deploy-site", {}); + expect(neither.status).toBe(400); + expect(neither.body.error).toContain("siteId"); + + const before = await listJobIds(); + const bad = await ops(request, "deploy-site", { siteIds: ["deploysite", "BAD ID"] }); + expect(bad.status).toBe(400); + expect(bad.body.error).toContain("not a valid site id"); + expect(await listJobIds()).toEqual(before); + + // Deploy-only has no all-sites form — build-deploy owns that — so `all` is + // an unknown key here rather than a second spelling of it. + const all = await ops(request, "deploy-site", { all: true }); + expect(all.status).toBe(400); + expect(all.body.error).toContain("unknown key"); +}); + +test("build-deploy refuses a preview alongside all rather than building everything", async ({ + request, +}) => { + await resetData("title-filter-channel"); + await settings(); + + const before = await listJobIds(); + const { status, body } = await ops(request, "build-deploy", { + all: true, + preview: "tags-exclude", + }); + expect(status).toBe(400); + expect(body.error).toContain('"preview" is not supported with "all"'); + expect(await listJobIds()).toEqual(before); +}); + test("build-site with a bare siteId starts one build-export job", async ({ request, }) => { diff --git a/editor/e2e/site-publish-preview.spec.ts b/editor/e2e/site-publish-preview.spec.ts @@ -0,0 +1,110 @@ +import { test, expect } from "@playwright/test"; +import type { Page } from "@playwright/test"; +import { resetData, writeSite } from "./helpers"; + +// The preview control on a site's Publish tab. +// +// NOTHING HERE CLICKS DEPLOY. Every external binary the suite touches has a +// fake in e2e/fixtures/bin; wrangler has none, because no spec has ever had a +// reason to reach a deploy. What is worth pinning anyway is everything decided +// BEFORE the job: that the two deploys are told apart by their labels, that the +// branch input refuses exactly what the server refuses and with the same +// sentence, and that the alias is shown while you type — it is a function of +// the project and the branch, so it is knowable without deploying anything. + +// The Deploy controls live under the "Individual steps" disclosure. +async function openIndividualSteps(page: Page, siteId: string) { + await page.goto(`/sites/${siteId}/publish`); + await page.getByText("Individual steps").click(); +} + +const preview = (page: Page) => + page.getByRole("region", { name: "Deploy preview" }); + +test.beforeEach(async () => { + await resetData("empty"); +}); + +test("the production deploy says production, and a preview sits beside it", async ({ + page, +}) => { + await writeSite("testsite", { cloudflareProject: "proj" }); + await openIndividualSteps(page, "testsite"); + + // "Deploy" alone is the button somebody clicks by reflex when they meant the + // safe one, which is the whole reason for the extra word. + await expect( + page.getByRole("button", { name: "Deploy to production", exact: true }), + ).toBeVisible(); + await expect( + page.getByRole("button", { name: "Deploy", exact: true }), + ).toHaveCount(0); + + const panel = preview(page); + await expect(panel.getByRole("button", { name: "Deploy preview" })).toBeVisible(); + // Defaulted, so the common case is one click. + await expect(panel.getByLabel("preview branch")).toHaveValue("preview"); +}); + +test("the alias is shown before the deploy, and follows the branch name", async ({ + page, +}) => { + await writeSite("testsite", { cloudflareProject: "proj" }); + await openIndividualSteps(page, "testsite"); + const panel = preview(page); + + const link = panel.getByRole("link", { + name: "https://preview.proj.pages.dev", + }); + await expect(link).toBeVisible(); + await expect(link).toHaveAttribute("href", "https://preview.proj.pages.dev"); + + const input = panel.getByLabel("preview branch"); + await input.fill("tags-exclude"); + await expect( + panel.getByRole("link", { name: "https://tags-exclude.proj.pages.dev" }), + ).toBeVisible(); +}); + +test("main disables the preview button and says why", async ({ page }) => { + await writeSite("testsite", { cloudflareProject: "proj" }); + await openIndividualSteps(page, "testsite"); + const panel = preview(page); + + const button = panel.getByRole("button", { name: "Deploy preview" }); + await expect(button).toBeEnabled(); + + await panel.getByLabel("preview branch").fill("main"); + await expect(button).toBeDisabled(); + await expect(panel.getByText(/is the production branch/)).toBeVisible(); + // No alias while the name is refused — a link to a hostname that will not + // exist is worse than none. + await expect(panel.getByRole("link")).toHaveCount(0); + + // Uppercase is refused for its shape rather than lowercased into production. + await panel.getByLabel("preview branch").fill("Main"); + await expect(button).toBeDisabled(); + await expect( + panel.getByText(/not a valid preview branch name/), + ).toBeVisible(); + + // And a good name re-arms it. + await panel.getByLabel("preview branch").fill("rc-1"); + await expect(button).toBeEnabled(); + await expect( + panel.getByRole("link", { name: "https://rc-1.proj.pages.dev" }), + ).toBeVisible(); +}); + +test("a site with no Cloudflare project says so instead of linking a hostname", async ({ + page, +}) => { + await writeSite("noproj", {}); + await openIndividualSteps(page, "noproj"); + const panel = preview(page); + + await expect( + panel.getByText(/Set a Cloudflare Pages project for this site/), + ).toBeVisible(); + await expect(panel.getByRole("link")).toHaveCount(0); +});