commit 7d56055f0b367966b51117811ff106441cbc0fb7
parent cf3bd181fc752aad1df56945e7f403d05159951a
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Tue, 28 Apr 2026 00:30:37 -0400
chore(editor): scaffold playwright alongside cypress
Both runners coexist until the cypress removal in a later phase, so
each migration step has a green-on-both verification gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat:
6 files changed, 165 insertions(+), 5 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -54,9 +54,11 @@ yarn-error.log*
/export/public/transcripts/
/transcripts/index.mdb/
-# editor cypress fixtures and ephemeral state
+# editor e2e fixtures and ephemeral state
/editor/test-transcripts/
/editor/test-settings.json
/editor/cypress/screenshots/
/editor/cypress/videos/
/editor/cypress/downloads/
+/editor/playwright-report/
+/editor/test-results/
diff --git a/editor/e2e/helpers.ts b/editor/e2e/helpers.ts
@@ -0,0 +1,69 @@
+import { dirname, resolve, join } from "node:path";
+import { fileURLToPath } from "node:url";
+import {
+ cp,
+ mkdir,
+ readFile,
+ rm,
+ stat,
+ writeFile,
+} from "node:fs/promises";
+
+const here = dirname(fileURLToPath(import.meta.url));
+const editorRoot = resolve(here, "..");
+const fixturesRoot = resolve(editorRoot, "cypress", "fixtures");
+const testTranscriptsDir = resolve(editorRoot, "test-transcripts");
+const testSettingsFile = resolve(editorRoot, "test-settings.json");
+
+const baseUrl = process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:3001";
+
+async function fileExists(p: string): Promise<boolean> {
+ try {
+ await stat(p);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+export async function resetData(fixtureName: string | null = null) {
+ await rm(testTranscriptsDir, { recursive: true, force: true });
+ await rm(testSettingsFile, { force: true });
+ if (fixtureName) {
+ const src = join(fixturesRoot, "test-transcripts", fixtureName);
+ if (!(await fileExists(src))) {
+ throw new Error(`Fixture not found: ${fixtureName}`);
+ }
+ await mkdir(testTranscriptsDir, { recursive: true });
+ await cp(src, testTranscriptsDir, { recursive: true });
+ } else {
+ await mkdir(join(testTranscriptsDir, "channels"), { recursive: true });
+ }
+ await fetch(`${baseUrl}/api/test/invalidate-cache`).catch(() => {});
+}
+
+export async function writeSettings(settings: Record<string, unknown>) {
+ await writeFile(testSettingsFile, JSON.stringify(settings, null, 2));
+ await fetch(`${baseUrl}/api/test/invalidate-cache`).catch(() => {});
+}
+
+export async function readJson<T = unknown>(relPath: string): Promise<T> {
+ const full = resolve(editorRoot, relPath);
+ const raw = await readFile(full, "utf8");
+ return JSON.parse(raw) as T;
+}
+
+export async function pathExists(relPath: string): Promise<boolean> {
+ return fileExists(resolve(editorRoot, relPath));
+}
+
+export function resolvePath(relPath: string): string {
+ return resolve(editorRoot, relPath);
+}
+
+export async function copyFixture(name: string) {
+ const dst = join(fixturesRoot, "test-transcripts", name);
+ await rm(dst, { recursive: true, force: true });
+ await mkdir(dst, { recursive: true });
+ await cp(testTranscriptsDir, dst, { recursive: true });
+}
diff --git a/editor/e2e/smoke.spec.ts b/editor/e2e/smoke.spec.ts
@@ -0,0 +1,15 @@
+import { test, expect } from "@playwright/test";
+import { resetData } from "./helpers";
+
+test.beforeEach(async () => {
+ await resetData();
+});
+
+test("dashboard renders heading and zero stats on empty fixture", async ({
+ page,
+}) => {
+ await page.goto("/");
+ await expect(
+ page.getByRole("heading", { level: 1, name: /dashboard/i }),
+ ).toBeVisible();
+});
diff --git a/editor/package.json b/editor/package.json
@@ -9,6 +9,8 @@
"build": "next build",
"start": "next start --port 3001",
"lint": "eslint",
+ "e2e": "playwright test",
+ "e2e:ui": "playwright test --ui",
"e2e-dev": "start-server-and-test dev:test http://localhost:3001 \"cypress open --e2e\"",
"e2e-dev:headless": "start-server-and-test dev:test http://localhost:3001 \"cypress run --e2e\""
},
@@ -20,6 +22,7 @@
"yt-dlp-transcript-common": "workspace:*"
},
"devDependencies": {
+ "@playwright/test": "^1.59.1",
"@tailwindcss/postcss": "^4.2.2",
"@testing-library/cypress": "^10.0.3",
"@types/node": "^20.19.39",
diff --git a/editor/playwright.config.ts b/editor/playwright.config.ts
@@ -0,0 +1,32 @@
+import { defineConfig, devices } from "@playwright/test";
+
+const PORT = Number(process.env.PORT ?? 3001);
+const baseURL = `http://localhost:${PORT}`;
+
+export default defineConfig({
+ testDir: "./e2e",
+ timeout: 30_000,
+ retries: process.env.CI ? 2 : 0,
+ reporter: process.env.CI ? "github" : "list",
+ outputDir: "test-results/",
+ fullyParallel: false,
+ workers: 1,
+ webServer: {
+ command: "pnpm dev:test",
+ url: baseURL,
+ timeout: 120_000,
+ reuseExistingServer: !process.env.CI,
+ },
+ use: {
+ baseURL,
+ trace: "on-first-retry",
+ screenshot: "only-on-failure",
+ video: "retain-on-failure",
+ },
+ projects: [
+ {
+ name: "chromium",
+ use: { ...devices["Desktop Chrome"] },
+ },
+ ],
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
@@ -31,7 +31,7 @@ importers:
version: 3.5.4
next:
specifier: 16.2.3
- version: 16.2.3(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
p-limit:
specifier: ^7.3.0
version: 7.3.0
@@ -71,7 +71,7 @@ importers:
version: 3.0.0
next:
specifier: 16.2.3
- version: 16.2.3(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react:
specifier: 19.2.4
version: 19.2.4
@@ -82,6 +82,9 @@ importers:
specifier: workspace:*
version: link:../common
devDependencies:
+ '@playwright/test':
+ specifier: ^1.59.1
+ version: 1.59.1
'@tailwindcss/postcss':
specifier: ^4.2.2
version: 4.2.4
@@ -123,7 +126,7 @@ importers:
version: 5.100.5(react@19.2.4)
next:
specifier: 16.2.3
- version: 16.2.3(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react:
specifier: 19.2.4
version: 19.2.4
@@ -809,6 +812,11 @@ packages:
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
engines: {node: '>=12.4.0'}
+ '@playwright/test@1.59.1':
+ resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
@@ -1905,6 +1913,11 @@ packages:
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
engines: {node: '>=10'}
+ fsevents@2.3.2:
+ resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -2701,6 +2714,16 @@ packages:
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'}
+ playwright-core@1.59.1:
+ resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ playwright@1.59.1:
+ resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==}
+ engines: {node: '>=18'}
+ hasBin: true
+
possible-typed-array-names@1.1.0:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
@@ -3794,6 +3817,10 @@ snapshots:
'@nolyfill/is-core-module@1.0.39': {}
+ '@playwright/test@1.59.1':
+ dependencies:
+ playwright: 1.59.1
+
'@rtsao/scc@1.1.0': {}
'@sec-ant/readable-stream@0.4.1': {}
@@ -5146,6 +5173,9 @@ snapshots:
jsonfile: 6.2.1
universalify: 2.0.1
+ fsevents@2.3.2:
+ optional: true
+
fsevents@2.3.3:
optional: true
@@ -5727,7 +5757,7 @@ snapshots:
negotiator@0.6.4: {}
- next@16.2.3(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
'@next/env': 16.2.3
'@swc/helpers': 0.5.15
@@ -5746,6 +5776,7 @@ snapshots:
'@next/swc-linux-x64-musl': 16.2.3
'@next/swc-win32-arm64-msvc': 16.2.3
'@next/swc-win32-x64-msvc': 16.2.3
+ '@playwright/test': 1.59.1
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
@@ -5896,6 +5927,14 @@ snapshots:
pify@2.3.0: {}
+ playwright-core@1.59.1: {}
+
+ playwright@1.59.1:
+ dependencies:
+ playwright-core: 1.59.1
+ optionalDependencies:
+ fsevents: 2.3.2
+
possible-typed-array-names@1.1.0: {}
postcss@8.4.31: