Files
paseo/packages/desktop/scripts/verify-electron-cdp.mjs
T

789 lines
27 KiB
JavaScript

import fs from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import { chromium } from "playwright";
const CDP_PORT = process.env.PASEO_ELECTRON_REMOTE_DEBUGGING_PORT ?? "9223";
const EXPO_PORT = process.env.EXPO_PORT ?? "8082";
const CDP_URL = process.env.CDP_URL ?? `http://127.0.0.1:${CDP_PORT}`;
const OUTPUT_DIR = process.env.ELECTRON_VERIFY_OUTPUT_DIR ?? "/tmp/electron-verification";
const APP_URL_FRAGMENT = process.env.ELECTRON_VERIFY_APP_URL_FRAGMENT ?? `localhost:${EXPO_PORT}`;
const REQUIRED_DESKTOP_KEYS = ["invoke", "events", "window", "dialog", "notification", "opener"];
const INTERACTIVE_SELECTOR = [
"button",
"a",
"input",
"textarea",
"select",
"[role='button']",
"[role='link']",
"[role='textbox']",
"[role='combobox']",
"[role='tab']",
"[role='switch']",
"[role='checkbox']",
"[role='slider']",
"[role='menuitem']",
"[tabindex]",
"[contenteditable='true']",
].join(", ");
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}
async function ensureDir(dirPath) {
await fs.mkdir(dirPath, { recursive: true });
}
async function captureScreenshot(page, fileName) {
const filePath = path.join(OUTPUT_DIR, fileName);
await page.screenshot({ path: filePath, fullPage: true });
return filePath;
}
function rectsIntersect(left, right) {
return (
left.left < right.left + right.width &&
left.left + left.width > right.left &&
left.top < right.top + right.height &&
left.top + left.height > right.top
);
}
function getWindowChromeObstruction(platform, innerWidth) {
if (platform === "darwin") {
return { corner: "top-left", left: 0, top: 0, width: 78, height: 45 };
}
return {
corner: "top-right",
left: innerWidth - 140,
top: 0,
width: 140,
height: 48,
};
}
async function inspectSettingsGeometry(page) {
return page.evaluate(() => {
function rect(selector) {
const element = document.querySelector(selector);
if (!(element instanceof HTMLElement)) return null;
const bounds = element.getBoundingClientRect();
return {
left: bounds.left,
top: bounds.top,
width: bounds.width,
height: bounds.height,
};
}
const title = document.querySelector('[data-testid="settings-detail-header-title"]');
const headerLeft = title instanceof HTMLElement ? title.parentElement : null;
const headerLeftBounds = headerLeft?.getBoundingClientRect() ?? null;
return {
innerWidth: window.innerWidth,
innerHeight: window.innerHeight,
devicePixelRatio: window.devicePixelRatio,
sidebarRect: rect('[data-testid="settings-sidebar"]'),
detailPaneRect: rect('[data-testid="settings-detail-pane"]'),
outerAppSidebarSettingsRect: rect('[data-testid="sidebar-settings"]'),
backButtonRect: rect('[data-testid="settings-back-to-workspace"]'),
detailTitleRect: rect('[data-testid="settings-detail-header-title"]'),
detailHeaderLeftRect: headerLeftBounds
? {
left: headerLeftBounds.left,
top: headerLeftBounds.top,
width: headerLeftBounds.width,
height: headerLeftBounds.height,
}
: null,
};
});
}
function settingsGeometryClearsWindowChrome(geometry, platform) {
const obstruction = getWindowChromeObstruction(platform, geometry.innerWidth);
const consumer = platform === "darwin" ? geometry.backButtonRect : geometry.detailHeaderLeftRect;
return Boolean(consumer && !rectsIntersect(consumer, obstruction));
}
async function readBridgeFullscreen(page) {
return page.evaluate(
async () =>
(await window.paseoDesktop?.window?.getCurrentWindow?.()?.isFullscreen?.()) === true,
);
}
async function setNativeFullscreen(page, fullscreen) {
await page.evaluate(async (nextFullscreen) => {
const win = window.paseoDesktop?.window?.getCurrentWindow?.();
if (typeof win?.setFullscreen !== "function") throw new Error("setFullscreen is unavailable");
await win.setFullscreen(nextFullscreen);
}, fullscreen);
}
async function waitForBridgeFullscreen(page, expected) {
for (let attempt = 0; attempt < 50; attempt += 1) {
if ((await readBridgeFullscreen(page)) === expected) return;
await page.waitForTimeout(200);
}
throw new Error(`Timed out waiting for fullscreen=${expected}`);
}
async function inspectTitlebarRegions(page) {
return page.evaluate((interactiveSelector) => {
const nodes = Array.from(document.querySelectorAll("*"));
const annotationId = "electron-verify-titlebar-style";
const existingAnnotation = document.getElementById(annotationId);
existingAnnotation?.remove();
const annotationStyle = document.createElement("style");
annotationStyle.id = annotationId;
annotationStyle.textContent = `
[data-electron-verify-drag="true"] {
outline: 3px solid #ff4d4f !important;
outline-offset: -3px !important;
}
[data-electron-verify-resizer="true"] {
outline: 3px solid #52c41a !important;
outline-offset: -3px !important;
}
[data-electron-verify-interactive="true"] {
outline: 3px solid #1677ff !important;
outline-offset: -3px !important;
}
`;
document.head.appendChild(annotationStyle);
function isVisible(element) {
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return (
rect.width > 0 &&
rect.height > 0 &&
style.display !== "none" &&
style.visibility !== "hidden" &&
style.opacity !== "0"
);
}
function summarizeText(element) {
return (element.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 120);
}
function readAppRegion(element) {
const style = window.getComputedStyle(element);
return style.webkitAppRegion || style.getPropertyValue("-webkit-app-region") || "none";
}
function rectInfo(element) {
const rect = element.getBoundingClientRect();
return {
top: rect.top,
left: rect.left,
width: rect.width,
height: rect.height,
};
}
function summarizeElement(element) {
const style = window.getComputedStyle(element);
return {
tagName: element.tagName.toLowerCase(),
text: summarizeText(element),
appRegion: readAppRegion(element),
position: style.position,
zIndex: style.zIndex,
paddingLeft: Number.parseFloat(style.paddingLeft || "0"),
paddingTop: Number.parseFloat(style.paddingTop || "0"),
...rectInfo(element),
};
}
function isNearTop(summary) {
return summary.top < 220;
}
function isTopResizer(element, overlayRect) {
if (!(element instanceof HTMLElement) || !isVisible(element)) {
return false;
}
const summary = summarizeElement(element);
return (
summary.appRegion === "no-drag" &&
summary.position === "absolute" &&
Math.abs(summary.height - 4) <= 1 &&
Math.abs(summary.top - overlayRect.top) <= 2 &&
Math.abs(summary.left - overlayRect.left) <= 2 &&
Math.abs(summary.width - overlayRect.width) <= 2
);
}
function summarizeInteractive(element) {
const summary = summarizeElement(element);
return {
...summary,
testId: element.getAttribute("data-testid"),
role: element.getAttribute("role"),
};
}
function buildDragRecord(node, summary) {
const parent = node.parentElement instanceof HTMLElement ? node.parentElement : null;
const parentSummary = parent ? summarizeElement(parent) : null;
const interactiveDescendants = Array.from(node.querySelectorAll(interactiveSelector))
.filter((child) => child instanceof HTMLElement)
.filter((child) => isVisible(child))
.map((child) => summarizeInteractive(child));
const siblingResizers = parent
? Array.from(parent.children)
.filter((child) => child !== node)
.filter((child) => child instanceof HTMLElement)
.filter((child) => isTopResizer(child, summary))
.map((child) => summarizeElement(child))
: [];
const parentInteractive = parent
? Array.from(parent.querySelectorAll(interactiveSelector))
.filter((child) => child instanceof HTMLElement)
.filter((child) => isVisible(child))
.map((child) => summarizeInteractive(child))
: [];
const explicitNoDragInteractive = parentInteractive.filter(
(child) => child.appRegion === "no-drag",
);
const record = {
...summary,
parent: parentSummary,
interactiveDescendants: interactiveDescendants.slice(0, 5),
siblingResizers: siblingResizers.slice(0, 3),
explicitNoDragInteractive: explicitNoDragInteractive.slice(0, 5),
parentInteractiveCount: parentInteractive.length,
};
const looksLikeHostShortcut =
isNearTop(summary) &&
(summary.position !== "absolute" ||
summary.text.length > 0 ||
interactiveDescendants.length > 0 ||
parentSummary?.appRegion === "drag");
return { record, looksLikeHostShortcut };
}
const dragSummaries = [];
const suspiciousDragHosts = [];
for (const node of nodes) {
if (!(node instanceof HTMLElement) || !isVisible(node)) continue;
const summary = summarizeElement(node);
if (summary.appRegion !== "drag") continue;
const { record, looksLikeHostShortcut } = buildDragRecord(node, summary);
dragSummaries.push(record);
if (looksLikeHostShortcut) suspiciousDragHosts.push(record);
}
const verifiedRegions = dragSummaries
.filter((entry) => isNearTop(entry))
.filter((entry) => entry.position === "absolute")
.filter((entry) => entry.text.length === 0)
.filter((entry) => entry.parent?.appRegion !== "drag")
.filter((entry) => entry.siblingResizers.length > 0)
.sort(
(left, right) =>
right.explicitNoDragInteractive.length - left.explicitNoDragInteractive.length ||
left.top - right.top ||
right.width - left.width,
);
const candidate = verifiedRegions[0] ?? null;
if (candidate) {
const matchingDragNode = nodes.find((node) => {
if (!(node instanceof HTMLElement) || !isVisible(node)) {
return false;
}
const summary = summarizeElement(node);
return (
summary.appRegion === "drag" &&
Math.abs(summary.top - candidate.top) <= 1 &&
Math.abs(summary.left - candidate.left) <= 1 &&
Math.abs(summary.width - candidate.width) <= 1 &&
Math.abs(summary.height - candidate.height) <= 1
);
});
function annotateMatchingParent(parent) {
if (!(parent instanceof HTMLElement)) return;
const resizers = Array.from(parent.children).filter(
(child) => child instanceof HTMLElement && isTopResizer(child, candidate),
);
for (const child of resizers) {
child.setAttribute("data-electron-verify-resizer", "true");
}
const interactiveChildren = Array.from(parent.querySelectorAll(interactiveSelector))
.filter((child) => child instanceof HTMLElement)
.filter((child) => isVisible(child))
.filter((child) => summarizeElement(child).appRegion === "no-drag")
.slice(0, 3);
for (const child of interactiveChildren) {
child.setAttribute("data-electron-verify-interactive", "true");
}
}
if (matchingDragNode instanceof HTMLElement) {
matchingDragNode.setAttribute("data-electron-verify-drag", "true");
annotateMatchingParent(matchingDragNode.parentElement);
}
}
return {
interactiveSelector,
dragRegionCount: dragSummaries.length,
verifiedRegionCount: verifiedRegions.length,
candidate,
suspiciousDragHosts: suspiciousDragHosts.slice(0, 10),
dragRegions: dragSummaries.slice(0, 10),
};
}, INTERACTIVE_SELECTOR);
}
async function inspectFullscreenWindowChrome(page, platform) {
const initiallyFullscreen = await readBridgeFullscreen(page);
try {
assert(!initiallyFullscreen, "Electron verifier requires a non-fullscreen QA window");
const before = await inspectSettingsGeometry(page);
await setNativeFullscreen(page, true);
await waitForBridgeFullscreen(page, true);
const details = await page.evaluate(async () => {
const bridge = window.paseoDesktop?.window?.getCurrentWindow?.();
const bridgeFullscreen =
typeof bridge?.isFullscreen === "function" ? await bridge.isFullscreen() : null;
return { bridgeFullscreen };
});
const fullscreen = await inspectSettingsGeometry(page);
const screenshot = await captureScreenshot(page, "04-fullscreen-window-chrome.png");
const clearanceRemoved =
platform === "darwin"
? Boolean(
before.backButtonRect &&
fullscreen.backButtonRect &&
before.backButtonRect.top >= 45 &&
fullscreen.backButtonRect.top < 45 &&
fullscreen.backButtonRect.top < before.backButtonRect.top,
)
: Boolean(
before.detailHeaderLeftRect &&
fullscreen.detailHeaderLeftRect &&
before.innerWidth -
(before.detailHeaderLeftRect.left + before.detailHeaderLeftRect.width) >=
140 &&
fullscreen.innerWidth -
(fullscreen.detailHeaderLeftRect.left + fullscreen.detailHeaderLeftRect.width) <
40,
);
return {
supported: true,
initiallyFullscreen,
before,
fullscreen,
clearanceRemoved,
screenshot,
...details,
passed: details.bridgeFullscreen === true && clearanceRemoved,
};
} catch (error) {
return {
supported: false,
error: String(error),
initiallyFullscreen,
};
} finally {
if (await readBridgeFullscreen(page)) {
await setNativeFullscreen(page, false);
await waitForBridgeFullscreen(page, false);
}
}
}
async function inspectHalfScreenSettingsLayout(page, platform) {
const initialBounds = await page.evaluate(() => ({
width: window.outerWidth,
height: window.outerHeight,
}));
try {
await page.evaluate(() => {
// Electron applies resizeTo to the native BrowserWindow. Unlike
// page.setViewportSize, this exercises the real window/layout boundary.
window.resizeTo(751, Math.max(window.outerHeight, 700));
});
await page.waitForFunction(() => window.innerWidth === 751, undefined, { timeout: 10_000 });
const sidebar = page.getByTestId("settings-sidebar");
const detail = page.getByTestId("settings-detail-pane");
const outerAppSidebarSettings = page.getByTestId("sidebar-settings");
await sidebar.waitFor({ state: "visible", timeout: 10_000 });
await detail.waitFor({ state: "visible", timeout: 10_000 });
await outerAppSidebarSettings.waitFor({ state: "hidden", timeout: 10_000 });
const details = await inspectSettingsGeometry(page);
const obstruction = getWindowChromeObstruction(platform, details.innerWidth);
const clearsWindowChrome = settingsGeometryClearsWindowChrome(details, platform);
const sidebarRight = details.sidebarRect
? details.sidebarRect.left + details.sidebarRect.width
: null;
const detailRight = details.detailPaneRect
? details.detailPaneRect.left + details.detailPaneRect.width
: null;
const screenshot = await captureScreenshot(page, "06-half-screen-settings.png");
return {
supported: true,
initialBounds,
...details,
obstruction,
clearsWindowChrome,
screenshot,
passed:
details.innerWidth === 751 &&
details.sidebarRect !== null &&
details.sidebarRect.width >= 300 &&
details.detailPaneRect !== null &&
details.detailPaneRect.width >= 400 &&
details.outerAppSidebarSettingsRect === null &&
Math.abs(details.sidebarRect.left) <= 1 &&
sidebarRight !== null &&
Math.abs(sidebarRight - details.detailPaneRect.left) <= 1 &&
detailRight !== null &&
Math.abs(detailRight - details.innerWidth) <= 1 &&
clearsWindowChrome,
};
} catch (error) {
return { supported: false, initialBounds, error: String(error) };
} finally {
await page.evaluate((bounds) => window.resizeTo(bounds.width, bounds.height), initialBounds);
await page.waitForFunction(
(width) => Math.abs(window.outerWidth - width) <= 1,
initialBounds.width,
{ timeout: 10_000 },
);
}
}
async function findAppPage(browser) {
function findMatchingPages() {
const matches = [];
for (const context of browser.contexts()) {
for (const page of context.pages()) {
if (page.url().includes(APP_URL_FRAGMENT) && !page.url().startsWith("devtools://")) {
matches.push(page);
}
}
}
return matches;
}
async function poll(attempt) {
const pages = findMatchingPages();
if (pages.length > 1) {
throw new Error(
`Expected one Electron QA page for ${APP_URL_FRAGMENT}, found ${pages.length}`,
);
}
if (pages.length === 1) return pages[0];
if (attempt >= 29) {
throw new Error(`Unable to find Electron app page for ${APP_URL_FRAGMENT}`);
}
await new Promise((resolve) => setTimeout(resolve, 500));
return poll(attempt + 1);
}
return poll(0);
}
function attachConsoleCollector(page, consoleMessages) {
page.on("console", (message) => {
consoleMessages.push({ type: message.type(), text: message.text() });
});
page.on("pageerror", (error) => {
consoleMessages.push({ type: "pageerror", text: String(error) });
});
}
async function navigateToWelcome(page) {
await page.waitForLoadState("domcontentloaded");
await page.waitForTimeout(1000);
if (!page.url().endsWith("/welcome")) {
await page.goto(`http://${APP_URL_FRAGMENT}/welcome`, { waitUntil: "domcontentloaded" });
await page.waitForTimeout(1000);
}
}
async function detectDesktopBridge(page) {
return page.evaluate(() => {
const bridge = window.paseoDesktop;
const keys = bridge && typeof bridge === "object" ? Object.keys(bridge) : [];
const keyTypes =
bridge && typeof bridge === "object"
? Object.fromEntries(Object.entries(bridge).map(([key, value]) => [key, typeof value]))
: {};
return {
exists: Boolean(bridge && typeof bridge === "object"),
keys,
keyTypes,
platform: bridge?.platform ?? null,
};
});
}
async function navigateToSettings(page, serverId) {
await page.evaluate((nextServerId) => {
window.location.href = `/h/${nextServerId}/settings`;
}, serverId);
await page.getByTestId("settings-sidebar").waitFor({ state: "visible", timeout: 30_000 });
await page
.getByTestId("settings-detail-header-title")
.waitFor({ state: "visible", timeout: 30_000 });
}
async function dismissOuterAppSidebarIfVisible(page) {
const sidebarSettingsButton = page.locator('[data-testid="sidebar-settings"]').first();
const menuToggle = page.locator('[data-testid="menu-button"]').first();
const bothVisible =
(await sidebarSettingsButton.isVisible().catch(() => false)) &&
(await menuToggle.isVisible().catch(() => false));
if (!bothVisible) return false;
await menuToggle.click();
await sidebarSettingsButton.waitFor({ state: "hidden", timeout: 10_000 });
await page.waitForTimeout(500);
return true;
}
async function restoreOuterAppSidebar(page, wasDismissed) {
if (!wasDismissed) return;
const menuToggle = page.locator('[data-testid="menu-button"]').first();
const sidebarSettingsButton = page.locator('[data-testid="sidebar-settings"]').first();
await menuToggle.click();
await sidebarSettingsButton.waitFor({ state: "visible", timeout: 10_000 });
}
async function clearTitlebarAnnotations(page) {
await page.evaluate(() => {
document.getElementById("electron-verify-titlebar-style")?.remove();
for (const attribute of [
"data-electron-verify-drag",
"data-electron-verify-resizer",
"data-electron-verify-interactive",
]) {
for (const element of document.querySelectorAll(`[${attribute}]`)) {
element.removeAttribute(attribute);
}
}
});
}
function evaluateDragRegionCheck(dragRegionCheck) {
return (
dragRegionCheck.dragRegionCount > 0 &&
dragRegionCheck.verifiedRegionCount > 0 &&
Boolean(dragRegionCheck.candidate) &&
dragRegionCheck.candidate.top < 220 &&
dragRegionCheck.candidate.parent?.appRegion !== "drag" &&
dragRegionCheck.candidate.siblingResizers.length > 0 &&
dragRegionCheck.suspiciousDragHosts.length === 0
);
}
function evaluateTrafficLightAvoidance(dragRegionCheck) {
const firstInteractive = dragRegionCheck.candidate?.explicitNoDragInteractive?.find(
(entry) => entry.testId === "settings-back-to-workspace",
);
return Boolean(
firstInteractive &&
!rectsIntersect(firstInteractive, { left: 0, top: 0, width: 78, height: 45 }),
);
}
async function collectDragRegionResults(page, dragRegionCheck, dragScreenshot, results) {
results.push({
check: "titlebar-drag-structure",
pass: evaluateDragRegionCheck(dragRegionCheck),
details: dragRegionCheck,
screenshot: dragScreenshot,
});
const trafficLightScreenshot = await captureScreenshot(page, "04-traffic-light-avoidance.png");
const firstInteractive = dragRegionCheck.candidate?.explicitNoDragInteractive?.find(
(entry) => entry.testId === "settings-back-to-workspace",
);
results.push({
check: "traffic-light-avoidance",
pass: process.platform === "darwin" ? evaluateTrafficLightAvoidance(dragRegionCheck) : true,
skipped: process.platform !== "darwin",
details: {
platform: process.platform,
obstruction: process.platform === "darwin" ? { width: 78, height: 45 } : null,
firstInteractive: firstInteractive ?? null,
note:
process.platform === "darwin"
? "The first interactive sidebar row must not intersect the traffic-light rectangle."
: "Skipped here; the half-screen check exercises the right-side obstruction on Windows/Linux.",
candidate: dragRegionCheck.candidate,
},
screenshot: trafficLightScreenshot,
});
results.push({
check: "interactive-no-drag-layering",
pass:
Boolean(dragRegionCheck.candidate) &&
Array.isArray(dragRegionCheck.candidate.explicitNoDragInteractive) &&
dragRegionCheck.candidate.explicitNoDragInteractive.length > 0,
details: {
candidate: dragRegionCheck.candidate,
explicitNoDragInteractive: dragRegionCheck.candidate?.explicitNoDragInteractive ?? [],
},
screenshot: dragScreenshot,
});
}
async function collectSettingsSplitResult(page, serverId, desktopStatus, results) {
const geometry = await inspectSettingsGeometry(page);
const sidebarRight = geometry.sidebarRect
? geometry.sidebarRect.left + geometry.sidebarRect.width
: null;
const settingsScreenshot = await captureScreenshot(page, "05-settings-split.png");
results.push({
check: "settings-split",
pass: Boolean(
geometry.sidebarRect &&
geometry.detailPaneRect &&
geometry.detailTitleRect &&
sidebarRight !== null &&
Math.abs(sidebarRight - geometry.detailPaneRect.left) <= 1,
),
details: {
route: page.url(),
serverId,
desktopStatus,
geometry,
},
screenshot: settingsScreenshot,
});
}
async function main() {
await ensureDir(OUTPUT_DIR);
const browser = await chromium.connectOverCDP(CDP_URL);
let page = null;
let initialPageUrl = null;
let outerSidebarDismissed = false;
try {
page = await findAppPage(browser);
initialPageUrl = page.url();
const consoleMessages = [];
const results = [];
attachConsoleCollector(page, consoleMessages);
await navigateToWelcome(page);
const welcomeScreenshot = await captureScreenshot(page, "01-welcome.png");
const desktopDetection = await detectDesktopBridge(page);
const hasExpectedDesktopShape =
desktopDetection.exists &&
REQUIRED_DESKTOP_KEYS.every((key) => desktopDetection.keys.includes(key));
assert(
["darwin", "win32", "linux"].includes(desktopDetection.platform),
`Unexpected Electron platform: ${desktopDetection.platform}`,
);
results.push({
check: "desktop-detection",
pass: hasExpectedDesktopShape,
details: desktopDetection,
screenshot: welcomeScreenshot,
});
const desktopStatus = await page.evaluate(() =>
window.paseoDesktop.invoke("desktop_daemon_status"),
);
assert(
typeof desktopStatus?.serverId === "string" && desktopStatus.serverId.trim().length > 0,
"desktop_daemon_status did not return a serverId",
);
const serverId = desktopStatus.serverId.trim();
await navigateToSettings(page, serverId);
await captureScreenshot(page, "02-settings-page.png");
outerSidebarDismissed = await dismissOuterAppSidebarIfVisible(page);
const dragRegionCheck = await inspectTitlebarRegions(page);
const dragScreenshot = await captureScreenshot(page, "03-drag-region.png");
await collectDragRegionResults(page, dragRegionCheck, dragScreenshot, results);
const fullscreenDetails = await inspectFullscreenWindowChrome(page, desktopDetection.platform);
results.push({
check: "fullscreen-window-chrome",
pass: fullscreenDetails.supported && fullscreenDetails.passed,
details: fullscreenDetails,
screenshot: fullscreenDetails.screenshot ?? null,
});
await collectSettingsSplitResult(page, serverId, desktopStatus, results);
if (outerSidebarDismissed) {
await restoreOuterAppSidebar(page, outerSidebarDismissed);
outerSidebarDismissed = false;
}
const halfScreenDetails = await inspectHalfScreenSettingsLayout(
page,
desktopDetection.platform,
);
results.push({
check: "half-screen-settings-layout",
pass: halfScreenDetails.supported && halfScreenDetails.passed,
details: halfScreenDetails,
screenshot: halfScreenDetails.screenshot ?? null,
});
const desktopDetectionScreenshot = await captureScreenshot(page, "07-desktop-detection.png");
results[0].screenshot = desktopDetectionScreenshot;
const report = {
cdpUrl: CDP_URL,
outputDir: OUTPUT_DIR,
pageUrl: page.url(),
desktopStatus,
results,
consoleMessages,
};
const reportPath = path.join(OUTPUT_DIR, "report.json");
await fs.writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
const failedChecks = results.filter((result) => !result.pass);
console.log(JSON.stringify(report, null, 2));
if (failedChecks.length > 0) process.exitCode = 1;
} finally {
if (page && !page.isClosed()) {
await clearTitlebarAnnotations(page);
await restoreOuterAppSidebar(page, outerSidebarDismissed);
if (initialPageUrl && page.url() !== initialPageUrl) {
await page.goto(initialPageUrl, { waitUntil: "domcontentloaded" });
}
}
await browser.close();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});