Set up tests/e2e/ with a login page test covering branding, accessibility, form structure, theme colors, and static asset serving. Includes run.sh that manages the app lifecycle (start, test, stop) automatically.
145 lines
5.7 KiB
JavaScript
145 lines
5.7 KiB
JavaScript
// Porchlight — Login page e2e test
|
|
//
|
|
// Tests branding, page structure, accessibility, theme, and responsive layout.
|
|
// Requires: TARGET_URL env var or defaults to http://localhost:8099
|
|
//
|
|
const { chromium } = require('playwright');
|
|
|
|
const TARGET_URL = process.env.TARGET_URL || 'http://localhost:8099';
|
|
|
|
(async () => {
|
|
const browser = await chromium.launch({ headless: true });
|
|
const page = await browser.newPage();
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
function assert(condition, description) {
|
|
if (condition) {
|
|
console.log(` PASS: ${description}`);
|
|
passed++;
|
|
} else {
|
|
console.log(` FAIL: ${description}`);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
// ---- Branding ----
|
|
console.log('\n--- Branding ---');
|
|
await page.goto(`${TARGET_URL}/login`);
|
|
|
|
const title = await page.title();
|
|
assert(title.includes('Porchlight'), `Page title contains "Porchlight" (got: "${title}")`);
|
|
assert(!title.includes('FastAPI'), `Page title does not contain "FastAPI" (got: "${title}")`);
|
|
|
|
// ---- Favicon ----
|
|
console.log('\n--- Favicon ---');
|
|
const favicon = await page.locator('link[rel="icon"]').getAttribute('href');
|
|
assert(favicon === '/static/favicon.png', `Favicon link points to /static/favicon.png (got: "${favicon}")`);
|
|
|
|
const faviconResp = await page.request.get(`${TARGET_URL}/static/favicon.png`);
|
|
assert(faviconResp.ok(), `Favicon file is served (status: ${faviconResp.status()})`);
|
|
|
|
// ---- Site header & logo ----
|
|
console.log('\n--- Site header & logo ---');
|
|
const header = page.locator('.site-header');
|
|
assert(await header.isVisible(), 'Site header is visible');
|
|
|
|
const logo = page.locator('.site-logo');
|
|
assert(await logo.isVisible(), 'Logo image is visible');
|
|
|
|
const logoSrc = await logo.getAttribute('src');
|
|
assert(logoSrc === '/static/logo.svg', `Logo src is /static/logo.svg (got: "${logoSrc}")`);
|
|
|
|
const logoResp = await page.request.get(`${TARGET_URL}/static/logo.svg`);
|
|
assert(logoResp.ok(), `Logo SVG file is served (status: ${logoResp.status()})`);
|
|
|
|
const siteTitle = page.locator('.site-title');
|
|
assert(await siteTitle.isVisible(), 'Site title text is visible');
|
|
const siteTitleText = await siteTitle.textContent();
|
|
assert(siteTitleText.trim() === 'Porchlight', `Site title text is "Porchlight" (got: "${siteTitleText.trim()}")`);
|
|
|
|
// ---- Accessibility ----
|
|
console.log('\n--- Accessibility ---');
|
|
const skipLink = page.locator('.skip-link');
|
|
assert(await skipLink.count() === 1, 'Skip link is present');
|
|
|
|
const main = page.locator('main#main');
|
|
assert(await main.count() === 1, 'Main landmark with id="main" exists');
|
|
|
|
const liveRegion = page.locator('#live[aria-live="polite"]');
|
|
assert(await liveRegion.count() === 1, 'Polite live region exists');
|
|
|
|
// ---- Login form structure ----
|
|
console.log('\n--- Login form structure ---');
|
|
const h1 = page.locator('h1');
|
|
assert(await h1.textContent() === 'Sign in', `H1 says "Sign in" (got: "${await h1.textContent()}")`);
|
|
|
|
const passwordForm = page.locator('form[hx-post="/login/password"]');
|
|
assert(await passwordForm.count() === 1, 'Password login form exists');
|
|
|
|
const usernameInput = page.locator('#username');
|
|
assert(await usernameInput.isVisible(), 'Username input is visible');
|
|
|
|
const passwordInput = page.locator('#password');
|
|
assert(await passwordInput.isVisible(), 'Password input is visible');
|
|
|
|
const submitBtn = passwordForm.locator('button[type="submit"]');
|
|
assert(await submitBtn.isVisible(), 'Submit button is visible');
|
|
|
|
const webauthnForm = page.locator('#webauthn-login-form');
|
|
assert(await webauthnForm.count() === 1, 'WebAuthn login form exists');
|
|
|
|
// ---- Theme / styling ----
|
|
console.log('\n--- Theme / styling ---');
|
|
const bgColor = await page.evaluate(() => {
|
|
return getComputedStyle(document.body).backgroundColor;
|
|
});
|
|
assert(
|
|
bgColor === 'rgb(250, 250, 249)' || bgColor === 'rgb(28, 25, 23)',
|
|
`Body background is themed (got: "${bgColor}")`
|
|
);
|
|
|
|
const btnBg = await page.evaluate(() => {
|
|
const btn = document.querySelector('button[type="submit"]');
|
|
return getComputedStyle(btn).backgroundColor;
|
|
});
|
|
assert(
|
|
btnBg === 'rgb(217, 119, 6)' || btnBg === 'rgb(245, 158, 11)',
|
|
`Button uses amber accent color (got: "${btnBg}")`
|
|
);
|
|
|
|
// ---- Section card styling ----
|
|
console.log('\n--- Section cards ---');
|
|
const sectionBg = await page.evaluate(() => {
|
|
const section = document.querySelector('section');
|
|
return getComputedStyle(section).backgroundColor;
|
|
});
|
|
assert(
|
|
sectionBg === 'rgb(245, 245, 244)' || sectionBg === 'rgb(41, 37, 36)',
|
|
`Sections have surface background (got: "${sectionBg}")`
|
|
);
|
|
|
|
const sectionBorder = await page.evaluate(() => {
|
|
const section = document.querySelector('section');
|
|
return getComputedStyle(section).borderStyle;
|
|
});
|
|
assert(sectionBorder === 'solid', `Sections have solid border (got: "${sectionBorder}")`);
|
|
|
|
// ---- Static assets ----
|
|
console.log('\n--- Static assets ---');
|
|
const cssResp = await page.request.get(`${TARGET_URL}/static/style.css`);
|
|
assert(cssResp.ok(), `style.css is served (status: ${cssResp.status()})`);
|
|
const cssBody = await cssResp.text();
|
|
assert(cssBody.includes('--accent'), 'CSS contains --accent custom property');
|
|
assert(cssBody.includes('#d97706'), 'CSS contains amber accent color #d97706');
|
|
assert(cssBody.includes('prefers-color-scheme: dark'), 'CSS contains dark mode media query');
|
|
assert(cssBody.includes('prefers-reduced-motion'), 'CSS contains reduced motion media query');
|
|
|
|
// ---- Summary ----
|
|
console.log(`\n========================================`);
|
|
console.log(`Results: ${passed} passed, ${failed} failed`);
|
|
console.log(`========================================\n`);
|
|
|
|
await browser.close();
|
|
process.exit(failed > 0 ? 1 : 0);
|
|
})();
|