fix(approvals): buyer-safe toast copy; flip runtimeHealth on action failure
Buyer-script QA caught two regressions: 1. Toast on action failure leaked 'runtime-values isn't reachable' and 'Backend can't accept' — engineering jargon a buyer would read as 'product is broken'. 2. After a real 500, the button stayed enabled so a buyer could click again into the same error. Now: - Failure toast says 'The runtime engine couldn't accept that decision. Action buttons are now disabled until it recovers.' (no jargon, no raw error strings). - handleAction sets runtimeHealth='down' on the failure, which flips the runtime-note banner on and disables every action button. - Non-runtime failures get the even-shorter 'Something went wrong handling that action. Try again in a moment.'
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
// Buyer-script behavioural QA. Walks every visible CTA the way a non-engineer
|
||||
// buyer would, then sweeps the rendered DOM text on each scene for embarrassing
|
||||
// strings ("not configured", "backend can't", "runtime-values", "provider key",
|
||||
// raw 32-hex IDs). Anything that surfaces on first glance is a buyer-facing
|
||||
// failure even if the underlying scaffolding is correct.
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const URL = "https://canvas.flow-master.ai/";
|
||||
const args = ["--host-resolver-rules=MAP canvas.flow-master.ai 65.21.71.186", "--ignore-certificate-errors"];
|
||||
|
||||
const FORBIDDEN_PATTERNS = [
|
||||
{ name: "raw 32-hex IDs", re: /[a-f0-9]{32}/i },
|
||||
{ name: "runtime-values raw mention", re: /runtime[_\s-]?values/i },
|
||||
{ name: "action_execution_failed raw", re: /action_execution_failed/i },
|
||||
{ name: "OPENAI_API_KEY raw", re: /OPENAI_API_KEY/ },
|
||||
{ name: "ANTHROPIC_API_KEY raw", re: /ANTHROPIC_API_KEY/ },
|
||||
{ name: "kubectl", re: /\bkubectl\b/ },
|
||||
{ name: "404 raw", re: /\bHTTP 404\b/i },
|
||||
{ name: "500 raw", re: /\bHTTP 500\b/i },
|
||||
{ name: "stack trace", re: /at\s+\w+\s+\(.*\.ts:\d+/ },
|
||||
];
|
||||
|
||||
const results = [];
|
||||
function record(name, ok, detail = "") {
|
||||
results.push({ name, ok, detail });
|
||||
console.log(`${ok ? "PASS" : "FAIL"} ${name}${detail ? " — " + detail : ""}`);
|
||||
}
|
||||
|
||||
const b = await chromium.launch({ headless: true, args });
|
||||
const ctx = await b.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const p = await ctx.newPage();
|
||||
|
||||
async function inspectScene(label) {
|
||||
await p.waitForTimeout(1200);
|
||||
const body = (await p.locator("body").innerText()).toLowerCase();
|
||||
const findings = FORBIDDEN_PATTERNS
|
||||
.filter((f) => f.re.test(body))
|
||||
.map((f) => `${f.name}: "${(body.match(f.re) || [""])[0]}"`)
|
||||
.slice(0, 4);
|
||||
record(`buyer_${label}_no_engineering_copy`, findings.length === 0, findings.length ? findings.join(" | ") : "clean");
|
||||
}
|
||||
|
||||
await p.goto(URL, { waitUntil: "networkidle" });
|
||||
await p.waitForTimeout(800);
|
||||
await inspectScene("login");
|
||||
|
||||
await p.locator(".persona-chip", { hasText: /Mariana/ }).click();
|
||||
await p.locator(".dev-login-btn").click();
|
||||
await p.waitForSelector(".hero-actions", { timeout: 15000 });
|
||||
await p.waitForFunction(() => document.querySelectorAll(".hub-chip").length >= 8, undefined, { timeout: 15000 }).catch(() => {});
|
||||
await inspectScene("landing");
|
||||
|
||||
const scenes = [
|
||||
{ chip: /Approvals queue/, label: "approvals" },
|
||||
{ chip: /^Documents$/, label: "documents" },
|
||||
{ chip: /Procurement Hub/, label: "procurement_hub" },
|
||||
{ chip: /People Hub/, label: "people_hub" },
|
||||
{ chip: /IT Hub/, label: "it_hub" },
|
||||
{ chip: /Attendance Map/, label: "geo" },
|
||||
{ chip: /Command Assistant/, label: "assistant" },
|
||||
{ chip: /Team Chat/, label: "chat" },
|
||||
{ chip: /What is FlowMaster/, label: "explainer" },
|
||||
];
|
||||
|
||||
for (const s of scenes) {
|
||||
await p.goto(URL, { waitUntil: "networkidle" }).catch(() => {});
|
||||
await p.waitForSelector(".hub-chip", { timeout: 15000 }).catch(() => {});
|
||||
await p.waitForTimeout(600);
|
||||
await p.locator(".hub-chip", { hasText: s.chip }).click().catch(() => {});
|
||||
await inspectScene(s.label);
|
||||
}
|
||||
|
||||
// Approvals: click the first action button — if the runtime is up the button
|
||||
// is enabled; if it's down it should be disabled (we should NEVER allow a
|
||||
// click through to an error). Either way no embarrassing copy may surface.
|
||||
await p.goto(URL, { waitUntil: "networkidle" }).catch(() => {});
|
||||
await p.waitForSelector(".hub-chip", { timeout: 15000 }).catch(() => {});
|
||||
await p.locator(".hub-chip", { hasText: /Approvals queue/ }).click();
|
||||
await p.waitForSelector(".approvals-row", { timeout: 15000 }).catch(() => {});
|
||||
await p.locator(".approvals-row").first().click();
|
||||
await p.waitForSelector(".approvals-card", { timeout: 8000 }).catch(() => {});
|
||||
await p.waitForTimeout(2500);
|
||||
const btn = p.locator(".approvals-actions .btn").first();
|
||||
if (await btn.count() > 0) {
|
||||
const isDisabled = await btn.isDisabled();
|
||||
if (!isDisabled) {
|
||||
await btn.click();
|
||||
await p.waitForTimeout(3000);
|
||||
}
|
||||
const tail = (await p.locator("body").innerText()).toLowerCase();
|
||||
const tailFindings = FORBIDDEN_PATTERNS.filter((f) => f.re.test(tail)).map((f) => f.name).slice(0, 3);
|
||||
record(
|
||||
"buyer_approvals_action_click_no_embarrassing_copy",
|
||||
tailFindings.length === 0,
|
||||
tailFindings.length ? `LEAK: ${tailFindings.join(", ")} (was disabled? ${isDisabled})` : `clean (disabled? ${isDisabled})`
|
||||
);
|
||||
}
|
||||
|
||||
const fails = results.filter((r) => !r.ok);
|
||||
console.log(`\n=== buyer script: ${results.length - fails.length}/${results.length} passed ===`);
|
||||
if (fails.length) {
|
||||
console.log("FAILED:");
|
||||
fails.forEach((f) => console.log(` - ${f.name}: ${f.detail}`));
|
||||
}
|
||||
await b.close();
|
||||
process.exit(fails.length === 0 ? 0 : 1);
|
||||
Reference in New Issue
Block a user