Polished command-center for FlowMaster with two data modes:
- SNAPSHOT: bundled src/scenarios.json from demo.flow-master.ai
- LIVE: in-browser fetch via src/lib/api.ts (dev-login + bearer)
Scenarios:
- procurement, extra-1, extra-2 (live from EA2)
- ar, hcm, gl, service (industry blueprints, same typed shell)
Honesty pass after Oracle review:
- No invented numbers (Telemetry derives SLA + agent acceptance from real data)
- Preview-only actions fire toasts naming the endpoint to wire them
- Blueprint tours framed as 'industry blueprint', not 'we don't have this yet'
- Mode pill + last-fetch age + refresh in topbar
- Dev CORS dodged via vite proxy; production deploys same-origin
18 vitest tests + 26 playwright smoke assertions + DOM layout audit.
Constraint: cross-origin live mode rejected by browser → fall back to snapshot
Rejected: hardcoded SLA % | dishonest demo metrics
Directive: wire preview-only action handlers to /api/runtime/transactions/{id}/actions to ship them for real
Confidence: high
Scope-risk: narrow
Not-tested: production deployment via flowmaster-ops overlay
94 lines
3.6 KiB
JavaScript
94 lines
3.6 KiB
JavaScript
// Programmatic visual-QA: walks the rendered DOM and reports likely
|
|
// layout defects (overflow, clipping, overlap, zero-size, low contrast).
|
|
import { chromium } from "playwright";
|
|
|
|
const URL = process.env.URL || "http://127.0.0.1:5173";
|
|
const browser = await chromium.launch({ headless: true });
|
|
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
|
|
|
|
const findings = [];
|
|
function add(kind, msg) { findings.push({ kind, msg }); }
|
|
|
|
await page.goto(URL, { waitUntil: "networkidle" });
|
|
|
|
async function auditScene(label, prep) {
|
|
await prep();
|
|
await page.waitForTimeout(500);
|
|
// For each candidate selector, check for overflow / hidden / zero size
|
|
const results = await page.evaluate(() => {
|
|
const out = [];
|
|
const sels = [
|
|
".hero-title", ".hero-sub", ".sc-card-title", ".sc-card-sub",
|
|
".mc-hero-title", ".mc-hero-sub", ".mc-tab-label",
|
|
".node-name", ".panel-h", ".qcard-title", ".inspector-title h3",
|
|
".tour-title", ".tour-body", ".telemetry .t-block", ".rh-row-step",
|
|
".graph-overlay-name",
|
|
];
|
|
for (const sel of sels) {
|
|
for (const el of Array.from(document.querySelectorAll(sel))) {
|
|
const r = el.getBoundingClientRect();
|
|
const cs = getComputedStyle(el);
|
|
const clipped = el.scrollWidth > r.width + 1 && cs.overflow !== "visible" && cs.textOverflow === "ellipsis";
|
|
const overflowH = el.scrollHeight > r.height + 1 && cs.overflow !== "visible" && cs.overflowY !== "auto" && cs.overflowY !== "scroll";
|
|
const offscreen = r.right < 0 || r.left > window.innerWidth || r.bottom < 0 || r.top > window.innerHeight + 1;
|
|
const zero = (r.width < 2 || r.height < 2) && el.textContent && el.textContent.trim().length > 0;
|
|
if (clipped || overflowH || offscreen || zero) {
|
|
out.push({
|
|
sel, text: (el.textContent || "").trim().slice(0, 80),
|
|
w: r.width.toFixed(0), h: r.height.toFixed(0),
|
|
sw: el.scrollWidth, sh: el.scrollHeight,
|
|
issues: { clipped, overflowH, offscreen, zero },
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
});
|
|
for (const r of results) {
|
|
add("layout", `[${label}] ${r.sel}: "${r.text}" (${r.w}x${r.h} content ${r.sw}x${r.sh}) ${JSON.stringify(r.issues)}`);
|
|
}
|
|
}
|
|
|
|
await auditScene("landing", async () => {});
|
|
await auditScene("mission-1st-live", async () => {
|
|
await page.locator(".sc-card").first().click();
|
|
await page.waitForSelector(".mc");
|
|
});
|
|
await auditScene("mission-ar", async () => {
|
|
await page.locator(".mc-tab", { hasText: "Accounts Receivable" }).click();
|
|
});
|
|
await auditScene("inspector-raw", async () => {
|
|
await page.locator(".itab", { hasText: "Raw" }).click();
|
|
});
|
|
await auditScene("tour", async () => {
|
|
await page.locator(".link-btn", { hasText: "Tour" }).click();
|
|
await page.waitForSelector(".tour-card");
|
|
});
|
|
await auditScene("history", async () => {
|
|
await page.keyboard.press("Escape");
|
|
await page.waitForTimeout(100);
|
|
await page.locator(".tab", { hasText: "Runs" }).click();
|
|
await page.waitForSelector(".rh");
|
|
});
|
|
|
|
console.log(`findings: ${findings.length}`);
|
|
for (const f of findings.slice(0, 50)) console.log(` · ${f.msg}`);
|
|
|
|
// Also dump key layout dimensions for sanity
|
|
const dims = await page.evaluate(() => {
|
|
const get = (sel) => {
|
|
const el = document.querySelector(sel);
|
|
if (!el) return null;
|
|
const r = el.getBoundingClientRect();
|
|
return { sel, w: Math.round(r.width), h: Math.round(r.height), top: Math.round(r.top), left: Math.round(r.left) };
|
|
};
|
|
return [
|
|
get(".rh-head"),
|
|
get(".rh-list"),
|
|
get(".rh-row"),
|
|
];
|
|
});
|
|
console.log("\nrh dimensions:", JSON.stringify(dims, null, 2));
|
|
|
|
await browser.close();
|