164 lines
7.9 KiB
JavaScript
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;
|