fix(cv): preserve rewrite language and qualify responsive document controls

This commit is contained in:
cesnimda
2026-10-25 22:55:22 +01:00
parent 2bfaf6c14b
commit 8948c988e2
12 changed files with 822 additions and 5 deletions
@@ -146,3 +146,119 @@ test('cover letter native preview/PDF fidelity records four independent physical
expect(await (await page.request.get(`${apiUrl}/career/profile`)).json()).toEqual(sourceBefore);
await fs.writeFile(path.join(output, 'measurements.json'), JSON.stringify({ browserVersion: browser.version(), fixtures }, null, 2), { flag: 'wx' });
});
test('cover letter workspace preserves keyboard drafts and localised controls across responsive widths', async ({ context, playwright }, info) => {
test.setTimeout(300_000);
await context.route('**/*', route => {
const url = new URL(route.request().url());
return ['localhost', '127.0.0.1'].includes(url.hostname) || ['data:', 'blob:', 'about:'].includes(url.protocol) ? route.continue() : route.abort();
});
await context.addInitScript(() => {
const query = new URLSearchParams(location.search);
if (query.has('responsiveMode')) localStorage.setItem('jobtracker.themeMode', query.get('responsiveMode')!);
if (query.has('responsiveLanguage')) localStorage.setItem('uiLanguage', query.get('responsiveLanguage')!);
});
const admin = await playwright.request.newContext();
const adminHeaders = await login(admin, 'e2e@example.test');
const email = `letter-responsive-${Date.now()}@example.test`;
try {
// Free ownership keeps the four AI actions visible but prevents any provider invocation.
expect((await admin.post(`${apiUrl}/users`, { headers: adminHeaders, data: { email, password, roles: [] } })).status()).toBe(200);
} finally { await admin.dispose(); }
const headers = await login(context.request, email);
const companyResponse = await context.request.post(`${apiUrl}/companies`, { headers, data: { name: 'Responsive Synthetic AS', source: 'User' } });
expect(companyResponse.status()).toBe(201);
const company = await companyResponse.json();
const created = await context.request.post(`${apiUrl}/jobapplications`, { headers, data: { companyId: company.id, jobTitle: 'Responsive reviewed letter', status: 'Saved' } });
expect(created.status()).toBe(201);
const job = await created.json();
const letterUrl = `${apiUrl}/jobapplications/${job.id}/cover-letter`;
const original = await (await context.request.get(letterUrl)).json();
const savedResponse = await context.request.put(letterUrl, { headers, data: { text: 'Original reviewed letter — æøå.', source: 'manual', expectedVersion: original.revision } });
expect(savedResponse.status()).toBe(200);
const saved = await (await context.request.get(letterUrl)).json();
const sourceBefore = await (await context.request.get(`${apiUrl}/career/profile`)).json();
const scenarios = [
[320, 'en', 'light'], [390, 'nb', 'dark'], [768, 'en', 'dark'], [899, 'nb', 'light'],
[900, 'en', 'light'], [1024, 'nb', 'dark'], [1440, 'nb', 'light'],
] as const;
for (const [width, language, mode] of scenarios) {
const t = translations[language];
expect((await context.request.put(`${apiUrl}/auth/preferences/language`, { headers, data: { language: language === 'nb' ? 'nb-NO' : 'en' } })).status()).toBe(204);
const page = await context.newPage();
await page.setViewportSize({ width, height: 900 });
const forbiddenRequests: string[] = [];
page.on('request', request => {
const pathname = new URL(request.url()).pathname;
if (request.method() === 'POST' && (pathname.endsWith('/ai/generate') || pathname.endsWith('/cover-letter/export-pdf'))
|| request.method() === 'PUT' && pathname === new URL(letterUrl).pathname) forbiddenRequests.push(`${request.method()} ${pathname}`);
});
try {
await page.goto(`/jobs/${job.id}?section=application&sub=letter&responsiveMode=${mode}&responsiveLanguage=${language}#materials`);
await expect(page.locator('html')).toHaveAttribute('lang', language === 'nb' ? 'nb-NO' : 'en');
await expect(page.locator('html')).toHaveAttribute('data-color-scheme', mode);
const workspace = page.getByRole('group', { name: t.workspaceCoverLetter, exact: true });
const body = workspace.getByRole('textbox', { name: t.jobDetailsCoverLetter, exact: true });
await expect(body).toHaveValue(saved.text);
const draft = `${width} ${language === 'nb' ? 'Ulagret søknadsbrev med gjennomgåtte fakta' : 'Unsaved letter with reviewed facts'} — æøå.`;
await body.focus(); await body.press('ControlOrMeta+A'); await body.pressSequentially(draft);
await expect(body).toHaveValue(draft);
const controls = [t.cvAiImprove, t.cvAiSuggestContent, t.cvAiGrammarCheck, t.cvAiShorter];
const bodyRect = await body.boundingBox();
expect(bodyRect).not.toBeNull();
for (const name of controls) {
const control = workspace.getByRole('button', { name, exact: true });
await expect(control).toBeVisible(); await expect(control).toBeDisabled();
const rect = await control.boundingBox(); expect(rect).not.toBeNull();
expect(rect!.y - (bodyRect!.y + bodyRect!.height)).toBeGreaterThanOrEqual(-2);
expect(rect!.y - (bodyRect!.y + bodyRect!.height)).toBeLessThan(240);
expect(rect!.x).toBeGreaterThanOrEqual(0); expect(rect!.x + rect!.width).toBeLessThanOrEqual(width + 1);
}
const geometry = await page.evaluate(() => {
const main = document.querySelector('main')!;
return { root: document.documentElement.scrollWidth - innerWidth, app: main.scrollWidth - main.clientWidth };
});
expect(geometry.root).toBeLessThanOrEqual(1); expect(geometry.app).toBeLessThanOrEqual(1);
const writePane = workspace.locator('[id$="-write-panel"]');
const previewPane = workspace.locator('[id$="-preview-panel"]');
if (width < 1024) {
const tabs = workspace.getByRole('tablist', { name: t.assetsLetterWorkspace, exact: true });
const write = tabs.getByRole('tab', { name: t.assetsLetterWrite, exact: true });
const preview = tabs.getByRole('tab', { name: t.assetsLetterPreview, exact: true });
await expect(previewPane).not.toBeVisible();
await write.focus(); await page.keyboard.press('ArrowRight'); await expect(preview).toBeFocused(); await page.keyboard.press('Enter');
await expect(preview).toHaveAttribute('aria-selected', 'true');
await expect(writePane).not.toBeVisible(); await expect(previewPane).toBeVisible();
await expect(workspace.getByRole('textbox', { name: t.jobDetailsCoverLetter, exact: true })).toHaveCount(0);
for (let step = 0; step < 8; step++) {
await page.keyboard.press('Tab');
expect(await writePane.evaluate(node => node.contains(document.activeElement))).toBe(false);
}
await preview.focus(); await page.keyboard.press('ArrowLeft'); await expect(write).toBeFocused(); await page.keyboard.press('Enter');
await expect(write).toHaveAttribute('aria-selected', 'true');
await expect(previewPane).not.toBeVisible(); await expect(body).toHaveValue(draft);
await page.keyboard.press('Tab');
expect(await previewPane.evaluate(node => node.contains(document.activeElement))).toBe(false);
} else {
await expect(workspace.getByRole('tablist', { name: t.assetsLetterWorkspace, exact: true })).toHaveCount(0);
await expect(writePane).toBeVisible(); await expect(previewPane).toBeVisible();
}
for (const name of [t.save, t.letterExportPdf]) {
const control = workspace.getByRole('button', { name, exact: true });
await expect(control).toBeEnabled(); await control.scrollIntoViewIfNeeded(); await expect(control).toBeInViewport();
await control.focus(); await expect(control).toBeFocused();
const rect = await control.boundingBox(); expect(rect).not.toBeNull();
expect(rect!.x).toBeGreaterThanOrEqual(0); expect(rect!.x + rect!.width).toBeLessThanOrEqual(width + 1);
expect(await control.evaluate(node => {
const rect = node.getBoundingClientRect();
return node.contains(document.elementFromPoint(rect.x + rect.width / 2, rect.y + rect.height / 2));
})).toBe(true);
}
await expect(body).toHaveValue(draft);
expect(forbiddenRequests).toEqual([]);
expect(await (await context.request.get(letterUrl)).json()).toEqual(saved);
await page.screenshot({ path: info.outputPath(`letter-responsive-${width}-${language}-${mode}.png`), fullPage: true, animations: 'disabled' });
await info.attach(`letter-responsive-${width}`, { body: JSON.stringify({ width, language, mode, geometry, draft, unchangedRevision: saved.revision, forbiddenRequests }), contentType: 'application/json' });
} finally { await page.close(); }
}
expect(await (await context.request.get(`${apiUrl}/career/profile`)).json()).toEqual(sourceBefore);
});