Files
Magent/scripts/review_search_ui.cjs
Assclaw 1851fa9753
Magent CI/CD / verify (push) Successful in 10m55s
Magent CI/CD / deploy-prod (push) Skipped
Magent CI/CD / deploy-beta (push) Successful in 2m4s
Show actual collector search activity in request pipeline
2026-09-06 21:14:42 +12:00

102 lines
5.2 KiB
JavaScript

// 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 })