129 lines
8.7 KiB
JavaScript
129 lines
8.7 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') && p !== '/api/admin/peering').length, 0,
|
|
'Personal settings fetched admin data beyond the top navigation peer indicator');
|
|
await clickSection('user/reading-hours', 'Reading hours');
|
|
await page.getByRole('heading', { name: 'Server reading hours' }).waitFor();
|
|
const schedule = await page.evaluate(() => fetch('/api/reading-schedule').then(r => r.json()));
|
|
if (schedule.enabled) {
|
|
await page.getByText(`Server timezone: ${schedule.timezone}`).waitFor();
|
|
assert.equal(await page.locator('.reading-hours-day').count(), 7);
|
|
} else await page.getByText('Reading is available at any time. No schedule is configured.').waitFor();
|
|
assert.equal(await page.locator('[data-settings-section="user/reading-hours"] input').count(), 0);
|
|
await page.screenshot({ path: path.join(shots, 'desktop-reading-hours.png'), fullPage: true });
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await noOverflow();
|
|
await page.screenshot({ path: path.join(shots, 'mobile-reading-hours.png'), fullPage: true });
|
|
await page.setViewportSize({ width: 1280, height: 900 });
|
|
await clickSection('user/reading', 'Reading');
|
|
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);
|
|
await clickSection('user/reading-hours', 'Reading hours');
|
|
await page.getByRole('heading', { name: 'Server reading hours' }).waitFor();
|
|
assert.equal(await page.evaluate(() => fetch('/api/reading-schedule').then(r => r.status)), 200);
|
|
await noOverflow();
|
|
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(); }
|
|
};
|