// Admin console QA: node apps/web/qa/admin/shoot-admin.mjs (Playwright from ~/Desktop/uqo-eval) // env: BASE (http://localhost:8310), TOKEN (dev-admin-token), DOC_DIFF (document id with ≥ 2 versions), SKIP_DEVTOOL=1 import { createRequire } from "node:module"; import { mkdirSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const require = createRequire("/Users/simon-pierreboucher/Desktop/uqo-eval/package.json"); const { chromium } = require("playwright"); const out = dirname(fileURLToPath(import.meta.url)); mkdirSync(out, { recursive: true }); const BASE = process.env.BASE ?? "http://localhost:8310"; const TOKEN = process.env.TOKEN ?? "dev-admin-token"; const DOC_DIFF = process.env.DOC_DIFF ?? ""; const SKIP_DEVTOOL = process.env.SKIP_DEVTOOL === "1"; const report = []; const log = (...a) => console.log(new Date().toISOString().slice(11, 19), ...a); async function login(page) { await page.goto(`${BASE}/admin/login`, { waitUntil: "load" }); await page.fill('input[name="token"]', TOKEN); await page.click('button[type="submit"]'); await page.waitForURL((u) => u.pathname === "/admin", { timeout: 30000 }); } async function shot(page, name, w, opts = {}) { await page.waitForTimeout(opts.wait ?? 800); const overflow = await page.evaluate(() => { const doc = document.documentElement; return { scrollWidth: doc.scrollWidth, clientWidth: doc.clientWidth }; }); report.push({ w, name, overflow: overflow.scrollWidth > overflow.clientWidth ? `OVERFLOW ${overflow.scrollWidth}>${overflow.clientWidth}` : "ok" }); await page.screenshot({ path: resolve(out, `${name}-${w}.png`), fullPage: opts.fullPage ?? true }); } async function api(page, path) { return page.evaluate(async (p) => { const r = await fetch(`/admin/api/${p}`, { credentials: "same-origin" }); return { status: r.status, json: await r.json().catch(() => null) }; }, path); } const browser = await chromium.launch(); // ------------------------------------------------------------------ desktop { const w = 1280; const ctx = await browser.newContext({ viewport: { width: w, height: 900 }, deviceScaleFactor: 1 }); const page = await ctx.newPage(); page.on("dialog", (d) => d.accept()); page.on("pageerror", (e) => log("PAGEERROR", String(e).slice(0, 200))); // wrong token first await page.goto(`${BASE}/admin/login`); await page.fill('input[name="token"]', "nope"); await page.click('button[type="submit"]'); await page.waitForSelector('[role="alert"]', { timeout: 20000 }); log("login error shown:", (await page.textContent('[role="alert"]')).trim()); await shot(page, "login-error", w, { fullPage: false }); await login(page); log("logged in"); await page.waitForSelector("text=Last 20 runs", { timeout: 30000 }); await page.waitForTimeout(2500); await shot(page, "overview", w); // connectors: run azure-regions await page.goto(`${BASE}/admin/connectors?q=azure`); await page.waitForSelector("text=azure-regions", { timeout: 30000 }); const row = page.locator("tr", { hasText: "azure-regions" }).first(); await row.getByRole("button", { name: "Run", exact: true }).click(); await page.waitForSelector("text=Task"); await page.selectOption("select", { label: "discover — sitemap / RSS / seeds" }).catch(() => {}); await shot(page, "connectors-run-form", w); await page.locator("tr.bg-surface-2\\/60 + tr button", { hasText: "Run" }).last().click(); await page.waitForSelector("text=Run enqueued", { timeout: 20000 }); log("run enqueued toast visible"); await page.goto(`${BASE}/admin/connectors`); await page.waitForSelector("text=Connector", { timeout: 30000 }); await page.waitForTimeout(2000); await shot(page, "connectors", w); await page.goto(`${BASE}/admin/connectors/azure-regions`); await page.waitForSelector("text=Health history", { timeout: 30000 }); await page.waitForTimeout(1500); await shot(page, "connector-detail", w); await page.getByRole("tab", { name: "Config" }).click(); await page.waitForTimeout(500); await shot(page, "connector-detail-config", w, { fullPage: false }); // documents await page.goto(`${BASE}/admin/documents`); await page.waitForSelector("table tbody tr", { timeout: 30000 }); await shot(page, "documents", w); const firstDoc = await page.locator("table tbody tr td a").first().getAttribute("href"); await page.goto(`${BASE}${firstDoc}`); await page.waitForSelector("text=Metadata", { timeout: 30000 }); await page.locator("iframe[title^='Rendered']").or(page.getByText("Raw body unavailable")).or(page.getByText("No stored versions")).first().waitFor({ timeout: 30000 }).catch(() => {}); await page.waitForTimeout(2500); await shot(page, "document-inspect", w); if (DOC_DIFF) { await page.goto(`${BASE}/admin/documents/${DOC_DIFF}`); await page.waitForSelector("text=Diff", { timeout: 30000 }); await page.getByText("Stored diff summary").or(page.getByText("No line differences")).first().waitFor({ timeout: 60000 }); await page.waitForTimeout(1500); await shot(page, "document-diff", w); log("diff rendered for", DOC_DIFF); } // matches: reject the lowest-scoring pending await page.goto(`${BASE}/admin/matches`); await page.waitForSelector("text=Entity matches", { timeout: 30000 }); await page.waitForTimeout(2500); await shot(page, "matches", w); const pending = await api(page, "matches?status=pending&per_page=200"); const list = pending.json?.data ?? []; if (list.length) { const target = list[list.length - 1]; log(`rejecting lowest-score pending match ${target.id} (${target.candidate?.name} → ${target.matched?.name}, score ${target.score})`); const card = page.locator("article", { hasText: target.candidateKey }).first(); if (await card.count()) { await card.getByRole("button", { name: "Reject" }).click(); await page.waitForSelector("text=Match rejected", { timeout: 20000 }); await shot(page, "matches-rejected", w, { fullPage: false }); const after = await api(page, `matches?status=rejected&per_page=200`); log("rejected now visible in rejected tab:", (after.json?.data ?? []).some((m) => m.id === target.id)); } else log("target card not on first page; skipped UI reject"); } else log("no pending matches to decide"); // events await page.goto(`${BASE}/admin/events`); await page.waitForSelector("text=Events review", { timeout: 30000 }); await page.waitForTimeout(2500); await shot(page, "events", w); // facility edit (no save) const fac = (pending.json?.data ?? []).find((m) => m.matched)?.matched?.id ?? "fac_2ezznz6uhn5b"; await page.goto(`${BASE}/admin/facilities/${fac}/edit`); await page.waitForSelector("text=Curation note", { timeout: 30000 }); await page.waitForTimeout(1000); await shot(page, "facility-edit", w); // sources await page.goto(`${BASE}/admin/sources`); await page.waitForSelector("table tbody tr", { timeout: 30000 }); await shot(page, "sources", w); // dev tool if (!SKIP_DEVTOOL) { await page.goto(`${BASE}/admin/devtool`); await page.waitForSelector("text=Connector development tool", { timeout: 30000 }); await page.evaluate(() => window.localStorage.removeItem("dci:devtool:draft:v1")); await page.reload(); await page.waitForSelector("text=Source URL and fetch options", { timeout: 30000 }); await page.fill('input[placeholder^="https://www.example.com"]', "https://www.digitalrealty.com/data-centers"); await page.getByRole("button", { name: "Fetch sample" }).click(); await page.waitForSelector("text=HTML source", { timeout: 120000 }); await page.waitForTimeout(1500); log("devtool fetch done"); await shot(page, "devtool-1-fetch", w); await page.getByRole("tab", { name: "HTML source" }).click(); await page.fill('input[placeholder^="Find"]', "h1"); await page.waitForTimeout(600); await shot(page, "devtool-1-source-search", w, { fullPage: false }); await page.getByRole("button", { name: /Extraction rules →/ }).click(); await page.waitForSelector("text=YAML preview", { timeout: 20000 }); await shot(page, "devtool-2-rules", w); await page.getByRole("button", { name: /Preview fields →/ }).click(); await page.waitForSelector("text=Preview extraction", { timeout: 20000 }); await page.getByRole("button", { name: "Preview fields", exact: true }).click(); await page.waitForSelector("text=Records (raw extraction)", { timeout: 120000 }); await page.waitForTimeout(1000); log("devtool preview done"); await shot(page, "devtool-3-preview", w); await page.getByRole("button", { name: /Save connector →/ }).click(); await page.waitForSelector("text=Connector YAML", { timeout: 20000 }); await page.fill('input[placeholder="digitalrealty"]', "devtool-test"); await page.getByRole("button", { name: "Regenerate from steps" }).click(); await page.waitForTimeout(400); await page.getByRole("button", { name: "Validate" }).click(); await page.waitForSelector("text=valid", { timeout: 30000 }); await shot(page, "devtool-4-validated", w); await page.getByRole("button", { name: "Save connector", exact: true }).click(); await page.waitForSelector("text=Saved config", { timeout: 30000 }).catch(() => {}); await page.waitForSelector("text=/Saved .*devtool-test\\.yaml/", { timeout: 30000 }); log("devtool save-config done"); await shot(page, "devtool-4-saved", w); await page.getByRole("button", { name: /Run on pages →/ }).click(); await page.waitForSelector("text=Run on multiple pages", { timeout: 20000 }); await page.fill("textarea", "https://www.digitalrealty.com/data-centers\nhttps://www.digitalrealty.com/data-centers/americas"); await page.getByRole("button", { name: "Run sample" }).click(); await page.waitForSelector("text=Comparison — first entity per URL", { timeout: 280000 }); await page.waitForTimeout(1000); log("devtool run-sample done"); await shot(page, "devtool-5-sample", w); await page.getByRole("button", { name: /Compare & deploy →/ }).click(); await page.waitForSelector("text=Compare with the saved connector", { timeout: 20000 }); await page.waitForSelector("text=Saved parser version", { timeout: 30000 }); await page.waitForTimeout(800); await shot(page, "devtool-6-compare", w); await page.getByLabel(/enqueue reprocess/).uncheck(); await page.getByRole("button", { name: "Deploy", exact: true }).click(); await page.waitForSelector("text=save-config", { timeout: 60000 }); await page.waitForTimeout(1500); log("devtool deploy done"); await shot(page, "devtool-6-deployed", w); } await ctx.close(); } // ------------------------------------------------------------------ phone { const w = 390; const ctx = await browser.newContext({ viewport: { width: w, height: 844 }, deviceScaleFactor: 1, isMobile: true, hasTouch: true }); const page = await ctx.newPage(); page.on("pageerror", (e) => log("PAGEERROR(m)", String(e).slice(0, 200))); await page.goto(`${BASE}/admin/login`); await shot(page, "login", w, { fullPage: false }); await login(page); await page.waitForSelector("text=Last 20 runs", { timeout: 30000 }); await page.waitForTimeout(2500); await shot(page, "overview", w); for (const [name, path, waitFor] of [ ["connectors", "/admin/connectors", "text=Connector"], ["connector-detail", "/admin/connectors/azure-regions", "text=Health history"], ["documents", "/admin/documents", "table tbody tr"], ["matches", "/admin/matches", "text=Entity matches"], ["events", "/admin/events", "text=Events review"], ["sources", "/admin/sources", "table tbody tr"], ["devtool", "/admin/devtool", "text=Source URL"], ]) { await page.goto(`${BASE}${path}`); await page.waitForSelector(waitFor, { timeout: 30000 }).catch(() => {}); await page.waitForTimeout(2000); await shot(page, name, w); } if (DOC_DIFF) { await page.goto(`${BASE}/admin/documents/${DOC_DIFF}`); await page.waitForSelector("text=Metadata", { timeout: 30000 }); await page.waitForTimeout(3000); await shot(page, "document-inspect", w); } await ctx.close(); } await browser.close(); for (const r of report) console.log(r.w, r.name, r.overflow);