test(launch): capture existing public homepage baseline

This commit is contained in:
cesnimda
2026-11-09 22:56:22 +01:00
parent 0bb7226725
commit bd34a3504f
24 changed files with 10359 additions and 10 deletions
@@ -7,12 +7,14 @@ import fs from 'node:fs';
import path from 'node:path';
const compiled = ts.transpileModule(fs.readFileSync(new URL('../src/i18n/translations.ts', import.meta.url), 'utf8'), { compilerOptions: { module: ts.ModuleKind.ESNext } }).outputText;
const { translations } = await import(`data:text/javascript;base64,${Buffer.from(compiled).toString('base64')}`);
const out = path.resolve(import.meta.dirname, '../../docs/ui-modernisation/visual-correction/evidence/public-entry');
const landingAudit = process.env.A3_LANDING_AUDIT === '1';
const out = path.resolve(import.meta.dirname, landingAudit ? '../../docs/ui-modernisation/visual-correction/evidence/landing-baseline' : '../../docs/ui-modernisation/visual-correction/evidence/public-entry');
fs.mkdirSync(out, { recursive: true });
const report = { screens: [], journeys: [], confirmations: [], signIns: [], challenges: [], unexpectedWrites: [], unavailableReads: [], blockedExternal: [], errors: [] };
const report = { screens: [], journeys: [], confirmations: [], signIns: [], challenges: [], preferenceDenials: [], unexpectedWrites: [], unavailableReads: [], blockedExternal: [], errors: [] };
const browser = await chromium.launch();
try {
const configurations = process.env.A3_PILOT ? [[320, 'en', 'light']] : [320, 360, 390, 430, 768, 1024, 1280, 1440, 1920].flatMap(width => ['en', 'nb'].flatMap(language => ['light', 'dark'].map(mode => [width, language, mode])));
const widths = landingAudit ? [320, 768, 1440] : [320, 360, 390, 430, 768, 1024, 1280, 1440, 1920];
const configurations = process.env.A3_PILOT ? [[320, 'en', 'light']] : widths.flatMap(width => ['en', 'nb'].flatMap(language => ['light', 'dark'].map(mode => [width, language, mode])));
for (const [width, language, mode] of configurations) {
console.log(`Checking ${width}/${language}/${mode}`);
const other = language === 'en' ? 'nb' : 'en';
@@ -25,9 +27,22 @@ try {
await page.route('**/*', route => {
const req = route.request(), url = new URL(req.url()), p = url.pathname;
if (url.origin !== 'http://localhost:4338' && !(url.origin === 'http://localhost:5202' && p.startsWith('/api/'))) { report.blockedExternal.push(url.origin); return route.abort(); }
if (landingAudit && p === '/mockups/dashboard.svg') {
const mockup = fs.readFileSync(path.resolve(import.meta.dirname, '../out/mockups/dashboard.svg'), 'utf8');
const safeMockup = mockup.replace(/[\w.%+-]+@[\w.-]+\.[A-Za-z]{2,}/g, 'alex@example.test');
return route.fulfill({ status: 200, contentType: 'image/svg+xml', body: safeMockup });
}
if (!p.startsWith('/api/')) return route.continue();
const answer = data => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(data) });
if (req.method() === 'GET' && p === '/api/auth/config') { configReads++; return answer({ requireAuth: true, localEnabled: true, allowRegistration: true, requireEmailVerification: true, googleEnabled: false, microsoftEnabled: false, turnstileEnabled: false }); }
if (landingAudit && req.method() === 'GET' && p === '/api/auth/me') return route.fulfill({ status: 401, contentType: 'application/json', body: '{}' });
if (landingAudit && req.method() === 'PUT' && p === '/api/auth/preferences/language') {
const preference = req.postDataJSON().language;
assert.ok(['en', 'nb-NO'].includes(preference));
report.preferenceDenials.push({ width, language, mode, preference });
return route.fulfill({ status: 401, contentType: 'application/json', body: '{}' });
}
if (landingAudit && req.method() !== 'GET') { report.unexpectedWrites.push(p); return route.abort(); }
if (req.method() === 'POST' && ['/api/auth/verify-email', '/api/auth/email-change/confirm'].includes(p)) {
const expected = { userId: 'synthetic-owner', ...(p.includes('email-change') ? { email: 'synthetic@example.test' } : {}), token: 'synthetic-confirmation' };
assert.deepEqual(req.postDataJSON(), expected); confirmations++; report.confirmations.push({ width, language, mode, path: p }); return answer({});
@@ -51,6 +66,43 @@ try {
};
const selectLanguage = locale => page.getByRole('button', { name: locale === 'en' ? 'English' : 'Norsk bokmål', exact: true }).click();
const ready = async (url, title, config = false) => { await page.goto(`http://localhost:4338${url}`); await expect(page.getByRole('heading', { level: 1, name: t(title), exact: true })).toBeVisible(); if (config) await expect(page.getByText(t('loading'), { exact: true })).toHaveCount(0); };
if (landingAudit) {
const landing = async locale => {
const heading = `${text(locale, 'landingHeroBefore')} ${text(locale, 'landingHeroAccent')}.`;
await expect(page.getByRole('heading', { level: 1, name: heading, exact: true })).toBeVisible();
};
await page.goto('http://localhost:4338/');
await landing(language);
await expect.poll(() => page.locator('img[src^="/mockups/"]').evaluateAll(images => images.filter(img => img.complete).length)).toBe(3);
if (!report.rawHtml) {
const response = await page.request.get('http://localhost:4338/');
assert.equal(response.status(), 200);
report.rawHtml = await page.evaluate(html => {
const raw = new DOMParser().parseFromString(html, 'text/html');
return { language: raw.documentElement.lang, title: raw.title, description: raw.querySelector('meta[name=description]')?.content ?? null, canonical: raw.querySelector('link[rel=canonical]')?.getAttribute('href') ?? null, alternateLanguages: [...raw.querySelectorAll('link[hreflang]')].map(link => link.hreflang), headings: [...raw.querySelectorAll('body h1')].map(heading => heading.textContent) };
}, await response.text());
}
const layout = await page.evaluate(() => ({
pageOverflow: document.documentElement.scrollWidth > innerWidth + 1,
landmarks: [...document.querySelectorAll('header, main, footer, nav')].map(node => node.tagName.toLowerCase()),
mockups: [...document.querySelectorAll('img[src^="/mockups/"]')].map(img => ({ src: img.getAttribute('src'), loaded: img.complete && img.naturalWidth > 0 })),
}));
const result = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa', 'best-practice']).analyze();
report.screens.push({ width, language, mode, state: 'landing-baseline', layout, violations: result.violations });
await page.screenshot({ path: path.join(out, `landing-${language}-${mode}-${width}.png`), fullPage: true });
await selectLanguage(other); await landing(other);
assert.equal(await page.evaluate(() => localStorage.getItem('uiLanguage')), other);
await selectLanguage(language); await landing(language);
await page.getByRole('link', { name: t('landingSeeFeatures'), exact: true }).press('Enter');
await expect.poll(() => new URL(page.url()).hash).toBe('#features');
await page.getByRole('button', { name: t('landingSignIn'), exact: true }).first().press('Enter');
await expect(page.getByRole('heading', { name: t('signInTitle'), level: 1, exact: true })).toBeVisible();
await page.goto('http://localhost:4338/'); await landing(language);
await page.getByRole('button', { name: t('landingPlanFreeCta'), exact: true }).press('Enter');
await expect(page.getByRole('heading', { name: t('createAccount'), level: 1, exact: true })).toBeVisible();
report.journeys.push({ width, language, mode, publicLanguageSwitch: 'PASS', featuresAnchorAndAuthNavigation: 'PASS', submissions: 'NOT RUN' });
await context.close(); continue;
}
await ready('/login', 'signInTitle', true); await scan('login');
await page.getByLabel(t('usernameOrEmail'), { exact: true }).fill('synthetic@example.test'); await page.getByLabel(t('profileCurrentPassword'), { exact: true }).fill('Synthetic unsubmitted example');
const reads = configReads; await selectLanguage(other);
@@ -74,9 +126,9 @@ try {
await selectLanguage(other); await expect(page.getByRole('main').getByRole('alert')).toContainText(text(other, 'twoFactorRateLimited')); await expect(page.getByLabel(text(other, 'twoFactorCodeLabel'), { exact: true })).toHaveValue('000000'); await selectLanguage(language); await scan('mfa-rate-limit');
assert.equal(confirmations, 2); assert.equal(signIns, 1); assert.equal(challenges, 1); report.journeys.push({ width, language, mode, publicLanguageAndDraft: 'PASS', clientValidationWithoutSubmission: 'PASS', confirmationLocaleWithoutReplay: 'PASS', mfaRateLimitAndCodeRetained: 'PASS' }); await context.close();
}
assert.equal(report.screens.length, configurations.length * 14); assert.equal(report.journeys.length, configurations.length); assert.equal(report.confirmations.length, configurations.length * 2); assert.equal(report.signIns.length, configurations.length); assert.equal(report.challenges.length, configurations.length);
assert.equal(report.screens.length, configurations.length * (landingAudit ? 1 : 14)); assert.equal(report.journeys.length, configurations.length); assert.equal(report.confirmations.length, landingAudit ? 0 : configurations.length * 2); assert.equal(report.signIns.length, landingAudit ? 0 : configurations.length); assert.equal(report.challenges.length, landingAudit ? 0 : configurations.length);
assert.deepEqual(report.errors, []); assert.deepEqual(report.unexpectedWrites, []); assert.deepEqual(report.unavailableReads, []); assert.ok(report.blockedExternal.every(origin => origin === 'https://fonts.googleapis.com'));
console.log(`PASS:${report.journeys.length} public journeys,${report.screens.length} scans/captures; synthetic confirmations and MFA only.`);
console.log(landingAudit ? `Audited ${report.screens.length} existing landing states. ${report.journeys.length} controlled navigation journeys passed. See recorded findings.` : `PASS:${report.journeys.length} public journeys,${report.screens.length} scans/captures; synthetic confirmations and MFA only.`);
} finally {
fs.writeFileSync(path.join(out, process.env.A3_PILOT ? 'PILOT.json' : 'BROWSER.json'), JSON.stringify({ ...report, limits: ['Every API response is intercepted; no real account, password, email delivery, verification, MFA or provider activation.', 'Synthetic confirmation tokens and challenge values cannot qualify server-side token, rate-limit or authentication enforcement.', 'No native/human, integrated security, release or production qualification.'] }, null, 2)); await browser.close();
}