Add private Jellystat viewing stats to Magent beta
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
// Fixture-only review. No requests reach Jellystat or a live Magent backend.
|
||||
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://127.0.0.1:3114'
|
||||
const output = process.env.REVIEW_DIR
|
||||
|
||||
;(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 = 'ready'
|
||||
let role = 'admin'
|
||||
const periods = []
|
||||
const mutations = []
|
||||
let settings = [
|
||||
{ key: 'jellystat_base_url', value: 'http://jellystat:3000', isSet: true, source: 'environment', sensitive: false },
|
||||
{ key: 'jellystat_api_key', value: null, isSet: true, source: 'environment', sensitive: true },
|
||||
]
|
||||
const daily = Array.from({ length: 31 }, (_, i) => ({ date: new Date(Date.UTC(2026, 7, 8 + i)).toISOString().slice(0, 10), minutes: i % 4 ? 30 + i * 2 : 0 }))
|
||||
const fixture = (days) => ({
|
||||
state: mode, is_admin: role === 'admin', days, source: 'Jellystat', timezone: 'UTC', updated_at: '2026-09-07T12:00:00Z',
|
||||
summary: { minutes: daily.reduce((sum, day) => sum + day.minutes, 0), movies: 8, episodes: 23, plays: 35, active_days: 21, current_streak: 3, longest_streak: 6 },
|
||||
daily,
|
||||
top_titles: [{ title: 'Severance', type: 'series', minutes: 460, plays: 10 }, { title: 'Arrival', type: 'movie', minutes: 116, plays: 1 }, { title: 'The Bear', type: 'series', minutes: 91, plays: 3 }],
|
||||
clients: [{ name: 'Jellyfin Web', minutes: 740 }, { name: 'Jellyfin for Android TV', minutes: 301 }],
|
||||
methods: [{ name: 'Direct play', minutes: 880 }, { name: 'Transcode', minutes: 161 }],
|
||||
recent: [{ id: '1', title: 'Good News About Hell', series: 'Severance', type: 'episode', episode: 'S1 · E1', minutes: 57, played_at: '2026-09-07T10:00:00Z', client: 'Jellyfin Web', method: 'Direct play' },
|
||||
{ id: '2', title: 'Arrival', series: '', type: 'movie', minutes: 116, played_at: '2026-09-06T10:00:00Z', client: 'Jellyfin for Android TV', method: 'Direct play' }],
|
||||
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())
|
||||
if (url.pathname === '/api/auth/me') return route.fulfill({ json: { username: 'Fixture viewer', role } })
|
||||
if (url.pathname === '/api/insights') {
|
||||
const days = Number(url.searchParams.get('days'))
|
||||
periods.push(days)
|
||||
if (mode === 'unauthorized') return route.fulfill({ status: 401, json: { detail: 'Sign in' } })
|
||||
if (mode === 'unavailable') return route.fulfill({ status: 502, json: { detail: 'Your viewing stats are temporarily unavailable. Please try again shortly.' } })
|
||||
const result = fixture(days)
|
||||
if (mode === 'empty') {
|
||||
result.state = 'ready'
|
||||
for (const key of Object.keys(result.summary)) result.summary[key] = 0
|
||||
result.daily = daily.map((day) => ({ ...day, minutes: 0 }))
|
||||
result.top_titles = result.recent = result.clients = result.methods = []
|
||||
} else if (mode !== 'ready') result.summary = null
|
||||
return route.fulfill({ json: result })
|
||||
}
|
||||
if (url.pathname === '/api/admin/settings') {
|
||||
if (request.method() === 'PUT') {
|
||||
const body = request.postDataJSON()
|
||||
mutations.push({ path: url.pathname, body })
|
||||
settings = settings.map((setting) => Object.hasOwn(body, setting.key) ? { ...setting, value: setting.sensitive ? null : body[setting.key], isSet: true, source: 'database' } : setting)
|
||||
return route.fulfill({ json: { updated: Object.keys(body).length } })
|
||||
}
|
||||
return route.fulfill({ json: { settings } })
|
||||
}
|
||||
if (url.pathname === '/api/status/services/jellystat/test') {
|
||||
mutations.push({ path: url.pathname })
|
||||
return route.fulfill({ json: { name: 'Jellystat', status: 'up', detail: { connected: true } } })
|
||||
}
|
||||
if (url.pathname === '/api/status/services') return route.fulfill({ json: { services: [{ name: 'Jellystat', status: 'up' }] } })
|
||||
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')
|
||||
await page.getByRole('heading', { name: 'Your viewing rhythm' }).waitFor()
|
||||
assert.notEqual(await page.locator('.stats-period [aria-pressed=true]').evaluate((element) => getComputedStyle(element).backgroundColor), await page.locator('.stats-period [aria-pressed=false]').first().evaluate((element) => getComputedStyle(element).backgroundColor), 'Selected period must be visibly different')
|
||||
await page.locator('.stats-chart-bars button').first().click()
|
||||
assert.match(await page.locator('.stats-chart-detail').innerText(), /minutes/)
|
||||
assert.equal(await page.getByRole('link', { name: 'Dune: Part Two' }).getAttribute('href'), '/requests/12')
|
||||
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), `Overflow at ${width}px`)
|
||||
if (width <= 980) {
|
||||
await page.getByRole('navigation', { name: 'Mobile navigation' }).getByRole('link', { name: 'Config' }).waitFor({ state: 'visible' })
|
||||
await page.getByRole('navigation', { name: 'Mobile navigation' }).getByRole('link', { name: 'Stats' }).waitFor({ state: 'visible' })
|
||||
}
|
||||
if (output) {
|
||||
fs.mkdirSync(output, { recursive: true })
|
||||
await page.screenshot({ path: path.join(output, `insights-${width}.png`), fullPage: true })
|
||||
}
|
||||
}
|
||||
await page.getByRole('button', { name: '90 days', exact: true }).click()
|
||||
await page.getByRole('heading', { name: 'Your viewing rhythm' }).waitFor()
|
||||
assert.equal(periods.at(-1), 90)
|
||||
for (const [state, text] of [['empty', 'No viewing history in this period yet.'], ['not_configured', 'Your viewing story starts here'], ['unlinked', 'Link your viewing account'], ['unavailable', 'Stats couldn’t load']]) {
|
||||
mode = state
|
||||
await page.reload()
|
||||
await page.getByText(text, { exact: false }).waitFor()
|
||||
if (state === 'not_configured') assert.equal(await page.getByRole('link', { name: 'Connect Jellystat' }).getAttribute('href'), '/admin/jellystat')
|
||||
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth))
|
||||
}
|
||||
mode = 'not_configured'
|
||||
role = 'user'
|
||||
await page.reload()
|
||||
await page.getByText('Viewing stats will appear here 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=%2Finsights')
|
||||
role = 'admin'
|
||||
for (const width of [1440, 390]) {
|
||||
await page.setViewportSize({ width, height: 1000 })
|
||||
await page.goto(base + '/admin/jellystat')
|
||||
await page.getByRole('heading', { name: 'Jellystat', exact: true }).waitFor()
|
||||
await page.getByRole('button', { name: 'Test connection', exact: true }).waitFor()
|
||||
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth))
|
||||
if (output) await page.screenshot({ path: path.join(output, `jellystat-settings-${width}.png`), fullPage: true })
|
||||
}
|
||||
await page.getByRole('button', { name: 'Test connection', exact: true }).click()
|
||||
await page.getByText('Jellystat connection test passed.').waitFor()
|
||||
assert(mutations.some((request) => request.path === '/api/status/services/jellystat/test'))
|
||||
const urlInput = page.locator('#setting-jellystat_base_url')
|
||||
await urlInput.fill('http://jellystat:3001')
|
||||
await page.getByRole('button', { name: 'Save changes', exact: true }).click()
|
||||
await page.getByText('Saved', { exact: true }).first().waitFor()
|
||||
assert(mutations.some((request) => request.body?.jellystat_base_url === 'http://jellystat:3001'))
|
||||
assert(mutations.filter((request) => request.body).every((request) => !Object.hasOwn(request.body, 'jellystat_api_key')), 'An unchanged secret must be preserved')
|
||||
assert.deepEqual(errors, [])
|
||||
console.log('Insights browser checks passed: desktop/mobile, chart, period, requests, empty/setup/unlinked/error/auth states, settings save/test, preserved secret.')
|
||||
} finally { await browser.close() }
|
||||
})().catch((error) => { console.error(error); process.exit(1) })
|
||||
Reference in New Issue
Block a user