Log tabs with a daemon I/O view, and Playwright UI tests

The log view splits into All / Daemon I/O / Scans / HTTP. Daemon I/O is
the control protocol itself, logged where every command funnels through
so it covers socket clients, the CLI and the web UI alike. stderr and the
in-app buffer now have separate filters, so the UI can keep debug detail
the terminal should not carry.

Playwright drives a real browser against a daemon on fixture feeds. Eight
tests, each mapping to a bug that reached a user -- the Rust tests and the
stub-DOM smoke test cannot see a wrong selector or a dead handler.

It immediately found one: OPML folders rendered expanded by default,
because the code stored closed groups, so any folder never toggled counted
as open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
This commit is contained in:
2026-09-10 17:07:19 +00:00
parent 4429f1119c
commit 8b21d19365
18 changed files with 457 additions and 20 deletions

43
tests/ui/global-setup.js Normal file
View File

@@ -0,0 +1,43 @@
// Builds a scratch config and data dir so the browser tests drive a real daemon with
// known feeds, rather than whatever happens to be on the machine.
const fs = require('fs');
const path = require('path');
const os = require('os');
const root = path.join(os.tmpdir(), 'ipx-ui-test');
const TOKEN = 'testtokentesttokentesttoken12345'; // fixed, so tests need not scrape a log
// Called from playwright.config.js at load time, NOT as globalSetup: Playwright starts
// webServer *before* globalSetup, so a config written there does not exist yet when the
// daemon launches -- it would fall back to the real config and fight the live daemon.
function prepare() {
fs.rmSync(root, { recursive: true, force: true });
for (const d of ['config', 'data', 'downloads']) {
fs.mkdirSync(path.join(root, d), { recursive: true });
}
fs.writeFileSync(path.join(root, 'config', 'config.toml'), `
[general]
download_dir = "${path.join(root, 'downloads')}"
socket = "${path.join(root, 'ipx.sock')}"
schedule = "every 60m"
max_new_per_check = 1
[torrent]
enabled = false
[web]
enabled = true
bind = "127.0.0.1:8791"
token = "${TOKEN}"
[feeds.test-show]
url = "http://127.0.0.1:8792/show.xml"
auto_download = true
[feeds.test-subscriptions]
url = "http://127.0.0.1:8792/subs.opml"
auto_download = false
`);
}
module.exports = { prepare, root, TOKEN };