// UI contract checks. Every API request is mocked: no accounts, passwords, // emails or live media are modified. Requires Node and Playwright. 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 site = { login: { message: '', showJellyfinLogin: true, showLocalLogin: true, showForgotPassword: true, showSignupLink: true }, navigation: { showRequests: true }, banner: { enabled: true, message: 'Beta environment', tone: 'warning', backgroundColor: null, borderColor: null } } ;(async () => { const browser = await chromium.launch({ headless: true }) const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } }) let options = structuredClone(site) let loginStatus = 401 let profileStatus = 200 let emailStatus = 200 let passwordStatus = 200 let provider = 'jellyfin' let supported = true let savedEmail = 'member@example.com' let adminSettings = [ ['site_banner_enabled', 'true'], ['site_banner_tone', 'warning'], ['site_banner_background_color', '#24172f'], ['site_banner_border_color', '#d946ef'], ['site_banner_message', 'Maintenance tonight'], ['site_login_message', 'Sign-in help is available from the media team.'], ['site_login_show_jellyfin_login', 'true'], ['site_login_show_local_login', 'true'], ['site_login_show_forgot_password', 'true'], ['site_login_show_signup_link', 'true'], ].map(([key, value]) => ({ key, value, isSet: true, source: 'db', sensitive: false })) const calls = [] const errors = [] await context.route('**/api/**', async (route) => { const request = route.request() const path = new URL(request.url()).pathname const method = request.method() const reply = (json, status = 200) => route.fulfill({ status, json }) if (path === '/api/site/public' || path === '/api/site/info') return reply(options) if (path === '/api/auth/me') return reply({ username: 'Grizzlyflix member', role: 'admin' }) if (path === '/api/admin/settings') { if (method === 'PUT') { const body = request.postDataJSON() calls.push({ path, method, body: request.postData() }) adminSettings = adminSettings.map((setting) => Object.hasOwn(body, setting.key) ? { ...setting, value: body[setting.key], isSet: Boolean(body[setting.key]) } : setting) return reply({ status: 'ok', updated: Object.keys(body).length }) } return reply({ settings: adminSettings }) } if (path === '/api/auth/profile') return reply(profileStatus === 200 ? { user: { username: 'Grizzlyflix member', role: 'user', email: savedEmail, auth_provider: provider, password_provider: provider, password_change_supported: supported }, stats: { total: 12, ready: 9, in_progress: 3 }, activity: { recent: Array.from({ length: 7 }, (_, i) => ({ ip: '192.0.2.' + (i + 1), user_agent: 'Mozilla/5.0 (Windows NT 10.0) Chrome/140.0', first_seen_at: '2026-09-01T00:00:00Z', last_seen_at: '2026-09-06T01:00:00Z' })) }, } : { detail: 'Service unavailable' }, profileStatus) if (method !== 'GET') calls.push({ path, method, body: request.postData() }) if (path === '/api/auth/profile/email') { await new Promise((resolve) => setTimeout(resolve, 250)) if (emailStatus !== 200) return reply({ detail: 'This email is already in use.' }, emailStatus) savedEmail = request.postDataJSON().email return reply({ email: savedEmail }) } if (path === '/api/auth/password') { await new Promise((resolve) => setTimeout(resolve, 250)) return reply(passwordStatus === 200 ? { provider } : { detail: 'Current password is incorrect' }, passwordStatus) } if (path.endsWith('/login')) { if (loginStatus === 0) return route.abort() return reply({ authenticated: loginStatus === 200 }, loginStatus) } if (path.includes('/events/stream')) return route.fulfill({ status: 200, contentType: 'text/event-stream', body: ': fixture\n\n' }) if (path.includes('/branding/')) return route.fulfill({ status: 404 }) return reply({ requests: [], items: [], services: [], results: [] }) }) const page = await context.newPage() page.on('pageerror', (error) => errors.push(error.message)) const screenshot = async (name) => { if (output) await page.screenshot({ path: output + '/' + name + '.png', fullPage: true, animations: 'disabled' }) } const openLogin = async () => { await page.goto(base + '/login'); await page.getByRole('heading', { name: 'Welcome back.' }).waitFor(); await page.waitForTimeout(250) } const login = async () => { await page.getByLabel('Username', { exact: true }).fill('member'); await page.getByLabel('Password', { exact: true }).fill('test-password'); await page.getByRole('button', { name: 'Sign in', exact: true }).click() } await openLogin() assert.equal(await page.locator('.header,.workspace-sidebar,.admin-sidebar').count(), 0) await screenshot('account-login-desktop') await login() await page.getByRole('alert').filter({ hasText: 'Check your username' }).waitFor() assert.equal(calls.at(-1).path, '/api/auth/jellyfin/login') await page.getByRole('button', { name: 'Magent', exact: true }).click() loginStatus = 429 await login() await page.getByRole('alert').filter({ hasText: 'Too many attempts' }).waitFor() assert.equal(calls.at(-1).path, '/api/auth/login') loginStatus = 503 await login() await page.getByRole('alert').filter({ hasText: 'temporarily unavailable' }).waitFor() loginStatus = 0 await login() await page.getByRole('alert').filter({ hasText: 'Check your connection' }).waitFor() await page.getByRole('button', { name: 'Show password' }).click() assert.equal(await page.getByLabel('Password', { exact: true }).getAttribute('type'), 'text') await page.getByRole('button', { name: 'Hide password' }).click() options.login.showJellyfinLogin = false await openLogin() assert.equal(await page.getByRole('group', { name: 'Sign-in account' }).count(), 0) loginStatus = 401 await login() await page.getByRole('alert').waitFor() assert.equal(calls.at(-1).path, '/api/auth/login') options.login.showLocalLogin = false options.login.showForgotPassword = false options.login.showSignupLink = false options.login.message = 'Sign-in help is available from the media team.' options.banner.message = 'Maintenance tonight' options.banner.backgroundColor = '#24172f' options.banner.borderColor = '#d946ef' await openLogin() assert.equal(await page.getByRole('button', { name: 'Sign in', exact: true }).count(), 0) assert.equal(await page.getByRole('link', { name: 'Forgot password?' }).count(), 0) assert.equal(await page.getByRole('link', { name: /Create an account/ }).count(), 0) assert.equal(await page.getByText('Maintenance tonight').count(), 0) assert(await page.getByText('Sign-in help is available from the media team.').isVisible()) options = structuredClone(site) options.login.showLocalLogin = false await openLogin() loginStatus = 200 await login() await page.waitForURL(base + '/welcome') assert((await context.cookies()).some((cookie) => cookie.name === 'magent_logged_in' && cookie.value === '1')) console.log('PASS: both sign-in providers, disabled methods, error states, password visibility, redirect, login-only message and hidden site banner') options.banner = { enabled: true, message: 'Custom site banner', tone: 'warning', backgroundColor: '#24172f', borderColor: '#d946ef' } await page.goto(base + '/admin/site') const signedInBannerStyle = await page.getByText('Custom site banner', { exact: true }).evaluate((element) => ({ background: getComputedStyle(element).backgroundColor, border: getComputedStyle(element).borderTopColor, })) assert.deepEqual(signedInBannerStyle, { background: 'rgb(36, 23, 47)', border: 'rgb(217, 70, 239)' }) const bannerRegion = page.locator('#config-site-banner') const backgroundHex = bannerRegion.getByLabel('Banner background colour', { exact: true }) await backgroundHex.waitFor() await backgroundHex.fill('red') assert.equal(await backgroundHex.evaluate((element) => element.checkValidity()), false) await bannerRegion.getByLabel('Choose banner background colour', { exact: true }).fill('#112233') assert.equal(await backgroundHex.inputValue(), '#112233') await bannerRegion.getByRole('button', { name: 'Save changes', exact: true }).click() await bannerRegion.getByText('Settings saved.').waitFor() const bannerSave = calls.filter((call) => call.path === '/api/admin/settings').at(-1) assert.equal(JSON.parse(bannerSave.body).site_banner_background_color, '#112233') const loginRegion = page.locator('#config-site-login') await loginRegion.getByLabel('Logged-out login page message', { exact: true }).fill('Welcome. Contact support if you cannot sign in.') await loginRegion.getByRole('button', { name: 'Save changes', exact: true }).click() await loginRegion.getByText('Settings saved.').waitFor() const loginSave = calls.filter((call) => call.path === '/api/admin/settings').at(-1) assert.equal(JSON.parse(loginSave.body).site_login_message, 'Welcome. Contact support if you cannot sign in.') assert(!Object.hasOwn(JSON.parse(loginSave.body), 'site_banner_message')) console.log('PASS: Site & sign-in colour picker, native hex validation, login message and region-only saves') await page.goto(base + '/profile') const email = page.getByLabel('Email address', { exact: true }) await email.waitFor() assert.equal(await page.locator('.workspace-sidebar,.admin-sidebar').count(), 0) assert(await page.getByRole('button', { name: 'Save email' }).isDisabled()) await screenshot('account-profile-desktop') await email.fill('changed@example.com') await page.getByRole('button', { name: 'Discard' }).click() assert.equal(await email.inputValue(), savedEmail) await email.fill('not-an-email') assert.equal(await email.evaluate((el) => el.checkValidity()), false) await email.fill('changed@example.com') await page.getByRole('button', { name: 'Save email' }).click() await page.getByRole('status').filter({ hasText: 'Email saved.' }).waitFor() assert.equal(JSON.parse(calls.at(-1).body).email, 'changed@example.com') await email.fill('') assert(await page.getByText('Saving without an email stops account and issue emails.').isVisible()) await page.getByRole('button', { name: 'Save email' }).click() await page.getByRole('status').filter({ hasText: 'Email removed.' }).waitFor() assert.equal(JSON.parse(calls.at(-1).body).email, null) emailStatus = 409 await email.fill('duplicate@example.com') await page.getByRole('button', { name: 'Save email' }).click() await page.getByRole('alert').filter({ hasText: 'already in use' }).waitFor() await page.getByRole('tab', { name: 'Account', exact: true }).focus() await page.keyboard.press('ArrowRight') assert.equal(await page.getByRole('tab', { name: 'Security', exact: true }).getAttribute('aria-selected'), 'true') await screenshot('account-security-desktop') await page.getByLabel('Current password', { exact: true }).fill('current-password') await page.getByLabel('New password', { exact: true }).fill('new-password') await page.getByLabel('Confirm new password', { exact: true }).fill('mismatch-password') const previousCalls = calls.length await page.getByRole('button', { name: 'Update password' }).click() await page.getByRole('alert').filter({ hasText: 'do not match' }).waitFor() assert.equal(calls.length, previousCalls) await page.getByLabel('Confirm new password', { exact: true }).fill('new-password') passwordStatus = 401 await page.getByRole('button', { name: 'Update password' }).click() await page.getByRole('alert').filter({ hasText: 'Current password is incorrect' }).waitFor() assert.equal(new URL(page.url()).pathname, '/profile') passwordStatus = 200 await page.getByRole('button', { name: 'Update password' }).click() await page.getByRole('status').filter({ hasText: 'Password updated for Grizzlyflix' }).waitFor() assert.equal(await page.getByLabel('Current password', { exact: true }).inputValue(), '') await page.getByRole('tab', { name: 'Activity', exact: true }).click() assert.equal(await page.locator('.account-access-list > li').count(), 5) assert.equal(await page.getByText('192.0.2.1', { exact: true }).isVisible(), false) await page.getByRole('button', { name: 'Show all activity' }).click() assert.equal(await page.locator('.account-access-list > li').count(), 7) await screenshot('account-activity-desktop') provider = 'local' await page.goto(base + '/profile?tab=security') await page.getByText('Keep your Magent account secure.').waitFor() supported = false await page.reload() await page.getByText('Password changes are managed by your sign-in provider.', { exact: false }).waitFor() assert.equal(await page.getByRole('button', { name: 'Update password' }).count(), 0) profileStatus = 503 await page.reload() await page.getByRole('button', { name: 'Try again' }).waitFor() assert.equal(new URL(page.url()).pathname, '/profile') profileStatus = 200 supported = true await page.getByRole('button', { name: 'Try again' }).click() await page.getByRole('tab', { name: 'Account', exact: true }).waitFor() console.log('PASS: email save/removal/discard/validation, password validation/providers, keyboard tabs, activity and load recovery') options = structuredClone(site) for (const width of [320, 390, 768, 1024, 1440]) { await page.setViewportSize({ width, height: 900 }) for (const path of ['/login', '/profile', '/profile?tab=security', '/profile?tab=activity']) { await page.goto(base + path) await page.waitForTimeout(300) assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > document.documentElement.clientWidth), false, path + ' overflows at ' + width) if (width === 390 && (path === '/profile' || path === '/login')) await screenshot('account-mobile-' + path.slice(1)) } } assert.deepEqual(errors, []) console.log('PASS: responsive layout at 320, 390, 768, 1024 and 1440px; no JavaScript errors') await context.unrouteAll({ behavior: 'ignoreErrors' }) await browser.close() })().catch((error) => { console.error(error.message); process.exit(1) })