Add the optional Playwright web UI smoke test
This commit is contained in:
@@ -119,4 +119,12 @@ pnpm lint
|
|||||||
cd poc
|
cd poc
|
||||||
pnpm check # type check the daemon
|
pnpm check # type check the daemon
|
||||||
pnpm test:comm # unit tests
|
pnpm test:comm # unit tests
|
||||||
|
pnpm test:ui # optional browser smoke test for the Media tab
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`pnpm test:ui` is optional. It needs a running daemon with media data and a Chromium: set
|
||||||
|
`PW_CHROMIUM` to a binary, or keep one in the Playwright cache (`~/.cache/ms-playwright`).
|
||||||
|
`playwright-core` is an optional dependency, so a missing browser never blocks the other tests.
|
||||||
|
`VESKSLOP_UI_URL` overrides the default `https://127.0.0.1:8789/`. It opens the Media tab, loads
|
||||||
|
tiles, applies the cached-only filter, checks the zoom slider and Reset, and fails on console
|
||||||
|
errors or broken images. See [docs/testing.md](docs/testing.md).
|
||||||
|
|||||||
@@ -177,6 +177,21 @@ Notes:
|
|||||||
locally and point `LD_LIBRARY_PATH` at it.
|
locally and point `LD_LIBRARY_PATH` at it.
|
||||||
- The live start was verified on 2026-09-13 through the web UI route.
|
- The live start was verified on 2026-09-13 through the web UI route.
|
||||||
|
|
||||||
|
## Web UI browser smoke test
|
||||||
|
|
||||||
|
Optional. `playwright-core` is an optional dependency and the test needs a Chromium (`PW_CHROMIUM`,
|
||||||
|
or a Playwright cache under `~/.cache/ms-playwright`). The daemon must be running with media data.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd poc
|
||||||
|
pnpm test:ui
|
||||||
|
VESKSLOP_UI_URL=https://192.168.0.178:8789/ pnpm test:ui
|
||||||
|
```
|
||||||
|
|
||||||
|
It opens the Media tab, loads tiles, verifies no broken images, applies the cached-only filter,
|
||||||
|
checks that the zoom slider changes the grid density, presses Reset, and fails on any console
|
||||||
|
error. A screenshot lands in `/tmp/opencode/ui-smoke.png`.
|
||||||
|
|
||||||
## Slopbot live check
|
## Slopbot live check
|
||||||
|
|
||||||
The bot core needs a real token and an invite to the test guild. The check logs in, lists guilds,
|
The bot core needs a real token and an invite to the test guild. The check logs in, lists guilds,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"test:media": "tsx ../media-scrape/test/run.ts && tsx ../media-scrape/test/slopbot.ts",
|
"test:media": "tsx ../media-scrape/test/run.ts && tsx ../media-scrape/test/slopbot.ts",
|
||||||
"test:slopbot:live": "tsx ../media-scrape/test/live-slopbot.ts",
|
"test:slopbot:live": "tsx ../media-scrape/test/live-slopbot.ts",
|
||||||
"test:slopbot:count": "tsx ../media-scrape/test/live-count.ts",
|
"test:slopbot:count": "tsx ../media-scrape/test/live-count.ts",
|
||||||
|
"test:ui": "node test/media-ui-smoke.mjs",
|
||||||
"audio:doctor": "bash scripts/audio-doctor.sh"
|
"audio:doctor": "bash scripts/audio-doctor.sh"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -30,6 +31,9 @@
|
|||||||
"ws": "^8.19.0",
|
"ws": "^8.19.0",
|
||||||
"zod": "^4.6.2"
|
"zod": "^4.6.2"
|
||||||
},
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"playwright-core": "^1.63.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
"@types/node": "^22.19.0",
|
"@types/node": "^22.19.0",
|
||||||
|
|||||||
Generated
+12
@@ -51,6 +51,10 @@ importers:
|
|||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.9.3
|
specifier: ^5.9.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
|
optionalDependencies:
|
||||||
|
playwright-core:
|
||||||
|
specifier: ^1.63.0
|
||||||
|
version: 1.63.0
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -599,6 +603,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==}
|
resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==}
|
||||||
engines: {node: '>=16.20.0'}
|
engines: {node: '>=16.20.0'}
|
||||||
|
|
||||||
|
playwright-core@1.63.0:
|
||||||
|
resolution: {integrity: sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
pngjs@5.0.0:
|
pngjs@5.0.0:
|
||||||
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
|
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
@@ -1266,6 +1275,9 @@ snapshots:
|
|||||||
|
|
||||||
pkce-challenge@5.0.1: {}
|
pkce-challenge@5.0.1: {}
|
||||||
|
|
||||||
|
playwright-core@1.63.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
pngjs@5.0.0: {}
|
pngjs@5.0.0: {}
|
||||||
|
|
||||||
proxy-addr@2.0.7:
|
proxy-addr@2.0.7:
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user