164 lines
12 KiB
JavaScript
164 lines
12 KiB
JavaScript
// Exported application and real MUI pickers with synthetic API fixtures only.
|
|
import { chromium, expect } from '@playwright/test';
|
|
import AxeBuilder from '@axe-core/playwright';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import ts from 'typescript';
|
|
|
|
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, process.env.A3_EVIDENCE_DIR ?? '../../docs/i18n/evidence/date-widgets');
|
|
const flatEdit = process.env.A3_FLAT_EDIT === '1';
|
|
fs.mkdirSync(out, { recursive: true });
|
|
const report = { journeys: [], captures: [], writes: [], errors: [], unavailableReads: [], blockedExternal: [] };
|
|
const browser = await chromium.launch();
|
|
try {
|
|
const zones = process.env.A3_PILOT ? ['America/Los_Angeles'] : ['America/Los_Angeles', 'Europe/Oslo', 'Pacific/Kiritimati'];
|
|
const screens = [{ language: 'en', width: 1440, touch: false, theme: 'light' }, { language: 'nb', width: 390, touch: true, theme: 'dark' }];
|
|
if (flatEdit) screens.push({ language: 'nb', width: 768, touch: true, theme: 'light' });
|
|
for (const zone of zones) for (const screen of screens) {
|
|
const { language, width, theme } = screen;
|
|
const mobile = screen.touch;
|
|
const copy = translations[language];
|
|
const name = `${zone.replaceAll('/', '-')}-${language}${flatEdit ? `-${width}-${theme}` : ''}`;
|
|
console.log(`Checking ${name}`);
|
|
const context = await browser.newContext({ timezoneId: zone, viewport: { width, height: 1000 }, hasTouch: mobile, isMobile: mobile, reducedMotion: 'reduce' });
|
|
const page = await context.newPage();
|
|
page.on('pageerror', error => report.errors.push(error.message));
|
|
await page.clock.setFixedTime(new Date('2026-11-23T00:30:00Z'));
|
|
await page.addInitScript(({ language, theme }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', theme); }, { language, theme });
|
|
const job = { id: 42, version: 7, companyId: 1, company: { id: 1, name: 'Synthetic school' }, jobTitle: 'Teacher', status: 'Applied', dateApplied: '2026-11-23', deadline: '2026-11-23', followUpAt: '2026-11-23', responseReceived: true, responseDate: '2026-11-23', isDeleted: false };
|
|
const writes = [];
|
|
let detailReads = 0;
|
|
await page.route('**/*', route => {
|
|
const request = route.request(), url = new URL(request.url());
|
|
if (url.origin !== 'http://localhost:4338' && !(url.origin === 'http://localhost:5202' && url.pathname.startsWith('/api/'))) { report.blockedExternal.push(url.origin); return route.abort(); }
|
|
if (!url.pathname.startsWith('/api/')) return route.continue();
|
|
if (request.method() !== 'GET') {
|
|
assert.ok((request.method() === 'PUT' && url.pathname === '/api/jobapplications/42') || (request.method() === 'POST' && url.pathname === '/api/jobapplications'), `Unexpected mutation ${request.method()} ${url.pathname}`);
|
|
const item = { zone, language, method: request.method(), body: request.postDataJSON() };
|
|
writes.push(item); report.writes.push(item);
|
|
return route.fulfill({ json: request.method() === 'POST' ? { id: 43 } : {} });
|
|
}
|
|
if (url.pathname === '/api/jobapplications/42') detailReads++;
|
|
const fixtures = {
|
|
'/api/auth/config': { requireAuth: true },
|
|
'/api/auth/me': { id: 'date-owner', sessionContext: 'date-session', displayName: 'Synthetic teacher', roles: [], uiLanguage: language === 'nb' ? 'nb-NO' : 'en' },
|
|
'/api/companies': [job.company],
|
|
'/api/jobapplications': { items: [job], total: 1, page: 1, pageSize: 15 },
|
|
'/api/jobapplications/42': job,
|
|
'/api/jobapplications/duplicate-check': { hasDuplicates: false, matches: [] },
|
|
'/api/jobapplications/reminders': [],
|
|
'/api/notifications/unread-count': { count: 0 },
|
|
};
|
|
if (!Object.hasOwn(fixtures, url.pathname)) report.unavailableReads.push(url.pathname);
|
|
return route.fulfill({ status: Object.hasOwn(fixtures, url.pathname) ? 200 : 404, json: fixtures[url.pathname] ?? {} });
|
|
});
|
|
const dayName = language === 'nb' ? 'Dag' : 'Day';
|
|
const assertDay = async (group, day, lang = language) => {
|
|
await expect(group.getByRole('spinbutton', { name: lang === 'nb' ? 'Dag' : 'Day', exact: true })).toHaveText(String(day));
|
|
await expect(group.getByRole('spinbutton', { name: lang === 'nb' ? 'År' : 'Year', exact: true })).toHaveText('2026');
|
|
};
|
|
const choose24 = async (group, reviewCalendar = false) => {
|
|
await group.getByRole('button').click();
|
|
const calendar = page.getByRole('dialog').filter({ has: page.getByRole('grid') });
|
|
await expect(calendar.getByRole('button', { name: language === 'nb' ? 'Neste måned' : 'Next month', exact: true })).toBeVisible();
|
|
if (reviewCalendar) await capture('calendar');
|
|
await calendar.getByRole('gridcell', { name: '24', exact: true }).click();
|
|
if (mobile) await calendar.getByRole('button', { name: 'OK', exact: true }).click();
|
|
await expect(page.getByRole('grid')).toHaveCount(0);
|
|
await assertDay(group, 24);
|
|
};
|
|
const capture = async state => {
|
|
const scan = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21aa']).analyze();
|
|
report.captures.push({ zone, language, state, violations: scan.violations });
|
|
assert.deepEqual(scan.violations, []);
|
|
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1));
|
|
await page.screenshot({ path: path.join(out, `${state}-${name}.png`) });
|
|
};
|
|
await page.goto('http://localhost:4338/jobs');
|
|
await page.getByRole('button', { name: `${copy.jobTableActions}: Teacher`, exact: true }).click();
|
|
await page.getByRole('menuitem', { name: copy.jobTableEdit, exact: true }).click();
|
|
const initialEdit = page.getByRole('dialog', { name: copy.editJobTitle, exact: true });
|
|
await expect(initialEdit).toBeVisible();
|
|
const titleId = await initialEdit.getAttribute('aria-labelledby');
|
|
assert.ok(titleId);
|
|
const edit = page.locator(`[role="dialog"][aria-labelledby="${titleId}"]`);
|
|
const assertSharedLabels = async lang => {
|
|
await expect(edit.locator('.MuiAutocomplete-popupIndicator')).toHaveAttribute('aria-label', lang === 'nb' ? 'Åpne' : 'Open');
|
|
await expect(edit.locator('.MuiAutocomplete-clearIndicator')).toHaveAttribute('aria-label', lang === 'nb' ? 'Tøm' : 'Clear');
|
|
await expect(page.locator('html')).toHaveAttribute('data-color-scheme', theme);
|
|
await expect(edit.getByRole('combobox', { name: translations[lang].company, exact: true })).toHaveValue('Synthetic school');
|
|
};
|
|
await assertSharedLabels(language);
|
|
if (flatEdit) {
|
|
await expect(edit.locator('.MuiDialogContent-root .MuiPaper-outlined')).toHaveCount(0);
|
|
for (const label of [copy.editJobApplicationDetails, copy.editJobStatusUpdate, copy.editJobRoleDetails, copy.editJobAttachmentsChecklist]) {
|
|
await expect(edit.getByRole('heading', { name: label, exact: true })).toBeVisible();
|
|
}
|
|
}
|
|
const deadline = edit.getByRole('group', { name: copy.editJobDeadline, exact: true });
|
|
await assertDay(deadline, 23);
|
|
await edit.getByRole('textbox', { name: copy.editJobJobTitle, exact: true }).fill('Retain teacher draft');
|
|
const reads = detailReads;
|
|
await page.evaluate(value => { localStorage.setItem('uiLanguage', value); window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: value })); }, language === 'nb' ? 'en' : 'nb');
|
|
const otherCopy = translations[language === 'nb' ? 'en' : 'nb'];
|
|
await assertDay(edit.getByRole('group', { name: otherCopy.editJobDeadline, exact: true }), 23, language === 'nb' ? 'en' : 'nb');
|
|
await assertSharedLabels(language === 'nb' ? 'en' : 'nb');
|
|
await page.evaluate(value => { localStorage.setItem('uiLanguage', value); window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: value })); }, language);
|
|
await expect(edit.getByRole('textbox', { name: copy.editJobJobTitle, exact: true })).toHaveValue('Retain teacher draft');
|
|
await assertSharedLabels(language);
|
|
assert.equal(detailReads, reads); assert.equal(writes.length, 0);
|
|
for (const label of [copy.editJobAppliedOn, copy.editJobStatusChangedOn, copy.editJobReplyReceivedOn, copy.editJobFollowUpOn, copy.editJobDeadline]) await choose24(edit.getByRole('group', { name: label, exact: true }), label === copy.editJobDeadline);
|
|
if (flatEdit) await edit.locator('.MuiDialogContent-root').evaluate(node => { node.scrollTop = 0; });
|
|
await capture('edit');
|
|
await edit.getByRole('button', { name: copy.save, exact: true }).click();
|
|
await expect(edit).toBeHidden();
|
|
assert.equal(writes.length, 1);
|
|
assert.deepEqual({ expectedVersion: writes[0].body.expectedVersion, dateApplied: writes[0].body.dateApplied, deadline: writes[0].body.deadline, followUpAt: writes[0].body.followUpAt, responseDate: writes[0].body.responseDate }, { expectedVersion: 7, dateApplied: '2026-11-24', deadline: '2026-11-24', followUpAt: '2026-11-24', responseDate: '2026-11-24' });
|
|
await page.getByRole('button', { name: copy.addJob, exact: true }).click();
|
|
const initialAdd = page.getByRole('dialog');
|
|
await expect(initialAdd).toBeVisible();
|
|
const addTitleId = await initialAdd.getAttribute('aria-labelledby');
|
|
assert.ok(addTitleId);
|
|
const add = page.locator(`[role="dialog"][aria-labelledby="${addTitleId}"]`);
|
|
await add.getByRole('button', { name: copy.addJobModalEnterManually, exact: true }).click();
|
|
const today = await page.evaluate(() => new Date().getDate());
|
|
await expect(add.getByRole('group', { name: copy.addJobModalDateApplied, exact: true }).getByRole('spinbutton', { name: dayName, exact: true })).toHaveText(String(today));
|
|
await add.getByRole('combobox', { name: copy.company, exact: true }).fill('Synthetic');
|
|
await page.getByRole('option', { name: 'Synthetic school', exact: true }).click();
|
|
await add.getByRole('textbox', { name: copy.addJobModalJobTitle, exact: true }).fill('Teacher application');
|
|
await choose24(add.getByRole('group', { name: copy.addJobModalDateApplied, exact: true }));
|
|
const addDeadline = add.getByRole('group', { name: copy.addJobModalDeadline, exact: true });
|
|
// Empty deadline opens the current fixed month, November2026.
|
|
await choose24(addDeadline);
|
|
await capture('add');
|
|
await add.getByRole('button', { name: copy.continue, exact: true }).click();
|
|
for (let step = 0; step < 3; step++) await add.getByRole('button', { name: copy.skipAndContinue, exact: true }).click();
|
|
await add.getByRole('button', { name: copy.createJob, exact: true }).click();
|
|
await expect(add).toBeHidden();
|
|
assert.equal(writes.length, 2);
|
|
assert.equal(writes[1].body.dateApplied, '2026-11-24'); assert.equal(writes[1].body.deadline, '2026-11-24');
|
|
job.status = 'Saved'; job.dateApplied = null;
|
|
await page.getByRole('button', { name: `${copy.jobTableActions}: Teacher`, exact: true }).click();
|
|
await page.getByRole('menuitem', { name: copy.jobTableEdit, exact: true }).click();
|
|
const unapplied = page.getByRole('dialog', { name: copy.editJobTitle, exact: true });
|
|
await expect(unapplied.getByRole('group', { name: copy.editJobAppliedOn, exact: true }).locator('input')).toHaveValue('');
|
|
await unapplied.getByRole('textbox', { name: copy.editJobJobTitle, exact: true }).fill('Retain unapplied teacher');
|
|
await capture('unapplied');
|
|
await unapplied.getByRole('button', { name: copy.save, exact: true }).click();
|
|
await expect(unapplied).toBeHidden();
|
|
assert.equal(writes.length, 3);
|
|
assert.equal(writes[2].body.dateApplied, null); assert.equal(writes[2].body.status, 'Saved');
|
|
assert.equal(writes[2].body.expectedVersion, 7); assert.equal(writes[2].body.jobTitle, 'Retain unapplied teacher');
|
|
report.journeys.push({ zone, language, width, touch: mobile, theme, sevenCalendarSelections: 'PASS', localeDraftRetention: 'PASS', localToday: today, fixtureSaveCreate: 'PASS' });
|
|
await context.close();
|
|
}
|
|
assert.equal(report.journeys.length, zones.length * screens.length); assert.deepEqual(report.errors, []);
|
|
console.log(`PASS ${report.journeys.length} real-widget journeys, ${report.captures.length} axe captures, ${report.writes.length} intercepted fixture writes`);
|
|
} finally {
|
|
fs.writeFileSync(path.join(out, process.env.A3_PILOT ? 'PILOT.json' : 'BROWSER.json'), JSON.stringify(report, null, 2));
|
|
await browser.close();
|
|
}
|