'use strict'; // Fixture-only state interception: no source traffic or production accounts. const assert = require('node:assert/strict'), fs = require('node:fs'), path = require('node:path'); module.exports = async function testQueueUI(url, cookie) { const { chromium } = require('playwright'); const browser = await chromium.launch({ channel: 'msedge', headless: true }); const context = await browser.newContext({ viewport: { width: 1280, height: 900 } }); const [name, ...values] = cookie.split('='); await context.addCookies([{ name, value: values.join('='), url }]); const page = await context.newPage(), errors = [], commands = [], upstream = []; page.on('pageerror', error => errors.push(error.message)); page.on('request', request => { if (new URL(request.url()).origin !== new URL(url).origin) upstream.push(request.url()); }); const state = await (await fetch(url + '/api/admin/scraper', { headers: { cookie } })).json(); state.library[0].sources = [{ name: 'MangaDex', adapter: 'mangadex', url: 'https://mangadex.org/title/fixture' }]; state.updateChecks = [{ mangaId: state.library[0].id, source: 'mangadex', lastCheckedAt: '2026-10-07T08:00:00Z', lastDownloadedAt: '2026-10-07T07:00:00Z', lastResult: 'no-updates' }]; state.history = [ { title: 'Moon Garden', source: 'mangadex', status: 'checked', time: '2026-10-07T08:00:00Z' }, { title: 'Another manga', source: 'weebcentral', status: 'checked', time: '2026-10-07T07:00:00Z' }, ]; state.queue = [ { id: 'pilot-fixture', title: 'bleach', source: 'mangadex', kind: 'inspection', status: 'ready', sourceTitle: 'Bleach (Pre-Serialization)', availability: { selectedChapters: 2, specialReleases: 2, coverage: 'unknown' } }, { id: 'main-fixture', title: 'Bleach', source: 'mangadex', kind: 'download', status: 'complete', publishedCount: 1, sourceTitle: 'Bleach', availability: { selectedChapters: 1, numberedChapters: 1, expectedLastChapter: '686', highestHostedChapter: 686.1, coverage: 'unknown' } }, { id: 'external-fixture', title: 'Claymore', source: 'mangadex', kind: 'download', status: 'unavailable', error: 'MangaDex lists 8 English external-link releases, but no hosted English pages. External links are not downloaded.' }, { id: 'other-source-fixture', title: 'Another manga', source: 'weebcentral', kind: 'download', status: 'queued' }, ]; await page.route('**/api/admin/scraper', async route => { if (route.request().method() === 'POST') { const command = route.request().postDataJSON(); commands.push(command); if (command.action === 'confirm') state.queue.find(item => item.id === command.id).status = 'queued'; if (command.action === 'inspect') state.queue.find(item => item.id === command.id).status = 'queued'; if (command.command === 'clear-completed') state.queue = state.queue.filter(item => item.status !== 'complete' || item.kind !== 'download'); } await route.fulfill({ contentType: 'application/json', body: JSON.stringify(state) }); }); const shots = path.resolve(__dirname, '..', 'data', 'qa', 'queue'); fs.mkdirSync(shots, { recursive: true }); try { await page.goto(url + '/settings.html#admin/scraper'); const activity = page.locator('[data-scraper-tab=activity]'), queuePanel = page.locator('[data-scraper-tab=queue]'); await activity.getByText('Source edition: Bleach (Pre-Serialization)', { exact: true }).waitFor(); await activity.getByText(/no hosted English pages/).waitFor(); assert.equal(commands.length, 0, 'Inspection status must not start a download'); await page.screenshot({ path: path.join(shots, 'desktop.png'), fullPage: true }); await activity.getByRole('button', { name: 'Confirm download', exact: true }).click(); await page.waitForFunction(() => ![...document.querySelectorAll('button')].some(b => b.textContent === 'Confirm download')); assert.deepEqual(commands.at(-1), { command: 'control', action: 'confirm', id: 'pilot-fixture' }); await activity.locator('.scraper-item').filter({ hasText: 'Claymore' }).getByRole('button', { name: 'Inspect again', exact: true }).click(); assert.deepEqual(commands.at(-1), { command: 'control', action: 'inspect', id: 'external-fixture' }); await page.getByRole('link', { name: 'Queue', exact: true }).click(); await queuePanel.getByText('Completed downloads · 1').click(); await page.waitForTimeout(3500); assert.equal(await queuePanel.locator('.scraper-completed').evaluate(el => el.open), true, 'Completed section collapsed on polling'); // Rebuild the panel during the summary click, before the native toggle. // A state snapshot taken only at redraw time misses this click and recloses it. await queuePanel.getByText('Completed downloads · 1').click(); await page.evaluate(() => { const summary = document.querySelector('.scraper-completed > summary'); summary.addEventListener('click', () => { const buttons = document.querySelectorAll('.scraper-filters button'); buttons[1].click(); buttons[0].click(); }, { capture: true, once: true }); }); await queuePanel.getByText('Completed downloads · 1').click(); await page.waitForTimeout(3500); assert.equal(await queuePanel.locator('.scraper-completed').evaluate(el => el.open), true, 'Completed section lost a click during redraw'); await queuePanel.getByText(/1 chapter published · finished/).waitFor(); await queuePanel.getByText('Hosted selection complete', { exact: true }).waitFor(); await queuePanel.getByText('Series coverage partial', { exact: true }).waitFor(); page.once('dialog', dialog => dialog.accept()); await page.getByRole('button', { name: 'Clear completed downloads (1)' }).click(); assert.deepEqual(commands.at(-1), { command: 'clear-completed', source: '' }); assert.equal(await queuePanel.locator('.scraper-completed').evaluate(el => el.open), true, 'Completed section collapsed after cleanup'); assert.equal(state.queue.some(item => item.id === 'main-fixture'), false); assert.ok(state.queue.some(item => item.id === 'pilot-fixture')); await page.getByRole('button', { name: 'MangaDex', exact: true }).click(); assert.equal(await page.getByText('Another manga').count(), 0); await page.getByRole('button', { name: 'All sources', exact: true }).click(); await queuePanel.getByText('Another manga').waitFor(); await page.getByRole('link', { name: 'Updates', exact: true }).click(); await page.getByText(/Last checked:.*2026/).waitFor(); await page.getByText(/Last downloaded:.*2026/).waitFor(); await page.getByRole('button', { name: 'MangaDex', exact: true }).click(); await page.getByText('Recent check activity').click(); await page.waitForTimeout(3500); assert.equal(await page.locator('.scraper-history').evaluate(el => el.open), true, 'Check history collapsed on polling'); assert.equal(await page.locator('[data-scraper-tab=updates] .scraper-history').getByText('Another manga', { exact: false }).count(), 0); await page.getByRole('button', { name: 'All sources', exact: true }).click(); await page.getByRole('link', { name: 'Queue', exact: true }).click(); await page.getByText('Add manga to queue').click(); await page.waitForTimeout(3500); assert.equal(await queuePanel.locator('.scraper-add').evaluate(el => el.open), true, 'Add form collapsed on polling'); await page.locator('input[type=url]').fill('https://mangadex.org/title/06a37094-b3ba-4f25-bf2b-54c9751e0e91'); await page.getByRole('button', { name: 'Inspect & queue', exact: true }).click(); assert.equal(commands.at(-1).inspect, true); assert.equal(commands.at(-1).command, 'enqueue'); await page.setViewportSize({ width: 390, height: 844 }); assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)); await page.screenshot({ path: path.join(shots, 'mobile.png'), fullPage: true }); const detailWidth = await page.locator('[data-scraper-tab=queue] .scraper-queue .scraper-item > .grow').first().evaluate(el => el.getBoundingClientRect().width); assert.ok(detailWidth > 280, `Queue details squeezed by action buttons (${detailWidth}px)`); await page.route('**/api/manga?*', async route => { const title = new URL(route.request().url()).searchParams.get('m'); const first = title === 'Bleach' ? 336 : 1; await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ id: title, title, meta: { title, sources: [{ name: 'MangaDex', url: 'https://mangadex.org/' }] }, cover: null, direction: 'rtl', books: [{ key: 'v01', label: 'Vol. 1' }], chapters: [{ id: `v01/c${first}`, book: 'v01', label: `Ch. ${first}`, title: '', versions: [{ id: 'fixture', pages: 1, added: '2026-10-01', source: 'MangaDex', score: 0, my: 0 }] }], collections: [], relations: [], following: false, reading: null, continue: null, }) }); }); await page.goto(url + '/manga.html?m=Bleach'); await page.getByText('Series coverage partial', { exact: true }).waitFor(); await page.getByText(/starts at chapter 336/).waitFor(); await page.goto(url + '/manga.html?m=Complete%20Fixture'); assert.equal(await page.getByText('Series coverage partial', { exact: true }).count(), 0); assert.deepEqual(errors, []); assert.deepEqual(upstream, []); console.log('PASS: scraper activity, queue coverage, manga gap warning, cleanup, source filters, edition controls and mobile layout; no source traffic'); } finally { await browser.close(); } };