scripts/generate-visual-regression-baseline.sh
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SHA="$(git -C "$ROOT" rev-parse --short HEAD)"
OUT="${1:-$ROOT/flinttrade-design/baselines/visual-regression/$SHA}"
TERM_PORT="${FLINTTRADE_TERMINAL_PORT:-5173}"
SITE_PORT="${FLINTTRADE_SITE_PORT:-3000}"
export OUT TERM_PORT SITE_PORT
mkdir -p "$OUT"
cleanup() {
if [[ -n "${TERM_PID:-}" ]]; then kill "$TERM_PID" 2>/dev/null || true; fi
if [[ -n "${SITE_PID:-}" ]]; then kill "$SITE_PID" 2>/dev/null || true; fi
}
trap cleanup EXIT
(
cd "$ROOT/packages/apps/terminal"
npm run dev -- --host 127.0.0.1 --port "$TERM_PORT"
) >/tmp/flinttrade-terminal-visual-baseline.log 2>&1 &
TERM_PID=$!
(
cd "$ROOT/packages/apps/site"
npm run dev -- --hostname 127.0.0.1 --port "$SITE_PORT"
) >/tmp/flinttrade-site-visual-baseline.log 2>&1 &
SITE_PID=$!
node <<'NODE'
const fs = require("node:fs");
const path = require("node:path");
const { chromium } = require("./packages/apps/terminal/node_modules/playwright");
const root = process.cwd();
const out = process.env.OUT;
const termPort = process.env.TERM_PORT;
const sitePort = process.env.SITE_PORT;
const terminalRoutes = [
"/", "/welcome", "/explore", "/setup", "/setup-account", "/home",
"/trade", "/terminal", "/invest", "/learn", "/lab", "/automate",
"/ai", "/ditto", "/settings", "/admin", "/login", "/missing-route-for-404"
];
const siteRoutes = ["/", "/docs", "/api-reference", "/mcp", "/contribute", "/api/mcp", "/api/search", "/missing-route-for-404"];
const terminalRouteText = {
"/": "Home",
"/welcome": "Explore FlintTrade",
"/explore": "Explore FlintTrade",
"/setup": "Welcome to FlintTrade",
"/setup-account": "Set up FlintTrade",
"/home": "Home",
"/trade": "Trading Workspace",
"/terminal": "Trading Workspace",
"/invest": "Investment Dashboard",
"/learn": "Learning Centre",
"/lab": "Strategy Lab",
"/automate": "Automation Hub",
"/ai": "AI Centre",
"/ditto": "Multi-Account Manager",
"/settings": "Settings",
"/admin": "Admin Dashboard",
"/login": "Page not found",
"/missing-route-for-404": "Page not found",
};
const terminalAppRoutes = new Set([
"/home", "/trade", "/terminal", "/invest", "/learn", "/lab",
"/automate", "/ai", "/ditto", "/settings", "/admin"
]);
const viewports = [
{ name: "1920x1080", width: 1920, height: 1080 },
{ name: "1366x768", width: 1366, height: 768 },
{ name: "768x1024", width: 768, height: 1024 },
];
const themes = ["dark", "light"];
const densities = ["compact", "comfortable"];
async function waitFor(url) {
const started = Date.now();
while (Date.now() - started < 90000) {
try {
const res = await fetch(url);
if (res.status < 500) return;
} catch {}
await new Promise(resolve => setTimeout(resolve, 1000));
}
throw new Error(`timed out waiting for ${url}`);
}
function slug(route) {
if (route === "/") return "root";
return route.replace(/^\//, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "") || "root";
}
function settingsState(density) {
return {
persona: "trader",
density,
fontSize: "normal",
defaultExchange: "NFO",
defaultProduct: "MIS",
defaultQty: 1,
defaultOrderType: "MARKET",
riskLimits: {
maxPositionLots: 10,
mtmStoploss: 5000,
mtmTarget: 10000,
maxOrdersPerMinute: 30,
},
llm: { provider: "", model: "", host: "", apiKey: "" },
telegram: { enabled: false, botToken: "", chatId: "" },
dataPaths: { fastStoragePath: "", archiveStoragePath: "" },
name: "Visual Baseline",
interests: [],
experience: "intermediate",
lastOpenTimestamp: 0,
tickerMode: "marquee",
tickerSymbols: [
"NSE_INDEX:NIFTY",
"BSE_INDEX:SENSEX",
"NSE_INDEX:BANKNIFTY",
"NSE_INDEX:INDIAVIX",
"MCX:GOLD",
"MCX:SILVER",
"MCX:CRUDEOIL",
"MCX:NATURALGAS",
],
tickerSpeed: 30,
};
}
function terminalInitState(theme, density) {
return {
theme: {
state: {
activeThemeId: "graphite",
mode: theme,
customThemes: [],
glass: true,
reduceMotion: true,
},
version: 4,
},
settings: {
state: settingsState(density),
version: 7,
},
mode: {
state: { mode: "explore" },
version: 2,
},
};
}
async function waitForTerminalRoute(page, route) {
const expected = terminalRouteText[route] ?? "FlintTrade";
await page.waitForLoadState("domcontentloaded", { timeout: 30000 });
if (terminalAppRoutes.has(route)) {
await page.waitForSelector("main", { timeout: 30000 });
}
await page.waitForFunction(
({ expected, allowLandmarkMatch }) => {
const text = document.body?.innerText ?? "";
const title = allowLandmarkMatch ? document.title ?? "" : "";
const mainLabels = allowLandmarkMatch
? Array.from(document.querySelectorAll("main"))
.map((node) => node.getAttribute("aria-label") ?? "")
.join(" ")
: "";
return (
(text.includes(expected) || title.includes(expected) || mainLabels.includes(expected)) &&
!text.includes("Loading FlintTrade...")
);
},
{ expected, allowLandmarkMatch: route === "/" || terminalAppRoutes.has(route) },
{ timeout: 30000 },
);
await page.waitForTimeout(route === "/welcome" || route === "/" ? 600 : 250);
}
async function capture(browser, app, baseUrl, route) {
for (const viewport of viewports) {
for (const theme of themes) {
for (const density of densities) {
const page = await browser.newPage({ viewport });
if (app === "terminal") {
await page.addInitScript(({ theme, density, state }) => {
localStorage.setItem("flinttrade-theme", JSON.stringify(state.theme));
localStorage.setItem("flinttrade:settings", JSON.stringify(state.settings));
localStorage.setItem("flinttrade:mode", JSON.stringify(state.mode));
localStorage.setItem("flinttrade:demoChoice", "explore");
localStorage.setItem("flinttrade:demo-session", "active");
localStorage.setItem("flinttrade:tourComplete", "true");
sessionStorage.setItem("flinttrade:dailyWelcomeDismissed", "true");
sessionStorage.setItem("flinttrade:smallScreenDismissed", "true");
sessionStorage.setItem("flinttrade:greeted-today", new Date().toDateString());
document.documentElement.dataset.theme = "graphite";
document.documentElement.dataset.mode = theme;
document.documentElement.dataset.density = density;
document.documentElement.classList.toggle("dark", theme === "dark");
document.documentElement.classList.toggle("theme-light", theme === "light");
document.documentElement.classList.toggle("density-compact", density === "compact");
document.documentElement.classList.toggle("density-comfortable", density === "comfortable");
}, { theme, density, state: terminalInitState(theme, density) });
} else {
await page.addInitScript(({ theme, density }) => {
localStorage.setItem("flinttrade.theme", theme);
localStorage.setItem("flinttrade.density", density);
document.documentElement.dataset.theme = theme;
document.documentElement.dataset.density = density;
}, { theme, density });
}
await page.goto(`${baseUrl}${route}`, { waitUntil: "networkidle", timeout: 30000 });
if (app === "terminal") {
await waitForTerminalRoute(page, route);
}
const dir = path.join(out, app, slug(route), viewport.name, theme);
fs.mkdirSync(dir, { recursive: true });
await page.screenshot({ path: path.join(dir, `${density}.png`), fullPage: false });
await page.close();
}
}
}
}
(async () => {
await waitFor(`http://127.0.0.1:${termPort}`);
await waitFor(`http://127.0.0.1:${sitePort}`);
const browser = await chromium.launch();
try {
for (const route of terminalRoutes) {
await capture(browser, "terminal", `http://127.0.0.1:${termPort}`, route);
}
for (const route of siteRoutes) {
await capture(browser, "site", `http://127.0.0.1:${sitePort}`, route);
}
} finally {
await browser.close();
}
})().catch(error => {
console.error(error);
process.exit(1);
});
NODE