Add the optional Playwright web UI smoke test

This commit is contained in:
Fisto
2026-09-17 08:18:30 +02:00
parent 5319a028e2
commit f5f3196382
5 changed files with 180 additions and 0 deletions
+141
View File
@@ -0,0 +1,141 @@
/*
* 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: "Advanced search" }).click();
await page.waitForSelector(".vk-media-drawer:not([hidden])", { timeout: 5000 });
step("people checkboxes", await page.locator(".vk-media-people input").count());
await page.locator(".vk-media-advanced summary").click();
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);
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");
}