test(calendar): qualify connected follow-up and agenda refresh

This commit is contained in:
cesnimda
2026-11-09 22:12:27 +01:00
parent c0ef4171d5
commit e585561a02
112 changed files with 2770 additions and 12 deletions
@@ -4,30 +4,41 @@ 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 visualMatrix = Boolean(process.env.A3_VISUAL_MATRIX);
const flatWorkspace = Boolean(process.env.A3_FLAT_WORKSPACE);
const matchingCountOnly = Boolean(process.env.A3_MATCHING_COUNT);
const followupJourney = Boolean(process.env.A3_FOLLOWUP_JOURNEY);
assert.ok(!followupJourney || !(matchingCountOnly || flatWorkspace || visualMatrix), 'Follow-up journey is a representative independent scope.');
assert.ok(!matchingCountOnly || !flatWorkspace, 'Count presentation does not repeat workspace qualification.');
assert.ok(!flatWorkspace || visualMatrix, 'Flat workspace qualification uses the visual matrix.');
const widths = visualMatrix || matchingCountOnly ? [320, 360, 390, 430, 768, 1024, 1280, 1440, 1920] : [320, 390, 768, 1440];
const configurations = process.env.A3_PILOT ? [[320, 'en', 'light']] : widths.flatMap(width => ['en', 'nb'].flatMap(language => ['light', 'dark'].map(mode => [width, language, mode])));
const out = path.resolve(import.meta.dirname, matchingCountOnly ? '../../docs/ui-modernisation/visual-correction/evidence/matching-count' : flatWorkspace ? '../../docs/ui-modernisation/visual-correction/evidence/applications-flat' : visualMatrix ? '../../docs/ui-modernisation/visual-correction/evidence/applications' : '../../docs/ui-modernisation/evidence/applications/browser');
const widths = followupJourney ? [390, 1440] : visualMatrix || matchingCountOnly ? [320, 360, 390, 430, 768, 1024, 1280, 1440, 1920] : [320, 390, 768, 1440];
const configurations = process.env.A3_PILOT ? [[followupJourney ? 390 : 320, 'en', 'light']] : widths.flatMap(width => ['en', 'nb'].flatMap(language => ['light', 'dark'].map(mode => [width, language, mode])));
const out = path.resolve(import.meta.dirname, followupJourney ? '../../docs/ui-modernisation/visual-correction/evidence/followup-journey' : matchingCountOnly ? '../../docs/ui-modernisation/visual-correction/evidence/matching-count' : flatWorkspace ? '../../docs/ui-modernisation/visual-correction/evidence/applications-flat' : visualMatrix ? '../../docs/ui-modernisation/visual-correction/evidence/applications' : '../../docs/ui-modernisation/evidence/applications/browser');
fs.mkdirSync(out, { recursive: true });
const report = { screens: [], journeys: [], aliases: [], focusChecks: [], errors: [], unexpectedWrites: [], mockedWrites: [], unavailableReads: [], blockedExternal: [] };
const browser = await chromium.launch();
try {
for (const [width, language, mode] of configurations) {
console.log(`Checking ${width}/${language}/${mode}`);
const t = (key, params = {}) => { assert.ok(translations[language][key], key); return 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(); page.on('pageerror', error => report.errors.push(error.message));
await page.addInitScript(({ language, mode }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', mode); }, { language, mode });
const jobs = ['Community nurse', 'Primary teacher', 'Electrical engineer'].map((jobTitle, index) => ({ id: 42 + index, version: 7, jobTitle, company: { id: 1, name: 'Fjord Care' }, companyId: 1, status: 'Saved', location: 'Oslo', countryCode: 'NO', source: 'nav', salary: null, dateApplied: null, savedAt: '2026-10-01T09:00:00Z', deadline: '2026-11-12', followUpAt: null, tags: 'Care coordination', description: 'Recorded source advertisement.', isDeleted: false, needsFollowUp: false, workflowSignal: null }));
if (matchingCountOnly) jobs.splice(1);
if (matchingCountOnly || followupJourney) jobs.splice(1);
let expectedFollowUp = null;
await page.route('**/*', route => {
const req = route.request(), url = new URL(req.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();
const answer = data => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(data) });
if (followupJourney && req.method() === 'PATCH' && url.pathname === '/api/jobapplications/42/followup') {
assert.ok(expectedFollowUp); assert.deepEqual(req.postDataJSON(), { followUpAt: expectedFollowUp }); assert.equal(jobs[0].status, 'Waiting'); jobs[0].followUpAt = expectedFollowUp;
report.mockedWrites.push({ width, language, mode, action: 'scheduleFollowUp', recordedDate: expectedFollowUp }); return answer({});
}
if (!matchingCountOnly && req.method() === 'PATCH' && url.pathname === '/api/jobapplications/42/status') {
const { status } = req.postDataJSON(); assert.equal(status, 'Waiting'); jobs[0].status = status;
report.mockedWrites.push({ width, language, mode, status }); return answer({});
@@ -40,7 +51,8 @@ try {
'/api/companies': [jobs[0].company],
'/api/jobapplications': { items: matching, total: matching.length, page: 1, pageSize: 15 },
'/api/jobapplications/board': matching,
'/api/jobapplications/reminders': [],
'/api/jobapplications/reminders': followupJourney && jobs[0].status === 'Waiting' ? [{ ...jobs[0], needsFollowUp: true }] : [],
...(followupJourney ? { '/api/calendar/agenda': jobs[0].followUpAt && jobs[0].followUpAt >= url.searchParams.get('from') && jobs[0].followUpAt <= url.searchParams.get('to') ? [{ jobApplicationId: 42, jobTitle: jobs[0].jobTitle, companyName: jobs[0].company.name, kind: 'follow-up', date: jobs[0].followUpAt, recordedAt: jobs[0].followUpAt }] : [], '/api/calendar/status': { google: { connected: false, writable: false }, microsoft: { connected: false, writable: false } } } : {}),
'/api/notifications/unread-count': { count: 0 },
'/api/jobapplications/42/workspace': { ...jobs[0], revision: 7, company: jobs[0].company.name, stageGroup: 'Applied', stageOrder: 1, jobUrl: null, translatedDescription: null, descriptionLanguage: 'en', tags: ['Care coordination'], notes: 'Ask about shift patterns.', applicationAnswerDraft: '', recruiterMessageDraft: '', hasJobDescription: true, cv: { variantId: null, variantName: null, themeId: null, hasTailoredCvText: false, updatedAtUtc: null }, hasCoverLetter: false, documentCount: 0, hasPortfolio: false, aiInteractionCount: 0, lastAiAtUtc: null, recentActivity: [], nextStep: null, checklistProgress: { total: 0, completed: 0, dismissed: 0, percent: 0 } },
'/api/jobapplications/42/status-suggestion': null,
@@ -49,16 +61,32 @@ try {
report.unavailableReads.push(url.pathname);
return route.fulfill({ status: 404, contentType: 'application/json', body: '{}' });
});
const scan = async state => {
const scan = async (state, target) => {
if (target) await target.evaluate(el => el.scrollIntoView({ block: 'center', behavior: 'instant' }));
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth + 1), false);
const result = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa']).analyze();
report.screens.push({ width, language, mode, state, violations: result.violations }); assert.deepEqual(result.violations, []);
await page.evaluate(() => window.scrollTo(0, 0));
await page.screenshot({ path: path.join(out, `${state}-${language}-${mode}-${width}.png`), fullPage: true });
if (!target) await page.evaluate(() => window.scrollTo(0, 0));
await page.screenshot({ path: path.join(out, `${state}-${language}-${mode}-${width}.png`), fullPage: !target && (width >= 768 || !followupJourney) });
};
await page.goto('http://localhost:4338/jobs');
const search = page.getByRole('textbox', { name: language === 'en' ? 'Search' : 'Søk', exact: true }); await search.waitFor();
await page.locator('[data-job-row-id="42"], [data-job-card-id="42"]').first().waitFor();
if (followupJourney) {
await scan('table'); await page.getByRole('link', { name: t('jobsBoardView'), exact: true }).click();
await page.getByRole('button', { name: `${t('jobTableQuickStatus')}: Community nurse`, exact: true }).click(); await page.getByRole('menuitem', { name: t('jobTableSetStatus', { status: t('statusWaiting') }), exact: true }).click();
await expect.poll(() => jobs[0].status).toBe('Waiting'); await scan('status');
let nav = page.getByRole('button', { name: t('reminders'), exact: true });
if (!(await nav.isVisible())) { await page.getByRole('button', { name: t('navigationMore'), exact: true }).click(); nav = page.getByRole('button', { name: t('reminders'), exact: true }); }
await nav.click(); await expect.poll(() => new URL(page.url()).pathname).toBe('/reminders');
const reminder = page.getByRole('article', { name: 'Community nurse', exact: true }); await expect(reminder).toBeVisible(); await scan('reminder', reminder);
expectedFollowUp = await page.evaluate(() => new Date(Date.now() + 3 * 86400000).toISOString().slice(0, 10));
await reminder.getByRole('button', { name: t('remindersSnoozeForJob', { days: 3, job: 'Community nurse' }), exact: true }).click(); await expect.poll(() => jobs[0].followUpAt).toBe(expectedFollowUp);
const date = await page.evaluate(({ value, language }) => new Date(value).toLocaleDateString(language === 'nb' ? 'nb-NO' : 'en'), { value: expectedFollowUp, language }); await expect(reminder).toContainText(t('remindersFollowUpDate', { date })); await scan('scheduled', reminder);
const agenda = page.getByRole('region', { name: t('calendarAgendaTitle'), exact: true }); await expect(agenda.getByRole('link', { name: /Community nurse/ })).toBeVisible(); await scan('agenda-refreshed', agenda);
await reminder.getByRole('button').first().click(); await expect.poll(() => new URL(page.url()).pathname).toBe('/jobs/42'); const aside = page.getByRole('complementary', { name: t('workspaceContext'), exact: true }); await expect(aside).toContainText(date); await scan('linked-context', aside);
report.journeys.push({ width, language, mode, statusToFollowUpThroughNavigation: 'PASS', recordedDateRetainedInLinkedContext: 'PASS', providersAndBackendScheduling: 'NOT RUN' }); await context.close(); continue;
}
if (matchingCountOnly) {
const label = language === 'en' ? '1 matching job' : '1 jobb samsvarer';
await expect(page.getByText(label, { exact: true })).toBeVisible(); await scan('table');
@@ -131,10 +159,10 @@ try {
}
await context.close();
}
assert.equal(report.screens.length, configurations.length * (matchingCountOnly ? 2 : flatWorkspace ? 6 : visualMatrix ? 4 : 3)); assert.equal(report.journeys.length, configurations.length); assert.equal(report.mockedWrites.length, matchingCountOnly ? 0 : configurations.length); assert.equal(report.aliases.length, visualMatrix && !matchingCountOnly ? 3 : 0);
assert.equal(report.screens.length, configurations.length * (followupJourney ? 6 : matchingCountOnly ? 2 : flatWorkspace ? 6 : visualMatrix ? 4 : 3)); assert.equal(report.journeys.length, configurations.length); assert.equal(report.mockedWrites.length, matchingCountOnly ? 0 : configurations.length * (followupJourney ? 2 : 1)); assert.equal(report.aliases.length, visualMatrix && !matchingCountOnly ? 3 : 0);
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} controlled table/board/detail journeys,${report.screens.length} axe scans/captures;${report.mockedWrites.length} mocked status writes.`);
console.log(`PASS:${report.journeys.length} controlled journeys,${report.screens.length} axe scans/captures;${report.mockedWrites.length} mocked mutation requests.`);
} finally {
fs.writeFileSync(path.join(out, process.env.A3_PILOT ? 'PILOT.json' : 'BROWSER.json'), JSON.stringify({ ...report, limits: ['controlled fixtures, not real backend integration', 'secondary service reads may return honest unavailable states', 'no native device, provider, model, security or release qualification'] }, null, 2));
await browser.close();