fix(cv): preserve rewrite language and qualify responsive document controls
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user