porchlight/tests/e2e/login.spec.js
2026-02-18 11:37:23 +01:00

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 form exists', async ({ page }) => {
await expect(page.locator('#webauthn-login-form')).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');
});
});
});