'use strict'; // Disposable local reader + browser; no production accounts or scraper traffic. const assert = require('node:assert/strict'); const fs = require('node:fs'), os = require('node:os'), path = require('node:path'), net = require('node:net'); const { spawn } = require('node:child_process'); const http = require('node:http'); const { chromium } = require('playwright'); const { loadCatalog } = require('../greatest'); const root = path.resolve(__dirname, '..'); const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); async function freePort() { const server = net.createServer(); await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); const port = server.address().port; await new Promise(resolve => server.close(resolve)); return port; } (async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mc-greatest-ui-')); const library = path.join(dir, 'library'), data = path.join(dir, 'data'); fs.mkdirSync(library); fs.mkdirSync(data); const port = await freePort(), base = `http://127.0.0.1:${port}`; const queue = [{ id: 'existing', title: 'Astro Boy', url: 'https://mangadex.org/title/ca4c84bb-7272-45aa-a22d-dc1282b52372', source: 'mangadex', seriesId: 'ca4c84bb-7272-45aa-a22d-dc1282b52372', kind: 'download', status: 'queued' }]; const fakeScraper = http.createServer(async (req, res) => { let result; if (req.method === 'GET' && req.url === '/api/state') result = { queue }; else if (req.method === 'POST' && req.url === '/api/queue') { let body = ''; for await (const chunk of req) body += chunk; const input = JSON.parse(body); queue.push({ id: 'added', title: input.title, url: input.url, source: 'weebcentral', seriesId: String(input.url || '').split('/series/')[1]?.split('/')[0] || '', kind: input.inspect ? 'inspection' : 'download', status: 'queued' }); result = { item: queue.at(-1) }; } else { res.writeHead(404); return res.end(); } res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify(result)); }); await new Promise(resolve => fakeScraper.listen(0, '127.0.0.1', resolve)); const scraperPort = fakeScraper.address().port; const child = spawn(process.execPath, ['server.js'], { cwd: root, windowsHide: true, env: { ...process.env, PORT: String(port), LIBRARY: library, DATA: data, SCAN_INTERVAL: '0', FULL_SCAN_INTERVAL: '0', SCRAPER_URL: `http://127.0.0.1:${scraperPort}`, COOKIE_SECURE: '0', GREATEST_COVER_WARM: '0' }, stdio: 'ignore' }); let browser; try { for (let i = 0; i < 80; i++) { if (await fetch(base + '/health').then(r => r.ok).catch(() => false)) break; await delay(100); } assert.equal((await fetch(base + '/health')).status, 200); const signup = await fetch(base + '/api/signup', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: 'curator', password: 'local-test-password' }) }); assert.equal(signup.status, 200); const cookie = signup.headers.get('set-cookie').split(';')[0]; 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, url: base }]); await context.route(/^https:\/\//, route => route.abort()); // Never load remote cover art in fixture. const page = await context.newPage(), errors = []; page.on('pageerror', error => errors.push(error.message)); await page.goto(base + '/greatest.html'); await page.getByRole('heading', { name: 'Shijō Saikō no Manga' }).waitFor(); assert.equal(await page.getByRole('link', { name: 'Shijō Saikō no Manga' }).textContent(), 'Shijō Saikō no Manga'); assert.equal(await page.locator('.greatest-period').count(), 13); assert.equal(await page.locator('.greatest-tile').count(), 106); const astro = page.locator('.greatest-tile').filter({ has: page.getByRole('button', { name: 'Details for Astro Boy' }) }); assert.equal(await astro.locator('.greatest-reason').isVisible(), true); const astroReason = loadCatalog().entries.find(entry => entry.title === 'Astro Boy').note; assert.match(await astro.locator('.greatest-reason').textContent(), /foundational postwar story manga/); assert.equal(await astro.locator('.greatest-reason-label').textContent(), 'Why it matters'); await astro.getByRole('button', { name: 'Definitely want: Astro Boy' }).click(); await page.getByRole('button', { name: '🔥 Flame' }).click(); assert.equal(await page.locator('.greatest-tile').count(), 1); await page.getByRole('button', { name: 'All', exact: true }).click(); await page.getByRole('button', { name: 'Rank by everyone' }).click(); await page.getByRole('button', { name: 'Details for Astro Boy' }).click(); await page.getByRole('dialog').getByRole('heading', { name: 'Why it matters' }).waitFor(); assert.ok((await page.getByRole('dialog').textContent()).includes(astroReason)); await page.getByRole('dialog').getByText('In scraper · queued').waitFor(); assert.equal(await page.getByRole('dialog').getByRole('button', { name: 'Already added' }).isDisabled(), true); await page.getByRole('dialog').getByText('Community votes: 🔥 1').waitFor(); await page.getByRole('dialog').getByRole('button', { name: 'Close' }).click(); await page.getByRole('button', { name: 'Details for Princess Knight' }).click(); await page.getByRole('dialog').getByRole('textbox', { name: 'Series URL' }) .fill('https://weebcentral.com/series/01J76XY7E9FNDZ1DBBM6PBJPF0'); await page.getByRole('dialog').getByRole('button', { name: 'Add URL to scraper' }).click(); await page.getByRole('dialog').getByText('In scraper · queued').waitFor(); assert.equal(queue.at(-1).kind, 'inspection'); assert.equal(queue.at(-1).title, 'Princess Knight'); await page.getByRole('dialog').getByRole('button', { name: 'Close' }).click(); await page.setViewportSize({ width: 390, height: 844 }); assert.equal(await astro.locator('.greatest-reason').isVisible(), true); assert.equal(await page.locator('.nav-greatest').evaluate(el => getComputedStyle(el, '::after').content), '"Saikō"'); assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), 'Mobile horizontal overflow'); await page.getByRole('button', { name: '📚 In library' }).click(); assert.equal(await page.locator('.greatest-tile').count(), 0); assert.deepEqual(errors, []); await context.close(); console.log('Greatest of All Time browser QA passed: periods, ratings, filters, source status, modal, mobile layout.'); } finally { if (browser) await browser.close(); await new Promise(resolve => { if (child.exitCode !== null) return resolve(); child.once('exit', resolve); child.kill(); }); await new Promise(resolve => fakeScraper.close(resolve)); fs.rmSync(dir, { recursive: true, force: true }); } })().catch(error => { console.error(error); process.exitCode = 1; });