test(settings): qualify privacy read and rejected save recovery

This commit is contained in:
cesnimda
2026-11-09 22:50:33 +01:00
parent 91e5a6bf3a
commit 511945b4f2
190 changed files with 3698 additions and 6 deletions
@@ -9,10 +9,11 @@ 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 reviewOnly = Boolean(process.env.A3_AI_REVIEW_ONLY);
const visualMatrix = Boolean(process.env.A3_VISUAL_MATRIX);
const settingsOnly = Boolean(process.env.A3_SETTINGS_FLAT);
assert.ok(!settingsOnly || !(visualMatrix || reviewOnly), 'Settings read-only qualification is a separate scope.');
const settingsRecovery = Boolean(process.env.A3_SETTINGS_RECOVERY);
const settingsOnly = Boolean(process.env.A3_SETTINGS_FLAT) || settingsRecovery;
assert.ok(!settingsOnly || !(visualMatrix || reviewOnly), 'Settings qualification is a separate scope.');
assert.ok(!visualMatrix || reviewOnly, 'Visual matrix reuses read/review-only journeys.');
const out = path.resolve(import.meta.dirname, settingsOnly ? '../../docs/ui-modernisation/visual-correction/evidence/settings-flat' : visualMatrix ? '../../docs/ui-modernisation/visual-correction/evidence/cv' : reviewOnly ? '../../docs/ui-modernisation/evidence/cross-app/browser-ai-review' : '../../docs/ui-modernisation/evidence/ai-settings/browser');
const out = path.resolve(import.meta.dirname, settingsRecovery ? '../../docs/ui-modernisation/visual-correction/evidence/settings-recovery' : settingsOnly ? '../../docs/ui-modernisation/visual-correction/evidence/settings-flat' : visualMatrix ? '../../docs/ui-modernisation/visual-correction/evidence/cv' : reviewOnly ? '../../docs/ui-modernisation/evidence/cross-app/browser-ai-review' : '../../docs/ui-modernisation/evidence/ai-settings/browser');
fs.mkdirSync(out, { recursive: true });
const report = { screens: [], journeys: [], errors: [], unexpectedWrites: [], writes: [], proposals: [], unavailableReads: [], blockedExternal: [] };
const browser = await chromium.launch();
@@ -32,6 +33,9 @@ try {
await page.addInitScript(({ language, mode }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', mode); }, { language, mode });
let notifications = { emailFollowUpRemindersEnabled: true };
let privacy = { enabled: true, externalProcessingAllowed: false, externalProcessingAvailable: false, effectiveExternalProcessing: false, provider: 'local' };
let privacyReads = 0;
let releasePrivacyRead;
const initialPrivacyRead = new Promise(resolve => { releasePrivacyRead = resolve; });
let variant = { id: 3, name: 'Teacher CV', version: 4, settings: { language: 'en', themeId: 'modern', showPhoto: false, showPageNumbers: false, showIcons: true, sections: [], overrides: {}, customSections: [], ...(reviewOnly ? { headline: 'Manual teacher headline' } : {}) }, isPublic: false, publicSlug: 'synthetic', jobApplicationId: null, updatedAtUtc: '2026-11-03T10:00:00Z' };
let proposal;
const writes = [], proposals = [];
@@ -42,6 +46,16 @@ try {
if (!p.startsWith('/api/')) return route.continue();
const answer = data => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(data) });
const note = action => { writes.push(action); report.writes.push({ width, language, mode, action }); };
if (settingsRecovery && p === '/api/ai/settings') {
if (req.method() === 'GET' && ++privacyReads === 1) {
return initialPrivacyRead.then(() => route.fulfill({ status: 503, contentType: 'application/problem+json', body: '{"detail":"Controlled preferences read unavailable."}' }));
}
if (req.method() === 'PUT') {
assert.deepEqual(req.postDataJSON(), { enabled: false, externalProcessingAllowed: false });
note('aiPreferenceConflict');
return route.fulfill({ status: 409, contentType: 'application/problem+json', body: '{"detail":"Controlled preference update rejected."}' });
}
}
if (settingsOnly && req.method() !== 'GET') { report.unexpectedWrites.push(p); return route.abort(); }
if (req.method() === 'PUT' && p === '/api/notification-settings') { assert.deepEqual(req.postDataJSON(), { emailFollowUpRemindersEnabled: false }); notifications = req.postDataJSON(); note('notificationPreference'); return answer(notifications); }
if (req.method() === 'PUT' && p === '/api/ai/settings') { const body = req.postDataJSON(); assert.equal(body.externalProcessingAllowed, false); privacy = { ...privacy, ...body }; note('aiPreference'); return answer(privacy); }
@@ -92,6 +106,39 @@ try {
if (width >= 600) await page.getByRole('tab', { name: t(key), exact: true }).click();
else { await page.getByRole('combobox', { name: t('settingsNavigation'), exact: true }).click(); await page.getByRole('option', { name: t(key), exact: true }).click(); }
};
if (settingsRecovery) {
await page.goto('http://localhost:4338/settings');
const card = page.getByRole('region', { name: t('settingsAiPrivacyTitle'), exact: true });
const body = card.getByRole('group', { name: t('settingsAiPrivacyTitle'), exact: true });
const enabled = card.getByRole('switch', { name: t('settingsAiEnabled'), exact: true });
const capture = async (state, target) => {
await target.evaluate(node => node.scrollIntoView({ block: 'center', behavior: 'instant' }));
await scan(state, true);
};
const loading = card.getByRole('status', { name: t('loading'), exact: true });
await expect(loading).toBeVisible(); await expect(enabled).toHaveCount(0);
await capture('ai-loading', loading);
releasePrivacyRead();
const unavailable = card.getByText(t('settingsAiPrivacyUnavailable'), { exact: true });
await expect(unavailable).toBeVisible(); await expect(enabled).toHaveCount(0);
await capture('ai-unavailable', unavailable);
await card.getByRole('button', { name: t('retry'), exact: true }).press('Enter');
await expect(body).toBeFocused(); await expect(enabled).toBeChecked();
await capture('ai-retry', enabled);
assert.equal(privacyReads, 2); assert.deepEqual(writes, []);
await enabled.uncheck(); await card.getByRole('button', { name: t('settingsAiSave'), exact: true }).press('Enter');
const warning = card.getByText(t('settingsAiSaveFailed'), { exact: true });
await expect(warning).toBeVisible(); await expect(enabled).not.toBeChecked();
await expect(card.getByText(t('assetsUnsavedChanges'), { exact: true })).toBeVisible();
await capture('ai-save-conflict', warning);
await card.getByRole('button', { name: t('assetsDiscardChanges'), exact: true }).press('Enter');
await expect(body).toBeFocused(); await expect(enabled).toBeChecked(); await expect(warning).toHaveCount(0);
await capture('ai-discard', enabled);
assert.deepEqual(writes, ['aiPreferenceConflict']); assert.deepEqual(proposals, []);
report.journeys.push({ width, language, mode, initialReadRetry: 'PASS', rejectedSavePreservedDraft: 'PASS', discardRestoredFixturePreference: 'PASS', aiGeneration: 'NOT RUN' });
await context.close(); continue;
}
if (settingsOnly) {
await page.goto('http://localhost:4338/settings');
for (const [key, state] of [['settingsTabGeneral', 'general'], ['settingsTabAutomation', 'automation'], ['settingsTabFollowUps', 'follow-ups'], ['settingsTabNotifications', 'notifications'], ['settingsTabBackup', 'data']]) {
@@ -143,7 +190,9 @@ try {
assert.deepEqual(writes, ['notificationPreference', 'aiPreference', 'manualCvSave', 'aiPreference', 'explicitAiApply']); assert.deepEqual(proposals, ['en', 'nb-NO']);
report.journeys.push({ width, language, mode, manualFallback: 'PASS', languageDiscardWithoutApply: 'PASS', explicitTokenApply: 'PASS', aiSettingsKeyboardFocus: 'PASS' }); await context.close();
}
assert.equal(report.screens.length, configurations.length * (settingsOnly ? 7 : reviewOnly ? visualMatrix ? 3 : 1 : 6)); assert.equal(report.journeys.length, configurations.length); assert.equal(report.writes.length, reviewOnly || settingsOnly ? 0 : configurations.length * 5); assert.equal(report.proposals.length, settingsOnly ? 0 : configurations.length * (reviewOnly ? 1 : 2));
const statesPerConfiguration = settingsRecovery ? 5 : settingsOnly ? 7 : reviewOnly ? visualMatrix ? 3 : 1 : 6;
const writesPerConfiguration = settingsRecovery ? 1 : reviewOnly || settingsOnly ? 0 : 5;
assert.equal(report.screens.length, configurations.length * statesPerConfiguration); assert.equal(report.journeys.length, configurations.length); assert.equal(report.writes.length, configurations.length * writesPerConfiguration); assert.equal(report.proposals.length, settingsOnly ? 0 : configurations.length * (reviewOnly ? 1 : 2));
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.`);
} finally {