102 lines
5.2 KiB
JavaScript
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 })
|