style(settings): use full rules form width without spacer rows
This commit is contained in:
@@ -10,15 +10,16 @@ const { translations } = await import(`data:text/javascript;base64,${Buffer.from
|
||||
const reviewOnly = Boolean(process.env.A3_AI_REVIEW_ONLY);
|
||||
const visualMatrix = Boolean(process.env.A3_VISUAL_MATRIX);
|
||||
const settingsRecovery = Boolean(process.env.A3_SETTINGS_RECOVERY);
|
||||
const settingsOnly = Boolean(process.env.A3_SETTINGS_FLAT) || settingsRecovery;
|
||||
const rulesRecovery = Boolean(process.env.A3_RULES_RECOVERY);
|
||||
const settingsOnly = Boolean(process.env.A3_SETTINGS_FLAT) || settingsRecovery || rulesRecovery;
|
||||
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, 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');
|
||||
const out = path.resolve(import.meta.dirname, rulesRecovery ? '../../docs/ui-modernisation/visual-correction/evidence/rules-recovery' : 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();
|
||||
try {
|
||||
const configurations = process.env.A3_PILOT ? (settingsOnly ? [[320, 'en', 'light'], [1440, 'en', 'light']] : [[320, 'en', 'light']]) : (visualMatrix || settingsOnly ? [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_PILOT ? (rulesRecovery ? [[390, 'nb', 'dark']] : settingsOnly ? [[320, 'en', 'light'], [1440, 'en', 'light']] : [[320, 'en', 'light']]) : (rulesRecovery ? [390, 768, 1440] : visualMatrix || settingsOnly ? [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]; };
|
||||
@@ -34,6 +35,9 @@ try {
|
||||
let notifications = { emailFollowUpRemindersEnabled: true };
|
||||
let privacy = { enabled: true, externalProcessingAllowed: false, externalProcessingAvailable: false, effectiveExternalProcessing: false, provider: 'local' };
|
||||
let privacyReads = 0;
|
||||
let ruleReads = 0, releaseRuleSave;
|
||||
const ruleSave = new Promise(resolve => { releaseRuleSave = resolve; });
|
||||
const savedRules = { id: 1, appliedFollowUpDays: 14, appliedGhostDays: 30, offerFollowUpDays: 7, offerGhostDays: 14, feedbackFollowUpDays: 7, feedbackGhostDays: 14 };
|
||||
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' };
|
||||
@@ -46,6 +50,15 @@ 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 (rulesRecovery && p === '/api/rules') {
|
||||
if (req.method() === 'GET' && ++ruleReads === 1) return route.fulfill({ status: 503, json: {} });
|
||||
if (req.method() === 'PUT') {
|
||||
assert.deepEqual(req.postDataJSON(), { ...savedRules, appliedFollowUpDays: writes.length ? 20 : 21 });
|
||||
note(writes.length ? 'rulesSaveRejected' : 'rulesSaveSnapshot');
|
||||
if (writes.length === 1) return ruleSave.then(() => answer({}));
|
||||
return route.fulfill({ status: 503, json: {} });
|
||||
}
|
||||
}
|
||||
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."}' }));
|
||||
@@ -83,6 +96,7 @@ try {
|
||||
'/api/billing/status': { enabled: false, canCheckout: false, canManage: false },
|
||||
...(settingsOnly ? { '/api/account-lifecycle/status': { deletionEnabled: true, deletionStatus: 'active', requiredConfirmation: 'DELETE alex@example.test', request: null } } : {}),
|
||||
'/api/rules': { id: 1, appliedFollowUpDays: 14, appliedGhostDays: 30, offerFollowUpDays: 7, offerGhostDays: 14, feedbackFollowUpDays: 7, feedbackGhostDays: 14 },
|
||||
...(rulesRecovery ? { '/api/companies': [] } : {}),
|
||||
'/api/cv/variants/3': variant, '/api/cv/variants/3/versions': [], '/api/cv/outline': { fullName: 'Alex Sample', headline: 'Recorded teacher headline', sections: [] },
|
||||
'/api/cv/themes': [{ 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'] }],
|
||||
};
|
||||
@@ -106,6 +120,41 @@ 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 (rulesRecovery) {
|
||||
await page.goto('http://localhost:4338/settings'); await settingsSection('settingsTabFollowUps');
|
||||
const card = page.getByRole('region', { name: t('rulesTitle'), exact: true });
|
||||
const retry = card.getByRole('button', { name: t('retry'), exact: true });
|
||||
await expect(retry).toBeVisible(); await scan('rules-read-error');
|
||||
await retry.press('Enter');
|
||||
const field = card.getByRole('spinbutton', { name: t('rulesAppliedFollowUpDays'), exact: true });
|
||||
await expect(field).toHaveValue('14'); await expect(card.getByRole('spinbutton')).toHaveCount(6);
|
||||
await scan('rules-loaded');
|
||||
const rects = await card.getByRole('spinbutton').evaluateAll(inputs => inputs.map(input => { const r=input.closest('.MuiFormControl-root').getBoundingClientRect(); return { x:r.x, y:r.y, width:r.width, height:r.height }; }));
|
||||
const formWidth = (await card.locator('form').boundingBox()).width;
|
||||
if (width >= 1024) {
|
||||
assert.ok(rects[0].width >= formWidth * 0.45, 'Two useful desktop columns fill the form');
|
||||
assert.ok(rects[1].x > rects[0].x); assert.ok(Math.abs(rects[2].x-rects[0].x)<2);
|
||||
} else {
|
||||
for (let i=1;i<rects.length;i++) { const gap=rects[i].y-rects[i-1].y-rects[i-1].height; assert.ok(gap>=8 && gap<=24, `No empty mobile/tablet spacer row: ${gap}`); }
|
||||
}
|
||||
const save = card.locator('button[type="submit"]');
|
||||
const discard = card.getByRole('button', { name: t('assetsDiscardChanges'), exact: true });
|
||||
await field.fill('0'); await save.press('Enter'); await expect(field).toHaveAttribute('aria-invalid','true');
|
||||
assert.deepEqual(writes, []); await scan('rules-invalid');
|
||||
await field.fill('21'); await save.press('Enter'); await expect(save).toBeDisabled();
|
||||
await expect(discard).toBeDisabled(); await field.fill('22'); await scan('rules-saving');
|
||||
releaseRuleSave(); await expect(save).toBeEnabled(); await expect(field).toHaveValue('22');
|
||||
await scan('rules-saved-newer-draft');
|
||||
await discard.press('Enter'); await expect(field).toHaveValue('21');
|
||||
await field.fill('20'); await save.press('Enter');
|
||||
await expect(card.getByText(t('rulesSaveFailed'), { exact: true })).toBeVisible(); await expect(field).toHaveValue('20');
|
||||
await scan('rules-save-error');
|
||||
await discard.press('Enter'); await expect(field).toHaveValue('21');
|
||||
await expect(card.getByText(t('rulesSaveFailed'), { exact: true })).toHaveCount(0); await scan('rules-discarded');
|
||||
assert.equal(ruleReads,2); assert.deepEqual(writes,['rulesSaveSnapshot','rulesSaveRejected']);
|
||||
report.journeys.push({ width, language, mode, readRetry:'PASS', validColumnsAndGaps:rects, invalidWriteExcluded:'PASS', pendingSnapshotRetainsNewerEdit:'PASS', rejectedSaveRetainsDraft:'PASS', discardRestoresCommittedFixture:'PASS' });
|
||||
await context.close(); continue;
|
||||
}
|
||||
if (settingsRecovery) {
|
||||
await page.goto('http://localhost:4338/settings');
|
||||
const card = page.getByRole('region', { name: t('settingsAiPrivacyTitle'), exact: true });
|
||||
@@ -190,10 +239,11 @@ 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();
|
||||
}
|
||||
const statesPerConfiguration = settingsRecovery ? 5 : settingsOnly ? 7 : reviewOnly ? visualMatrix ? 3 : 1 : 6;
|
||||
const writesPerConfiguration = settingsRecovery ? 1 : reviewOnly || settingsOnly ? 0 : 5;
|
||||
const statesPerConfiguration = rulesRecovery ? 7 : settingsRecovery ? 5 : settingsOnly ? 7 : reviewOnly ? visualMatrix ? 3 : 1 : 6;
|
||||
const writesPerConfiguration = rulesRecovery ? 2 : 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'));
|
||||
if (rulesRecovery) assert.deepEqual(report.unavailableReads,[]);
|
||||
console.log(`PASS:${report.journeys.length} controlled journeys,${report.screens.length} axe scans/captures.`);
|
||||
} finally {
|
||||
fs.writeFileSync(path.join(out, reviewOnly ? 'REVIEW.json' : process.env.A3_PILOT ? 'PILOT.json' : 'BROWSER.json'), JSON.stringify({ ...report, limits: ['All API, AI and preview results are synthetic fixtures. No model activation, semantic translation quality, backend end-to-end or physical PDF qualification.', 'No native device, screen reader, provider, integrated security, deployment or release acceptance.'] }, null, 2));
|
||||
|
||||
@@ -56,16 +56,13 @@ export default function RulesSettingsCard({ draft, loadError, saveError, invalid
|
||||
<PageSection title={t("rulesTitle")} description={t("rulesBody")}>
|
||||
<Box component="form" noValidate onSubmit={(event) => { event.preventDefault(); onSave(); }}>
|
||||
{saveError ? <Alert severity="error" sx={{ mb: 2 }}>{t("rulesSaveFailed")}</Alert> : null}
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr 1fr" }, gap: 2 }}>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 2 }}>
|
||||
<TextField label={t("rulesAppliedFollowUpDays")} {...num("appliedFollowUpDays")} />
|
||||
<TextField label={t("rulesAppliedGhostDays")} {...num("appliedGhostDays")} />
|
||||
<Box />
|
||||
<TextField label={t("rulesOfferFollowUpDays")} {...num("offerFollowUpDays")} />
|
||||
<TextField label={t("rulesOfferGhostDays")} {...num("offerGhostDays")} />
|
||||
<Box />
|
||||
<TextField label={t("rulesFeedbackFollowUpDays")} {...num("feedbackFollowUpDays")} />
|
||||
<TextField label={t("rulesFeedbackGhostDays")} {...num("feedbackGhostDays")} />
|
||||
<Box />
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", flexWrap: "wrap", gap: 1, mt: 2 }}>
|
||||
|
||||
Reference in New Issue
Block a user