// All application APIs are mocked. No real issues, emails or media repairs are created. 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 movie = { title: 'The Matrix Reloaded', year: 2003, type: 'movie', tmdbId: 604, requestId: 1424, statusLabel: 'Ready to watch' } const series = { title: 'Family Guy', year: 1999, type: 'tv', tmdbId: 1434, requestId: 113, statusLabel: 'Partially ready' } const options = (media, hasFile = true, canAct = true) => ({ request_id: String(media.requestId), request_type: media.type, title: media.title, can_act: canAct, movie: media.type === 'movie' ? { selected_label: media.title, has_file: hasFile, missing: !hasFile, file_id: hasFile ? 77 : null } : null, seasons: media.type === 'tv' ? [5, 6].map(n => ({ season_number: n, label: `Season ${n}`, episode_count: 2, available_count: hasFile ? 2 : 0, missing_count: hasFile ? 0 : 2 })) : [], episodes: media.type === 'tv' ? [5, 6].flatMap(n => [9, 10].map(e => ({ id: n * 100 + e, season_number: n, episode_number: e, code: `S0${n}E${e.toString().padStart(2, '0')}`, title: `Episode ${n}-${e}`, released: true, monitored: true, has_file: hasFile, missing: !hasFile, file_id: hasFile ? n * 1000 + e : null, }))) : [], }) ;(async () => { const browser = await chromium.launch({ headless: true }) try { const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } }) await context.addCookies([{ name: 'magent_logged_in', value: '1', url: base }]) let currentMedia = movie let hasFile = true let canAct = true let serverDown = false let optionDelay = 0 const calls = [] const errors = [] await context.route('**/api/**', async route => { const request = route.request() const path = new URL(request.url()).pathname const reply = json => route.fulfill({ json }) if (request.method() !== 'GET') calls.push({ path, body: request.postDataJSON() }) if (path === '/api/auth/me') return reply({ username: 'Member', role: 'user' }) if (path === '/api/requests/search') return reply({ results: [currentMedia] }) if (path.endsWith('/issue-options')) { const result = options(currentMedia, hasFile, canAct) if (optionDelay) await new Promise(resolve => setTimeout(resolve, optionDelay)) return reply(result) } if (path === '/api/portal/issues/media-status') return reply({ status: serverDown ? 'down' : 'up', headline: serverDown ? 'Server unavailable' : 'Server is responding', message: 'Media-server check attached.', checked_at: '2026-09-06T00:00:00Z', latency_ms: 12 }) if (path === '/api/portal/items' && request.method() === 'POST') return reply({ item: { id: 900 } }) if (path.includes('/actions/')) return reply({ status: 'ok', message: 'Fixture action accepted.' }) 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 }, items: [], total: 0, services: [] }) }) const page = await context.newPage() page.on('pageerror', error => errors.push(error.message)) const active = () => page.locator('.issue-procedure-step.is-current') const button = name => active().getByRole('button', { name, exact: true }) const visibleStep = async title => { await active().getByRole('heading', { name: title, exact: true }).waitFor() assert.equal(await page.locator('.issue-procedure-step.is-current').count(), 1) assert.equal(await page.evaluate(() => document.documentElement.scrollWidth - innerWidth), 0) assert.equal(await page.locator('.issue-movie-target').count(), 0) } const screenshot = async name => { await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'instant' })) if (output) await page.screenshot({ path: `${output}/issue-flow-${name}.png`, fullPage: true, animations: 'disabled' }) } const start = async (category, media = movie) => { calls.length = 0 currentMedia = media await page.goto(base + '/portal/issues') await visibleStep('What is wrong?') await active().getByRole('button').filter({ hasText: category }).click() await visibleStep('Which title is affected?') assert.equal(await page.locator('.issue-category-grid').count(), 0) await active().getByPlaceholder('Search the Grizzlyflix catalogue').fill(media.title) await button('Search').click() await active().locator('.issue-media-result').click() await visibleStep('What needs to be corrected?') assert.equal(await page.getByPlaceholder('Search the Grizzlyflix catalogue').count(), 0) } const submit = async (path, body) => { await visibleStep('Review and submit') assert.equal(calls.length, 0, 'Choices must not start repairs before submission') await active().getByRole('button', { name: /Submit/ }).click() await visibleStep('What is wrong?') assert.equal(calls[0].path, '/api/portal/items') if (path) { assert.equal(calls.length, 2) assert.equal(calls[1].path, `/api/requests/${currentMedia.requestId}/actions/${path}`) assert.deepEqual(calls[1].body, { issue_id: 900, ...body }) } else assert.equal(calls.length, 1) return calls[0].body } // Movie selection is automatic; device toggles visibly change, persist through Change, and reach the report. for (const width of [1440, 390]) { await page.setViewportSize({ width, height: 1000 }) await start('Audio is wrong') await button('Wrong language').click() await button('No audio').click() assert.equal(await button('Wrong language').getAttribute('aria-pressed'), 'true') await button('Continue').click() await visibleStep('Where did it happen?') const before = await button('TV app').evaluate(el => getComputedStyle(el).backgroundColor) await button('TV app').click() await button('Web browser').click() await button('Phone or tablet').click() await button('Phone or tablet').click() assert.equal(await button('TV app').getAttribute('aria-pressed'), 'true') assert.equal(await button('Phone or tablet').getAttribute('aria-pressed'), 'false') assert.notEqual(await button('TV app').evaluate(el => getComputedStyle(el).backgroundColor), before) await active().getByRole('status').filter({ hasText: 'Selected: TV app, Web browser' }).waitFor() await screenshot(`${width}-devices`) await button('Review issue').click() await visibleStep('Review and submit') await screenshot(`${width}-review`) await page.getByRole('button', { name: /^Change Where did it happen/ }).click() await visibleStep('Where did it happen?') assert.equal(await button('TV app').getAttribute('aria-pressed'), 'true') await button('Review issue').click() const report = await submit('replace', { file_ids: [77], confirmed: true }) assert.ok(report.description.includes('Devices: TV app, Web browser')) assert.ok(report.description.includes('What needs correction: Wrong language, No audio')) assert.ok(!report.description.includes('Phone or tablet')) } await page.setViewportSize({ width: 1440, height: 1000 }) // TV selection remains multi-season / multi-episode and sends only the chosen files. await start('Picture or file is broken', series) await button('Visual artefacts or corruption').click() await button('Continue').click() await visibleStep('Which seasons or episodes?') await active().locator('.issue-episode-grid button').filter({ hasText: 'Episode 5-9' }).click() await active().locator('.issue-season-tabs button').filter({ hasText: 'Season 6' }).click() await active().locator('.issue-episode-grid button').filter({ hasText: 'Episode 6-10' }).click() await screenshot('tv-targets') await button('Continue').click() let report = await submit('replace', { file_ids: [5009, 6010], confirmed: true }) assert.ok(report.description.includes('Episodes: S05E09, S06E10')) // Subtitle issues must never replace a video; optional device choices can be skipped. await start('Subtitles are wrong') await button('Subtitles are missing').click() await button('Continue').click() await button('TV app').click() await button('Skip').click() report = await submit('repair-subtitles', { episode_ids: [], forced: false }) assert.ok(!report.description.includes('Devices:')) // A missing movie does not require a nonexistent movie file or a second movie selection. hasFile = false await start('Movie or episode is missing') await button('Movie is missing').click() await button('Continue').click() await submit('search-missing', { episode_ids: [], season_numbers: [] }) await start('Movie or episode is missing', series) await button('Season is missing').click() await button('Continue').click() for (const season of ['Season 5', 'Season 6']) await active().locator('.issue-season-grid button').filter({ hasText: season }).click() await button('Continue').click() await submit('search-missing', { episode_ids: [509, 510, 609, 610], season_numbers: [5, 6] }) // A repair without a file is blocked, with a click-through to the missing-content path. await start('Audio is wrong') await button('No audio').click() assert.equal(await button('Continue').isDisabled(), true) await button('Report missing movie instead').click() await button('Movie is missing').click() await button('Continue').click() await submit('search-missing', { episode_ids: [], season_numbers: [] }) hasFile = true canAct = false await start('Wrong thing downloaded') await button('Different movie or show').click() await button('Continue').click() await submit(null) canAct = true // Device choices + a down server are attached to the report, without replacing media. serverDown = true await start('Playback or transcoding problem') await button('Transcode error').click() await button('Continue').click() await button('Multiple devices').click() await button('Review issue').click() report = await submit(null) assert.equal(report.issue_type, 'transcode') assert.ok(report.description.includes('Devices: Multiple devices')) assert.ok(report.description.includes('Media server check: Server unavailable')) // Returning to an earlier step while a lookup is pending cannot reopen a stale result. optionDelay = 500 await page.goto(base + '/portal/issues') await active().getByRole('button').filter({ hasText: 'Audio is wrong' }).click() await active().getByPlaceholder('Search the Grizzlyflix catalogue').fill(movie.title) currentMedia = movie await button('Search').click() await active().locator('.issue-media-result').click() await page.getByRole('button', { name: /^Change What is wrong/ }).click() await page.waitForTimeout(700) await visibleStep('What is wrong?') assert.deepEqual(errors, []) console.log('PASS: progressive issue flow, movie auto-selection, visible multi-device toggles, saved report data, TV multi-selection, subtitle-only fixes, permissions, missing files, and stale lookup protection. All APIs mocked.') } finally { await browser.close() } })().catch(error => { console.error(error); process.exitCode = 1 })