119 lines
9.4 KiB
JavaScript
119 lines
9.4 KiB
JavaScript
'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(); }
|
|
};
|