// Every full-screen overlay hosts its own toasts (see ui/Toaster). // // The app's Toaster lives in the content column, a stacking context: anything that covers the // viewport at z-overlay paints over it, so a toast raised while that overlay is open is unreadable and // a click on it lands on the overlay's backdrop. The fix is a convention — the overlay mounts // `` inside itself — and a convention stated in a comment // is one the next overlay forgets (deep-review round 3 found every ui/Modal and the onboarding wizard // without one). So it is checked here: a component that renders a viewport-covering layer at // z-overlay or above mounts an overlay host, ranked by when the overlay opened. // // The class TOKENS are matched in any order, and the layer need not be `inset-0`: the first version // matched the exact string "fixed inset-0 z-overlay", so PlexPlayer's own root (`fixed left-0 top-0 // z-overlay`) was never checked at all, and a new viewer copying it would have slipped past // (deep-review round 4). import { describe, it, expect } from "vitest"; // BOTH patterns: `../**/*.tsx` from this directory does not match the siblings in `ui/` itself, so // ui/Modal.tsx — the overlay behind every dialog in the app — was never checked by this guard at all // (deep-review round 4). const sources = import.meta.glob(["../**/*.tsx", "./*.tsx"], { query: "?raw", import: "default", eager: true, }); // Overlays with nothing a toast could be about: a lazy chunk's black placeholder while the player // loads, and the signed-out landing page's screenshot zoom (the app's Toaster is not even mounted // there — App renders Welcome instead of the shell). const EXEMPT = new Set(["../PlexBrowse.tsx", "../Welcome.tsx"]); /** Every class string in the file (quoted or templated), including multi-line ones. */ const classStrings = (src: string): string[] => [...src.matchAll(/className=\{?[`"']([^`"']*)[`"']/g)].map((m) => m[1]!); const coversTheViewport = (src: string) => classStrings(src).some( (cls) => /(^|\s)fixed(\s|$)/.test(cls) && /(^|\s)z-(overlay|popover|tooltip)(\s|$)/.test(cls) && (/(^|\s)inset-0(\s|$)/.test(cls) || (/(^|\s)left-0(\s|$)/.test(cls) && /(^|\s)top-0(\s|$)/.test(cls))), ); const overlays = () => Object.entries(sources) .filter(([path]) => !path.endsWith(".test.tsx") && !EXEMPT.has(path)) .filter(([, src]) => coversTheViewport(src as string)); describe("full-screen overlays host their own toasts", () => { it("every one that covers the viewport mounts an overlay host", () => { const missing = overlays() .filter(([, src]) => !(src as string).includes(" path); expect(missing).toEqual([]); }); it("and ranks it by when the overlay opened, not by when the host mounted", () => { // A host that takes its own rank jumps to the top whenever it remounts — which PlayerModal's does // on every fullscreen change, putting it back over a dialog opened above the player. const unranked = overlays() .filter(([, src]) => !(src as string).includes("useOverlayOrder()")) .map(([path]) => path); expect(unranked).toEqual([]); }); it("finds the overlays it is meant to check", () => { // Modal, PlayerModal, PlexPlayer and the wizard at least — PlexPlayer is the one the exact-string // check used to miss. const paths = overlays().map(([p]) => p); expect(paths).toEqual( expect.arrayContaining([ "./Modal.tsx", "../PlayerModal.tsx", "../PlexPlayer.tsx", "../OnboardingWizard.tsx", ]), ); }); });