Archilyzer · Source

archilyzer

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

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:
Aexport/e2e/archives-off.spec.ts | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexport/e2e/transcript-downloads.spec.ts | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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(); +});