import { test, expect } from "@playwright/test"; import { BASE, requireAuth, injectAuth } from "./helpers"; test.describe("Video player & media signing", () => { test("lesson endpoint returns a signed videoUrl", async ({ page }) => { const auth = requireAuth(); const treeRes = await page.request.get(`${BASE}/api/tree`, { headers: { Authorization: `Bearer ${auth.accessToken}` }, }); expect(treeRes.status()).toBe(200); const tree = await treeRes.json(); const firstLesson = tree.chapters?.[0]?.lessons?.[0]; if (!firstLesson) { test.skip(true, "No lessons in tree"); return; } const lessonRes = await page.request.get(`${BASE}/api/lessons/${firstLesson.id}`, { headers: { Authorization: `Bearer ${auth.accessToken}` }, }); expect(lessonRes.status()).toBe(200); const lesson = await lessonRes.json(); expect(lesson.videoUrl).toBeTruthy(); expect(lesson.videoUrl).toMatch(/^https?:\/\//); // Must be signed (has query params) or served via /media/ proxy const url = new URL(lesson.videoUrl); const isSigned = url.searchParams.size > 0 || lesson.videoUrl.includes("/media/"); expect(isSigned, `videoUrl not signed: ${lesson.videoUrl}`).toBe(true); }); test("player page renders without JS errors for first lesson", async ({ page }) => { const auth = requireAuth(); const treeRes = await page.request.get(`${BASE}/api/tree`, { headers: { Authorization: `Bearer ${auth.accessToken}` }, }); const tree = await treeRes.json(); const firstLesson = tree.chapters?.[0]?.lessons?.[0]; if (!firstLesson) { test.skip(true, "No lessons in tree"); return; } const jsErrors: string[] = []; page.on("pageerror", (err) => jsErrors.push(err.message)); await injectAuth(page); await page.goto(`/player/${firstLesson.id}`); await page.waitForLoadState("networkidle", { timeout: 20_000 }); // Filter known expected media errors (video load failures in headless are expected) const realErrors = jsErrors.filter( (e) => !e.includes("video") && !e.includes("media") && !e.includes("hls") && !e.includes("AbortError") ); expect(realErrors, `JS errors on player page: ${realErrors.join(", ")}`).toHaveLength(0); }); test("admin videos panel fires no media requests on grid load", async ({ page }) => { requireAuth(); const videoRequests: string[] = []; page.on("request", (req) => { if (req.resourceType() === "media" || /\.(mp4|m3u8|ts)(\?|$)/.test(req.url())) { videoRequests.push(req.url()); } }); await injectAuth(page); await page.goto("/admin"); await page.waitForLoadState("networkidle", { timeout: 15_000 }); // Click Videos tab if present const videosTab = page.getByRole("button", { name: /ویدیو/i }); if (await videosTab.count() > 0) { await videosTab.first().click(); await page.waitForLoadState("networkidle", { timeout: 10_000 }); } expect(videoRequests, `Unexpected media requests on grid load: ${videoRequests.join(", ")}`).toHaveLength(0); }); });