247 lines
10 KiB
JavaScript
247 lines
10 KiB
JavaScript
/*
|
|
* 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);
|
|
check("stream buffer present", (await page.locator(".vk-media-buffer").count()) === 1);
|
|
check("no load-more button", (await page.locator(".vk-media-load-more").count()) === 0);
|
|
|
|
// Infinite scroll: scrolling into the buffer appends cards without rebuilding the grid.
|
|
const beforeScroll = await page.locator(".vk-media-figure").count();
|
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
await page.locator("#media-grid").evaluate(node => {
|
|
node.scrollTop = node.scrollHeight;
|
|
});
|
|
await page.waitForTimeout(1200);
|
|
}
|
|
const afterScroll = await page.locator(".vk-media-figure").count();
|
|
step("infinite scroll", { before: beforeScroll, after: afterScroll });
|
|
check("infinite scroll appends cards", afterScroll > beforeScroll, {
|
|
before: beforeScroll,
|
|
after: afterScroll
|
|
});
|
|
check(
|
|
"appended cards do not corrupt loaded ones",
|
|
(await page.$$eval(".vk-media-figure .vk-media-media img", images =>
|
|
images.filter(image => image.complete && image.naturalWidth === 0).length
|
|
)) === 0
|
|
);
|
|
await page.locator("#media-grid").evaluate(node => {
|
|
node.scrollTop = 0;
|
|
});
|
|
await page.waitForTimeout(300);
|
|
|
|
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();
|
|
|
|
const providerFilters = await page.evaluate(async () => {
|
|
const response = await fetch("/api/media/status");
|
|
const body = await response.json();
|
|
return body?.result?.providerFilters ?? [];
|
|
});
|
|
step("provider filters from the API", providerFilters);
|
|
|
|
await page.getByRole("button", { name: "Advanced search" }).click();
|
|
await page.waitForSelector(".vk-media-drawer:not([hidden])", { timeout: 5000 });
|
|
check(
|
|
"title search field present",
|
|
(await page.locator('.vk-media-drawer input[type="text"]').count()) >= 1
|
|
);
|
|
check(
|
|
"exact-match toggle present",
|
|
(await page.locator(".vk-media-drawer .vk-media-check", { hasText: "Exact match" }).count()) === 1
|
|
);
|
|
|
|
const sourceBoxes = await page
|
|
.locator(".vk-media-exclude-group")
|
|
.first()
|
|
.locator(".vk-media-check")
|
|
.count();
|
|
step("source checkboxes", sourceBoxes);
|
|
check(
|
|
"provider checkboxes match the API list",
|
|
sourceBoxes === providerFilters.length + 3,
|
|
{ sourceBoxes, providers: providerFilters.length }
|
|
);
|
|
const includeSources = await page
|
|
.locator(".vk-media-exclude-group")
|
|
.nth(2)
|
|
.locator(".vk-media-check")
|
|
.count();
|
|
step("must-include source checkboxes", includeSources);
|
|
check(
|
|
"must-include mirrors the provider list",
|
|
includeSources === providerFilters.length + 3,
|
|
{ includeSources, providers: providerFilters.length }
|
|
);
|
|
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 }
|
|
);
|
|
|
|
check("media toolbar hidden on the bot tab", !(await page.locator("#media-toolbar").isVisible()));
|
|
check("media filter drawer hidden on the bot tab", !(await page.locator(".vk-media-drawer").isVisible()));
|
|
|
|
await page.waitForFunction(
|
|
() => document.querySelector("#title-lane")?.textContent?.includes("Titles (YouTube)"),
|
|
{ timeout: 15000 }
|
|
);
|
|
check("title lane card renders", (await page.locator("#title-lane .vk-bot-job-head").count()) === 1);
|
|
|
|
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");
|
|
}
|