// Fixture-only browser review. Every API request is intercepted. const assert = require('node:assert/strict') const fs = require('node:fs') const path = require('node:path') const { chromium } = require(process.env.REVIEW_PLAYWRIGHT || 'playwright') const base = process.env.REVIEW_BASE || 'http://localhost:3114' const output = process.env.REVIEW_DIR ;(async () => { const browser = await chromium.launch({ headless: true }) try { const context = await browser.newContext({ acceptDownloads: true }) await context.addCookies([{ name: 'magent_logged_in', value: '1', url: base }]) const calls = [] let mode = 'ready' let role = 'admin' let failDownload = false const months = Array.from({ length: 24 }, (_, index) => new Date(Date.UTC(2026, 8 - index, 1)).toISOString().slice(0, 7)) const delta = (current, previous) => ({ current, previous, difference: current - previous, percent: previous ? (current - previous) / previous * 100 : current ? null : 0 }) const fixture = (month = '2026-08') => { const index = months.indexOf(month) const next = new Date(`${month}-01T00:00:00Z`) next.setUTCMonth(next.getUTCMonth() + 1) const days = month === '2026-09' ? 7 : new Date(next.getTime() - 1).getUTCDate() const summary = { minutes: 1500, movies: 8, episodes: 23, plays: 35, active_days: 20, longest_streak: 6, current_streak: 0 } return { state: 'ready', month, available_months: months, timezone: 'UTC', is_admin: role === 'admin', is_partial: month === '2026-09', comparison_capped: false, period_start: `${month}-01T00:00:00Z`, period_end: month === '2026-09' ? '2026-09-07T12:00:00Z' : next.toISOString(), comparison_month: months[index + 1] || '2024-09', comparison_start: `${months[index + 1] || '2024-09'}-01T00:00:00Z`, comparison_end: `${month}-01T00:00:00Z`, updated_at: '2026-09-07T12:00:00Z', summary, previous_summary: { ...summary, minutes: 1000 }, changes: { minutes: delta(1500, 1000), movies: delta(8, 10), episodes: delta(23, 0), requests: delta(3, 3), plays: delta(35, 25), active_days: delta(20, 15), longest_streak: delta(6, 4) }, daily: Array.from({ length: days }, (_, day) => ({ date: `${month}-${String(day + 1).padStart(2, '0')}`, minutes: day % 4 ? 40 + day * 2 : 0 })), top_titles: [{ title: 'Severance', type: 'series', minutes: 460, plays: 10 }, { title: 'Arrival', type: 'movie', minutes: 116, plays: 1 }], clients: [{ name: 'Jellyfin Web', minutes: 1200 }, { name: 'Jellyfin for Android TV', minutes: 300 }], methods: [{ name: 'Direct play', minutes: 1300 }, { name: 'Transcode', minutes: 200 }], transcoding: { video_minutes: 100, audio_minutes: 61, hardware_video_minutes: 80, software_video_minutes: 20, unknown_hardware_minutes: 0, unknown_video_minutes: 0, unknown_audio_minutes: 0, hardware: [{ name: 'NVIDIA NVENC', minutes: 80 }], audio_codecs: [{ name: 'AAC', minutes: 61 }], gpu_busy_minutes: null }, recent: [{ id: 'play', title: 'Arrival', series: '', type: 'movie', minutes: 116, played_at: `${month}-05T10:00:00Z`, client: 'Jellyfin Web', method: 'Direct play', artwork_url: '/insights/artwork/fixture?token=fixture' }], requests: { total: 3, movies: 2, tv: 1, pending: 1, approved: 2, declined: 0, recent: [{ request_id: 12, title: 'Dune: Part Two', media_type: 'movie', status: 2 }] }, } } await context.route('**/api/**', async (route) => { const request = route.request() const url = new URL(request.url()) calls.push({ method: request.method(), path: url.pathname, month: url.searchParams.get('month') }) if (url.pathname === '/api/auth/me') return route.fulfill({ json: { username: 'Fixture viewer', role } }) if (url.pathname === '/api/auth/jellyfin/login' || url.pathname === '/api/auth/login') return route.fulfill({ json: { authenticated: true } }) if (url.pathname.startsWith('/api/insights/artwork/')) return route.fulfill({ contentType: 'image/svg+xml', body: '' }) if (url.pathname === '/api/insights/reports/monthly.csv') { if (failDownload) return route.fulfill({ status: 502, json: { detail: 'Unavailable' } }) return route.fulfill({ contentType: 'text/csv; charset=utf-8', body: `\ufeffMonth,Minutes\r\n${url.searchParams.get('month')},1500\r\n` }) } if (url.pathname === '/api/insights/reports/monthly' || url.pathname === '/api/insights') { if (mode === 'unauthorized') return route.fulfill({ status: 401, json: { detail: 'Sign in' } }) if (mode === 'unavailable') return route.fulfill({ status: 502, json: { detail: 'Your monthly report is temporarily unavailable. Please try again shortly.' } }) if (mode === 'limit') return route.fulfill({ status: 422, json: { detail: "This report exceeds Jellystat's history limit. No partial report has been generated." } }) const data = { ...fixture(url.searchParams.get('month') || '2026-08'), days: 30 } if (mode === 'empty') { for (const key of Object.keys(data.summary)) data.summary[key] = 0 for (const key of Object.keys(data.changes)) data.changes[key] = delta(0, 0) data.requests = { total: 0, movies: 0, tv: 0, pending: 0, approved: 0, declined: 0, recent: [] } data.daily = data.daily.map((day) => ({ ...day, minutes: 0 })) data.top_titles = data.recent = data.methods = data.clients = [] } else if (mode !== 'ready') { data.state = mode; data.summary = null } return route.fulfill({ json: data }) } if (url.pathname.includes('/events/stream')) return route.fulfill({ contentType: 'text/event-stream', body: ': fixture\n\n' }) return route.fulfill({ json: {} }) }) const page = await context.newPage() const errors = [] page.on('pageerror', (error) => errors.push(error.message)) for (const width of [1440, 980, 390, 320]) { await page.setViewportSize({ width, height: 1000 }) await page.goto(base + '/insights/reports') await page.getByRole('heading', { name: 'August 2026', exact: true }).waitFor() assert.equal(await page.getByLabel('Report month').inputValue(), '2026-08') assert.equal(await page.getByLabel('Report month').locator('option').count(), 24) assert.equal(await page.locator('.stats-view-tabs a[aria-current=page]').innerText(), 'Monthly reports') assert.equal(await page.locator('.header-actions a.is-active').innerText(), 'My Stats') assert.match(await page.locator('.stats-metrics').innerText(), /\+500 min \(\+50%\)/) assert.match(await page.locator('.stats-metrics').innerText(), /−2 \(−20%\)/) assert.match(await page.locator('.stats-metrics').innerText(), /No activity recorded in the comparison period/) assert.equal(await page.getByRole('link', { name: 'Dune: Part Two' }).getAttribute('href'), '/requests/12') await page.locator('.stats-history').scrollIntoViewIfNeeded() await page.waitForFunction(() => document.querySelector('.stats-media-icon img')?.naturalWidth > 0) await page.evaluate(() => window.scrollTo(0, 0)) assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), `Overflow at ${width}px`) if (width <= 980) assert.equal(await page.locator('.workspace-mobile-nav a.is-active').innerText(), 'Stats') if (output) { fs.mkdirSync(output, { recursive: true }) await page.screenshot({ path: path.join(output, `monthly-report-${width}.png`), fullPage: true }) } } const beforeLink = calls.length await page.goto(base + '/insights/reports?month=2026-07') await page.getByRole('heading', { name: 'July 2026', exact: true }).waitFor() const linkedCalls = calls.slice(beforeLink).filter((call) => call.path === '/api/insights/reports/monthly') assert.deepEqual(linkedCalls.map((call) => call.month), ['2026-07']) await page.reload() await page.getByRole('heading', { name: 'July 2026', exact: true }).waitFor() await page.goto(base + '/insights/reports') await page.getByRole('heading', { name: 'August 2026', exact: true }).waitFor() await page.getByRole('button', { name: 'Previous month', exact: true }).click() await page.getByRole('heading', { name: 'July 2026', exact: true }).waitFor() await page.getByRole('button', { name: 'Next month', exact: true }).click() await page.getByRole('heading', { name: 'August 2026', exact: true }).waitFor() await page.getByLabel('Report month').selectOption('2026-09') await page.getByRole('heading', { name: 'September 2026', exact: true }).waitFor() await page.getByText('Compared with the same elapsed time in August 2026.', { exact: true }).waitFor() assert(await page.getByRole('button', { name: 'Next month', exact: true }).isDisabled()) const downloaded = page.waitForEvent('download') await page.getByRole('button', { name: 'Download CSV', exact: true }).click() const download = await downloaded assert.equal(download.suggestedFilename(), 'magent-monthly-report-2026-09.csv') assert.match(fs.readFileSync(await download.path(), 'utf8'), /2026-09,1500/) failDownload = true await page.getByRole('button', { name: 'Download CSV', exact: true }).click() await page.getByRole('alert').filter({ hasText: 'could not be downloaded' }).waitFor() failDownload = false await page.getByLabel('Report month').selectOption(months.at(-1)) await page.getByRole('heading', { name: 'October 2024', exact: true }).waitFor() assert(await page.getByRole('button', { name: 'Previous month', exact: true }).isDisabled()) for (const [state, text] of [['empty', 'No viewing history was recorded for this month.'], ['unlinked', 'Link your viewing account'], ['not_configured', 'Your monthly story starts here'], ['unavailable', 'Report couldn’t load'], ['limit', 'No partial report has been generated.']]) { mode = state await page.reload() await page.getByText(text, { exact: false }).waitFor() if (state !== 'empty') assert(await page.getByRole('button', { name: 'Download CSV', exact: true }).isDisabled()) assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)) } mode = 'not_configured' role = 'user' await page.reload() await page.getByText('Monthly reports will appear once your administrator connects Jellystat.').waitFor() assert.equal(await page.getByRole('link', { name: 'Connect Jellystat' }).count(), 0) mode = 'unauthorized' await page.reload() await page.waitForURL(/\/login\?next=/) assert.equal(new URL(page.url()).searchParams.get('next'), `/insights/reports?month=${months.at(-1)}`) mode = 'ready' await page.getByLabel('Username', { exact: true }).fill('fixture-viewer') await page.getByLabel('Password', { exact: true }).fill('Fixture-only-password') await page.getByRole('button', { name: 'Sign in', exact: true }).click() await page.getByRole('heading', { name: 'October 2024', exact: true }).waitFor() assert.equal(new URL(page.url()).searchParams.get('month'), months.at(-1)) // An external or unrecognized destination must still use the safe welcome fallback. for (const next of ['https://outside.example.test', '//outside.example.test', '/insights/reports?month=2026-07&redirect=https://outside.example.test']) { await page.goto(`${base}/login?next=${encodeURIComponent(next)}`) await page.getByLabel('Username', { exact: true }).fill('fixture-viewer') await page.getByLabel('Password', { exact: true }).fill('Fixture-only-password') await page.getByRole('button', { name: 'Sign in', exact: true }).click() await page.waitForURL(`${base}/welcome`) } await page.goto(base + '/insights/reports') await page.getByRole('heading', { name: 'August 2026', exact: true }).waitFor() await page.getByRole('navigation', { name: 'My Stats views' }).getByRole('link', { name: 'Overview' }).click() await page.waitForURL('**/insights') await page.getByRole('heading', { name: 'Your viewing rhythm' }).waitFor() assert.equal(await page.locator('.stats-view-tabs a[aria-current=page]').innerText(), 'Overview') assert(calls.filter((call) => call.path.startsWith('/api/insights/reports')).every((call) => call.method === 'GET')) assert.deepEqual(errors, []) console.log('Monthly report browser checks passed: desktop/mobile, comparison labels, periods, navigation, artwork, CSV download/error, empty/setup/link/error/auth states.') } finally { await browser.close() } })().catch((error) => { console.error(error); process.exit(1) })