Files
Magent/scripts/review_account_ui.cjs
T
Assclaw 05a540ecbb
Magent CI/CD / verify (push) Successful in 1m50s
Magent CI/CD / deploy-prod (push) Skipped
Magent CI/CD / deploy-beta (push) Successful in 56s
fix: keep site banner off login page
2026-09-16 14:37:00 +12:00

241 lines
14 KiB
JavaScript

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