import { test, expect, type Page } from "@playwright/test"; import { openApp, openFirstVideo } from "./helpers"; // Locks the in-app player's open/close contract. The player is a portaled overlay whose // inconsistent mounting caused a regression — exactly the kind of thing the refactor's // overlay-root convention must not break. We assert the modal shell, not YouTube playback. test.describe("player modal", () => { test.beforeEach(async ({ page }) => { // Hermetic: don't actually load the YouTube iframe/player. await page.route(/youtube\.com/, (route) => route.abort()); }); test("opens from a card and closes via the close button", async ({ page }) => { await openApp(page); await openFirstVideo(page); await page.getByTestId("player-close").click(); await expect(page.getByTestId("player-modal")).toHaveCount(0); }); test("closes on Escape", async ({ page }) => { await openApp(page); await openFirstVideo(page); await page.keyboard.press("Escape"); await expect(page.getByTestId("player-modal")).toHaveCount(0); }); }); // The video's size, in the windows the owner meets: layout bugs no DOM test can see. Each viewport // is a real one — 1920×911 is a 1080p screen at 100% (the side-panel layout), 1097×520 the same // screen at 175% or a small laptop at 100% (the stacked layout, on a short window), 1400×380 a wide // window made short by docked devtools (the side-panel layout, whose card does not scroll). test.describe("player modal sizing", () => { test.beforeEach(async ({ page }) => { await page.route(/youtube\.com/, (route) => route.abort()); }); const box = async (page: Page, selector: string) => (await page.locator(selector).first().boundingBox())!; for (const [w, h] of [ [1920, 911], [1097, 520], [1400, 380], ] as const) { test(`the video keeps a real size at ${w}×${h}, and maximised fills the window`, async ({ page, }) => { await page.setViewportSize({ width: w, height: h }); await openApp(page); await openFirstVideo(page); // A short window narrows the card so the chrome under the video fits — but the chrome grows // as the card narrows (its text wraps), and that loop once ran the card down to 2 px. const stage = await box(page, ".player-stage"); expect(stage.width).toBeGreaterThanOrEqual(Math.min(w * 0.5, 560)); expect(stage.height).toBeGreaterThan(stage.width * 0.5); // …and all of it is in sight: its bottom edge is the YouTube control bar, and the side-panel // card clips what overflows it (it does not scroll), the stacked card scrolls to it. const overflow = await page .locator(".player-stage") .first() .evaluate((el) => { const card = el.parentElement!; return el.getBoundingClientRect().bottom - card.getBoundingClientRect().bottom; }); expect(overflow).toBeLessThanOrEqual(0.5); // Maximise (F): the stage is fixed to the viewport. In the side-panel layout its `self-start` // once aligned the fixed box too, and it shrank to its content's height — 0 px, a thin line. await page.getByTestId("player-modal").press("f"); await expect(page.locator(".player-stage--max")).toHaveCount(1); const max = await box(page, ".player-stage--max"); expect([Math.round(max.width), Math.round(max.height)]).toEqual([w, h]); }); } });