/* * Media tab browser smoke test (optional). * * Needs a running daemon whose web UI has media data, and a Chromium binary. * VESKSLOP_UI_URL web UI base URL (default https://127.0.0.1:8789/) * PW_CHROMIUM Chromium binary; defaults to the Playwright cache under * ~/.cache/ms-playwright, then to Playwright's own resolution * * Run from poc/: pnpm test:ui */ import { chromium } from "playwright-core"; import { existsSync, readdirSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; const URL = process.env.VESKSLOP_UI_URL ?? "https://127.0.0.1:8789/"; const results = { steps: [], consoleErrors: [], failures: [] }; function chromiumPath() { if (process.env.PW_CHROMIUM) return process.env.PW_CHROMIUM; const cache = join(homedir(), ".cache", "ms-playwright"); try { for (const entry of readdirSync(cache)) { if (!entry.startsWith("chromium-")) continue; const candidate = join(cache, entry, "chrome-linux", "chrome"); if (existsSync(candidate)) return candidate; } } catch { // no cache; fall through to the Playwright default } return undefined; } function step(name, value) { results.steps.push({ name, value }); console.log(`- ${name}: ${JSON.stringify(value)}`); } function check(name, condition, detail) { if (condition) return; results.failures.push(`${name}${detail === undefined ? "" : ` (${JSON.stringify(detail)})`}`); console.error(`FAIL: ${name}`, detail ?? ""); } const browser = await chromium.launch({ executablePath: chromiumPath(), headless: true, args: ["--no-sandbox", "--disable-dev-shm-usage"] }); try { const context = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1400, height: 900 } }); const page = await context.newPage(); page.on("console", message => { if (message.type() === "error") results.consoleErrors.push(message.text()); }); page.on("pageerror", error => results.consoleErrors.push(String(error))); await page.goto(URL, { waitUntil: "domcontentloaded", timeout: 30000 }); await page.waitForSelector("#scope-media", { timeout: 20000 }); await page.click("#scope-media"); await page.waitForSelector(".vk-media-overlay:not([hidden])", { timeout: 10000 }); const promptVisible = await page.locator(".vk-media-prompt").isVisible(); step("prompt visible", promptVisible); check("prompt is shown before loading", promptVisible); await page.getByRole("button", { name: "Load media" }).click(); await page.waitForSelector(".vk-media-figure", { timeout: 45000 }); const loaded = await page.locator(".vk-media-figure").count(); await page.waitForTimeout(2500); step("figures", loaded); step("status", (await page.locator("#media-status").textContent())?.trim()); check("media tiles render", loaded > 0, loaded); const broken = await page.$$eval(".vk-media-media img", images => images.filter(image => image.naturalWidth === 0).length ); step("broken images", broken); check("no broken images", broken === 0, broken); await page.getByRole("button", { name: /^People/ }).click(); step("people checkboxes", await page.locator(".vk-media-people input").count()); await page.getByRole("button", { name: /^People/ }).click(); await page.getByRole("button", { name: "Advanced search" }).click(); await page.waitForSelector(".vk-media-drawer:not([hidden])", { timeout: 5000 }); await page.locator(".vk-media-check", { hasText: "Show only cached previews" }).locator("input").check(); await page.getByRole("button", { name: "Apply filters" }).click(); await page.waitForTimeout(2500); const cachedFigures = await page.locator(".vk-media-figure").count(); step("figures cachedOnly", cachedFigures); check("cachedOnly keeps results", cachedFigures > 0, cachedFigures); check( "cachedOnly hides uncached tiles", (await page.$$eval(".vk-media-media img", images => images.filter(image => image.naturalWidth === 0).length )) === 0 ); await page.getByRole("button", { name: "Advanced search" }).click(); const zoom = page.locator(".vk-media-zoom"); const columns = {}; for (const value of ["0", "100"]) { await zoom.evaluate((node, v) => { node.value = v; node.dispatchEvent(new Event("input", { bubbles: true })); }, value); await page.waitForTimeout(400); columns[value] = await page .locator("#media-grid") .evaluate(node => getComputedStyle(node).gridTemplateColumns.split(" ").length); } step("columns by zoom", columns); check("zoom changes the grid density", columns["0"] > columns["100"], columns); await page.getByRole("button", { name: "Reset" }).click(); await page.waitForTimeout(800); const resetPrompt = await page.locator(".vk-media-prompt").isVisible(); step("prompt after reset", resetPrompt); check("reset returns the fresh view", resetPrompt); // Bot tab: backfill progress, stats, and the three settings groups. await page.click("#scope-bot"); await page.waitForSelector("#media-bot:not([hidden])", { timeout: 10000 }); await page.waitForFunction( () => document.querySelector("#backfill-summary")?.textContent?.includes("pending"), { timeout: 20000 } ); const summaryLines = await page.locator(".vk-bot-summary-line").count(); step("backfill summary lines", summaryLines); check("backfill summary renders", summaryLines >= 3, summaryLines); step("backfill totals", (await page.locator("#backfill-summary").textContent())?.trim().slice(0, 160)); step("backend split", (await page.locator(".vk-bot-summary-line.vk-dim").first().textContent())?.trim()); const etaTitles = await page.locator(".vk-bot-eta").evaluateAll(nodes => nodes.map(node => node.getAttribute("title") ?? "") ); step("eta tooltips", etaTitles); check("queue eta tooltip", etaTitles.some(title => title.includes("queue drains in"))); check("job eta tooltip", etaTitles.some(title => /starts in .* runs for/.test(title))); const groups = await page.locator("#bot-tunables details").count(); step("tunable groups", groups); check("three tunable groups", groups === 3, groups); const fields = await page.locator("#bot-tunables input").count(); step("tunable fields", fields); check("tunable fields render", fields >= 12, fields); check("start button visible", await page.locator("#backfill-start").isVisible()); step("console errors", results.consoleErrors.length); check("no console errors", results.consoleErrors.length === 0, results.consoleErrors.slice(0, 3)); await page.screenshot({ path: "/tmp/opencode/ui-smoke.png" }); } finally { await browser.close(); } if (results.failures.length) { console.error(`ui smoke failed: ${results.failures.length} check(s)`); process.exitCode = 1; } else { console.log("ui smoke passed"); }