Files
deerflow-code/frontend-web/scripts/smoke-firefox52-themes.mjs
T

164 lines
7.9 KiB
JavaScript

import net from "node:net";
const debugPort = Number(process.env.FIREFOX_DEBUG_PORT || 6001);
const baseUrl = process.env.FIREFOX_BASE_URL || "http://localhost:5174/";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const themes = ["light", "dark", "lightblue", "lightgreen", "iceblue", "lightyellow"];
const layouts = ["full", "compact"];
const results = [];
const browserErrors = [];
const socket = net.createConnection({ host: "127.0.0.1", port: debugPort });
let buffer = Buffer.alloc(0);
let rootActor;
let consoleActor;
let pendingEvaluation;
function send(packet) {
const body = Buffer.from(JSON.stringify(packet));
socket.write(Buffer.concat([Buffer.from(`${body.length}:`), body]));
}
function evaluate(expression) {
return new Promise((resolve, reject) => {
pendingEvaluation = { expression, resolve, reject };
send({ to: consoleActor, type: "evaluateJS", text: expression });
});
}
function formatArgument(argument) {
if (argument == null || typeof argument !== "object") return String(argument);
if (argument.preview?.message) return `${argument.preview.name || argument.class || "Error"}: ${argument.preview.message}`;
return argument.description || argument.class || JSON.stringify(argument);
}
function handle(packet) {
if (packet.applicationType && !rootActor) {
rootActor = packet.from;
send({ to: rootActor, type: "listTabs" });
return;
}
if (packet.tabs && !consoleActor) {
consoleActor = packet.tabs[packet.selected].consoleActor;
send({ to: consoleActor, type: "startListeners", listeners: ["PageError", "ConsoleAPI"] });
return;
}
if (packet.type === "pageError" && packet.pageError && packet.pageError.warning !== true && packet.pageError.category !== "CSS Parser") {
const message = String(packet.pageError.errorMessage || packet.pageError.message || "").trim();
if (message || packet.pageError.stacktrace) browserErrors.push(`PAGE: ${message || "unknown page error"}`);
return;
}
if (packet.type === "consoleAPICall" && packet.message?.level === "error") {
const message = (packet.message.arguments || []).map(formatArgument).join(" ") || "error";
if (!message.includes("validateDOMNesting") && !message.includes("init intent failed") && !message.includes("ResizeObserver loop") && message !== "[object Object]") {
browserErrors.push(`CONSOLE: ${message}`);
}
return;
}
if (pendingEvaluation && packet.input === pendingEvaluation.expression) {
const pending = pendingEvaluation;
pendingEvaluation = null;
if (packet.exception) pending.reject(new Error(packet.exceptionMessage || packet.exception?.description || String(packet.exception)));
else pending.resolve(packet.result);
}
}
function consume() {
while (true) {
const colon = buffer.indexOf(58);
if (colon < 0) return;
const length = Number(buffer.subarray(0, colon).toString());
if (buffer.length < colon + 1 + length) return;
const packet = JSON.parse(buffer.subarray(colon + 1, colon + 1 + length).toString());
buffer = buffer.subarray(colon + 1 + length);
handle(packet);
}
}
socket.on("data", (chunk) => {
buffer = Buffer.concat([buffer, chunk]);
consume();
});
await new Promise((resolve, reject) => {
socket.once("connect", resolve);
socket.once("error", reject);
});
while (!consoleActor) await sleep(50);
async function waitFor(expression, timeoutMs = 30_000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
if (await evaluate(expression)) return true;
} catch {
// A reload briefly invalidates the old evaluation context.
}
await sleep(250);
}
return false;
}
for (const layout of layouts) {
for (const theme of themes) {
const errorStart = browserErrors.length;
let detail = true;
try {
const settings = JSON.stringify({
notification: { enabled: true },
display: {
streamSpeedMeterEnabled: true,
ragModeEnabled: false,
artifactDirectoryModeEnabled: false,
contextCompressionEnabled: false,
citationCleanupWords: [],
fontScale: 1,
layoutMode: layout,
},
context: {},
});
const url = new URL("#/page/workspace/chats/new", baseUrl).href;
try {
await evaluate(`(function(){localStorage.setItem('strategy-theme',${JSON.stringify(theme)});localStorage.setItem('strategy-theme-user-set','1');localStorage.setItem('deerflow.local-settings',${JSON.stringify(settings)});sessionStorage.removeItem('embed.session');location.href=${JSON.stringify(url)};location.reload();return true})()`);
} catch {
// A successful full reload can destroy the old console request.
}
// The hash route is identical for every matrix item. Give Firefox time
// to tear down the previous document before accepting a ready DOM.
await sleep(700);
const ready = await waitFor(`(function(){var root=document.getElementById('root'),textarea=document.querySelector('textarea[placeholder="今天我能为你做些什么?"]');return location.hash.indexOf('#/page/workspace/chats/new')===0&&document.readyState==='complete'&&!!root&&root.getBoundingClientRect().width>0&&!!textarea&&textarea.getBoundingClientRect().height>0})()`);
if (!ready) {
detail = "页面未在 30 秒内完成渲染";
} else {
const state = await evaluate(`(function(){var html=document.documentElement,root=document.getElementById('root'),textarea=document.querySelector('textarea[placeholder="今天我能为你做些什么?"]'),style=getComputedStyle(html),sidebar=document.querySelector('[data-sidebar="sidebar"],[data-compact-guide="sidebar-nav"]');return JSON.stringify({classes:html.className,dataTheme:html.getAttribute('data-theme')||'',themeMode:html.getAttribute('theme-mode')||'',storedTheme:localStorage.getItem('strategy-theme'),layout:JSON.parse(localStorage.getItem('deerflow.local-settings')||'{}').display.layoutMode,rootWidth:Math.round(root.getBoundingClientRect().width),inputWidth:Math.round(textarea.getBoundingClientRect().width),sidebarVisible:!!sidebar&&sidebar.getBoundingClientRect().width>0,background:style.getPropertyValue('--background').trim(),foreground:style.getPropertyValue('--foreground').trim(),primary:style.getPropertyValue('--primary').trim()})})()`);
const parsed = JSON.parse(state);
const themeApplied = parsed.storedTheme === theme && parsed.classes.split(/\s+/).includes(theme);
const modeApplied = theme === "dark"
? parsed.themeMode === "dark" && parsed.classes.split(/\s+/).includes("dark")
: theme === "light"
? parsed.dataTheme === "" && parsed.themeMode === ""
: parsed.dataTheme === theme && parsed.themeMode === "";
const tokensApplied = parsed.background && parsed.foreground && parsed.primary;
const layoutApplied = parsed.layout === layout;
const dimensionsHealthy = parsed.rootWidth > 0 && parsed.inputWidth > 200;
if (!themeApplied || !modeApplied || !tokensApplied || !layoutApplied || !dimensionsHealthy) detail = state;
}
} catch (error) {
detail = String(error);
}
await sleep(150);
const errors = browserErrors.slice(errorStart);
const pass = detail === true && errors.length === 0;
results.push({ theme, layout, pass, detail, errors });
console.log(`${pass ? "PASS" : "FAIL"} ${layout}/${theme}${pass ? "" : ` — ${detail}`}`);
for (const error of errors) console.log(` ${error}`);
}
}
const viewport = await evaluate(`JSON.stringify({innerWidth:window.innerWidth,innerHeight:window.innerHeight,devicePixelRatio:window.devicePixelRatio,screenWidth:screen.width,screenHeight:screen.height})`);
socket.end();
const failed = results.filter((result) => !result.pass);
console.log(`\nFirefox 52 theme/layout smoke: ${results.length - failed.length}/${results.length} passed`);
console.log(`Viewport: ${viewport}`);
if (failed.length > 0) process.exitCode = 1;