- Use localhost instead of 127.0.0.1 as TARGET_URL so the WebAuthn RP ID
is a valid domain (the spec forbids IP addresses)
- Replace request.post('/logout') with page.context().clearCookies() since
Playwright's request fixture has a separate cookie jar from the page
- Add registerPasskey() helper that waits for 'load' event to reliably
detect the page reload after successful registration
- Track credential count with getCredentialCount() since credentials
accumulate across serial tests sharing the same database
- Fix login.spec.js selector from #webauthn-login-form to #webauthn-login-btn
to match the actual template
All 57 E2E tests now pass (50 migrated + 7 WebAuthn).
155 lines
5.4 KiB
JavaScript
155 lines
5.4 KiB
JavaScript
// @ts-check
|
|
const { test, expect } = require('@playwright/test');
|
|
|
|
test.describe('Login page', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/login');
|
|
});
|
|
|
|
test.describe('Branding', () => {
|
|
test('page title contains Porchlight', async ({ page }) => {
|
|
await expect(page).toHaveTitle(/Porchlight/);
|
|
});
|
|
|
|
test('page title does not contain FastAPI', async ({ page }) => {
|
|
const title = await page.title();
|
|
expect(title).not.toContain('FastAPI');
|
|
});
|
|
|
|
test('favicon link points to /static/favicon.png', async ({ page }) => {
|
|
await expect(page.locator('link[rel="icon"]')).toHaveAttribute('href', '/static/favicon.png');
|
|
});
|
|
|
|
test('favicon file is served', async ({ request }) => {
|
|
const resp = await request.get('/static/favicon.png');
|
|
expect(resp.ok()).toBe(true);
|
|
});
|
|
});
|
|
|
|
test.describe('Site header & logo', () => {
|
|
test('site header is visible', async ({ page }) => {
|
|
await expect(page.locator('.site-header')).toBeVisible();
|
|
});
|
|
|
|
test('logo image is visible', async ({ page }) => {
|
|
await expect(page.locator('.site-logo')).toBeVisible();
|
|
});
|
|
|
|
test('logo src is /static/logo.svg', async ({ page }) => {
|
|
await expect(page.locator('.site-logo')).toHaveAttribute('src', '/static/logo.svg');
|
|
});
|
|
|
|
test('logo SVG file is served', async ({ request }) => {
|
|
const resp = await request.get('/static/logo.svg');
|
|
expect(resp.ok()).toBe(true);
|
|
});
|
|
|
|
test('site title text is Porchlight', async ({ page }) => {
|
|
const siteTitle = page.locator('.site-title');
|
|
await expect(siteTitle).toBeVisible();
|
|
await expect(siteTitle).toHaveText('Porchlight');
|
|
});
|
|
});
|
|
|
|
test.describe('Accessibility', () => {
|
|
test('skip link is present', async ({ page }) => {
|
|
await expect(page.locator('.skip-link')).toHaveCount(1);
|
|
});
|
|
|
|
test('main landmark with id="main" exists', async ({ page }) => {
|
|
await expect(page.locator('main#main')).toHaveCount(1);
|
|
});
|
|
|
|
test('polite live region exists', async ({ page }) => {
|
|
await expect(page.locator('#live[aria-live="polite"]')).toHaveCount(1);
|
|
});
|
|
});
|
|
|
|
test.describe('Login form structure', () => {
|
|
test('H1 says "Sign in"', async ({ page }) => {
|
|
await expect(page.locator('h1')).toHaveText('Sign in');
|
|
});
|
|
|
|
test('password login form exists', async ({ page }) => {
|
|
await expect(page.locator('form[hx-post="/login/password"]')).toHaveCount(1);
|
|
});
|
|
|
|
test('username input is visible', async ({ page }) => {
|
|
await expect(page.locator('#username')).toBeVisible();
|
|
});
|
|
|
|
test('password input is visible', async ({ page }) => {
|
|
await expect(page.locator('#password')).toBeVisible();
|
|
});
|
|
|
|
test('submit button is visible', async ({ page }) => {
|
|
await expect(page.locator('form[hx-post="/login/password"] button[type="submit"]')).toBeVisible();
|
|
});
|
|
|
|
test('WebAuthn login button exists', async ({ page }) => {
|
|
await expect(page.locator('#webauthn-login-btn')).toHaveCount(1);
|
|
});
|
|
});
|
|
|
|
test.describe('Theme / styling', () => {
|
|
test('body background is themed', async ({ page }) => {
|
|
const bgColor = await page.evaluate(() => getComputedStyle(document.body).backgroundColor);
|
|
expect(['rgb(250, 250, 249)', 'rgb(28, 25, 23)']).toContain(bgColor);
|
|
});
|
|
|
|
test('button uses amber accent color', async ({ page }) => {
|
|
const btnBg = await page.evaluate(() => {
|
|
const btn = document.querySelector('button[type="submit"]');
|
|
return getComputedStyle(btn).backgroundColor;
|
|
});
|
|
expect(['rgb(217, 119, 6)', 'rgb(245, 158, 11)']).toContain(btnBg);
|
|
});
|
|
|
|
test('sections have surface background', async ({ page }) => {
|
|
const sectionBg = await page.evaluate(() => {
|
|
const section = document.querySelector('section');
|
|
return getComputedStyle(section).backgroundColor;
|
|
});
|
|
expect(['rgb(245, 245, 244)', 'rgb(41, 37, 36)']).toContain(sectionBg);
|
|
});
|
|
|
|
test('sections have solid border', async ({ page }) => {
|
|
const sectionBorder = await page.evaluate(() => {
|
|
const section = document.querySelector('section');
|
|
return getComputedStyle(section).borderStyle;
|
|
});
|
|
expect(sectionBorder).toBe('solid');
|
|
});
|
|
});
|
|
|
|
test.describe('Static assets', () => {
|
|
test('style.css is served', async ({ request }) => {
|
|
const resp = await request.get('/static/style.css');
|
|
expect(resp.ok()).toBe(true);
|
|
});
|
|
|
|
test('CSS contains --accent custom property', async ({ request }) => {
|
|
const resp = await request.get('/static/style.css');
|
|
const body = await resp.text();
|
|
expect(body).toContain('--accent');
|
|
});
|
|
|
|
test('CSS contains amber accent color #d97706', async ({ request }) => {
|
|
const resp = await request.get('/static/style.css');
|
|
const body = await resp.text();
|
|
expect(body).toContain('#d97706');
|
|
});
|
|
|
|
test('CSS contains dark mode media query', async ({ request }) => {
|
|
const resp = await request.get('/static/style.css');
|
|
const body = await resp.text();
|
|
expect(body).toContain('prefers-color-scheme: dark');
|
|
});
|
|
|
|
test('CSS contains reduced motion media query', async ({ request }) => {
|
|
const resp = await request.get('/static/style.css');
|
|
const body = await resp.text();
|
|
expect(body).toContain('prefers-reduced-motion');
|
|
});
|
|
});
|
|
});
|