/** * Every opaque layer the native-video CSS claims to clear must actually exist. * * TRACES: UR-003, UR-004 | DR-185 | UT-186 * * The compositing rules in app.css clear the page's painted backgrounds so the * ExoPlayer SurfaceView behind the WebView can be seen. One of the three * selectors, `[data-app-shell]`, was written against an attribute that **no * component ever set** — in any commit — so the app shell went on painting * `--color-background` across the whole viewport, underneath a player that had * correctly made itself transparent. The WebView therefore composited opaque * and the surface could never show through. * * That failure is invisible three ways over: the CSS is valid, the selector is * plausible, and the symptom (black screen, audio fine) is identical to a * genuine compositing failure — which is how it survived DR-150 through DR-172. * A rule that matches nothing is the specific defect worth a tripwire, so this * asserts the relationship rather than the rule: every attribute the block * targets is set somewhere in the app. */ import { describe, it, expect } from "vitest"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const here = path.dirname(fileURLToPath(import.meta.url)); const srcRoot = path.resolve(here, "../.."); function read(file: string): string { return fs.readFileSync(file, "utf-8"); } /** Every .svelte file under src/. */ function svelteFiles(dir: string, found: string[] = []): string[] { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory()) svelteFiles(full, found); else if (entry.name.endsWith(".svelte")) found.push(full); } return found; } /** * The selector list of the `[data-native-video="active"]` rule in app.css. * Returned verbatim, one selector per entry. */ function compositingSelectors(css: string): string[] { const marker = 'html[data-native-video="active"]'; const start = css.indexOf(marker); expect(start, "app.css no longer contains the native-video rule").toBeGreaterThan(-1); const open = css.indexOf("{", start); return css .slice(start, open) .split(",") .map((s) => s.trim()) .filter(Boolean); } describe("native-video compositing layers (DR-185)", () => { const css = read(path.join(srcRoot, "app.css")); const selectors = compositingSelectors(css); const markup = svelteFiles(srcRoot).map(read).join("\n"); it("clears the app shell, which paints over the whole viewport", () => { // The shell is the layer directly between the player and the WebView; if it // stays painted, nothing below it can be seen however transparent the // player and the WebView widget are. expect(selectors.some((s) => s.includes("[data-app-shell]"))).toBe(true); expect(markup).toContain("data-app-shell"); }); it("targets no attribute that nothing in the app sets", () => { const attributes = selectors .flatMap((selector) => [...selector.matchAll(/\[([a-zA-Z-]+)(?:[=\]])/g)]) .map((match) => match[1]) // data-native-video is set imperatively on by nativeVideo.ts, not // in markup, so it is verified against that module instead. .filter((attr) => attr !== "data-native-video"); const unset = [...new Set(attributes)].filter((attr) => !markup.includes(attr)); expect(unset, `app.css targets attributes no component sets: ${unset.join(", ")}`) .toEqual([]); }); it("still sets data-native-video on from the store", () => { const store = read(path.join(srcRoot, "lib/stores/nativeVideo.ts")); expect(store).toContain("data-native-video"); expect(store).toContain("documentElement"); }); });