76 lines
3.0 KiB
TypeScript
76 lines
3.0 KiB
TypeScript
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);
|
||
|
|
});
|
||
|
|
});
|