fix(ui): retain locale-aware document review labels
This commit is contained in:
@@ -7,7 +7,8 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
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, '../../docs/ui-modernisation/evidence/ai-settings/browser');
|
||||
const reviewOnly = Boolean(process.env.A3_AI_REVIEW_ONLY);
|
||||
const out = path.resolve(import.meta.dirname, 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();
|
||||
@@ -21,7 +22,7 @@ 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 variant = { id: 3, name: 'Teacher CV', version: 4, settings: { language: 'en', themeId: 'modern', showPhoto: false, showPageNumbers: false, showIcons: true, sections: [], overrides: {}, customSections: [] }, isPublic: false, publicSlug: 'synthetic', jobApplicationId: null, updatedAtUtc: '2026-11-03T10:00:00Z' };
|
||||
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 = [];
|
||||
await page.route('**/*', route => {
|
||||
@@ -73,6 +74,7 @@ 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 (!reviewOnly) {
|
||||
await page.goto('http://localhost:4338/settings');
|
||||
await settingsSection('settingsTabAutomation');
|
||||
const reminder = page.getByRole('switch', { name: t('settingsNotificationsFollowUpReminders'), exact: true }); await reminder.uncheck(); assert.equal(writes.length, 0);
|
||||
@@ -85,8 +87,14 @@ try {
|
||||
await page.getByRole('tab', { name: t('cvEditorContent'), exact: true }).click();
|
||||
await page.getByRole('textbox', { name: t('cvEditorHeadlineOverride'), exact: true }).fill('Manual teacher headline'); await expect.poll(() => variant.version, { timeout: 15000 }).toBe(5); assert.equal(proposals.length, 0); await scan('manual-fallback');
|
||||
await page.goto('http://localhost:4338/settings'); await page.getByRole('switch', { name: t('settingsAiEnabled'), exact: true }).check(); await page.getByRole('button', { name: t('settingsAiSave'), exact: true }).click(); await expect.poll(() => privacy.enabled).toBe(true);
|
||||
}
|
||||
await page.goto('http://localhost:4338/career/builder/3'); await page.getByRole('tab', { name: t('cvEditorAiTools'), exact: true }).click();
|
||||
await page.getByRole('button', { name: t('cvDocumentAiReview'), exact: true }).first().click(); await expect(page.getByRole('button', { name: t('cvAiApply'), exact: true })).toBeVisible(); assert.equal(writes.length, 4); await scan('ai-review');
|
||||
await page.getByRole('button', { name: t('cvDocumentAiReview'), exact: true }).first().click(); await expect(page.getByRole('button', { name: t('cvAiApply'), exact: true })).toBeVisible(); assert.equal(writes.length, reviewOnly ? 0 : 4);
|
||||
await expect(page.locator('#cv-studio-panel-3').getByText(t('cvEditorHeadlineOverride'), { exact: true })).toBeVisible(); await scan('ai-review');
|
||||
if (reviewOnly) {
|
||||
assert.deepEqual(writes, []); assert.deepEqual(proposals, ['en']);
|
||||
report.journeys.push({ width, language, mode, translatedReviewCaption: 'PASS', explicitReviewWithoutApply: 'PASS', uiLanguageDistinctFromOutputLanguage: 'PASS' }); await context.close(); continue;
|
||||
}
|
||||
await page.getByRole('combobox', { name: new RegExp(`^${t('cvAiLanguage')}`) }).click(); await page.getByRole('option', { name: t('languageNorwegianBokmal'), exact: true }).click();
|
||||
await expect(page.getByRole('button', { name: t('cvAiApply'), exact: true })).toHaveCount(0); assert.equal(proposals.length, 1); assert.equal(variant.settings.headline, 'Manual teacher headline'); await scan('language-discard');
|
||||
await page.getByRole('button', { name: t('cvDocumentAiReview'), exact: true }).first().click(); await page.getByRole('button', { name: t('cvAiApply'), exact: true }).click(); await expect.poll(() => variant.version).toBe(6);
|
||||
@@ -94,10 +102,10 @@ 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 * 6); assert.equal(report.journeys.length, configurations.length); assert.equal(report.writes.length, configurations.length * 5); assert.equal(report.proposals.length, configurations.length * 2);
|
||||
assert.equal(report.screens.length, configurations.length * (reviewOnly ? 1 : 6)); assert.equal(report.journeys.length, configurations.length); assert.equal(report.writes.length, reviewOnly ? 0 : configurations.length * 5); assert.equal(report.proposals.length, 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 {
|
||||
fs.writeFileSync(path.join(out, 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));
|
||||
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));
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Read-only source census. Route presence does not establish workflow or security acceptance.
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import ts from 'typescript';
|
||||
const root = path.resolve(import.meta.dirname, '../..');
|
||||
const registerPath = 'docs/pre-phase10-ux/preservation.json';
|
||||
const registerBytes = fs.readFileSync(path.join(root, registerPath));
|
||||
const register = JSON.parse(registerBytes);
|
||||
const base = 'd3d338376444b8abdcaf7200f682faa3daf1ad10';
|
||||
const digest = bytes => crypto.createHash('sha256').update(bytes.toString('utf8').replace(/\r\n/g, '\n')).digest('hex');
|
||||
assert.equal(digest(registerBytes), digest(execFileSync('git', ['show', `${base}:${registerPath}`], { cwd: root })), 'Original preservation register must remain unchanged');
|
||||
assert.equal(register.groups.length, 57); assert.equal(register.routes.length, 34);
|
||||
const source = fs.readFileSync(path.join(root, 'job-tracker-ui/src/App.tsx'), 'utf8');
|
||||
const ast = ts.createSourceFile('App.tsx', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
||||
const declared = new Set();
|
||||
function visit(node) {
|
||||
if (ts.isJsxAttribute(node) && node.name.getText(ast) === 'path' && node.initializer && ts.isStringLiteral(node.initializer)) declared.add(node.initializer.text);
|
||||
if (ts.isPropertyAssignment(node) && node.name.getText(ast) === 'path' && ts.isStringLiteral(node.initializer)) declared.add(node.initializer.text);
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
visit(ast);
|
||||
const missing = register.routes.filter(item => !declared.has(item.route));
|
||||
assert.deepEqual(missing, [], 'All inherited route patterns must remain registered');
|
||||
const result = {
|
||||
base, policy: 'Source membership and unchanged criteria only; no acceptance promotion.',
|
||||
sourceSha256: digest(source), registerSha256: digest(registerBytes),
|
||||
groups: register.groups.map(({ id, currentStatus, requiredClosure, sourceCriterion }) => ({ id, currentStatus, requiredClosure, sourceCriterion })),
|
||||
routes: register.routes.map(({ route, owners, requiredClosure }) => ({ route, owners, requiredClosure, sourceMembership: 'PRESENT' })),
|
||||
limits: ['57 historical group statuses and original criteria retained, not promoted.', 'Not browser/auth/provider/native/deployed or whole-capability acceptance. CalendarPage retains the original reminder route and owner internally.'],
|
||||
};
|
||||
const out = path.join(root, 'docs/ui-modernisation/evidence/cross-app');
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
fs.writeFileSync(path.join(out, 'PRESERVATION_CENSUS.json'), JSON.stringify(result, null, 2));
|
||||
console.log('PASS:57 unchanged preservation groups,34 inherited route patterns present in the TypeScript AST.');
|
||||
Reference in New Issue
Block a user