Files
Magent/scripts/review_issue_flow_ui.cjs
T
Assclaw 8d720de500
Magent CI/CD / verify (push) Successful in 10m37s
Magent CI/CD / deploy-prod (push) Skipped
Magent CI/CD / deploy-beta (push) Successful in 2m2s
Streamline issue wizard and make selections explicit
2026-09-06 21:44:44 +12:00

205 lines
12 KiB
JavaScript

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