359 lines
25 KiB
TypeScript
359 lines
25 KiB
TypeScript
import AxeBuilder from '@axe-core/playwright';
|
|
import { expect, test as base, type APIRequestContext, type Page, type TestInfo } from '@playwright/test';
|
|
import { translations } from '../src/i18n/translations';
|
|
import type { CvVariant } from '../src/cvBuilder';
|
|
|
|
const apiUrl = 'http://localhost:5302/api';
|
|
const password = 'E2ePassword123!';
|
|
type Owner = { id: string; email: string; headers: Record<string, string> };
|
|
type Sample = { language: 'en' | 'nb'; theme: 'light' | 'dark'; width: number; height: number };
|
|
|
|
async function login(api: APIRequestContext, email: string): Promise<Owner> {
|
|
expect((await api.post(`${apiUrl}/auth/login`, { data: { email, password } })).status()).toBe(200);
|
|
const response = await api.get(`${apiUrl}/auth/me`);
|
|
expect(response.status()).toBe(200);
|
|
const me = await response.json();
|
|
const csrf = (await api.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN');
|
|
expect(csrf).toBeDefined();
|
|
return { id: me.id, email, headers: { 'X-CSRF-TOKEN': csrf!.value, 'X-Session-Context': me.sessionContext } };
|
|
}
|
|
|
|
async function createOwner(api: APIRequestContext, headers: Record<string, string>) {
|
|
const email = `p7-career-${Date.now()}-${Math.random().toString(36).slice(2)}@example.test`;
|
|
expect((await api.post(`${apiUrl}/users`, { headers, data: { email, password, roles: [] } })).status()).toBe(200);
|
|
return email;
|
|
}
|
|
|
|
const test = base.extend<{ owner: Owner }>({
|
|
owner: async ({ page, context, playwright }, provide) => {
|
|
const admin = await playwright.request.newContext();
|
|
try {
|
|
const adminOwner = await login(admin, 'e2e@example.test');
|
|
const owner = await login(page.request, await createOwner(admin, adminOwner.headers));
|
|
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(() => {
|
|
if (!localStorage.getItem('uiLanguage')) localStorage.setItem('uiLanguage', 'en');
|
|
if (!localStorage.getItem('jobtracker.themeMode')) localStorage.setItem('jobtracker.themeMode', 'light');
|
|
});
|
|
await provide(owner);
|
|
} finally { await admin.dispose(); }
|
|
},
|
|
});
|
|
|
|
async function enter(page: Page, owner: Owner, sample: Sample, section = 'overview') {
|
|
expect((await page.request.put(`${apiUrl}/auth/preferences/language`, { headers: owner.headers,
|
|
data: { language: sample.language === 'nb' ? 'nb-NO' : 'en' } })).status()).toBe(204);
|
|
await page.setViewportSize({ width: sample.width, height: sample.height });
|
|
await page.goto(`/career?section=${section}&source=phase7#preserved`);
|
|
await page.evaluate(({ language, theme }) => {
|
|
localStorage.setItem('uiLanguage', language);
|
|
localStorage.setItem('jobtracker.themeMode', theme);
|
|
}, sample);
|
|
await page.emulateMedia({ reducedMotion: 'reduce' });
|
|
await page.reload();
|
|
await expect(page.locator('html')).toHaveAttribute('lang', sample.language === 'nb' ? 'nb-NO' : 'en');
|
|
await expect(page.locator('html')).toHaveAttribute('data-color-scheme', sample.theme);
|
|
}
|
|
|
|
async function navigateSection(page: Page, language: Sample['language'], section: 'overview' | 'profile' | 'evidence' | 'import') {
|
|
const t = translations[language];
|
|
const label = { overview: t.careerWorkspaceOverviewTab, profile: t.careerWorkspaceProfileTab,
|
|
evidence: t.careerWorkspaceEvidenceTab, import: t.careerWorkspaceImportTab }[section];
|
|
const selector = page.getByRole('combobox', { name: t.careerWorkspaceNavigation, exact: true });
|
|
if (await selector.isVisible()) {
|
|
await selector.click();
|
|
await page.getByRole('option', { name: label, exact: true }).click();
|
|
} else {
|
|
await page.getByRole('tab', { name: label, exact: true }).click();
|
|
}
|
|
const url = new URL(page.url());
|
|
expect(url.searchParams.get('section')).toBe(section === 'overview' ? null : section);
|
|
expect(url.searchParams.get('source')).toBe('phase7');
|
|
expect(url.hash).toBe('#preserved');
|
|
}
|
|
|
|
async function capture(page: Page, info: TestInfo, sample: Sample, state: string) {
|
|
await page.evaluate(async () => { await document.fonts.ready; await new Promise<void>(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))); });
|
|
const geometry = await page.evaluate(() => ({ width: innerWidth, height: innerHeight,
|
|
rootOverflow: Math.max(0, document.documentElement.scrollWidth - document.documentElement.clientWidth) }));
|
|
const axe = await new AxeBuilder({ page }).options({ rules: { 'target-size': { enabled: true } } }).analyze();
|
|
const name = `p7-career-${sample.language}-${sample.theme}-${sample.width}-${state}`;
|
|
await info.attach(`${name}-audit`, { body: JSON.stringify({ sample, state, geometry,
|
|
ariaSnapshot: await page.getByRole('main').ariaSnapshot(),
|
|
axe: { scope: 'All enabled rules plus target-size; complete findings retained', violations: axe.violations,
|
|
incomplete: axe.incomplete, passes: axe.passes.map(rule => ({ id: rule.id, nodes: rule.nodes.length })) },
|
|
limits: ['No human screen reader or physical touch/software keyboard', 'No actual browser zoom or production font/photo qualification', 'Controlled faults are explicit; no model/provider invocation'],
|
|
}), contentType: 'application/json' });
|
|
await info.attach(name, { body: await page.screenshot({ fullPage: true, animations: 'disabled' }), contentType: 'image/png' });
|
|
expect(geometry.rootOverflow).toBe(0);
|
|
}
|
|
|
|
const matrix: Sample[] = [
|
|
{ language: 'nb', theme: 'dark', width: 320, height: 844 },
|
|
{ language: 'en', theme: 'light', width: 360, height: 800 },
|
|
{ language: 'nb', theme: 'light', width: 390, height: 844 },
|
|
{ language: 'en', theme: 'dark', width: 768, height: 1024 },
|
|
{ language: 'nb', theme: 'dark', width: 1024, height: 768 },
|
|
{ language: 'en', theme: 'light', width: 1280, height: 320 },
|
|
{ language: 'nb', theme: 'light', width: 1440, height: 900 },
|
|
{ language: 'en', theme: 'dark', width: 1920, height: 1080 },
|
|
];
|
|
|
|
test('[P7-02 PRES-C04 E09] real SQLite evidence list preserves owner boundaries and existing validation', async ({ page, owner, playwright }, info) => {
|
|
const created = await page.request.post(`${apiUrl}/career/evidence`, { headers: owner.headers,
|
|
data: { category: 'project', title: 'Owner A evidence', statement: 'Recorded, reviewed fact.', tags: [' C# ', 'c#', 'Norsk'], sourceType: 'manual', sourceReference: ' Retrospective ', isVerified: true } });
|
|
expect(created.status()).toBe(200);
|
|
const item = await created.json();
|
|
const listed = await page.request.get(`${apiUrl}/career/evidence`);
|
|
expect(listed.status(), 'the actual local SQLite HTTP list must succeed').toBe(200);
|
|
expect(await listed.json()).toEqual([item]);
|
|
expect(item.tags).toEqual(['C#', 'Norsk']);
|
|
expect(item.sourceReference).toBe('Retrospective');
|
|
const admin = await playwright.request.newContext();
|
|
const other = await playwright.request.newContext();
|
|
const anonymous = await playwright.request.newContext();
|
|
try {
|
|
const adminOwner = await login(admin, 'e2e@example.test');
|
|
const otherOwner = await login(other, await createOwner(admin, adminOwner.headers));
|
|
expect(await (await other.get(`${apiUrl}/career/evidence?ownerUserId=${encodeURIComponent(owner.id)}`)).json()).toEqual([]);
|
|
expect((await other.put(`${apiUrl}/career/evidence/${item.id}`, { headers: otherOwner.headers,
|
|
data: { title: 'Foreign overwrite', statement: 'Denied' } })).status()).toBe(404);
|
|
expect((await other.delete(`${apiUrl}/career/evidence/${item.id}`, { headers: otherOwner.headers })).status()).toBe(404);
|
|
expect((await anonymous.get(`${apiUrl}/career/evidence`)).status()).toBe(401);
|
|
expect((await page.request.post(`${apiUrl}/career/evidence`, { headers: owner.headers,
|
|
data: { title: ' ', statement: 'Not saved' } })).status()).toBe(400);
|
|
expect((await page.request.post(`${apiUrl}/career/evidence`, { headers: owner.headers,
|
|
data: { title: 'Not saved', statement: ' ' } })).status()).toBe(400);
|
|
expect(await (await page.request.get(`${apiUrl}/career/evidence`)).json()).toEqual([item]);
|
|
await info.attach('native-evidence-owner-provider-contract', { body: JSON.stringify({ provider: 'Local Playwright SQLite', list200: true,
|
|
foreignUpdate404: true, foreignDelete404: true, anonymous401: true, invalidCreate400: true, noForeignMutation: true,
|
|
qualification: 'Actual preceding HTTP assertions; no InMemory or production-provider inference' }), contentType: 'application/json' });
|
|
} finally { await admin.dispose(); await other.dispose(); await anonymous.dispose(); }
|
|
});
|
|
|
|
for (const sample of matrix) {
|
|
test(`[P7-02 PRES-C01 C02 C04 C05 E08 E09] ${sample.language}/${sample.theme} ${sample.width}px overview and factual evidence preserve canonical source and recent CV links`, async ({ page, owner }, info) => {
|
|
test.setTimeout(120_000);
|
|
const profile = { version: '1', contact: { fullName: 'Åse Kunnskapsbank', headline: 'Canonical source' },
|
|
jobs: [{ title: 'Engineer', company: 'Dokumentasjon AS', bullets: ['Recorded work, no invented outcomes.'] }], skills: ['C#', 'Norsk'] };
|
|
expect((await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: { profile } })).status()).toBe(200);
|
|
const before = await (await page.request.get(`${apiUrl}/career/profile`)).json();
|
|
const cvs: CvVariant[] = [];
|
|
for (let i = 0; i < 4; i++) {
|
|
const r = await page.request.post(`${apiUrl}/cv/variants`, { headers: owner.headers, data: { name: `CV ${i + 1} — ${'Lang dokumenttittel '.repeat(5)}` } });
|
|
expect(r.status()).toBe(200); cvs.push(await r.json());
|
|
}
|
|
const summariesResponse = await page.request.get(`${apiUrl}/cv/variants`);
|
|
expect(summariesResponse.status()).toBe(200);
|
|
const authoritativeRecent = (await summariesResponse.json()).slice(0, 3).map((cv: { id: number }) => `/career/builder/${cv.id}`);
|
|
const item = await page.request.post(`${apiUrl}/career/evidence`, { headers: owner.headers, data: {
|
|
title: 'Et dokumentert prosjekt med en lang tittel — æøå '.repeat(4), statement: 'Faktisk dokumentasjon fra et gjennomført prosjekt. '.repeat(8),
|
|
category: 'project', tags: ['Tilgjengelighet', 'Systematisk dokumentasjon'], sourceType: 'manual', sourceReference: 'Prosjektgjennomgang — æøå', isVerified: true,
|
|
} });
|
|
expect(item.status()).toBe(200);
|
|
const evidence = await item.json();
|
|
if (sample.width === 1440) {
|
|
for (let i = 1; i <= 10; i++) {
|
|
expect((await page.request.post(`${apiUrl}/career/evidence`, { headers: owner.headers, data: {
|
|
title: `Additional owned evidence ${i}`, statement: 'Another supported statement. '.repeat(8), sourceType: 'manual', isVerified: true,
|
|
} })).status()).toBe(200);
|
|
}
|
|
expect(await (await page.request.get(`${apiUrl}/career/evidence`)).json()).toHaveLength(11);
|
|
}
|
|
await enter(page, owner, sample);
|
|
const t = translations[sample.language];
|
|
await expect(page.getByRole('heading', { name: t.careerWorkspaceOverviewTab, level: 2 })).toBeVisible();
|
|
await expect(page.getByRole('progressbar', { name: t.careerOverviewCompletenessLabel })).toHaveAttribute('aria-valuenow', String(before.completeness.percent));
|
|
const recent = page.getByRole('region', { name: t.careerOverviewRecentCvs, exact: true });
|
|
await expect(recent.getByRole('link', { name: t.cvDashboardOpen, exact: true })).toHaveCount(3);
|
|
const links = await recent.getByRole('link', { name: t.cvDashboardOpen, exact: true }).evaluateAll(nodes => nodes.map(node => node.getAttribute('href')));
|
|
expect(new Set(links).size).toBe(3);
|
|
expect(links).toEqual(authoritativeRecent);
|
|
await capture(page, info, sample, 'populated-overview');
|
|
await navigateSection(page, sample.language, 'evidence');
|
|
await expect(page.getByRole('heading', { name: t.careerEvidenceTitle, level: 2 })).toBeVisible();
|
|
await expect(page.getByText(evidence.title, { exact: true })).toBeVisible();
|
|
await expect(page.getByLabel(t.careerEvidenceStatement)).toBeVisible();
|
|
if (sample.width === 1440) await expect(page.getByText('Additional owned evidence 10', { exact: true })).toBeVisible();
|
|
await capture(page, info, sample, 'long-evidence');
|
|
expect(await (await page.request.get(`${apiUrl}/career/profile`)).json()).toEqual(before);
|
|
for (const cv of cvs) expect(await (await page.request.get(`${apiUrl}/cv/variants/${cv.id}`)).json()).toEqual(cv);
|
|
await page.reload();
|
|
await expect(page.getByText(evidence.title, { exact: true })).toBeVisible();
|
|
});
|
|
}
|
|
|
|
for (const failedPath of ['/career/profile', '/profile-cv/runs', '/cv/variants']) {
|
|
test(`[P7-02 PRES-C02 E09] unavailable ${failedPath} stays unknown and retries its reads without writes or false first-run guidance`, async ({ page, owner }, info) => {
|
|
const sample: Sample = { language: 'en', theme: 'dark', width: 390, height: 844 };
|
|
const actualProfile = await (await page.request.get(`${apiUrl}/career/profile`)).json();
|
|
let fail = true;
|
|
let reads = 0;
|
|
await page.route(`**/api${failedPath}`, async route => {
|
|
if (route.request().method() === 'GET') {
|
|
reads++;
|
|
if (fail) return route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ message: 'Controlled source read unavailable' }) });
|
|
}
|
|
return route.continue();
|
|
});
|
|
await enter(page, owner, sample);
|
|
const t = translations.en;
|
|
const message = failedPath === '/career/profile' ? t.careerOverviewUnavailable : failedPath === '/profile-cv/runs' ? t.careerOverviewImportUnavailable : t.careerOverviewRecentFailed;
|
|
const alert = page.getByRole('alert').filter({ hasText: message });
|
|
await expect(alert).toBeVisible();
|
|
await expect(page.getByText(t.careerOverviewFirstRun, { exact: true })).toHaveCount(0);
|
|
if (failedPath === '/career/profile') await expect(page.getByRole('progressbar', { name: t.careerOverviewCompletenessLabel })).toHaveCount(0);
|
|
if (failedPath === '/profile-cv/runs') await expect(page.getByText(t.careerOverviewNoImport, { exact: true })).toHaveCount(0);
|
|
if (failedPath === '/cv/variants') await expect(page.getByText(t.careerOverviewNoCvs, { exact: true })).toHaveCount(0);
|
|
await capture(page, info, sample, `unavailable-${failedPath.split('/').pop()}`);
|
|
const writes: string[] = [];
|
|
page.on('request', request => { if (request.url().startsWith(apiUrl) && !['GET', 'HEAD', 'OPTIONS'].includes(request.method())) writes.push(request.method() + ' ' + request.url()); });
|
|
const beforeReads = reads;
|
|
fail = false;
|
|
await alert.getByRole('button', { name: t.retry, exact: true }).focus();
|
|
await page.keyboard.press('Enter');
|
|
await expect(alert).toHaveCount(0);
|
|
const focusRegion = failedPath === '/career/profile' ? t.careerOverviewProfileStatus : failedPath === '/profile-cv/runs' ? t.careerOverviewCvImport : t.careerOverviewRecentCvs;
|
|
const focusLabel = failedPath === '/career/profile' ? t.careerOverviewImproveProfile : failedPath === '/profile-cv/runs' ? t.careerOverviewImportCv : t.careerOverviewViewAll;
|
|
await expect(page.getByRole('region', { name: focusRegion, exact: true }).getByRole('link', { name: focusLabel, exact: true })).toBeFocused();
|
|
await expect(page.getByRole('progressbar', { name: t.careerOverviewCompletenessLabel })).toHaveAttribute('aria-valuenow', String(actualProfile.completeness.percent));
|
|
expect(reads).toBe(beforeReads + 1);
|
|
expect(writes).toEqual([]);
|
|
});
|
|
}
|
|
|
|
test('[P7-02 PRES-C04 E09] evidence pending/failure/read retry and failed save preserve drafts across section changes, cancellation and actual deletion', async ({ page, owner }, info) => {
|
|
test.setTimeout(120_000);
|
|
const sample: Sample = { language: 'en', theme: 'light', width: 390, height: 844 };
|
|
expect((await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: {
|
|
profile: { version: '1', contact: { fullName: 'Independent canonical source' }, skills: ['Original source fact'] }, cvText: 'Original raw source',
|
|
} })).status()).toBe(200);
|
|
const canonicalBefore = await (await page.request.get(`${apiUrl}/career/profile`)).json();
|
|
const retainedCvs: CvVariant[] = [];
|
|
for (const name of ['Selected CV', 'Sibling CV']) {
|
|
const response = await page.request.post(`${apiUrl}/cv/variants`, { headers: owner.headers, data: { name } });
|
|
expect(response.status()).toBe(200); retainedCvs.push(await response.json());
|
|
}
|
|
let readFailure = true;
|
|
let holdRead = false;
|
|
let releaseRead!: () => void;
|
|
const delayedRead = new Promise<void>(resolve => { releaseRead = resolve; });
|
|
let saveFailure = true;
|
|
const payloads: unknown[] = [];
|
|
await page.route('**/api/career/evidence', async route => {
|
|
const request = route.request();
|
|
if (request.method() === 'GET' && readFailure) return route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ message: 'Controlled evidence read unavailable' }) });
|
|
if (request.method() === 'GET' && holdRead) await delayedRead;
|
|
if (request.method() === 'POST') {
|
|
payloads.push(request.postDataJSON());
|
|
if (saveFailure) return route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ message: 'Controlled evidence write unavailable' }) });
|
|
}
|
|
return route.continue();
|
|
});
|
|
await enter(page, owner, sample, 'evidence');
|
|
const t = translations.en;
|
|
const readAlert = page.getByRole('alert').filter({ hasText: 'Controlled evidence read unavailable' });
|
|
await expect(readAlert).toBeVisible();
|
|
await expect(page.getByText(t.careerEvidenceEmpty, { exact: true })).toHaveCount(0);
|
|
readFailure = false;
|
|
holdRead = true;
|
|
await readAlert.getByRole('button', { name: t.retry, exact: true }).click();
|
|
const bank = page.getByRole('region', { name: t.careerEvidenceTitle, exact: true });
|
|
await expect(bank.getByText(t.loading, { exact: true })).toBeVisible();
|
|
await expect(page.getByText(t.careerEvidenceEmpty, { exact: true })).toHaveCount(0);
|
|
await expect(page.getByRole('button', { name: t.careerEvidenceSave, exact: true })).toBeDisabled();
|
|
await capture(page, info, sample, 'pending-evidence-read');
|
|
releaseRead();
|
|
await expect(page.getByText(t.careerEvidenceEmpty, { exact: true })).toBeVisible();
|
|
await page.getByLabel(t.careerEvidenceItemTitle).fill('Reviewed project — æøå');
|
|
await page.getByLabel(t.careerEvidenceStatement).fill('A fact supplied by the owner.');
|
|
await page.getByLabel(t.careerEvidenceTags, { exact: true }).fill('C#, Norsk');
|
|
await page.getByLabel(t.careerEvidenceSource, { exact: true }).fill('Project retrospective');
|
|
await page.getByRole('button', { name: t.careerEvidenceSave, exact: true }).click();
|
|
const saveAlert = page.getByRole('alert').filter({ hasText: 'Controlled evidence write unavailable' });
|
|
await expect(saveAlert).toBeVisible();
|
|
await expect(saveAlert.getByRole('button', { name: t.retry, exact: true })).toHaveCount(0);
|
|
await capture(page, info, sample, 'failed-save-retained-draft');
|
|
await navigateSection(page, 'en', 'overview');
|
|
await navigateSection(page, 'en', 'evidence');
|
|
await expect(page.getByLabel(t.careerEvidenceItemTitle)).toHaveValue('Reviewed project — æøå');
|
|
await expect(page.getByLabel(t.careerEvidenceStatement)).toHaveValue('A fact supplied by the owner.');
|
|
saveFailure = false;
|
|
const saved = page.waitForResponse(response => response.url() === `${apiUrl}/career/evidence` && response.request().method() === 'POST');
|
|
await page.getByRole('button', { name: t.careerEvidenceSave, exact: true }).focus();
|
|
await page.keyboard.press('Enter');
|
|
expect((await saved).status()).toBe(200);
|
|
expect(payloads).toEqual(Array(2).fill({ category: 'achievement', title: 'Reviewed project — æøå', statement: 'A fact supplied by the owner.', tags: ['C#', ' Norsk'], sourceType: 'manual', sourceReference: 'Project retrospective', isVerified: true }));
|
|
const [item] = await (await page.request.get(`${apiUrl}/career/evidence`)).json();
|
|
await expect(page.getByText(item.title, { exact: true })).toBeVisible();
|
|
await expect(page.getByLabel(t.careerEvidenceItemTitle)).toHaveValue('');
|
|
expect(await (await page.request.get(`${apiUrl}/career/profile`)).json()).toEqual(canonicalBefore);
|
|
for (const cv of retainedCvs) expect(await (await page.request.get(`${apiUrl}/cv/variants/${cv.id}`)).json()).toEqual(cv);
|
|
const row = page.getByRole('article').filter({ hasText: item.title });
|
|
const remove = row.getByRole('button', { name: t.careerEvidenceDeleteMessage.replace('{title}', item.title), exact: true });
|
|
await remove.focus();
|
|
await page.keyboard.press('Enter');
|
|
const dialog = page.getByRole('dialog', { name: t.careerEvidenceDeleteTitle, exact: true });
|
|
await expect(dialog).toBeVisible();
|
|
await dialog.getByRole('button', { name: t.cancel, exact: true }).click();
|
|
await expect(remove).toBeFocused();
|
|
expect(await (await page.request.get(`${apiUrl}/career/evidence`)).json()).toEqual([item]);
|
|
let deleteFailure = true;
|
|
await page.route(`**/api/career/evidence/${item.id}`, route => route.request().method() === 'DELETE' && deleteFailure
|
|
? route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ message: 'Controlled evidence deletion unavailable' }) }) : route.continue());
|
|
await remove.click();
|
|
const failedDelete = page.waitForResponse(response => response.url() === `${apiUrl}/career/evidence/${item.id}` && response.request().method() === 'DELETE');
|
|
await dialog.getByRole('button', { name: t.deleteAction, exact: true }).click();
|
|
expect((await failedDelete).status()).toBe(500);
|
|
const deleteAlert = row.getByRole('alert').filter({ hasText: 'Controlled evidence deletion unavailable' });
|
|
await expect(deleteAlert).toBeVisible();
|
|
expect(await (await page.request.get(`${apiUrl}/career/evidence`)).json()).toEqual([item]);
|
|
await capture(page, info, sample, 'failed-delete-retained-row');
|
|
deleteFailure = false;
|
|
await deleteAlert.getByRole('button', { name: t.retry, exact: true }).focus();
|
|
await page.keyboard.press('Enter');
|
|
await expect(dialog).toBeVisible();
|
|
const deleted = page.waitForResponse(response => response.url() === `${apiUrl}/career/evidence/${item.id}` && response.request().method() === 'DELETE');
|
|
await dialog.getByRole('button', { name: t.deleteAction, exact: true }).click();
|
|
expect((await deleted).status()).toBe(204);
|
|
await expect(page.getByText(t.careerEvidenceEmpty, { exact: true })).toBeVisible();
|
|
await expect(page.getByLabel(t.careerEvidenceItemTitle)).toBeFocused();
|
|
await page.reload();
|
|
await expect(page.getByText(t.careerEvidenceEmpty, { exact: true })).toBeVisible();
|
|
expect(await (await page.request.get(`${apiUrl}/career/profile`)).json()).toEqual(canonicalBefore);
|
|
for (const cv of retainedCvs) expect(await (await page.request.get(`${apiUrl}/cv/variants/${cv.id}`)).json()).toEqual(cv);
|
|
});
|
|
|
|
test('[P7-02 PRES-C01 C02] existing raw source copy and history entry remain read-only', async ({ page, owner, context }, info) => {
|
|
const sample: Sample = { language: 'en', theme: 'light', width: 1440, height: 900 };
|
|
const profile = { version: '1', contact: { fullName: 'Canonical source' }, skills: ['Recorded skill'] };
|
|
const raw = 'Exact raw source — æøå\nA supported owner claim.\n';
|
|
expect((await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: { profile: { ...profile, skills: ['Recorded skill', 'Recorded later skill'] }, cvText: raw } })).status()).toBe(200);
|
|
expect((await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: { profile, cvText: raw } })).status()).toBe(200);
|
|
const before = await (await page.request.get(`${apiUrl}/career/profile`)).json();
|
|
const history = await (await page.request.get(`${apiUrl}/career/profile/versions`)).json();
|
|
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
|
|
await enter(page, owner, sample);
|
|
await navigateSection(page, 'en', 'import');
|
|
const t = translations.en;
|
|
await page.getByRole('button', { name: new RegExp(t.profileCvRawPanelTitle) }).click();
|
|
await page.getByRole('button', { name: t.profileCopyCvText, exact: true }).click();
|
|
const copied = await page.evaluate(() => navigator.clipboard.readText());
|
|
await info.attach('native-clipboard-line-endings', { body: JSON.stringify({ requestedSource: raw, copied,
|
|
requestedCodePoints: [...raw].map(value => value.codePointAt(0)), copiedCodePoints: [...copied].map(value => value.codePointAt(0)),
|
|
qualification: 'Windows clipboard may convert LF to CRLF; preserve exact text after this explicit platform normalization, without trimming or other changes' }), contentType: 'application/json' });
|
|
expect(copied.replace(/\r\n/g, '\n')).toBe(raw);
|
|
await navigateSection(page, 'en', 'profile');
|
|
const label = t.profileCvVersionHistory.replace('{count}', String(history.length));
|
|
await page.getByRole('button', { name: label, exact: true }).click();
|
|
await expect(page.getByRole('button', { name: new RegExp(`^${t.careerRestoreVersion.replace('{version}', '\\d+')}$`) }).first()).toBeVisible();
|
|
expect(await (await page.request.get(`${apiUrl}/career/profile`)).json()).toEqual(before);
|
|
expect(await (await page.request.get(`${apiUrl}/career/profile/versions`)).json()).toEqual(history);
|
|
await info.attach('native-career-source-history-entry', { body: JSON.stringify({ clipboardContentPreservedWithExplicitWindowsLineEndingNormalization: true, historyEntryVisible: true,
|
|
canonicalAndHistoryUnchanged: true, qualification: 'Browser clipboard permission granted; exact OS clipboard bytes/human acceptance not claimed; restore/save/import-review migration is P7-03' }), contentType: 'application/json' });
|
|
});
|