// The window's own frame in the real app, as the desktop window draws it on macOS and Windows: the top bar (the
// traffic lights or the caption buttons, the rail's button, the place on screen in its middle, an update, Settings),
// the rail of places beside it in the bar's colour, the sidebar and the chat in the app's card, the sidebar's edge dragged to
// its avatars and back, the same window with the frame turned off, and the rail's tiles up close. Against a disposable fake-engine server, with the desktop bridge
// of scripts/testing/rail-preview.tsx (the window's own rail drawn by its own page, in a frame).
// Run: node --experimental-strip-types scripts/verify-window-frame.ts --shots
[--only ] [--skins all]
// Each view is photographed in Daylight and Midnight (every skin with --skins all) at 1280x800, at twice that size,
// and the views of the bar also as a strip of the bar alone. `tiles` draws each state of a rail tile with every pixel
// as a square, at 1x and 2x; `colours` puts the frame's colour today, the colour derived from the app's and the app's
// own side by side; `skins` shows the window's corner in every skin, and `corners` the card's three corners in
// every skin, three times their size.
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { setTimeout as delay } from "node:timers/promises";
import { launchVerificationServer, runControlOmb } from "./control-omb.ts";
import { openHeadlessChrome, type HeadlessPage } from "./testing/headless-chrome.ts";
import { zoomedCrop } from "./testing/png.ts";
import { mountPreview, parkUntilSignal, type MountedPreview } from "./testing/preview-fixture.ts";
const argument = (name: string) => { const at = process.argv.indexOf(name); return at === -1 ? null : process.argv[at + 1] ?? ""; };
const shots = argument("--shots") === null ? null : resolve(argument("--shots")!);
const only = argument("--only") === null ? null : new Set(argument("--only")!.split(",").filter(Boolean));
const WIDTH = 1280, HEIGHT = 800;
const ALL_SKINS = ["daylight", "midnight", "atelier", "foundry", "lagoon", "graphite", "linen", "dusk", "meadow", "pop", "halloween", "diwali", "aurora"];
const SKINS = argument("--skins") === "all" ? ALL_SKINS : ["daylight", "midnight"];
const LOOK: Record = { daylight: "light", midnight: "dark" };
const BAR = "[data-window-frame-bar]";
const RAIL_PAGE = "document.querySelector('iframe[data-rail-page]')?.contentDocument";
const RAIL_READY = `${RAIL_PAGE}?.querySelector('[data-rail-place]')`;
type Step = (page: HeadlessPage) => Promise;
/** Each view: its query, what must be on screen before the picture, the window's width, and what to do first. */
const VIEWS: Array<{ name: string; query: string; ready: string; width?: number; bar?: true; act?: Step }> = [
{ name: "frame", query: "chrome=1", ready: RAIL_READY, bar: true },
{ name: "frame-simple", query: "chrome=1&mode=simple", ready: RAIL_READY, bar: true },
{ name: "frame-windows", query: "chrome=1&platform=win32", ready: RAIL_READY, bar: true },
{ name: "frame-windows-simple", query: "chrome=1&platform=win32&mode=simple", ready: RAIL_READY, bar: true },
{ name: "frame-update", query: "chrome=1&update=downloaded", ready: `${RAIL_READY} && document.querySelector('${BAR} [data-update-indicator]')`, bar: true },
{ name: "frame-downloading", query: "chrome=1&update=downloading", ready: `${RAIL_READY} && document.querySelector('${BAR} [data-update-indicator]')`, bar: true },
{ name: "frame-team", query: "chrome=1&team=1", ready: RAIL_READY, bar: true },
{ name: "frame-team-windows", query: "chrome=1&team=1&platform=win32&update=downloaded", ready: `${RAIL_READY} && document.querySelector('${BAR} [data-update-indicator]')`, bar: true },
{ name: "frame-computer", query: "chrome=1&place=computer&rail=1", ready: RAIL_READY, bar: true },
{ name: "frame-long-name", query: "chrome=1&team=1&teamname=Northwind%20Analytics%20Research%20and%20Planning%20Group", ready: RAIL_READY, width: 900, bar: true },
{ name: "frame-icons", query: "chrome=1&density=icons", ready: RAIL_READY },
{ name: "frame-icons-simple", query: "chrome=1&density=icons&mode=simple", ready: RAIL_READY },
// The sidebar's edge, dragged: narrower (the edge lit), then past the point where it becomes the column of avatars.
{ name: "frame-drag", query: "chrome=1", ready: `${RAIL_READY} && document.querySelector('[data-sidebar-resize]')`, act: (page) => dragEdge(page, 250) },
{ name: "frame-drag-simple", query: "chrome=1&mode=simple", ready: `${RAIL_READY} && document.querySelector('[data-sidebar-resize]')`, act: (page) => dragEdge(page, 250) },
{ name: "frame-drag-avatars", query: "chrome=1", ready: `${RAIL_READY} && document.querySelector('[data-sidebar-resize]')`, act: (page) => dragEdge(page, 110) },
{ name: "frame-drag-out", query: "chrome=1&density=icons", ready: `${RAIL_READY} && document.querySelector('[data-sidebar-resize]')`, act: (page) => dragEdge(page, 170, true) },
{ name: "frame-edge-hover", query: "chrome=1&density=icons&mode=simple", ready: `${RAIL_READY} && document.querySelector('[data-sidebar-resize]')`, act: hoverEdge },
// The frame turned off: macOS's window as it was before the frame, and Windows' own title bar once the app started again.
{ name: "frame-off", query: "chrome=1&frame=off", ready: RAIL_READY },
{ name: "frame-off-simple", query: "chrome=1&frame=off&mode=simple", ready: RAIL_READY },
{ name: "frame-off-icons", query: "chrome=1&frame=off&density=icons", ready: RAIL_READY },
{ name: "frame-off-windows", query: "chrome=1&frame=off&platform=win32", ready: RAIL_READY },
{ name: "frame-setting", query: "chrome=1&settings=frame", ready: `${RAIL_READY} && document.querySelector('[data-window-frame-switch]')` },
{ name: "frame-setting-windows", query: "chrome=1&settings=frame&platform=win32&framepending=1", ready: `${RAIL_READY} && document.querySelector('[data-window-frame-restart]')` },
];
/** Where an element of the window's rail page is, in the window. */
const railRect = (page: HeadlessPage, selector: string) =>
page.evaluate(`(() => { const box = ${RAIL_PAGE}.querySelector(${JSON.stringify(selector)}).getBoundingClientRect(); return { x: box.x, y: box.y, width: box.width, height: box.height }; })()`) as Promise<{ x: number; y: number; width: number; height: number }>;
const rectOf = (page: HeadlessPage, selector: string) =>
page.evaluate(`(() => { const box = document.querySelector(${JSON.stringify(selector)}).getBoundingClientRect(); return { x: box.x, y: box.y, width: box.width, height: box.height }; })()`) as Promise<{ x: number; y: number; width: number; height: number }>;
const mouse = (page: HeadlessPage, type: "mouseMoved" | "mousePressed" | "mouseReleased", x: number, y: number, pressed = false) =>
page.send("Input.dispatchMouseEvent", { type, x, y, button: type === "mouseMoved" && !pressed ? "none" : "left", buttons: pressed ? 1 : 0, clickCount: type === "mouseMoved" ? 0 : 1 });
/** Presses on the sidebar's edge and drags it until the sidebar would be `width` wide; still pressed unless `release`. */
async function dragEdge(page: HeadlessPage, width: number, release = false) {
const edge = await rectOf(page, "[data-sidebar-resize]"), sidebar = await rectOf(page, "[data-sidebar]");
const y = sidebar.y + sidebar.height / 2, from = edge.x + edge.width / 2;
await mouse(page, "mouseMoved", from, y);
await mouse(page, "mousePressed", from, y, true);
const to = sidebar.x + width;
for (let step = 1; step <= 12; step++) await mouse(page, "mouseMoved", from + ((to - from) * step) / 12, y, true);
if (release) await mouse(page, "mouseReleased", to, y);
await delay(400);
}
async function hoverEdge(page: HeadlessPage) {
const edge = await rectOf(page, "[data-sidebar-resize]");
await mouse(page, "mouseMoved", edge.x + edge.width / 2, edge.y + edge.height / 2);
await delay(300);
}
async function open(page: HeadlessPage, url: string, ready: string, width = WIDTH, scale = 2) {
await page.send("Emulation.setDeviceMetricsOverride", { width, height: HEIGHT, deviceScaleFactor: scale, mobile: false });
await mouse(page, "mouseMoved", width - 4, HEIGHT - 4);
await page.send("Page.navigate", { url });
await page.until(`(${ready}) && document.querySelector("[data-sidebar-bot-row], [data-sidebar-hero]")`);
await delay(900);
}
type Picture = { label: string; png: Buffer; width: number };
/** A page of labelled pictures, photographed at twice its size: `rows` of pictures side by side, each `width` CSS pixels
* wide (a picture taken at twice the window's size shows at half its pixels, so pixel for pixel). */
async function sheet(page: HeadlessPage, title: string, rows: Array<{ label: string; pictures: Picture[] }>, file: string) {
const cell = ({ label, png, width }: Picture) => `${label}`;
const html = `