chore(infra): update dev compose, add playwright e2e suite & assets
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user