commit f130084a614cf3757349bb6ec8f2bdfac7d8f58d
parent 9f6beed2c115142e5b3036411c5133f4ebf877fd
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 24 Sep 2026 22:07:30 -0400
export: e2e for transcriptDownloads and archives: false
transcript-downloads: the three controls present by default, absent by
accessible name with the key false (the one served fixture site's
site.json is flipped for one test and restored; a leftover key is stripped
on the next run). archives-off: compose-site into a scratch public dir
writes no archives/manifest.json with archives: false and clears a stale
one; with the key absent it writes a fresh manifest.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 165 insertions(+), 0 deletions(-)
diff --git a/export/e2e/archives-off.spec.ts b/export/e2e/archives-off.spec.ts
@@ -0,0 +1,74 @@
+import { execFileSync } from "node:child_process";
+import fs from "node:fs";
+import path from "node:path";
+import { expect, test } from "@playwright/test";
+
+// site.json `archives: false` is a BUILD-time switch: compose-site skips the
+// zips and writes no archives/manifest.json, and the Downloads page plus its
+// Header/Footer links all key off that manifest's presence (app/lib/archives.ts
+// hasArchives()). The dev server this suite drives reads the checked-out
+// export/public, so the switch is exercised where it acts — one compose-site
+// run per case into a scratch EXPORT_PUBLIC_DIR under the test's output dir,
+// over a scratch site with no channels. No browser.
+
+const TSX = path.resolve(process.cwd(), "node_modules", ".bin", "tsx");
+const COMPOSE = path.resolve(process.cwd(), "..", "common", "bin", "compose-site.ts");
+
+function compose(root: string, site: Record<string, unknown>) {
+ const sitesDir = path.join(root, "transcripts", "sites", "scratchsite");
+ fs.mkdirSync(sitesDir, { recursive: true });
+ fs.writeFileSync(
+ path.join(sitesDir, "site.json"),
+ JSON.stringify({
+ siteId: "scratchsite",
+ siteTitle: "Scratch",
+ siteDescription: "",
+ headerTitle: "Scratch",
+ homeTagline: "",
+ groups: [{ id: "default", name: "All", selectedByDefault: true }],
+ defaultGroupId: "default",
+ channels: [],
+ ...site,
+ }),
+ );
+ const publicDir = path.join(root, "public");
+ // A stale bundle from an earlier (archives-on) build: a disabled site must
+ // never serve it.
+ fs.mkdirSync(path.join(publicDir, "archives"), { recursive: true });
+ fs.writeFileSync(
+ path.join(publicDir, "archives", "manifest.json"),
+ JSON.stringify({ entries: [{ filename: "stale.zip" }] }),
+ );
+ fs.writeFileSync(path.join(root, "settings.json"), "{}");
+ execFileSync(TSX, [COMPOSE], {
+ cwd: process.cwd(),
+ env: {
+ ...process.env,
+ SITE_ID: "scratchsite",
+ TRANSCRIPTS_DIR: path.join(root, "transcripts"),
+ SITES_DIR: path.join(root, "transcripts", "sites"),
+ EXPORT_PUBLIC_DIR: publicDir,
+ EXPORT_INDEX_DIR: path.join(root, ".export-index"),
+ EXPORT_BUILDS_DIR: path.join(root, ".export-builds"),
+ SETTINGS_FILE: path.join(root, "settings.json"),
+ BUILD_ARCHIVES: "1",
+ },
+ stdio: "pipe",
+ });
+ return path.join(publicDir, "archives", "manifest.json");
+}
+
+test("archives: false composes no zip manifest (and clears a stale one)", async ({}, testInfo) => {
+ const manifest = compose(testInfo.outputPath("off"), { archives: false });
+ expect(fs.existsSync(manifest)).toBe(false);
+});
+
+test("archives absent (default on) composes a manifest", async ({}, testInfo) => {
+ const manifest = compose(testInfo.outputPath("on"), {});
+ expect(fs.existsSync(manifest)).toBe(true);
+ const parsed = JSON.parse(fs.readFileSync(manifest, "utf8")) as {
+ entries: { filename: string }[];
+ };
+ // A fresh manifest from this build, never the stale bundle.
+ expect(parsed.entries.map((e) => e.filename)).not.toContain("stale.zip");
+});
diff --git a/export/e2e/transcript-downloads.spec.ts b/export/e2e/transcript-downloads.spec.ts
@@ -0,0 +1,91 @@
+import fs from "node:fs";
+import path from "node:path";
+import { expect, test, type Page } from "@playwright/test";
+import { CHANNEL_SLUG, VIDEO_TRANSCRIPT_ONLY } from "./fixtures/data";
+import { expectModalOpen, installRoutes } from "./helpers";
+
+// site.json `transcriptDownloads` gates the transcript modal's three per-video
+// export controls. The suite serves ONE site (SITE_ID=testsite, SITES_DIR =
+// e2e/fixtures/sites) and currentSite() re-reads site.json on every dev render,
+// so the off case flips the key in the fixture for the duration of one test and
+// puts the file back. A run killed mid-test leaves the key behind; beforeAll
+// strips it, so the next run starts from the committed fixture either way.
+const SITE_FILE = path.resolve(
+ process.cwd(),
+ "e2e",
+ "fixtures",
+ "sites",
+ "testsite",
+ "site.json",
+);
+
+function writeSiteFile(value: Record<string, unknown>) {
+ fs.writeFileSync(SITE_FILE, `${JSON.stringify(value, null, 2)}\n`);
+}
+
+let pristine = "";
+
+test.describe.configure({ mode: "serial" });
+
+test.beforeAll(() => {
+ pristine = fs.readFileSync(SITE_FILE, "utf8");
+ const parsed = JSON.parse(pristine) as Record<string, unknown>;
+ if ("transcriptDownloads" in parsed) {
+ delete parsed.transcriptDownloads;
+ writeSiteFile(parsed);
+ pristine = fs.readFileSync(SITE_FILE, "utf8");
+ }
+});
+
+test.afterEach(() => {
+ fs.writeFileSync(SITE_FILE, pristine);
+});
+
+// The three controls, by the accessible names they carry once the transcript
+// has loaded and no clip is marked.
+function exportControls(page: Page) {
+ return [
+ page.getByRole("button", { name: "Mark both clip start and end first" }),
+ page.getByRole("button", { name: "Download this transcript as a file" }),
+ page.getByRole("button", {
+ name: "Copy this transcript as Markdown (for AI)",
+ }),
+ ];
+}
+
+async function openModal(page: Page) {
+ await installRoutes(page);
+ await page.goto(`/?v=${CHANNEL_SLUG}/${VIDEO_TRANSCRIPT_ONLY}`);
+ await expectModalOpen(page);
+ // Loaded: the cue list is up, so the file controls are past "Nothing to …".
+ await expect(page.locator("li", { hasText: "beta line" })).toBeVisible();
+}
+
+test("default (key absent): the three export controls are present", async ({
+ page,
+}) => {
+ await openModal(page);
+ for (const control of exportControls(page)) {
+ await expect(control).toBeVisible();
+ }
+ await expect(
+ page.getByRole("button", { name: "Copy share link at current time" }),
+ ).toBeVisible();
+});
+
+test("transcriptDownloads: false hides all three; Share stays", async ({
+ page,
+}) => {
+ writeSiteFile({ ...JSON.parse(pristine), transcriptDownloads: false });
+ await openModal(page);
+ for (const control of exportControls(page)) {
+ await expect(control).toHaveCount(0);
+ }
+ // Nothing else in the bar went with them.
+ await expect(
+ page.getByRole("button", { name: "Copy share link at current time" }),
+ ).toBeVisible();
+ await expect(
+ page.getByRole("button", { name: "Mark clip end at current time" }),
+ ).toBeVisible();
+});