import assert from 'node:assert/strict'; import process from 'node:process'; const { chromium } = await import( process.env.PLAYWRIGHT_MODULE ?? 'playwright' ); const browser = await chromium.launch({ channel: 'chrome', headless: true }); const base = 'http://127.0.0.1:4177'; const context = await browser.newContext(); const page = await context.newPage(); page.setDefaultTimeout(10000); const errors = []; const submissions = []; page.on('pageerror', (error) => errors.push(error.message)); page.on('request', (request) => { if (request.method() === 'POST') { submissions.push({ path: new URL(request.url()).pathname, data: request.postDataJSON(), }); } }); const outcome = (value) => context.addCookies([{ name: 'auth_test_result', value, url: base }]); const open = async (path) => { await page.goto(base + path); await page.locator('main h1').waitFor(); }; const state = (value) => page.locator(`[data-form-state="${value}"]`).waitFor({ state: 'visible' }); const credentials = async () => { await page .getByLabel('Email address', { exact: true }) .fill('alex@example.test'); await page .getByLabel('Password', { exact: true }) .fill('fixture-private-password'); }; const noStoredSecrets = async () => { const persisted = await page.evaluate(() => JSON.stringify({ local: { ...localStorage }, session: { ...sessionStorage }, history: history.state, }), ); for (const secret of [ 'fixture-private-password', '012345', 'fixture-recovery-code', ]) { assert.ok( !persisted.includes(secret), 'Credentials must not be remembered in browser storage', ); } }; try { for (const width of [320, 390, 1440]) { await page.setViewportSize({ width, height: 900 }); for (const theme of ['light', 'dark']) { await page.emulateMedia({ colorScheme: theme }); for (const path of [ '/login', '/email/verify', '/two-factor-challenge', ]) { await open(path); await page .getByRole('radio', { name: theme === 'light' ? /^Light / : /^Dark /, }) .press('Space'); await page .getByRole('radio', { name: theme === 'light' ? /^Light / : /^Dark /, checked: true, }) .waitFor(); assert.equal( await page .locator('html') .evaluate((el) => el.classList.contains('dark')), theme === 'dark', ); assert.equal( await page.evaluate( () => document.documentElement.scrollWidth <= innerWidth, ), true, `${path} must fit at ${width}`, ); const controls = await page .locator( 'main button:not([role="checkbox"]), main label[for="remember"], main input:not([type="checkbox"]):not([aria-hidden="true"])', ) .evaluateAll((els) => els .filter( (el) => el.getBoundingClientRect().width > 0, ) .map((el) => el.getBoundingClientRect().height), ); assert.ok( controls.every((height) => height >= 44), `${path} has touch-sized controls`, ); await page.screenshot({ path: `/private/tmp/kan21-${path.split('/').at(-1)}-${width}-${theme}.png`, fullPage: true, }); } } } await page.setViewportSize({ width: 390, height: 480 }); await open('/login'); await page.getByRole('radio', { name: /^System / }).press('Space'); await page.emulateMedia({ colorScheme: 'light' }); await page.waitForFunction( () => !document.documentElement.classList.contains('dark'), ); await page.emulateMedia({ colorScheme: 'dark' }); await page.waitForFunction(() => document.documentElement.classList.contains('dark'), ); await credentials(); const password = page.locator('#password'); assert.equal( await password.getAttribute('autocomplete'), 'current-password', ); assert.equal( await page.locator('#email').getAttribute('autocomplete'), 'username', ); await password.focus(); await page.keyboard.press('Tab'); assert.equal( await page .getByRole('button', { name: 'Show password' }) .evaluate((el) => el === document.activeElement), true, ); await page.keyboard.press('Enter'); assert.equal(await password.getAttribute('type'), 'text'); await page.getByRole('button', { name: 'Hide password' }).click(); await page.getByRole('checkbox', { name: 'Remember me' }).check(); await outcome('validation'); await password.press('Enter'); await state('validation'); assert.equal(await password.inputValue(), 'fixture-private-password'); assert.equal( await page.locator('#email').inputValue(), 'alex@example.test', ); assert.equal( await page.locator('#email').getAttribute('aria-invalid'), 'true', ); assert.equal(submissions.at(-1).data.remember, true); assert.equal( await page .locator('[data-form-state="validation"]') .evaluate((el) => el === document.activeElement), true, ); await noStoredSecrets(); await page.route('**/login', (route) => route.request().method() === 'POST' ? route.abort('failed') : route.continue(), ); await page.getByRole('button', { name: 'Sign in', exact: true }).click(); await state('network'); assert.equal(await password.inputValue(), 'fixture-private-password'); await page.unroute('**/login'); await outcome('limited'); await page.getByRole('button', { name: 'Try again' }).click(); await page.getByText('Too many attempts', { exact: true }).waitFor(); await outcome('expired'); await page.getByRole('button', { name: 'Try again' }).click(); await page.getByText('Your session has expired', { exact: true }).waitFor(); assert.equal( await page.getByRole('button', { name: 'Try again' }).count(), 0, ); await page.getByRole('link', { name: 'Return to sign in' }).click(); await page.waitForFunction( () => document.querySelector('#password')?.value === '', ); await outcome('success'); await credentials(); let release; const held = new Promise((resolve) => { release = resolve; }); await page.route('**/login', async (route) => { await held; await route.continue(); }); const before = submissions.length; await page.getByRole('button', { name: 'Sign in', exact: true }).click(); await state('loading'); assert.equal(await password.isDisabled(), true); assert.equal( await page.getByRole('button', { name: 'Show password' }).isDisabled(), true, ); await page .locator('form') .evaluate((el) => el.dispatchEvent( new Event('submit', { bubbles: true, cancelable: true }), ), ); assert.equal(submissions.length, before + 1); release(); await page .getByRole('heading', { name: 'Signed in destination' }) .waitFor(); await page.unroute('**/login'); await noStoredSecrets(); await page.goBack(); await page.locator('#password').waitFor(); assert.equal(await password.inputValue(), ''); await open('/login?no-reset=1'); assert.equal( await page.getByRole('link', { name: 'Forgot password?' }).count(), 0, ); await credentials(); await outcome('challenge'); await page.getByRole('button', { name: 'Sign in', exact: true }).click(); await page.locator('#code').waitFor(); assert.equal( await page.locator('#code').getAttribute('inputmode'), 'numeric', ); assert.equal( await page.locator('#code').getAttribute('autocomplete'), 'one-time-code', ); await page.locator('#code').evaluate((el) => { const clipboardData = new DataTransfer(); clipboardData.setData('text', '012 345'); el.dispatchEvent( new ClipboardEvent('paste', { clipboardData, bubbles: true, cancelable: true, }), ); }); assert.equal(await page.locator('#code').inputValue(), '012345'); await outcome('validation'); await page.locator('#code').press('Enter'); await state('validation'); assert.deepEqual(submissions.at(-1).data, { code: '012345' }); assert.equal(await page.locator('#code').inputValue(), '012345'); await page .getByRole('button', { name: 'Use a recovery code instead' }) .click(); assert.equal( await page .locator('#recovery_code') .evaluate((el) => el === document.activeElement), true, ); assert.equal(await page.getByRole('alert').count(), 0); await page.locator('#recovery_code').fill('fixture-recovery-code'); await page.locator('#recovery_code').press('Enter'); await state('validation'); assert.deepEqual(submissions.at(-1).data, { recovery_code: 'fixture-recovery-code', }); assert.equal( await page.locator('#recovery_code').getAttribute('aria-invalid'), 'true', ); assert.equal( await page.locator('#recovery_code').inputValue(), 'fixture-recovery-code', ); await noStoredSecrets(); await page .getByRole('button', { name: 'Use an authenticator code instead' }) .click(); assert.equal(await page.locator('#code').inputValue(), ''); await page .getByRole('button', { name: 'Use a recovery code instead' }) .click(); assert.equal(await page.locator('#recovery_code').inputValue(), ''); await page.locator('#recovery_code').fill('fixture-recovery-code'); await outcome('server'); await page.getByRole('button', { name: 'Verify and sign in' }).click(); await state('server'); assert.equal( await page.locator('#recovery_code').inputValue(), 'fixture-recovery-code', ); await outcome('success'); await page.getByRole('button', { name: 'Try again' }).click(); await page .getByRole('heading', { name: 'Signed in destination' }) .waitFor(); await noStoredSecrets(); await open('/email/verify'); await page.getByText('alex.morgan@example.test', { exact: true }).waitFor(); await page .getByRole('button', { name: 'Resend verification email', exact: true }) .click(); await page.getByText('Verification link sent', { exact: true }).waitFor(); assert.equal(submissions.at(-1).path, '/email/verification-notification'); await outcome('limited'); await page .getByRole('button', { name: 'Resend verification email', exact: true }) .click(); await page.getByText('Too many attempts', { exact: true }).waitFor(); await outcome('success'); await page.getByRole('button', { name: 'Continue to workspace' }).click(); await page .getByRole('heading', { name: 'Signed in destination' }) .waitFor(); await open('/email/verify'); await page.getByRole('button', { name: 'Sign out', exact: true }).click(); await page.locator('#email').waitFor(); assert.equal(submissions.at(-1).path, '/logout'); assert.deepEqual(errors, []); console.log( 'Auth browser checks passed: themes, responsive layout, keyboard, autofill, retention, retry, expiry, throttling, 2FA, recovery, verification and logout.', ); } finally { await browser.close(); }