// Isolated UI checks: all API responses are fixtures. No searches or downloads run. const assert = require('node:assert/strict') const { chromium } = require(process.env.REVIEW_PLAYWRIGHT || 'playwright') const base = process.env.REVIEW_BASE || 'http://127.0.0.1:3101' const output = process.env.REVIEW_DIR const states = { queued: ['active', 'Search queued', 'Search queued — waiting for Radarr to start.'], searching: ['active', 'Searching', 'Radarr is searching for a matching release.'], idle: ['waiting', 'Not searching', 'Not currently searching for this movie.'], unavailable: ['attention', 'Search unknown', 'Search status unavailable — unable to check Radarr.'], complete: ['complete', 'complete', 'Collection complete — no search needed'], } const fixture = (mode, partial = false) => { const [state, stateLabel, summary] = states[mode] const library = { id: 'library', label: 'Library collection', state: partial ? 'partial' : state, stateLabel, searchStatus: mode === 'complete' ? 'idle' : mode, summary: partial ? '22 of 24 episodes collected. ' + summary : summary, total: partial ? 24 : 1, available: partial ? 22 : mode === 'complete' ? 1 : 0, missing: partial ? 2 : mode === 'complete' ? 0 : 1, } return { request_id: '12', title: 'Search status review', request_type: partial ? 'tv' : 'movie', state: mode === 'complete' ? 'COMPLETED' : 'ADDED_TO_ARR', timeline: [], actions: [], presentation: { status: { label: 'In the library queue', meaning: 'Tracking collection.' }, download: { visible: false }, nextStep: { title: 'Tracking your request', description: 'The status updates automatically.', actionIds: [] }, pipeline: [ { id: 'requested', label: 'Requested', state: 'complete', summary: 'Request received' }, { id: 'approved', label: 'Approved', state: 'complete', summary: 'Approved for collection' }, library, { id: 'search', label: 'Release search', state, summary }, { id: 'download', label: 'Download', state: 'waiting', summary: 'No download attempt yet' }, { id: 'available', label: 'Media server', state: 'waiting', summary: 'Not yet available' }, ], }, } } ;(async () => { const browser = await chromium.launch({ headless: true }) try { const context = await browser.newContext() await context.addCookies([{ name: 'magent_logged_in', value: '1', url: base }]) let mode = 'queued' let partial = false let polls = 0 const mutations = [] const errors = [] await context.route('**/api/**', (route) => { const request = route.request() const path = new URL(request.url()).pathname if (request.method() !== 'GET') mutations.push(path) const reply = (json) => route.fulfill({ json }) if (path === '/api/auth/me') return reply({ username: 'Review member', role: 'user' }) if (path.endsWith('/snapshot')) { polls++; return reply(fixture(mode, partial)) } if (path.includes('/events/stream')) return route.fulfill({ contentType: 'text/event-stream', body: ': fixture\n\n' }) if (path.includes('/branding/')) return route.fulfill({ status: 404 }) return reply({ navigation: { showRequests: true }, requests: [], services: [] }) }) const page = await context.newPage() page.on('pageerror', (error) => errors.push(error.message)) const card = page.locator('.request-stage').filter({ has: page.getByRole('heading', { name: 'Library collection', exact: true }) }) const verify = async () => { await card.getByText(states[mode][1], { exact: true }).waitFor() assert.equal(await card.getByRole('progressbar').getAttribute('aria-valuenow'), partial ? '22' : mode === 'complete' ? '1' : '0') assert.equal(await page.evaluate(() => document.documentElement.scrollWidth - innerWidth), 0) await card.evaluate((element) => element.scrollIntoView({ block: 'center', behavior: 'instant' })) if (output) await card.screenshot({ path: output + `/search-${page.viewportSize().width}-${partial ? 'partial-' : ''}${mode}.png` }) } for (const width of [1440, 390]) { await page.setViewportSize({ width, height: 1000 }) for (mode of Object.keys(states)) { await page.goto(base + '/requests/12') await verify() } mode = 'idle'; partial = true await page.goto(base + '/requests/12') await verify() partial = false } // Confirm queued -> searching -> idle updates without a click or page reload. mode = 'queued' await page.goto(base + '/requests/12') await verify() const initialPolls = polls mode = 'searching' await card.getByText('Searching', { exact: true }).waitFor({ timeout: 8000 }) await verify() mode = 'idle' await card.getByText('Not searching', { exact: true }).waitFor({ timeout: 8000 }) await verify() assert.ok(polls >= initialPolls + 2, 'Active search must refresh automatically') assert.deepEqual(errors, []) assert.deepEqual(mutations, []) console.log('PASS: desktop/mobile search cards, partial collection, and queued → searching → idle automatic updates; no API mutations.') } finally { await browser.close() } })().catch((error) => { console.error(error); process.exitCode = 1 })