67 lines
7.8 KiB
JavaScript
67 lines
7.8 KiB
JavaScript
// Built React, recorded-date fixtures only; every API/external request is intercepted.
|
|
import { chromium, expect } from '@playwright/test';
|
|
import AxeBuilder from '@axe-core/playwright';
|
|
import ts from 'typescript';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import assert from 'node:assert/strict';
|
|
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/evidence/calendar/browser'); fs.mkdirSync(out, { recursive: true });
|
|
const report = { screens: [], journeys: [], errors: [], unexpectedWrites: [], reads: [], blockedExternal: [] };
|
|
const now = new Date(), month = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
|
const browser = await chromium.launch();
|
|
try {
|
|
for (const width of (process.env.A3_PILOT ? [320] : [320, 390, 768, 1440])) for (const language of (process.env.A3_PILOT ? ['en'] : ['en', 'nb'])) for (const mode of (process.env.A3_PILOT ? ['light'] : ['light', 'dark'])) {
|
|
console.log(`Checking ${width}/${language}/${mode}`);
|
|
const t = (key, params = {}) => translations[language][key].replace(/\{(\w+)\}/g, (_, name) => String(params[name] ?? `{${name}}`));
|
|
const context = await browser.newContext({ viewport: { width, height: 1000 }, reducedMotion: 'reduce' }); const page = await context.newPage();
|
|
await page.addInitScript(({ language, mode }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', mode); }, { language, mode });
|
|
page.on('pageerror', error => report.errors.push(error.message));
|
|
const events = ['Primary teacher', 'Community nurse', 'Electrical engineer'].map((jobTitle, index) => ({ jobApplicationId: 42 + index, jobTitle, companyName: 'Synthetic employer', kind: index === 2 ? 'follow-up' : 'deadline', date: `${month}-${index === 2 ? '20' : '15'}`, recordedAt: `${month}-${index === 2 ? '20' : '15'}T00:00:00` }));
|
|
await page.route('**/*', route => {
|
|
const request = route.request(), url = new URL(request.url()), p = url.pathname;
|
|
if (!['localhost', '127.0.0.1'].includes(url.hostname)) { report.blockedExternal.push(url.origin); return route.abort(); }
|
|
if (!p.startsWith('/api/')) return route.continue();
|
|
if (request.method() !== 'GET') { report.unexpectedWrites.push(p); return route.abort(); }
|
|
const answer = data => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(data) });
|
|
if (p === '/api/calendar/agenda') {
|
|
const from = url.searchParams.get('from'), to = url.searchParams.get('to'); assert.match(from, /^\d{4}-\d{2}-01$/); assert.ok(Date.parse(to) - Date.parse(from) <= 31 * 86400000);
|
|
report.reads.push({ width, language, mode, from, to }); return answer(events.filter(item => item.date >= from && item.date <= to));
|
|
}
|
|
const fixtures = {
|
|
'/api/auth/config': { requireAuth: true }, '/api/auth/me': { id: 'a3-calendar-owner', sessionContext: 'a3-calendar-session', provider: 'local', firstName: 'Alex', displayName: 'Alex Sample', roles: [], uiLanguage: language === 'nb' ? 'nb-NO' : 'en', aiEnabled: false },
|
|
'/api/companies': [], '/api/notifications/unread-count': { count: 0 }, '/api/jobapplications/reminders': [],
|
|
'/api/jobapplications/42/workspace': { id: 42, revision: 7, jobTitle: 'Primary teacher', company: 'Synthetic employer', status: 'Saved', location: 'Oslo', deadline: events[0].recordedAt, tags: [], hasJobDescription: true, description: 'Recorded source advertisement.', cv: { variantId: null }, recentActivity: [], nextStep: null, checklistProgress: { total: 0, completed: 0, dismissed: 0, percent: 0 } }, '/api/jobapplications/42/status-suggestion': null,
|
|
};
|
|
return route.fulfill({ status: Object.hasOwn(fixtures, p) ? 200 : 404, contentType: 'application/json', body: JSON.stringify(fixtures[p] ?? {}) });
|
|
});
|
|
const scan = async state => {
|
|
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth + 1), false, state);
|
|
const axe = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa']).analyze(); report.screens.push({ width, language, mode, state, violations: axe.violations }); assert.deepEqual(axe.violations, []);
|
|
await page.evaluate(() => window.scrollTo(0, 0)); await page.screenshot({ path: path.join(out, `${state}-${language}-${mode}-${width}.png`), fullPage: true });
|
|
};
|
|
await page.goto('http://localhost:4338/reminders');
|
|
const calendar = page.getByRole('region', { name: t('calendarAgendaTitle'), exact: true }); await calendar.getByRole('link', { name: /Electrical engineer/ }).waitFor();
|
|
await expect(page.getByRole('combobox', { name: new RegExp(`^${t('calendarSelectedMonth')}`) })).toContainText(new Intl.DateTimeFormat(language === 'nb' ? 'nb-NO' : 'en-GB', { month: 'long', year: 'numeric', timeZone: 'UTC' }).format(new Date(`${month}-01T12:00:00Z`)));
|
|
await expect(page.getByRole('button', { name: t('calendarAgenda'), exact: true })).toHaveAttribute('aria-pressed', 'true');
|
|
assert.equal(await calendar.getByRole('link').count(), 3); assert.equal(await calendar.locator('time').first().getAttribute('datetime'), events[0].recordedAt); await scan('agenda');
|
|
await page.getByRole('button', { name: t('calendarMonth'), exact: true }).click();
|
|
const table = page.getByRole('table', { name: t('calendarMonth'), exact: true }); await table.waitFor();
|
|
const date = new Intl.DateTimeFormat(language === 'nb' ? 'nb-NO' : 'en-GB', { dateStyle: 'medium', timeZone: 'UTC' }).format(new Date(`${month}-15T12:00:00Z`));
|
|
await table.getByRole('button', { name: t('calendarDayEvents', { date, count: 2 }), exact: true }).press('Enter');
|
|
await expect(calendar.getByRole('link')).toHaveCount(2); await scan('month');
|
|
await page.getByRole('button', { name: t('calendarNextMonth'), exact: true }).click(); await page.getByText(t('calendarAgendaEmpty'), { exact: true }).waitFor();
|
|
await page.getByRole('button', { name: t('calendarPreviousMonth'), exact: true }).click(); await expect(calendar).toHaveAttribute('aria-busy', 'false');
|
|
await page.reload(); await page.getByRole('table', { name: t('calendarMonth'), exact: true }).waitFor();
|
|
await page.getByRole('button', { name: t('calendarAgenda'), exact: true }).click();
|
|
await calendar.getByRole('link', { name: /Primary teacher/ }).press('Enter'); await page.waitForURL(/\/jobs\/42/);
|
|
await page.getByRole('complementary').last().waitFor();
|
|
await expect(page.getByRole('heading', { name: 'Primary teacher', exact: true })).toBeVisible();
|
|
assert.equal((await page.locator('main').innerText()).includes('Invalid Date'), false); await scan('linked-application');
|
|
report.journeys.push({ width, language, mode, agendaMonthSelectionPersistedPreferenceBoundedMonthNavigationLinkedDeadline: 'PASS' }); await context.close();
|
|
}
|
|
assert.deepEqual(report.errors, []); assert.deepEqual(report.unexpectedWrites, []); assert.ok(report.blockedExternal.every(origin => origin === 'https://fonts.googleapis.com'));
|
|
console.log(`PASS:${report.journeys.length} calendar journeys,${report.screens.length} axe scans/captures,zero writes.`);
|
|
} finally { fs.writeFileSync(path.join(out, process.env.A3_PILOT ? 'PILOT.json' : 'BROWSER.json'), JSON.stringify({ ...report, limits: ['Controlled API fixtures, not a live backend/provider or reminder delivery.', 'Date-level recorded deadline/follow-up display; no fabricated interview appointments or source timezone reconstruction.', 'No native-device, physical rendering, final security or release qualification.'] }, null, 2)); await browser.close(); }
|