import { rm, writeFile } from "node:fs/promises"; import { test, expect } from "@playwright/test"; import { channelStage, pathExists, readJson, resetData, resolvePath, } from "./helpers"; const CHANNEL = "test-youtube"; const VIDEO_DIR = "20240101_test1234567"; const ROOT = `test-transcripts/channels/${CHANNEL}`; const DATA = `${ROOT}/data/${VIDEO_DIR}`; const VIDEO_URL = `/channels/${CHANNEL}/videos/${VIDEO_DIR}`; const VTT = "WEBVTT\n\n00:00:00.000 --> 00:00:05.000\nhello\n"; // The video page must recognize a regional/auto English VTT as the transcript, // not just the canonical transcript.en.vtt. test("video page treats transcript.en-US.vtt as the transcript", async ({ page, }) => { await resetData("one-youtube-channel-with-data"); await rm(resolvePath(`${DATA}/transcript.en.vtt`), { force: true }); await writeFile(resolvePath(`${DATA}/transcript.en-US.vtt`), VTT); await page.goto(VIDEO_URL); // hasTranscript === true: the transcribe card says so and the // "Mark untranscribable" card (shown only when untranscribed) is absent. await expect(page.getByText("Transcript present.")).toBeVisible(); await expect( page.getByRole("heading", { name: "Mark untranscribable" }), ).toHaveCount(0); // The regional track is marked the primary transcript. await expect( page.getByLabel("primary transcript transcript.en-US.vtt"), ).toBeVisible(); }); // The user can switch which VTT track is the primary transcript; the choice is // copied to the canonical transcript.en.vtt. test("switching the transcript source promotes a track to transcript.en.vtt", async ({ page, }) => { await resetData("one-youtube-channel-with-data"); await rm(resolvePath(`${DATA}/transcript.en.vtt`), { force: true }); await writeFile(resolvePath(`${DATA}/transcript.en-US.vtt`), VTT); await writeFile(resolvePath(`${DATA}/transcript.en-en-US.vtt`), VTT); await page.goto(VIDEO_URL); // en-US (manual/regional) outranks en-en-US (auto-translated) as the default. await expect( page.getByLabel("primary transcript transcript.en-US.vtt"), ).toBeVisible(); // Switch the primary to the auto track. await page .getByRole("button", { name: "set transcript.en-en-US.vtt as primary transcript", }) .click(); // The chosen track is copied to the canonical name… await expect .poll(() => pathExists(`${DATA}/transcript.en.vtt`), { timeout: 15_000 }) .toBe(true); // …and transcript.en.vtt becomes the primary (rank 0) after revalidation. await expect( page.getByLabel("primary transcript transcript.en.vtt"), ).toBeVisible(); }); // The page's transcript reader shows the primary and switches to another // English track only where its words differ — a reader's choice, nothing on // disk changes. test("the transcript reader switches to a track whose words differ", async ({ page, }) => { await resetData("one-youtube-channel-with-data"); await writeFile( resolvePath(`${DATA}/transcript.en-orig.vtt`), "WEBVTT\n\n00:00:00.000 --> 00:00:05.000\nwords as spoken\n", ); await writeFile( resolvePath(`${DATA}/transcript.en.vtt`), "WEBVTT\n\n00:00:00.000 --> 00:00:05.000\nwords as uploaded\n", ); await page.goto(VIDEO_URL); await page.getByLabel("Transcript stage summary", { exact: true }).click(); await page.getByRole("button", { name: "read transcript" }).click(); const cuesList = page.getByLabel("transcript cues", { exact: true }); await expect(cuesList).toContainText("words as spoken"); const switcher = page.getByLabel("transcript track", { exact: true }); await expect(switcher).toHaveValue("en-orig"); await expect(switcher.locator("option")).toHaveText([ "original audio captions (default)", "uploaded captions", ]); await switcher.selectOption("en"); await expect(cuesList).toContainText("words as uploaded"); // Nothing was pinned. expect(await pathExists(`${DATA}/transcript-pin.json`)).toBe(false); }); // The channel diagnostics surface a "non-standard transcript VTT name" bucket // for videos whose transcript rides on a non-canonical VTT name. test("diagnostics list a video with only transcript.en-US.vtt", async ({ page, }) => { await resetData("one-youtube-channel-with-data"); await rm(resolvePath(`${DATA}/transcript.en.vtt`), { force: true }); await writeFile(resolvePath(`${DATA}/transcript.en-US.vtt`), VTT); // A REPORT OF THE SWAPPED TREE, NOT JUST A REPORT. resetData's // invalidate-cache clears the snapshot scheduler's TIMER, but a regeneration // already in flight (armed by the previous spec's action) runs to the end // and writes snapshot.json from the tree it read — before the swap above. // generateReport returns as soon as any snapshot.json exists, so this test // used to render that stale report: 1 failure in release 11's full suite, // 9/9 alone. So: drop whatever is there, then regenerate until the report // shows the bucket (chat-only.spec's refreshReport does the same). await rm(resolvePath(`${ROOT}/snapshot.json`), { force: true }); await page.goto(`/channels/${CHANNEL}`); const refresh = page.getByRole("button", { name: /refresh report/i }); await refresh.waitFor({ state: "visible" }); await expect .poll( async () => { // Retried: a click before hydration fires nothing, and a stale write // landing after a good one is answered by the next click. await refresh.click({ timeout: 5_000 }).catch(() => {}); for (let i = 0; i < 20; i++) { const snap = await readJson<{ buckets?: { nonStandardVtt?: string[] }; }>(`${ROOT}/snapshot.json`).catch(() => null); if (snap?.buckets?.nonStandardVtt?.length === 1) return true; await new Promise((r) => setTimeout(r, 250)); } return false; }, { timeout: 60_000, intervals: [1000] }, ) .toBe(true); await page.goto(channelStage(CHANNEL, "diagnostics")); await expect( page.getByRole("heading", { name: /Non-standard transcript VTT name \(1\)/, }), ).toBeVisible(); });