test(ui): qualify profile selection to CV download workflow
This commit is contained in:
@@ -9,13 +9,15 @@ const compiled = ts.transpileModule(fs.readFileSync(new URL('../src/i18n/transla
|
||||
const { translations } = await import(`data:text/javascript;base64,${Buffer.from(compiled).toString('base64')}`);
|
||||
const visualMatrix = process.env.A3_VISUAL_MATRIX === '1';
|
||||
const evidenceOnly = process.env.A3_EVIDENCE_REVIEW === '1';
|
||||
const cvJourney = process.env.A3_CV_JOURNEY === '1';
|
||||
assert.ok(!cvJourney || !(evidenceOnly || visualMatrix || process.env.A3_BOUNDARY), 'CV journey is a separate representative workflow scope.');
|
||||
assert.ok(!evidenceOnly || !process.env.A3_BOUNDARY, 'Evidence review does not repeat editor boundary qualification.');
|
||||
const out = path.resolve(import.meta.dirname, evidenceOnly ? '../../docs/ui-modernisation/visual-correction/evidence/career-evidence' : visualMatrix ? '../../docs/ui-modernisation/visual-correction/evidence/career' : '../../docs/ui-modernisation/evidence/career/browser');
|
||||
const out = path.resolve(import.meta.dirname, cvJourney ? '../../docs/ui-modernisation/visual-correction/evidence/cv-journey' : evidenceOnly ? '../../docs/ui-modernisation/visual-correction/evidence/career-evidence' : visualMatrix ? '../../docs/ui-modernisation/visual-correction/evidence/career' : '../../docs/ui-modernisation/evidence/career/browser');
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
const report = { screens: [], journeys: [], errors: [], unexpectedWrites: [], mockedWrites: [], renderReads: 0, unavailableReads: [], blockedExternal: [] };
|
||||
const report = { screens: [], journeys: [], errors: [], unexpectedWrites: [], mockedWrites: [], exportRequests: [], downloads: [], renderReads: 0, unavailableReads: [], blockedExternal: [] };
|
||||
const browser = await chromium.launch();
|
||||
try {
|
||||
const configurations = process.env.A3_BOUNDARY ? [320, 390, 768, 1000, 1001, 1440].map(width => [width, 'en', 'light']) : process.env.A3_PILOT ? (evidenceOnly ? [[320, 'en', 'light'], [1440, 'en', 'light']] : [[320, 'en', 'light']]) : (visualMatrix || evidenceOnly ? [320, 360, 390, 430, 768, 1024, 1280, 1440, 1920] : [320, 390, 768, 1440]).flatMap(width => ['en', 'nb'].flatMap(language => ['light', 'dark'].map(mode => [width, language, mode])));
|
||||
const configurations = process.env.A3_BOUNDARY ? [320, 390, 768, 1000, 1001, 1440].map(width => [width, 'en', 'light']) : process.env.A3_PILOT ? (cvJourney ? [[390, 'en', 'light'], [1440, 'en', 'light']] : evidenceOnly ? [[320, 'en', 'light'], [1440, 'en', 'light']] : [[320, 'en', 'light']]) : (cvJourney ? [390, 1440] : visualMatrix || evidenceOnly ? [320, 360, 390, 430, 768, 1024, 1280, 1440, 1920] : [320, 390, 768, 1440]).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 t = key => { assert.ok(translations[language][key], key); return translations[language][key]; };
|
||||
@@ -29,19 +31,29 @@ try {
|
||||
jobs: [], education: [], interests: [], awards: [], publications: [], organisations: [], references: [], otherSections: [], sections: [],
|
||||
}, completeness: { percent: 70, missing: ['Experience', 'Education'], sections: ['personal', 'summary', 'experience', 'education', 'skills', 'projects', 'languages'].map(key => ({ key, label: key, complete: !['experience', 'education'].includes(key), count: 1 })) } };
|
||||
let variant = { id: 3, name: 'Teacher CV', version: 4, settings: { themeId: 'modern', showPhoto: false, showPageNumbers: false, showIcons: true, sections: [], overrides: {}, customSections: [] }, isPublic: false, publicSlug: 'synthetic', jobApplicationId: null, updatedAtUtc: '2026-11-03T10:00:00Z' };
|
||||
let created = !cvJourney;
|
||||
const exportBytes = Buffer.from('Synthetic download transport fixture. Not a rendered PDF or CV.');
|
||||
let letter = { revision: 7, text: 'Dear team\n\nRecorded teaching experience.', currentVersion: 2, versions: [{ version: 2, source: 'manual', length: 45, createdAtUtc: '2026-11-03T10:00:00Z', isCurrent: true }], aiSuggestionCount: 0 };
|
||||
const theme = { id: 'modern', name: 'Modern', category: 'Professional', description: 'Synthetic template', layout: 'header-band', accent: '#3157d5', photoShape: 'none', supportsIcons: false, atsFriendly: true, requiresPro: false, available: true, swatches: ['#3157d5'] };
|
||||
let evidenceState = 'populated';
|
||||
const evidenceItems = [{ id: 1, category: 'achievement', title: 'Recorded nurse mentoring', statement: 'Mentored new colleagues in a synthetic training scenario.', tags: ['Training', 'Communication'], sourceReference: 'Synthetic review' }, { id: 2, category: 'project', title: 'Recorded electrician project', statement: 'Documented a synthetic installation handover.', tags: ['Safety'], sourceReference: 'Synthetic project notes' }];
|
||||
await page.route('**/*', route => {
|
||||
const req = route.request(), url = new URL(req.url()), p = url.pathname;
|
||||
if (evidenceOnly && url.origin !== 'http://localhost:4338' && !(url.origin === 'http://localhost:5202' && p.startsWith('/api/'))) { report.blockedExternal.push(url.origin); return route.abort(); }
|
||||
if ((evidenceOnly || cvJourney) && url.origin !== 'http://localhost:4338' && !(url.origin === 'http://localhost:5202' && p.startsWith('/api/'))) { report.blockedExternal.push(url.origin); return route.abort(); }
|
||||
if (!['localhost', '127.0.0.1'].includes(url.hostname)) { report.blockedExternal.push(url.origin); return route.abort(); }
|
||||
if (!p.startsWith('/api/')) return route.continue();
|
||||
const answer = data => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(data) });
|
||||
const note = action => report.mockedWrites.push({ width, language, mode, action });
|
||||
if (evidenceOnly && req.method() !== 'GET') { report.unexpectedWrites.push(p); return route.abort(); }
|
||||
if (evidenceOnly && p === '/api/career/evidence') return evidenceState === 'error' ? route.fulfill({ status: 503, contentType: 'application/json', body: '{}' }) : answer(evidenceState === 'empty' ? [] : evidenceItems);
|
||||
if (cvJourney && req.method() === 'POST' && p === '/api/cv/variants') {
|
||||
const body = req.postDataJSON(); assert.equal(created, false); assert.equal(body.name, 'Teacher career CV'); assert.equal(body.settings.includeProfileContact, true); assert.equal(body.settings.sectionsExplicit, true); assert.deepEqual(body.settings.sections, [{ key: 'summary' }]);
|
||||
variant = { ...variant, name: body.name, settings: body.settings, version: 1 }; created = true; note('createVariantFromProfile'); return answer(variant);
|
||||
}
|
||||
if (cvJourney && req.method() === 'POST' && p === '/api/cv/variants/3/export-pdf') {
|
||||
assert.equal(created, true); assert.equal(variant.version, 2); assert.equal(url.searchParams.get('expectedVersion'), String(variant.version)); assert.deepEqual(req.postDataJSON(), {});
|
||||
report.exportRequests.push({ width, language, mode, expectedVersion: variant.version }); return route.fulfill({ status: 200, contentType: 'application/octet-stream', body: exportBytes });
|
||||
}
|
||||
if (req.method() === 'PUT' && p === '/api/career/profile') {
|
||||
const body = req.postDataJSON(); assert.equal(body.expectedVersion, career.revision); assert.equal(body.cvText, career.cvText);
|
||||
assert.equal(body.profile.projects[0].id, 'project-one'); assert.equal(body.profile.projects[0].name, 'Updated community project');
|
||||
@@ -57,6 +69,7 @@ try {
|
||||
letter = { ...letter, text: body.text, revision: letter.revision + 1, currentVersion: 3 }; note('manualLetterSave'); return answer(letter);
|
||||
}
|
||||
if (req.method() === 'POST' && p === '/api/cv/preview') {
|
||||
if (cvJourney) { assert.equal(req.postDataJSON().settings.includeProfileContact, true); assert.deepEqual(req.postDataJSON().settings.sections, [{ key: 'summary' }]); }
|
||||
report.renderReads++; return answer({ themeId: 'modern', html: '<!doctype html><html lang="en"><head><title>Controlled CV preview</title></head><body><main><h1>Alex Sample</h1><p>Recorded teaching experience</p></main></body></html>', suggestedFileName: 'teacher.pdf' });
|
||||
}
|
||||
if (req.method() !== 'GET') { report.unexpectedWrites.push(p); return route.abort(); }
|
||||
@@ -66,7 +79,7 @@ try {
|
||||
'/api/career/profile': career, '/api/career/profile/versions': [], '/api/career/evidence': [],
|
||||
'/api/profile-cv/runs': [{ id: 41, trigger: 'upload', status: 'pending_review', startedAtUtc: '2026-11-03T10:00:00Z', parserVersion: 'offline-fixture', normalizerVersion: 'offline-fixture', llmPromptVersion: 'offline-fixture' }],
|
||||
'/api/profile-cv/runs/41/diff': { runId: 41, status: 'pending_review', revision: career.revision, diff: { totalAdded: 1, totalUpdated: 1, totalLowConfidence: 1, hasChanges: true, categories: [{ category: 'Languages', unchangedCount: 0, lowConfidenceCount: 1, added: [{ id: 'Languages|french', label: 'French', confidence: 'Low', fieldChanges: [] }], updated: [{ id: 'Languages|norwegian', label: 'Norwegian', confidence: 'Medium', fieldChanges: [{ field: 'Level', oldValue: 'A2', newValue: 'B1' }] }] }] } },
|
||||
'/api/cv/variants': [{ ...variant, themeId: 'modern', language: 'en' }], '/api/cv/variants/3': variant, '/api/cv/themes': [theme], '/api/cv/variants/3/versions': [],
|
||||
'/api/cv/variants': created ? [{ ...variant, themeId: 'modern', language: 'en' }] : [], '/api/cv/variants/3': variant, '/api/cv/themes': [theme], '/api/cv/variants/3/versions': [],
|
||||
'/api/cv/outline': { fullName: 'Alex Sample', headline: 'Teacher', sections: [{ key: 'summary', title: 'Professional Summary', kind: 'bullets', bullets: ['Recorded summary'], tags: [], entries: [] }] },
|
||||
'/api/jobapplications/42/workspace': { id: 42, revision: letter.revision, jobTitle: 'Primary teacher', company: 'Synthetic school', status: 'Saved', stageGroup: 'Applied', stageOrder: 1, location: 'Oslo', countryCode: 'NO', jobUrl: null, source: 'nav', description: 'Recorded source advertisement.', descriptionLanguage: 'en', tags: [], notes: '', applicationAnswerDraft: '', recruiterMessageDraft: '', hasJobDescription: true, cv: { variantId: 3, variantName: variant.name, themeId: 'modern', hasTailoredCvText: false }, hasCoverLetter: true, documentCount: 0, hasPortfolio: false, aiInteractionCount: 0, recentActivity: [], nextStep: null, checklistProgress: { total: 0, completed: 0, dismissed: 0, percent: 0 } },
|
||||
'/api/jobapplications/42/cover-letter': letter, '/api/jobapplications/42/status-suggestion': null,
|
||||
@@ -86,6 +99,21 @@ try {
|
||||
if (width >= 600) await page.getByRole('tab', { name: t(key), exact: true }).click();
|
||||
else { await page.getByRole('combobox', { name: t('careerWorkspaceNavigation'), exact: true }).click(); await page.getByRole('option', { name: t(key), exact: true }).click(); }
|
||||
};
|
||||
if (cvJourney) {
|
||||
await page.goto('http://localhost:4338/career/builder'); await expect(page.getByRole('button', { name: t('cvDashboardNewCv'), exact: true })).toBeEnabled(); await scan('library');
|
||||
await page.getByRole('button', { name: t('cvDashboardNewCv'), exact: true }).click();
|
||||
const dialog = page.getByRole('dialog', { name: t('cvDashboardCreateTitle'), exact: true });
|
||||
await dialog.getByRole('textbox', { name: new RegExp(t('cvEditorName')) }).fill('Teacher career CV'); await dialog.getByRole('radio', { name: t('cvDashboardFromProfile'), exact: true }).check();
|
||||
await dialog.getByRole('checkbox', { name: t('cvEditorIncludeProfileContact'), exact: true }).check(); await dialog.getByRole('checkbox', { name: 'Professional Summary', exact: true }).check(); await scan('create-from-profile', dialog);
|
||||
await dialog.getByRole('button', { name: t('cvDashboardCreateCv'), exact: true }).click(); await expect.poll(() => new URL(page.url()).pathname).toBe('/career/builder/3');
|
||||
await expect(dialog).toBeHidden(); await expect(page.getByRole('region', { name: t('cvEditorEditingRegion'), exact: true })).toBeVisible();
|
||||
const name = page.getByRole('textbox', { name: t('cvEditorName'), exact: true }); await expect(name).toHaveValue('Teacher career CV'); await name.fill('Reviewed teacher career CV'); await expect.poll(() => variant.version, { timeout: 15000 }).toBe(2); await scan('edited', name);
|
||||
if (width < 768) await page.getByRole('button', { name: t('cvEditorPreview'), exact: true }).click();
|
||||
const preview = page.getByRole('region', { name: t('cvEditorLivePreview'), exact: true }); await expect(preview).toBeVisible(); await expect(preview.frameLocator('iframe').getByRole('heading', { name: career.profile.contact.fullName, exact: true })).toBeVisible(); await scan('preview', preview);
|
||||
const downloading = page.waitForEvent('download'); await page.getByRole('button', { name: t('cvEditorDownloadPdf'), exact: true }).click(); const download = await downloading; const stream = await download.createReadStream(); const chunks = []; for await (const chunk of stream) chunks.push(chunk); assert.deepEqual(Buffer.concat(chunks), exportBytes); assert.equal(download.suggestedFilename(), 'Reviewed teacher career CV.pdf');
|
||||
report.downloads.push({ width, language, mode, filename: download.suggestedFilename(), bytes: exportBytes.length, renderedPdfValidation: 'NOT RUN' }); await scan('download');
|
||||
report.journeys.push({ width, language, mode, selectedProfileInheritance: 'PASS', createEditPreviewDownloadTransport: 'PASS', exportUsesSavedRevision: 'PASS', importAndPhysicalPdf: 'NOT RUN' }); await context.close(); continue;
|
||||
}
|
||||
if (evidenceOnly) {
|
||||
const openEvidence = async () => { await page.goto('http://localhost:4338/career'); await careerSection('careerWorkspaceEvidenceTab'); };
|
||||
await openEvidence();
|
||||
@@ -146,10 +174,10 @@ try {
|
||||
report.journeys.push({ width, language, mode, all14KeyboardEditTargets: 'PASS', canonicalSaveStableIds: 'PASS', legacyImportComparisonSelectionNoApply: 'PASS', libraryKeyboardOpen: 'PASS', cvResponsiveRename: 'PASS', letterResponsiveManualSave: 'PASS' });
|
||||
await context.close();
|
||||
}
|
||||
assert.equal(report.screens.length, configurations.length * 6); assert.equal(report.journeys.length, configurations.length); assert.equal(report.mockedWrites.length, evidenceOnly ? 0 : configurations.length * 3);
|
||||
assert.equal(report.screens.length, configurations.length * (cvJourney ? 5 : 6)); assert.equal(report.journeys.length, configurations.length); assert.equal(report.mockedWrites.length, evidenceOnly ? 0 : configurations.length * (cvJourney ? 2 : 3)); assert.equal(report.exportRequests.length, cvJourney ? configurations.length : 0); assert.equal(report.downloads.length, cvJourney ? configurations.length : 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 journeys,${report.screens.length} axe scans/captures,${report.mockedWrites.length} fixture mutations.`);
|
||||
} finally {
|
||||
fs.writeFileSync(path.join(out, process.env.A3_BOUNDARY ? 'BOUNDARY.json' : process.env.A3_PILOT ? 'PILOT.json' : 'BROWSER.json'), JSON.stringify({ ...report, limits: ['All API and preview responses are controlled fixtures; not backend end-to-end or physical PDF qualification.', evidenceOnly ? 'Evidence-only scope: delete confirmation cancelled, all mutations blocked; profile/editor/import/export workflows NOT RUN.' : 'Legacy import comparison and selection only; no parser, model, import acceptance or source mutation.', 'No native device, screen reader, provider, integrated security, deployment or release acceptance.'] }, null, 2));
|
||||
fs.writeFileSync(path.join(out, process.env.A3_BOUNDARY ? 'BOUNDARY.json' : process.env.A3_PILOT ? 'PILOT.json' : 'BROWSER.json'), JSON.stringify({ ...report, limits: ['All API and preview responses are controlled fixtures; not backend end-to-end or physical PDF qualification.', cvJourney ? 'Synthetic download transport bytes are not a PDF; only client revision/filename/download flow qualified. Profile inheritance uses fixture data; import, physical rendering and semantic quality NOT RUN.' : evidenceOnly ? 'Evidence-only scope: delete confirmation cancelled, all mutations blocked; profile/editor/import/export workflows NOT RUN.' : 'Legacy import comparison and selection only; no parser, model, import acceptance or source mutation.', 'No native device, screen reader, provider, integrated security, deployment or release acceptance.'] }, null, 2));
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user