Files
MangaCircus/tools/test-settings-ui.js
T

110 lines
7.4 KiB
JavaScript

'use strict';
// Optional headless browser QA against the disposable Compose fixture only.
// Playwright is a development tool, not an application dependency.
const assert = require('node:assert/strict');
const fs = require('node:fs'), path = require('node:path');
module.exports = async function testSettingsUI(url, cookie) {
const { chromium } = require('playwright');
const browser = await chromium.launch({ channel: process.env.QA_BROWSER_CHANNEL || 'msedge', headless: true });
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
const [name, ...value] = cookie.split('=');
await context.addCookies([{ name, value: value.join('='), url }]);
const page = await context.newPage(), errors = [], requests = [];
page.on('pageerror', e => errors.push(e.message));
page.on('request', r => requests.push(new URL(r.url()).pathname));
const shots = path.resolve(__dirname, '..', 'data', 'qa', 'settings');
fs.mkdirSync(shots, { recursive: true });
const clickSection = async (id, title) => {
await page.locator(`.settings-menu a[href="settings.html#${id}"]`).click();
await page.getByRole('heading', { name: title, exact: true, level: 1 }).waitFor();
};
const noOverflow = async () => assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), 'Horizontal page overflow');
try {
await page.goto(url + '/settings.html');
await page.getByRole('heading', { name: 'Reading', exact: true, level: 1 }).waitFor();
assert.equal(await page.locator('.settings-menu-group').count(), 2);
assert.equal(requests.filter(p => p.startsWith('/api/admin')).length, 0, 'Personal settings fetched admin data');
await page.getByLabel('Skip chapter cover pages', { exact: true }).check();
await page.waitForFunction(async () => (await (await fetch('/api/me')).json()).settings.skipCovers === true);
await page.screenshot({ path: path.join(shots, 'desktop-reading.png'), fullPage: true });
await clickSection('user/notifications', 'Notifications');
const webhook = page.locator('input[type=url]');
await webhook.fill('https://example.test/unsaved-draft');
await clickSection('admin/scraper', 'Scraper');
await page.getByRole('heading', { name: 'Activity', exact: false, level: 2 }).waitFor();
assert.equal(await page.locator('[data-scraper-tab]:visible').getAttribute('data-scraper-tab'), 'activity');
await page.getByRole('link', { name: 'Sources', exact: true }).click();
const min = page.locator('[data-scraper-tab=sources] input[type=number]').first();
await min.fill('1.5');
await page.getByRole('link', { name: 'Updates', exact: true }).click();
await page.getByRole('link', { name: 'Sources', exact: true }).click();
assert.equal(await min.inputValue(), '1.5', 'Tab switch discarded a draft');
await page.waitForTimeout(3300);
assert.equal(await min.inputValue(), '1.5', 'Polling discarded a draft');
await page.getByRole('button', { name: 'Save WeebCentral', exact: true }).click();
await page.waitForFunction(() => document.querySelector('[data-scraper-tab=sources] input[type=number]')?.value === '1.5');
const state = await page.evaluate(() => fetch('/api/admin/scraper').then(r => r.json()));
assert.equal(state.settings.sourceRates.weebcentral.minMs, 1500);
await noOverflow();
await page.screenshot({ path: path.join(shots, 'desktop-hosters.png'), fullPage: true });
await clickSection('user/notifications', 'Notifications');
assert.equal(await page.locator('input[type=url]').inputValue(), 'https://example.test/unsaved-draft');
await page.waitForTimeout(300);
const before = requests.filter(p => p === '/api/admin/scraper').length;
await page.waitForTimeout(3300);
assert.equal(requests.filter(p => p === '/api/admin/scraper').length, before, 'Hidden scraper keeps polling');
await page.goBack();
await page.locator('[data-scraper-tab=sources]:visible').waitFor();
await page.goForward();
await page.getByRole('heading', { name: 'Notifications', exact: true, level: 1 }).waitFor();
await clickSection('user/reading-time', 'Reading time & privacy');
await page.getByText('Track and show the Active Reading Clock').waitFor();
await clickSection('admin/library', 'Server & library');
await page.getByRole('button', { name: 'Full rescan', exact: true }).waitFor();
assert.equal(await page.getByRole('button', { name: 'Save user', exact: true }).count(), 0);
await clickSection('admin/users', 'Users & access');
await page.getByRole('button', { name: 'Save user', exact: true }).waitFor();
assert.equal(await page.getByRole('button', { name: 'Full rescan', exact: true }).count(), 0);
await page.locator('.settings-menu a[href="collections.html"]').click();
await page.getByRole('heading', { name: 'Collections & metadata', exact: true, level: 1 }).waitFor();
assert.equal(await page.locator('.settings-menu a[aria-current=page]').getAttribute('href'), 'collections.html');
await page.locator('.settings-menu a[href="health.html"]').click();
await page.getByRole('heading', { name: 'Service health', exact: true, level: 1 }).waitFor();
await page.getByText('No failed jobs.', { exact: true }).waitFor();
await page.setViewportSize({ width: 390, height: 844 });
await noOverflow();
await clickSection('admin/scraper', 'Scraper');
await page.getByRole('heading', { name: 'Activity', exact: false, level: 2 }).waitFor();
await noOverflow();
await page.screenshot({ path: path.join(shots, 'mobile-queue.png'), fullPage: true });
await page.getByRole('link', { name: 'Sources', exact: true }).click();
await noOverflow();
await page.screenshot({ path: path.join(shots, 'mobile-hosters.png'), fullPage: true });
// Create/login a fixture-only ordinary reader. No production accounts are touched.
const result = await page.evaluate(async () => {
const post = (p, b) => fetch(p, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(b) });
const created = await post('/api/admin/user', { name: 'ordinary-qa', password: 'disposable-reader-test', admin: false });
if (!created.ok) return created.status;
await post('/api/logout', {});
return (await post('/api/login', { name: 'ordinary-qa', password: 'disposable-reader-test' })).status;
});
assert.equal(result, 200);
const readerRequestStart = requests.length;
// Change the document URL, not just its hash, after switching fixture accounts.
await page.goto(url + '/settings.html?qa=ordinary#admin/users');
await page.getByRole('heading', { name: 'Reading', exact: true, level: 1 }).waitFor();
assert.equal(await page.locator('.settings-menu-group').count(), 1);
assert.equal(await page.locator('.settings-menu a[href*="admin"]').count(), 0);
assert.equal(requests.slice(readerRequestStart).filter(p => p.startsWith('/api/admin')).length, 0, 'Ordinary reader fetched admin data');
assert.equal(await page.evaluate(() => fetch('/api/admin').then(r => r.status)), 403);
await noOverflow();
assert.deepEqual(errors, []);
console.log('PASS: settings desktop/mobile navigation, deep links, role boundaries, draft retention, saves and polling lifecycle');
console.log('Screenshots: ' + shots);
} catch (e) {
await page.screenshot({ path: path.join(shots, 'failure.png'), fullPage: true }).catch(() => {});
throw e;
} finally { await browser.close(); }
};