fix(rd21): retain draft revisions and qualify conflict recovery
This commit is contained in:
@@ -23,7 +23,7 @@ const test = base.extend<{ accounts: { a: Account; b: Account } }>({
|
||||
expect((await admin.api.post(`${apiUrl}/users`, { headers: admin.headers, data: { email, password, roles: [] } })).status()).toBe(200);
|
||||
const account = await signIn(playwright, email);
|
||||
accounts.push(account);
|
||||
expect((await account.api.put(`${apiUrl}/career/profile`, { headers: account.headers, data: profile(`Private ${name.toUpperCase()}`) })).status()).toBe(200);
|
||||
expect((await account.api.put(`${apiUrl}/career/profile`, { headers: account.headers, data: { ...profile(`Private ${name.toUpperCase()}`), expectedVersion: (await (await account.api.get(`${apiUrl}/career/profile`)).json()).revision } })).status()).toBe(200);
|
||||
}
|
||||
await provide({ a: accounts[0], b: accounts[1] });
|
||||
} finally {
|
||||
@@ -52,7 +52,7 @@ test("retained profile request from A cannot write using B cookies and fresh B C
|
||||
expect((await browserSession.api.post(`${apiUrl}/auth/login`, { data: { email: b.email, password } })).status()).toBe(200);
|
||||
const csrfB = (await browserSession.api.storageState()).cookies.find(cookie => cookie.name === "XSRF-TOKEN")!.value;
|
||||
const response = await browserSession.api.put(`${apiUrl}/career/profile`, {
|
||||
headers: { "X-CSRF-TOKEN": csrfB, "X-Session-Context": staleContext }, data: retained,
|
||||
headers: { "X-CSRF-TOKEN": csrfB, "X-Session-Context": staleContext }, data: { ...retained, expectedVersion: retained.revision },
|
||||
});
|
||||
const persisted = { status: response.status(), a: await storedName(a), b: await storedName(b) };
|
||||
await info.attach("retained-request-persistence", { body: JSON.stringify(persisted, null, 2), contentType: "application/json" });
|
||||
@@ -66,7 +66,7 @@ test("missing and forged session contexts fail closed while same-account profile
|
||||
for (const sessionContext of [undefined, a.sessionContext, a.id, "crafted-session"]) {
|
||||
const response = await b.api.put(`${apiUrl}/career/profile`, {
|
||||
headers: { "X-CSRF-TOKEN": b.headers["X-CSRF-TOKEN"], ...(sessionContext ? { "X-Session-Context": sessionContext } : {}) },
|
||||
data: { ...profile("forged"), userId: a.id, ownerUserId: a.id, accountId: a.id },
|
||||
data: { expectedVersion: (await (await b.api.get(`${apiUrl}/career/profile`)).json()).revision, ...profile("forged"), userId: a.id, ownerUserId: a.id, accountId: a.id },
|
||||
});
|
||||
expect(response.status()).toBe(sessionContext ? 409 : 428);
|
||||
}
|
||||
@@ -76,7 +76,7 @@ test("missing and forged session contexts fail closed while same-account profile
|
||||
const singletonRead = await b.api.get(`${apiUrl}/career/profile?ownerUserId=${a.id}`);
|
||||
expect((await singletonRead.json()).profile.contact.fullName).toBe("Private B");
|
||||
const saved = await b.api.put(`${apiUrl}/career/profile`, {
|
||||
headers: b.headers, data: { ...profile("Legitimate B"), userId: a.id, ownerUserId: a.id, roles: ["Admin"], plan: "pro" },
|
||||
headers: b.headers, data: { expectedVersion: (await (await b.api.get(`${apiUrl}/career/profile`)).json()).revision, ...profile("Legitimate B"), userId: a.id, ownerUserId: a.id, roles: ["Admin"], plan: "pro" },
|
||||
});
|
||||
expect(saved.status()).toBe(200); // Over-posted ownership/security fields are not DTO properties.
|
||||
expect(await storedName(a)).toBe("Private A");
|
||||
|
||||
@@ -34,7 +34,7 @@ test('[PRES-S01 S02 E07 E08 E09 J05 J09] Analytics native reports, direct entry,
|
||||
const data = { companyId, jobTitle: `Utvikler og faglig rådgiver med lang stillingstittel ${index}`, status: ['Applied', 'Interview', 'Offer', 'Faglig vurdering', 'Saved', 'Rejected'][index], dateApplied: index === 4 ? null : date, tags: index === 4 ? null : 'Azure, TypeScript, Drift, Test, Design, Data, Plattform, Sikkerhet, Analyse, Integrasjon, Norsk, Samarbeid', ...(index % 2 ? {} : { salaryMin: 600000, salaryMax: 800000, salaryCurrency: 'NOK', salaryPeriod: 'year' }) };
|
||||
const created = await page.request.post(`${apiUrl}/jobapplications`, { headers, data });
|
||||
expect(created.status()).toBe(201); ids.push((await created.json()).id);
|
||||
if (index === 1) expect((await page.request.put(`${apiUrl}/jobapplications/${ids[index]}`, { headers, data: { ...data, responseReceived: true, responseDate: date } })).status()).toBe(204);
|
||||
if (index === 1) expect((await page.request.put(`${apiUrl}/jobapplications/${ids[index]}`, { headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/jobapplications/${ids[index]}`)).json()).version, ...data, responseReceived: true, responseDate: date } })).status()).toBe(204);
|
||||
}
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await page.goto('/analytics?months=6');
|
||||
|
||||
@@ -109,7 +109,7 @@ for (const [language, mode] of [['en', 'light'], ['nb', 'dark']] as const) {
|
||||
const headers = await login(page.request, email);
|
||||
expect((await page.request.put(`${apiUrl}/auth/preferences/language`, { headers, data: { language: language === 'nb' ? 'nb-NO' : 'en' } })).status()).toBe(204);
|
||||
const profile = { version: '1', contact: { fullName: 'Åse Zoom Fixture', headline: 'Documented application workflows', location: 'Oslo' }, summary: ['Supported source summary.'], skills: ['C#', 'React'], jobs: [{ id: 'zoom-role', title: 'Engineer', company: 'Synthetic company', bullets: ['Built and maintained internal web applications.'] }] };
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { profile } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile } })).status()).toBe(200);
|
||||
const source = await (await page.request.get(`${apiUrl}/career/profile`)).json();
|
||||
const company = await page.request.post(`${apiUrl}/companies`, { headers, data: { name: 'Representative Norwegian company — æøå and long title' } });
|
||||
expect(company.status()).toBe(201);
|
||||
|
||||
@@ -23,7 +23,7 @@ const test = base.extend<{ owner: { headers: Record<string, string>; shortId: nu
|
||||
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();
|
||||
});
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { profile: {
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile: {
|
||||
version: "1", contact: { fullName: "Åse Eksempel", headline: "Canonical engineer" }, skills: ["Norsk", "C#"],
|
||||
} } })).status()).toBe(200);
|
||||
const ids: number[] = [];
|
||||
|
||||
@@ -41,7 +41,7 @@ for (const [mode, language] of [['light', 'en'], ['dark', 'nb'], ['light', 'nb']
|
||||
education: [{ id: 'education-1', qualification: 'Computer science', institution: 'Fixture University', startDate: '2016-08', endDate: '2020-06' }],
|
||||
projects: [{ id: 'project-1', name: 'Evidence workspace', bullets: ['Documented source-grounded reviews.'] }], skills: ['C#', 'React', 'Norsk'],
|
||||
};
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { profile } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile } })).status()).toBe(200);
|
||||
const source = await (await page.request.get(`${apiUrl}/career/profile`)).json();
|
||||
const company = await page.request.post(`${apiUrl}/companies`, { headers, data: { name: 'Nordlys fixture AS' } });
|
||||
expect(company.status()).toBe(201);
|
||||
|
||||
@@ -48,7 +48,7 @@ for (const [mode, language] of [['light', 'en'], ['dark', 'nb']] as const) {
|
||||
? 'Utviklet pålitelige arbeidsflyter med tilgjengelig navigasjon, dokumentert kildegrunnlag og tydelige vurderinger for norske arbeidssøkere.'
|
||||
: 'Developed reliable workflows with accessible navigation, documented source evidence and explicit review for people managing their careers.';
|
||||
const shortProfile = { version: '1', contact: { fullName: 'Åse Eksempel', headline: 'Canonical engineer — æøå', location: 'Oslo, Norge' }, summary: ['Documented career evidence.'], skills: ['C#', 'React', 'Norsk'] };
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { profile: shortProfile } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile: shortProfile } })).status()).toBe(200);
|
||||
const create = async (name: string, themeId: string, customItems: string[] = []): Promise<CvVariant> => {
|
||||
const response = await page.request.post(`${apiUrl}/cv/variants`, { headers, data: { name, settings: {
|
||||
themeId, language, sections: [{ key: 'summary' }, { key: 'experience' }, { key: 'skills' }, ...(customItems.length ? [{ key: 'custom:preview-evidence' }] : [])],
|
||||
@@ -167,7 +167,7 @@ for (const [mode, language] of [['light', 'en'], ['dark', 'nb']] as const) {
|
||||
|
||||
const longProfile = { ...shortProfile, jobs: Array.from({ length: 3 }, (_, index) => ({ id: `long-job-${index + 1}`, title: `Source-grounded engineer ${index + 1} — æøå`, company: 'Nordlys Fixture AS', startDate: '2017-01', endDate: '2025-12',
|
||||
bullets: Array.from({ length: 30 }, (_, bullet) => `Source contribution ${index * 30 + bullet + 1}: ${sentence} ${sentence}`) })) };
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { profile: longProfile } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile: longProfile } })).status()).toBe(200);
|
||||
const source = await (await page.request.get(`${apiUrl}/career/profile`)).json();
|
||||
const veryLong = await create('Very long source and custom CV — æøå', 'minimal', Array.from({ length: 45 }, (_, index) => `CV-only long contribution ${index + 1}: ${sentence} ${sentence}`));
|
||||
const unchanged = async () => {
|
||||
|
||||
@@ -60,7 +60,7 @@ test("[PRES-C06 C08 C09 C12] native preview/PDF fidelity corpus records unchange
|
||||
const results = [];
|
||||
for (const fixture of fixtures) {
|
||||
const profile = fixture.long ? longProfile(fixture.language as "en" | "nb") : shortProfile;
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { profile } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile } })).status()).toBe(200);
|
||||
const create = await page.request.post(`${apiUrl}/cv/variants`, { headers, data: { name: fixture.key, settings: {
|
||||
themeId: fixture.themeId, language: fixture.language, sections: [{ key: "summary" }, { key: "experience" }, { key: "education" }, { key: "projects" }, { key: "skills" }, { key: "languages" }, { key: "interests", hidden: true }], ...fixture.settings,
|
||||
} } });
|
||||
|
||||
@@ -35,7 +35,7 @@ async function seed(page: Page, context: BrowserContext, admin: APIRequestContex
|
||||
education: [{ id: 'education-1', qualification: 'Computer science', institution: 'Fixture University', startDate: '2013-08', endDate: '2016-06' }],
|
||||
projects: [{ id: 'project-1', name: 'Evidence workspace', bullets: [sentence] }, { id: 'project-2', name: 'Accessible integration', bullets: [sentence] }], skills: ['C#', 'React', 'Norsk'],
|
||||
};
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { profile } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile } })).status()).toBe(200);
|
||||
const source = await (await page.request.get(`${apiUrl}/career/profile`)).json();
|
||||
const outline: CvOutline = await (await page.request.get(`${apiUrl}/cv/outline`)).json();
|
||||
const sections = ['summary', 'experience', 'education', 'projects', 'skills', 'certifications', 'languages', 'awards', 'publications', 'organisations', 'interests', 'references'].map(key => ({ key, hidden: !outline.sections.some(section => section.key === key) }));
|
||||
|
||||
@@ -51,7 +51,7 @@ for (const [mode, language] of [['light', 'en'], ['dark', 'nb'], ['light', 'nb']
|
||||
education: [{ id: 'education-1', qualification: 'Computer science', institution: 'Fixture University', startDate: '2013-08', endDate: '2016-06' }],
|
||||
projects: [{ id: 'project-1', name: 'Evidence workspace', bullets: ['Documented source-grounded reviews.'] }], skills: ['C#', 'React', 'Norsk'],
|
||||
};
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { profile } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile } })).status()).toBe(200);
|
||||
const source = await (await page.request.get(`${apiUrl}/career/profile`)).json();
|
||||
const outline: CvOutline = await (await page.request.get(`${apiUrl}/cv/outline`)).json();
|
||||
const entries = outline.sections.find(section => section.key === 'experience')!.entries;
|
||||
@@ -141,7 +141,7 @@ for (const [mode, language] of [['light', 'en'], ['dark', 'nb'], ['light', 'nb']
|
||||
await expect.poll(async () => (await read()).settings.overrides[first.key!].title).toBe('Retained CV title');
|
||||
await saved();
|
||||
const changedProfile = { ...profile, jobs: profile.jobs.map(job => job.id === 'job-1' ? { ...job, title: 'Updated source title', company: 'Updated source employer', bullets: ['Updated source evidence — æøå.'] } : job) };
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { profile: changedProfile } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile: changedProfile } })).status()).toBe(200);
|
||||
const changedSource = await (await page.request.get(`${apiUrl}/career/profile`)).json();
|
||||
const beforeReload = await read();
|
||||
await page.reload();
|
||||
|
||||
@@ -92,7 +92,7 @@ for (const [mode, language, premium] of [['light', 'en', false], ['dark', 'nb',
|
||||
const profile = { version: '1', contact: { fullName: 'Åse Fixture', headline: 'Verified career information', location: 'Oslo, Norge' }, summary: ['R01 Verified source content — æøå.'],
|
||||
jobs: Array.from({ length: 5 }, (_, index) => ({ id: `job-${index}`, title: `E${String(index + 1).padStart(2, '0')} Plattformutvikler og dokumenterte integrasjoner`, company: 'Nordlys Fixture AS', startDate: '2020-01', endDate: '2025-01', bullets: Array.from({ length: 3 }, (_, bullet) => `B${String(index * 3 + bullet + 1).padStart(2, '0')} ${sentence.repeat(5)}`) })),
|
||||
education: [{ id: 'edu-1', qualification: 'E08 Computer science', institution: 'Fixture University', startDate: '2016-08', endDate: '2020-06' }], skills: ['React', 'C#', 'Norsk'], interests: ['HIDDEN-MARKER Not included'] };
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { profile } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile } })).status()).toBe(200);
|
||||
const source = await (await page.request.get(`${apiUrl}/career/profile`)).json();
|
||||
const created = await page.request.post(`${apiUrl}/cv/variants`, { headers, data: { name: 'Dokumenterte kvalifikasjoner — innstillinger for denne CV-en', settings: {
|
||||
themeId: 'minimal', pageMarginMm: 16, baseFontSizePt: 10, bodyFont: 'Arial, Helvetica, sans-serif', accentColor: '#126b55', language: language === 'nb' ? 'nb-NO' : 'en',
|
||||
|
||||
@@ -34,7 +34,7 @@ test('[PRES-S01 S02 E07 E08 E09] Dashboard native data, visibility and responsiv
|
||||
expect(created.status()).toBe(201);
|
||||
if (index === 2 || index === 3) {
|
||||
const id = (await created.json()).id;
|
||||
expect((await page.request.put(`${apiUrl}/jobapplications/${id}`, { headers, data: { ...data, responseReceived: true, responseDate: date } })).status()).toBe(204);
|
||||
expect((await page.request.put(`${apiUrl}/jobapplications/${id}`, { headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/jobapplications/${id}`)).json()).version, ...data, responseReceived: true, responseDate: date } })).status()).toBe(204);
|
||||
}
|
||||
}
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
|
||||
@@ -86,7 +86,7 @@ test('[PRES-W01 W02 W03 J05 E08 E09] native Job header/Overview retains actions,
|
||||
}
|
||||
// Existing native status write, not a new header status API or inferred progression.
|
||||
await page.goto('about:blank');
|
||||
expect((await page.request.put(`${apiUrl}/jobapplications/${id}`, { headers, data: { ...jobData, status: 'Faglig vurdering' } })).status()).toBe(204);
|
||||
expect((await page.request.put(`${apiUrl}/jobapplications/${id}`, { headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/jobapplications/${id}`)).json()).version, ...jobData, status: 'Faglig vurdering' } })).status()).toBe(204);
|
||||
await page.goto(`/applications/${id}?section=overview`);
|
||||
const custom = page.getByRole('region', { name: `${translations.nb.workspaceProgress}: Faglig vurdering` });
|
||||
await expect(custom).toContainText(translations.nb.workspaceCustomProgress);
|
||||
|
||||
@@ -71,7 +71,7 @@ for (const language of ['en', 'nb'] as const) for (const mode of ['light', 'dark
|
||||
await page.route('**/*', route => ['localhost', '127.0.0.1'].includes(new URL(route.request().url()).hostname) || ['blob:', 'data:', 'about:'].includes(new URL(route.request().url()).protocol) ? route.continue() : route.abort());
|
||||
await page.addInitScript(({ language, mode }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', mode); }, { language, mode });
|
||||
expect((await page.request.put(`${apiUrl}/auth/preferences/language`, { headers, data: { language: language === 'nb' ? 'nb-NO' : 'en' } })).status()).toBe(204);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { profile: { version: '1', contact: { fullName: 'Åse Eksempel', headline: 'Recorded engineer' }, skills: ['C#', 'SQL'] } } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile: { version: '1', contact: { fullName: 'Åse Eksempel', headline: 'Recorded engineer' }, skills: ['C#', 'SQL'] } } })).status()).toBe(200);
|
||||
const company = await page.request.post(`${apiUrl}/companies`, { headers, data: { name: 'Nordlys samfunnskritiske plattformer og inkluderende digitale tjenester æøå' } });
|
||||
expect(company.status()).toBe(201); const companyId = (await company.json()).id;
|
||||
const title = 'Senior utvikler og rådgiver for langsiktig plattformutvikling';
|
||||
@@ -81,8 +81,8 @@ for (const language of ['en', 'nb'] as const) for (const mode of ['light', 'dark
|
||||
const cv = await page.request.post(`${apiUrl}/cv/variants`, { headers, data: { name: 'Application CV — Åse', jobApplicationId: id } });
|
||||
expect(cv.status()).toBe(200); const variant = await cv.json();
|
||||
expect((await page.request.put(`${jobUrl}/cv`, { headers, data: { variantId: variant.id } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${jobUrl}/cover-letter`, { headers, data: { text: 'Reviewed letter — æøå', source: 'manual' } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${jobUrl}/application-drafts`, { headers, data: { applicationAnswerDraft: 'Reviewed answer — æøå', recruiterMessageDraft: 'Message draft; never sent' } })).status()).toBe(204);
|
||||
expect((await page.request.put(`${jobUrl}/cover-letter`, { headers, data: { expectedVersion: (await (await page.request.get(`${jobUrl}/cover-letter`)).json()).revision, text: 'Reviewed letter — æøå', source: 'manual' } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${jobUrl}/application-drafts`, { headers, data: { expectedVersion: (await (await page.request.get(`${jobUrl}`)).json()).version, applicationAnswerDraft: 'Reviewed answer — æøå', recruiterMessageDraft: 'Message draft; never sent' } })).status()).toBe(204);
|
||||
expect((await page.request.post(`${jobUrl}/checklist`, { headers, data: { title: 'Review employer questions — æøå', category: 'custom' } })).status()).toBe(200);
|
||||
expect((await page.request.post(`${jobUrl}/interview-prep`, { headers, data: { title: 'Ask about sustainable delivery', category: 'question', content: 'My recorded question', source: 'manual' } })).status()).toBe(200);
|
||||
expect((await page.request.post(`${apiUrl}/correspondence`, { headers, data: { jobApplicationId: id, from: 'Company', subject: 'Interview invitation — æøå', content: 'Recorded invitation; no inferred company facts', channel: 'Email' } })).ok()).toBe(true);
|
||||
|
||||
@@ -23,7 +23,7 @@ const test = base.extend<{ owner: { headers: Record<string, string>; companyId:
|
||||
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();
|
||||
});
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { profile: { version: "1", contact: { fullName: "Åse Eksempel" }, skills: ["Norsk", "C#"] } } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile: { version: "1", contact: { fullName: "Åse Eksempel" }, skills: ["Norsk", "C#"] } } })).status()).toBe(200);
|
||||
const company = await page.request.post(`${apiUrl}/companies`, { headers, data: { name: "Nordlys æøå AS" } });
|
||||
expect(company.status()).toBe(201);
|
||||
const companyId = (await company.json()).id;
|
||||
|
||||
@@ -143,7 +143,7 @@ for (const sample of matrix) {
|
||||
jobs: [{ title: 'Recorded engineer', company: 'Source-only employer', bullets: ['Recorded work, never an account field'] }],
|
||||
skills: ['C#', 'Norsk'],
|
||||
};
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: { profile: canonical } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile: canonical } })).status()).toBe(200);
|
||||
const cvIds: number[] = [];
|
||||
for (const name of ['First protected CV', 'Sibling protected CV']) {
|
||||
const created = await page.request.post(`${apiUrl}/cv/variants`, { headers: owner.headers,
|
||||
|
||||
@@ -13,7 +13,7 @@ const password = 'E2ePassword123!';
|
||||
type Owner = { id: string; headers: Record<string, string> };
|
||||
type Sample = { language: 'en' | 'nb'; theme: 'light' | 'dark'; width: number; height: number };
|
||||
type Subsection = 'cv' | 'letter' | 'answers' | 'submission';
|
||||
type Job = { id: number; jobTitle: string; notes: string | null };
|
||||
type Job = { id: number; jobTitle: string; notes: string | null; version: number };
|
||||
type RequestRecord = { method: string; path: string; body: unknown };
|
||||
type Session = { owner: Owner; admin: APIRequestContext; administrator: Owner; requests: RequestRecord[] };
|
||||
const desktop: Sample = { language: 'en', theme: 'light', width: 1440, height: 900 };
|
||||
@@ -86,7 +86,7 @@ function profile(headline = 'Original reviewed career evidence') {
|
||||
summary: ['Canonical owner-reviewed evidence — æøå'], skills: ['C#', 'SQL'] };
|
||||
}
|
||||
async function source(api: APIRequestContext, owner: Owner, headline?: string) {
|
||||
return json(await api.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: { profile: profile(headline), cvText: 'Exact owner-reviewed source — æøå' } }));
|
||||
return json(await api.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: { expectedVersion: (await (await api.get(`${apiUrl}/career/profile`)).json()).revision, profile: profile(headline), cvText: 'Exact owner-reviewed source — æøå' } }));
|
||||
}
|
||||
async function createJob(api: APIRequestContext, owner: Owner, title = 'Recorded application material — æøå') {
|
||||
const company = await json<{ id: number }>(await api.post(`${apiUrl}/companies`, { headers: owner.headers, data: { name: 'Recorded application company — æøå', source: 'User' } }), 201);
|
||||
@@ -100,10 +100,10 @@ async function variant(api: APIRequestContext, owner: Owner, name: string) {
|
||||
} }));
|
||||
}
|
||||
async function saveLetter(api: APIRequestContext, owner: Owner, id: number, text: string) {
|
||||
return json<CoverLetter>(await api.put(`${jobUrl(id)}/cover-letter`, { headers: owner.headers, data: { text, source: 'manual' } }));
|
||||
return json<CoverLetter>(await api.put(`${jobUrl(id)}/cover-letter`, { headers: owner.headers, data: { expectedVersion: (await (await api.get(`${jobUrl(id)}/cover-letter`)).json()).revision, text, source: 'manual' } }));
|
||||
}
|
||||
async function saveDrafts(api: APIRequestContext, owner: Owner, id: number, answer: string, message: string) {
|
||||
expect((await api.put(`${jobUrl(id)}/application-drafts`, { headers: owner.headers, data: { applicationAnswerDraft: answer, recruiterMessageDraft: message } })).status()).toBe(204);
|
||||
expect((await api.put(`${jobUrl(id)}/application-drafts`, { headers: owner.headers, data: { expectedVersion: (await (await api.get(`${jobUrl(id)}`)).json()).version, applicationAnswerDraft: answer, recruiterMessageDraft: message } })).status()).toBe(204);
|
||||
}
|
||||
async function upload(api: APIRequestContext, owner: Owner, id: number) {
|
||||
const bytes = Buffer.from('Exact archived application evidence — æøå\nOriginal bytes retained.\n', 'utf8');
|
||||
@@ -186,7 +186,7 @@ test('[P7-06 PRES-W01 W04 W10] same-route job navigation waits for owned answers
|
||||
await expect(drafts(page).message).toHaveCount(0);
|
||||
expect(count(session, 'PUT', `/api/jobapplications/${second.id}/application-drafts`)).toHaveLength(0);
|
||||
await capture(page, info, desktop, 'held-second-job-aggregate-no-foreign-drafts');
|
||||
const loaded = response(page, 'GET', workspaceUrl); held.release(); await loaded;
|
||||
const loaded = response(page, 'GET', workspaceUrl); held.release(); const loadedOverview = await json<WorkspaceOverview>(await loaded);
|
||||
await expect(drafts(page).answer).toHaveValue(secondAnswer);
|
||||
await expect(drafts(page).message).toHaveValue(secondMessage);
|
||||
const reviewed = 'Edited second-job answer only — æøå';
|
||||
@@ -201,7 +201,7 @@ test('[P7-06 PRES-W01 W04 W10] same-route job navigation waits for owned answers
|
||||
const persistedFirst = await json<WorkspaceOverview>(await page.request.get(`${jobUrl(first.job.id)}/workspace`));
|
||||
expect(persistedFirst.applicationAnswerDraft).toBe(first.answer);
|
||||
expect(persistedFirst.recruiterMessageDraft).toBe(first.message);
|
||||
expect(count(session, 'PUT', `/api/jobapplications/${second.id}/application-drafts`)).toEqual([{ method: 'PUT', path: `/api/jobapplications/${second.id}/application-drafts`, body: { applicationAnswerDraft: reviewed, recruiterMessageDraft: secondMessage } }]);
|
||||
expect(count(session, 'PUT', `/api/jobapplications/${second.id}/application-drafts`)).toEqual([{ method: 'PUT', path: `/api/jobapplications/${second.id}/application-drafts`, body: { applicationAnswerDraft: reviewed, recruiterMessageDraft: secondMessage, expectedVersion: loadedOverview.revision } }]);
|
||||
await capture(page, info, desktop, 'second-job-answer-saved-first-job-preserved');
|
||||
} finally { held.release(); await page.unroute(workspaceUrl); }
|
||||
});
|
||||
@@ -302,7 +302,7 @@ test('[P7-06 forms] long letter failed held Save retains snapshot and dirty rest
|
||||
await keyboard(page, body.getByRole('button', { name: translations.en.save, exact: true })); await expect.poll(() => saves).toBe(1);
|
||||
await expect(letter(page)).toBeDisabled(); await expect(childFocus(page, translations.en.workspaceCoverLetter)).toBeFocused();
|
||||
await expect(body.getByRole('button', { name: translations.en.assetsDiscardChanges, exact: true })).toBeDisabled();
|
||||
expect(count(session, 'PUT', `/api/jobapplications/${seeded.job.id}/cover-letter`).map(value => value.body)).toEqual([{ text: draft, source: 'manual' }]);
|
||||
expect(count(session, 'PUT', `/api/jobapplications/${seeded.job.id}/cover-letter`).map(value => value.body)).toEqual([{ text: draft, source: 'manual', expectedVersion: before.revision }]);
|
||||
await capture(page, info, desktop, 'held-letter-save'); gate.release();
|
||||
await expect(body.getByRole('alert')).toContainText(translations.en.assetsCoverSaveFailed); await expect(letter(page)).toHaveValue(draft);
|
||||
expect(await json<CoverLetter>(await page.request.get(url))).toEqual(before); expect(saves).toBe(1);
|
||||
@@ -337,7 +337,7 @@ test('[P7-06 forms] failed held Answers save preserves both drafts and ordinary
|
||||
const body = section(page, translations.en.assetsApplicationDrafts);
|
||||
await keyboard(page, body.getByRole('button', { name: translations.en.assetsSaveDrafts, exact: true }));
|
||||
await expect.poll(() => writes).toBe(1); await expect(fields.answer).toBeDisabled(); await expect(fields.message).toBeDisabled(); await expect(childFocus(page, translations.en.assetsApplicationDrafts)).toBeFocused();
|
||||
expect(count(session, 'PUT', `/api/jobapplications/${seeded.job.id}/application-drafts`).map(value => value.body)).toEqual([{ applicationAnswerDraft: answer, recruiterMessageDraft: message }]);
|
||||
expect(count(session, 'PUT', `/api/jobapplications/${seeded.job.id}/application-drafts`).map(value => value.body)).toEqual([{ applicationAnswerDraft: answer, recruiterMessageDraft: message, expectedVersion: original.version }]);
|
||||
await capture(page, info, desktop, 'held-two-draft-save'); gate.release();
|
||||
await expect(body.getByRole('alert')).toContainText(translations.en.assetsDraftsSaveFailed); await expect(fields.answer).toHaveValue(answer); await expect(fields.message).toHaveValue(message);
|
||||
expect((await json<WorkspaceOverview>(await page.request.get(`${jobUrl(seeded.job.id)}/workspace`))).applicationAnswerDraft).toBe(seeded.answer); expect(writes).toBe(1);
|
||||
|
||||
@@ -69,7 +69,7 @@ async function seed(page: Page, owner: Owner) {
|
||||
items: ['Recorded custom fact with relevant professional context.', 'Another recorded custom fact, preserved during unrelated contact editing.'] }],
|
||||
};
|
||||
const raw = 'Exact original career source — æøå\nRecorded work and qualifications.\n';
|
||||
await json<CareerReadback>(await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: { profile, cvText: raw } }));
|
||||
await json<CareerReadback>(await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile, cvText: raw } }));
|
||||
return { canonical: await json<CareerReadback>(await page.request.get(`${apiUrl}/career/profile`)),
|
||||
history: await json<unknown[]>(await page.request.get(`${apiUrl}/career/profile/versions`)) };
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ 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 };
|
||||
type CareerReadback = { profile: StructuredCvProfile; completeness: unknown; cvText: string | null };
|
||||
type CareerReadback = { profile: StructuredCvProfile; completeness: unknown; cvText: string | null; revision: number };
|
||||
type Version = { version: number; source: string; isCurrent: boolean; createdAtUtc: string };
|
||||
type Run = { id: number; status: string; artifactFileName: string | null; operation: unknown };
|
||||
type ReviewDiff = { runId: number; status: string; diff: { categories: { category: string;
|
||||
@@ -98,9 +98,9 @@ async function readSource(api: APIRequestContext) {
|
||||
|
||||
async function seedSource(page: Page, owner: Owner, twoVersions = false) {
|
||||
const raw = 'Exact CURRENT original career source — æøå\nReviewed work and qualifications.\n';
|
||||
await json<CareerReadback>(await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: { profile: source(), cvText: raw } }));
|
||||
await json<CareerReadback>(await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile: source(), cvText: raw } }));
|
||||
if (twoVersions) await json<CareerReadback>(await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers,
|
||||
data: { profile: source('Second reviewed canonical context'), cvText: raw } }));
|
||||
data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile: source('Second reviewed canonical context'), cvText: raw } }));
|
||||
return readSource(page.request);
|
||||
}
|
||||
|
||||
@@ -280,7 +280,7 @@ test('[P7-03 PRES-C01 C02 C03 C04 C05 W07] explicit select/deselect acceptance u
|
||||
await page.keyboard.press('Enter');
|
||||
const firstAccepted = await firstResponse;
|
||||
expect(firstAccepted.status()).toBe(200);
|
||||
expect(firstAccepted.request().postDataJSON()).toEqual({ acceptedLowConfidenceIds: [] });
|
||||
expect(firstAccepted.request().postDataJSON()).toEqual({ acceptedLowConfidenceIds: [], expectedVersion: before.canonical.revision });
|
||||
await expect(article.getByText(translations.en.careerImportApplied, { exact: true })).toBeVisible();
|
||||
await expect(article).toBeFocused();
|
||||
const afterFirst = await readSource(page.request);
|
||||
@@ -306,7 +306,7 @@ test('[P7-03 PRES-C01 C02 C03 C04 C05 W07] explicit select/deselect acceptance u
|
||||
await secondArticle.getByRole('button', { name: translations.nb.profileCvApplyChanges, exact: true }).click();
|
||||
const secondAccepted = await secondResponse;
|
||||
expect(secondAccepted.status()).toBe(200);
|
||||
expect(secondAccepted.request().postDataJSON()).toEqual({ acceptedLowConfidenceIds: ['Contact'] });
|
||||
expect(secondAccepted.request().postDataJSON()).toEqual({ acceptedLowConfidenceIds: ['Contact'], expectedVersion: afterFirst.canonical.revision });
|
||||
await expect(secondArticle).toBeFocused();
|
||||
const afterSecond = await readSource(page.request);
|
||||
expect(afterSecond.canonical.profile.contact.email).toBe(importedContactEmail);
|
||||
@@ -314,7 +314,7 @@ test('[P7-03 PRES-C01 C02 C03 C04 C05 W07] explicit select/deselect acceptance u
|
||||
expect((await json<{ email: string }>(await page.request.get(`${apiUrl}/auth/me`))).email).toBe(owner.email);
|
||||
expect(afterSecond.canonical.cvText).toBe(before.canonical.cvText);
|
||||
expect(afterSecond.history).toHaveLength(afterFirst.history.length + 1);
|
||||
expect((await page.request.post(`${apiUrl}/profile-cv/runs/${secondRun}/accept`, { headers: owner.headers, data: { acceptedLowConfidenceIds: ['Contact'] } })).status()).toBe(409);
|
||||
expect((await page.request.post(`${apiUrl}/profile-cv/runs/${secondRun}/accept`, { headers: owner.headers, data: { expectedVersion: afterFirst.canonical.revision, acceptedLowConfidenceIds: ['Contact'] } })).status()).toBe(409);
|
||||
expect(await readSource(page.request)).toEqual(afterSecond);
|
||||
const afterDocuments = await Promise.all(documents.urls.map(async url => json<unknown>(await page.request.get(url))));
|
||||
expect(afterDocuments).toEqual(documents.before);
|
||||
@@ -365,7 +365,7 @@ test('[P7-03 PRES-C02 C04 E09] stored reviews are owner scoped and foreign/anony
|
||||
try {
|
||||
const adminOwner = await login(admin, 'e2e@example.test');
|
||||
const ownerB = await login(other, await createOwner(admin, adminOwner));
|
||||
await json<CareerReadback>(await other.put(`${apiUrl}/career/profile`, { headers: ownerB.headers, data: { profile: source('Owner B reviewed context'), cvText: 'Owner B exact raw' } }));
|
||||
await json<CareerReadback>(await other.put(`${apiUrl}/career/profile`, { headers: ownerB.headers, data: { expectedVersion: (await (await other.get(`${apiUrl}/career/profile`)).json()).revision, profile: source('Owner B reviewed context'), cvText: 'Owner B exact raw' } }));
|
||||
const beforeB = await readSource(other);
|
||||
const runB = seedPending(info, ownerB);
|
||||
await assertReachableLowContact(other, runB, beforeB.canonical.profile.contact.email);
|
||||
@@ -373,12 +373,12 @@ test('[P7-03 PRES-C02 C04 E09] stored reviews are owner scoped and foreign/anony
|
||||
expect((await json<Run[]>(await other.get(`${apiUrl}/profile-cv/runs`))).map(run => run.id)).toEqual([runB]);
|
||||
for (const [api, foreign, headers] of [[page.request, runB, owner.headers], [other, runA, ownerB.headers]] as const) {
|
||||
expect((await api.get(`${apiUrl}/profile-cv/runs/${foreign}/diff`)).status()).toBe(404);
|
||||
expect((await api.post(`${apiUrl}/profile-cv/runs/${foreign}/accept`, { headers, data: { acceptedLowConfidenceIds: ['Contact'] } })).status()).toBe(404);
|
||||
expect((await api.post(`${apiUrl}/profile-cv/runs/${foreign}/accept`, { headers, data: { expectedVersion: 0, acceptedLowConfidenceIds: ['Contact'] } })).status()).toBe(404);
|
||||
expect((await api.post(`${apiUrl}/profile-cv/runs/${foreign}/discard`, { headers })).status()).toBe(404);
|
||||
}
|
||||
expect((await anonymous.get(`${apiUrl}/profile-cv/runs`)).status()).toBe(401);
|
||||
expect((await anonymous.get(`${apiUrl}/profile-cv/runs/${runA}/diff`)).status()).toBe(401);
|
||||
expect((await anonymous.post(`${apiUrl}/profile-cv/runs/${runA}/accept`, { data: { acceptedLowConfidenceIds: [] } })).status()).toBe(401);
|
||||
expect((await anonymous.post(`${apiUrl}/profile-cv/runs/${runA}/accept`, { data: { expectedVersion: 0, acceptedLowConfidenceIds: [] } })).status()).toBe(401);
|
||||
expect((await anonymous.post(`${apiUrl}/profile-cv/runs/${runA}/discard`)).status()).toBe(401);
|
||||
expect(await readSource(page.request)).toEqual(beforeA);
|
||||
expect(await readSource(other)).toEqual(beforeB);
|
||||
|
||||
@@ -140,7 +140,7 @@ for (const sample of matrix) {
|
||||
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);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, 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++) {
|
||||
@@ -229,7 +229,7 @@ for (const failedPath of ['/career/profile', '/profile-cv/runs', '/cv/variants']
|
||||
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: {
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision,
|
||||
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();
|
||||
@@ -332,8 +332,8 @@ test('[P7-02 PRES-C01 C02] existing raw source copy and history entry remain rea
|
||||
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);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, 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: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, 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']);
|
||||
|
||||
@@ -13,7 +13,7 @@ type CareerProfile = Record<string, unknown> & {
|
||||
projects: SourceItem[];
|
||||
certifications: SourceItem[];
|
||||
};
|
||||
type CareerReadback = { profile: CareerProfile; completeness: unknown; cvText: string | null };
|
||||
type CareerReadback = { profile: CareerProfile; completeness: unknown; cvText: string | null; revision: number };
|
||||
type CareerVersion = { version: number; source: string; createdAtUtc: string; isCurrent: boolean };
|
||||
type Preview = { themeId: string; html: string; suggestedFileName: string };
|
||||
|
||||
@@ -89,7 +89,7 @@ test('[P7-03 PRES-C01 C03 C06 C07 C10 W05 W06] unrelated canonical UI save prese
|
||||
organisations: ['Reviewed organisation'], references: ['Available by owner agreement'],
|
||||
otherSections: [{ title: 'Custom factual section', items: ['Recorded custom fact.', 'Another reviewed custom fact.'] }],
|
||||
};
|
||||
await json<CareerReadback>(await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: { profile, cvText: raw } }));
|
||||
await json<CareerReadback>(await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile, cvText: raw } }));
|
||||
const before = await json<CareerReadback>(await page.request.get(`${apiUrl}/career/profile`));
|
||||
expect(sourceIds(before.profile)).toEqual({ jobs: ['job-source'], education: ['education-source'], projects: ['project-source'], certifications: ['cert-source'] });
|
||||
const company = await json<{ id: number }>(await page.request.post(`${apiUrl}/companies`, { headers: owner.headers, data: { name: 'Source-ID fixture AS' } }), 201);
|
||||
@@ -182,12 +182,12 @@ test('[P7-03 PRES-C01 C03 C06 C07 C10 W05 W06] unrelated canonical UI save prese
|
||||
await expect(page.getByLabel(t.profileCvContactPhone, { exact: true })).toHaveValue('+47 900 00 002');
|
||||
});
|
||||
|
||||
test('[P7-03 PRES-C01 C03 E05 E09] real HTTP bounds canonical last-writer saves and structured-history recovery without claiming stale-write protection', async ({ page, owner, playwright }, info) => {
|
||||
test('[P7-03 PRES-C01 C03 E05 E09] real HTTP rejects a stale canonical snapshot and preserves structured-history recovery', async ({ page, owner, playwright }, info) => {
|
||||
test.setTimeout(90_000);
|
||||
const initialProfile = { version: '1', contact: { fullName: 'Canonical recovery owner', headline: 'Initial source' },
|
||||
jobs: [{ id: 'recovery-source', title: 'Recorded role', company: 'Fixture AS', bullets: ['Recorded evidence.'] }], skills: ['Norsk'] };
|
||||
const initialRaw = 'Initial raw source — æøå\n';
|
||||
await json<CareerReadback>(await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: { profile: initialProfile, cvText: initialRaw } }));
|
||||
await json<CareerReadback>(await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile: initialProfile, cvText: initialRaw } }));
|
||||
const staleSnapshot = await json<CareerReadback>(await page.request.get(`${apiUrl}/career/profile`));
|
||||
const initialHistory = await json<CareerVersion[]>(await page.request.get(`${apiUrl}/career/profile/versions`));
|
||||
expect(initialHistory.map(row => row.version)).toEqual([1]);
|
||||
@@ -202,43 +202,43 @@ test('[P7-03 PRES-C01 C03 E05 E09] real HTTP bounds canonical last-writer saves
|
||||
expect(secondSnapshot).toEqual(staleSnapshot);
|
||||
const remoteRaw = 'Remote current raw source — æøå\n';
|
||||
const remote = await json<CareerReadback>(await secondSession.put(`${apiUrl}/career/profile`, { headers: secondOwner.headers,
|
||||
data: { profile: { ...secondSnapshot.profile, contact: { ...secondSnapshot.profile.contact, headline: 'Remote reviewed source' } }, cvText: remoteRaw } }));
|
||||
data: { expectedVersion: secondSnapshot.revision, profile: { ...secondSnapshot.profile, contact: { ...secondSnapshot.profile.contact, headline: 'Remote reviewed source' } }, cvText: remoteRaw } }));
|
||||
const remoteHistory = await json<CareerVersion[]>(await page.request.get(`${apiUrl}/career/profile/versions`));
|
||||
expect(remoteHistory.map(row => row.version)).toEqual([2, 1]);
|
||||
const currentRaw = 'Last writer raw source remains current after structured restore — æøå\n';
|
||||
const staleBody = { profile: { ...staleSnapshot.profile, contact: { ...staleSnapshot.profile.contact, headline: 'Stale snapshot accepted as last writer' } }, cvText: currentRaw };
|
||||
const staleBody = { expectedVersion: staleSnapshot.revision, profile: { ...staleSnapshot.profile, contact: { ...staleSnapshot.profile.contact, headline: 'Stale snapshot rejected' } }, cvText: currentRaw };
|
||||
const staleWrite = await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: staleBody });
|
||||
const staleStatus = staleWrite.status();
|
||||
const lastWriter = await json<CareerReadback>(staleWrite);
|
||||
expect(await staleWrite.json()).toMatchObject({ code: 'revision_conflict' });
|
||||
const afterStale = await json<CareerReadback>(await secondSession.get(`${apiUrl}/career/profile`));
|
||||
const historyAfterStale = await json<CareerVersion[]>(await secondSession.get(`${apiUrl}/career/profile/versions`));
|
||||
expect(staleStatus).toBe(200);
|
||||
expect(staleBody).not.toHaveProperty('expectedVersion');
|
||||
expect(afterStale).toEqual(lastWriter);
|
||||
expect(afterStale.profile.contact.headline).toBe('Stale snapshot accepted as last writer');
|
||||
expect(historyAfterStale.map(row => row.version)).toEqual([3, 2, 1]);
|
||||
expect(historyMetadata(historyAfterStale.slice(1))).toEqual(historyMetadata(remoteHistory));
|
||||
const restored = await json<CareerReadback>(await secondSession.post(`${apiUrl}/career/profile/versions/2/restore`, { headers: secondOwner.headers }));
|
||||
expect(staleStatus).toBe(409);
|
||||
expect(staleBody.expectedVersion).toBe(staleSnapshot.revision);
|
||||
expect(afterStale).toEqual(remote);
|
||||
expect(afterStale.profile.contact.headline).toBe('Remote reviewed source');
|
||||
expect(historyAfterStale.map(row => row.version)).toEqual([2, 1]);
|
||||
expect(historyMetadata(historyAfterStale)).toEqual(historyMetadata(remoteHistory));
|
||||
const restored = await json<CareerReadback>(await secondSession.post(`${apiUrl}/career/profile/versions/1/restore`, { data: { expectedVersion: afterStale.revision }, headers: secondOwner.headers }));
|
||||
const recovered = await json<CareerReadback>(await page.request.get(`${apiUrl}/career/profile`));
|
||||
const restoredHistory = await json<CareerVersion[]>(await page.request.get(`${apiUrl}/career/profile/versions`));
|
||||
expect(restored.profile).toEqual(remote.profile);
|
||||
expect(restored.profile).toEqual(staleSnapshot.profile);
|
||||
expect(recovered).toEqual(restored);
|
||||
expect(recovered.cvText).toBe(currentRaw);
|
||||
expect(recovered.cvText).not.toBe(remoteRaw);
|
||||
expect(restoredHistory.map(row => row.version)).toEqual([4, 3, 2, 1]);
|
||||
expect(restoredHistory[0]).toMatchObject({ source: 'restore:v2', isCurrent: true });
|
||||
expect(recovered.cvText).toBe(remoteRaw);
|
||||
expect(recovered.cvText).not.toBe(currentRaw);
|
||||
expect(restoredHistory.map(row => row.version)).toEqual([3, 2, 1]);
|
||||
expect(restoredHistory[0]).toMatchObject({ source: 'restore:v1', isCurrent: true });
|
||||
expect(restoredHistory.slice(1).every(row => !row.isCurrent)).toBe(true);
|
||||
expect(historyMetadata(restoredHistory.slice(1))).toEqual(historyMetadata(historyAfterStale));
|
||||
expect((await page.request.post(`${apiUrl}/career/profile/versions/99999/restore`, { headers: owner.headers })).status()).toBe(404);
|
||||
expect((await page.request.post(`${apiUrl}/career/profile/versions/99999/restore`, { data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision }, headers: owner.headers })).status()).toBe(404);
|
||||
|
||||
const adminOwner = await login(admin, 'e2e@example.test');
|
||||
const otherOwner = await login(other, await createOwner(admin, adminOwner.headers));
|
||||
const otherBefore = await json<CareerReadback>(await other.get(`${apiUrl}/career/profile`));
|
||||
expect(await json<CareerReadback>(await other.get(`${apiUrl}/career/profile?ownerUserId=${encodeURIComponent(owner.id)}`))).toEqual(otherBefore);
|
||||
expect(await json<CareerVersion[]>(await other.get(`${apiUrl}/career/profile/versions?ownerUserId=${encodeURIComponent(owner.id)}`))).toEqual([]);
|
||||
expect((await other.post(`${apiUrl}/career/profile/versions/2/restore`, { headers: otherOwner.headers })).status()).toBe(404);
|
||||
expect((await other.post(`${apiUrl}/career/profile/versions/2/restore`, { data: { expectedVersion: (await (await other.get(`${apiUrl}/career/profile`)).json()).revision }, headers: otherOwner.headers })).status()).toBe(404);
|
||||
await json<CareerReadback>(await other.put(`${apiUrl}/career/profile`, { headers: otherOwner.headers,
|
||||
data: { ownerUserId: owner.id, profile: { version: '1', contact: { fullName: 'Other account only' }, skills: ['Other owner fact'] }, cvText: 'Other owner raw source' } }));
|
||||
data: { expectedVersion: (await (await other.get(`${apiUrl}/career/profile`)).json()).revision, ownerUserId: owner.id, profile: { version: '1', contact: { fullName: 'Other account only' }, skills: ['Other owner fact'] }, cvText: 'Other owner raw source' } }));
|
||||
expect((await json<CareerReadback>(await other.get(`${apiUrl}/career/profile`))).profile.contact.fullName).toBe('Other account only');
|
||||
expect((await anonymous.get(`${apiUrl}/career/profile`)).status()).toBe(401);
|
||||
expect((await anonymous.get(`${apiUrl}/career/profile/versions`)).status()).toBe(401);
|
||||
@@ -249,6 +249,6 @@ test('[P7-03 PRES-C01 C03 E05 E09] real HTTP bounds canonical last-writer saves
|
||||
await info.attach('native-canonical-stale-contract-characterization', { body: JSON.stringify({ initial: staleSnapshot, remote,
|
||||
staleRequest: staleBody, staleStatus, lastWriter: afterStale, historyAfterStale, recovered, restoredHistory,
|
||||
ownerIsolation: { forgedOwnerQueryCannotReadForeign: true, foreignStructuredVersion404: true, forgedBodyWritesOnlyOwnProfile: true, anonymous401: true },
|
||||
qualification: 'Two real authenticated HTTP sessions in disposable SQLite. Canonical stale saves remain accepted last-writer replacements; history recovery appends a structured revision and does not restore raw cvText. No conflict prevention, browser draft-recovery, attachment-byte, model/provider or production acceptance is claimed.' }), contentType: 'application/json' });
|
||||
qualification: 'Two authenticated HTTP sessions with source revisions. Stale replacement is rejected and the winner remains unchanged. Structured restore appends history while preserving current raw cvText. Historical unguarded characterization is retained in the frozen Phase 9 diagnostics. No browser draft-recovery, attachment-byte, model/provider or production acceptance is claimed.' }), contentType: 'application/json' });
|
||||
} finally { await secondSession.dispose(); await admin.dispose(); await other.dispose(); await anonymous.dispose(); }
|
||||
});
|
||||
|
||||
@@ -57,7 +57,7 @@ async function seedSource(page: Page, owner: Owner) {
|
||||
jobs: [{ id: 'library-source-job', title: 'Recorded engineer', company: 'Nordlys AS', bullets: ['Recorded source evidence.'], skills: ['C#'] }],
|
||||
summary: ['Reviewed canonical summary, kept separate from CV-specific presentation.'] };
|
||||
await json(await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers,
|
||||
data: { profile, cvText: 'Exact owner-reviewed source — æøå\nRecorded work.\n' } }));
|
||||
data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile, cvText: 'Exact owner-reviewed source — æøå\nRecorded work.\n' } }));
|
||||
return readSource(page.request);
|
||||
}
|
||||
|
||||
|
||||
@@ -58,9 +58,9 @@ async function upload(api: APIRequestContext, owner: Owner, parent: Job, name =
|
||||
const bytes = Buffer.from(`${privateMarker}\nExact owned file bytes — æøå\n`, 'utf8'); expect((await api.post(`${apiUrl}/attachments`, { headers: owner.headers, multipart: { jobId: String(parent.id), files: { name, mimeType: 'text/plain', buffer: bytes } } })).ok()).toBe(true); const attachment = (await json<{ id: number; fileName: string }[]>(await api.get(`${apiUrl}/attachments/${parent.id}`))).find(value => value.fileName === name)!; expect(attachment.id).toBeGreaterThan(0); expect((await api.patch(`${apiUrl}/attachments/${attachment.id}`, { headers: owner.headers, data: { useForAi: false } })).status()).toBe(204); return { attachment, bytes, name };
|
||||
}
|
||||
async function seven(page: Page, owner: Owner, long = false) {
|
||||
const empty = emptyStructuredCv(); await json(await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: { profile: { ...empty, contact: { ...empty.contact, fullName: 'Åse Recorded Documents', headline: 'Owner-reviewed engineering evidence' }, summary: [privateMarker], skills: ['C#', 'SQL'] }, cvText: privateMarker } }));
|
||||
const empty = emptyStructuredCv(); await json(await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile: { ...empty, contact: { ...empty.contact, fullName: 'Åse Recorded Documents', headline: 'Owner-reviewed engineering evidence' }, summary: [privateMarker], skills: ['C#', 'SQL'] }, cvText: privateMarker } }));
|
||||
const parent = await job(page.request, owner, long ? 'Langvarig plattformutvikling for samfunnskritiske tjenester og internasjonale samarbeidspartnere Æ Ø Å' : undefined); const variant = await cv(page.request, owner, long ? 'CV med lang dokumenttittel om samfunnskritisk plattformutvikling og inkluderende arbeidsprosesser Æ Ø Å' : 'Recorded current and captured CV — æøå'); await json(await page.request.put(`${apiUrl}/jobapplications/${parent.id}/cv`, { headers: owner.headers, data: { variantId: variant.id } })); const published = await json<CvVariant>(await page.request.put(`${apiUrl}/cv/variants/${variant.id}/public`, { headers: owner.headers, data: { isPublic: true } }));
|
||||
const letter = `${privateMarker}\nOriginal reviewed cover letter — æøå`; await json(await page.request.put(`${apiUrl}/jobapplications/${parent.id}/cover-letter`, { headers: owner.headers, data: { text: letter, source: 'manual' } })); expect((await page.request.put(`${apiUrl}/jobapplications/${parent.id}/application-drafts`, { headers: owner.headers, data: { applicationAnswerDraft: `${privateMarker} answer`, recruiterMessageDraft: `${privateMarker} recruiter draft` } })).status()).toBe(204); expect((await page.request.put(`${apiUrl}/jobapplications/${parent.id}/tailored-cv`, { headers: owner.headers, data: { tailoredCvText: `${privateMarker} manually tailored CV` } })).status()).toBe(204);
|
||||
const letter = `${privateMarker}\nOriginal reviewed cover letter — æøå`; await json(await page.request.put(`${apiUrl}/jobapplications/${parent.id}/cover-letter`, { headers: owner.headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/jobapplications/${parent.id}/cover-letter`)).json()).revision, text: letter, source: 'manual' } })); expect((await page.request.put(`${apiUrl}/jobapplications/${parent.id}/application-drafts`, { headers: owner.headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/jobapplications/${parent.id}`)).json()).version, applicationAnswerDraft: `${privateMarker} answer`, recruiterMessageDraft: `${privateMarker} recruiter draft` } })).status()).toBe(204); expect((await page.request.put(`${apiUrl}/jobapplications/${parent.id}/tailored-cv`, { headers: owner.headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/jobapplications/${parent.id}`)).json()).version, tailoredCvText: `${privateMarker} manually tailored CV` } })).status()).toBe(204);
|
||||
const file = await upload(page.request, owner, parent, long ? 'Dokumentasjon av samfunnskritiske tjenester og inkluderende arbeidsprosesser med lange dokumentnavn Æ Ø Å.txt' : undefined); const draft = await email(page.request, owner, parent, long ? 'Lagret e-postutkast om samfunnskritiske tjenester og internasjonalt samarbeid Æ Ø Å' : 'Recorded saved email draft — æøå'); const preserved = await json<SubmittedPackage>(await page.request.post(`${apiUrl}/jobapplications/${parent.id}/submitted-packages`, { headers: owner.headers })); const items = (await readIndex(page.request)).items; expect(items.map(value => value.type).sort()).toEqual(Object.keys(typeKeys).sort()); return { parent, variant: published, letter, draft, preserved, file, items };
|
||||
}
|
||||
async function enter(page: Page, owner: Owner, sample: Sample = desktop, path = '/documents?source=phase7-documents&keep=%C3%A6%26x&followMode=waiting-update#library') {
|
||||
@@ -148,9 +148,9 @@ test('[P7-07 Documents] actual sign-out clears private rows and a separate owner
|
||||
});
|
||||
|
||||
test('[P7-07 Documents] existing letter source keeps dirty edits on navigation Cancel and reindexes only the explicit saved revision', async ({ page, session }, info) => {
|
||||
test.setTimeout(150_000); const parent = await job(page.request, session.owner); const original = await json<CoverLetter>(await page.request.put(`${apiUrl}/jobapplications/${parent.id}/cover-letter`, { headers: session.owner.headers, data: { text: 'Original source letter — æøå', source: 'manual' } })); await enter(page, session.owner); const item = (await readIndex(page.request)).items[0]; const from = indexPath(page); await keyboard(page, row(page, item).getByRole('link', { name: copy('documentsOpenSource'), exact: true })); await expect(page).toHaveURL(new RegExp(`/jobs/${parent.id}\\?section=application&sub=letter$`)); const field = page.getByRole('textbox', { name: copy('jobDetailsCoverLetter'), exact: true }); await expect(field).toHaveValue(original.text!); const dirty = 'Reviewed local source change survives Documents navigation Cancel — æøå'; await field.fill(dirty);
|
||||
test.setTimeout(150_000); const parent = await job(page.request, session.owner); const original = await json<CoverLetter>(await page.request.put(`${apiUrl}/jobapplications/${parent.id}/cover-letter`, { headers: session.owner.headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/jobapplications/${parent.id}/cover-letter`)).json()).revision, text: 'Original source letter — æøå', source: 'manual' } })); await enter(page, session.owner); const item = (await readIndex(page.request)).items[0]; const from = indexPath(page); await keyboard(page, row(page, item).getByRole('link', { name: copy('documentsOpenSource'), exact: true })); await expect(page).toHaveURL(new RegExp(`/jobs/${parent.id}\\?section=application&sub=letter$`)); const field = page.getByRole('textbox', { name: copy('jobDetailsCoverLetter'), exact: true }); await expect(field).toHaveValue(original.text!); const dirty = 'Reviewed local source change survives Documents navigation Cancel — æøå'; await field.fill(dirty);
|
||||
const navigation = page.getByRole('button', { name: copy('documentsTitle'), exact: true }); await keyboard(page, navigation); const dialog = page.getByRole('dialog', { name: copy('workspaceUnsavedTitle'), exact: true }); await keyboard(page, dialog.getByRole('button', { name: copy('workspaceKeepEditing'), exact: true })); await expect(dialog).toHaveCount(0); await expect(field).toHaveValue(dirty); expect(count(session, 'PUT', `/api/jobapplications/${parent.id}/cover-letter`)).toHaveLength(0); expect(await json<CoverLetter>(await page.request.get(`${apiUrl}/jobapplications/${parent.id}/cover-letter`))).toEqual(original); await capture(page, info, desktop, 'existing-letter-dirty-navigation-cancel');
|
||||
const body = page.getByRole('region', { name: copy('workspaceCoverLetter'), exact: true }); const saved = waitResponse(page, 'PUT', `/api/jobapplications/${parent.id}/cover-letter`); await keyboard(page, body.getByRole('button', { name: copy('save'), exact: true })); const revision = await json<CoverLetter>(await saved); expect(revision.text).toBe(dirty); expect(revision.currentVersion).toBe(2); await expect(body.getByRole('button', { name: copy('save'), exact: true })).toBeDisabled(); expect(count(session, 'PUT', `/api/jobapplications/${parent.id}/cover-letter`).map(value => value.body)).toEqual([{ text: dirty, source: 'manual' }]); await page.goBack(); await expect(results(page)).toBeVisible(); expect(indexPath(page)).toBe(from); const indexed = (await readIndex(page.request)).items[0]; expect(indexed).toMatchObject({ id: item.id, historyCount: 2, version: 2, hasCurrentContent: true }); await expect(row(page, indexed)).toContainText(copy('documentsHistoryVersions', { count: 2 })); expect(writes(session)).toHaveLength(1);
|
||||
const body = page.getByRole('region', { name: copy('workspaceCoverLetter'), exact: true }); const saved = waitResponse(page, 'PUT', `/api/jobapplications/${parent.id}/cover-letter`); await keyboard(page, body.getByRole('button', { name: copy('save'), exact: true })); const revision = await json<CoverLetter>(await saved); expect(revision.text).toBe(dirty); expect(revision.currentVersion).toBe(2); await expect(body.getByRole('button', { name: copy('save'), exact: true })).toBeDisabled(); expect(count(session, 'PUT', `/api/jobapplications/${parent.id}/cover-letter`).map(value => value.body)).toEqual([{ text: dirty, source: 'manual', expectedVersion: original.revision }]); await page.goBack(); await expect(results(page)).toBeVisible(); expect(indexPath(page)).toBe(from); const indexed = (await readIndex(page.request)).items[0]; expect(indexed).toMatchObject({ id: item.id, historyCount: 2, version: 2, hasCurrentContent: true }); await expect(row(page, indexed)).toContainText(copy('documentsHistoryVersions', { count: 2 })); expect(writes(session)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('[P7-07 Documents] eight finite bilingual theme widths retain long metadata keyboard actions and complete axe vectors', async ({ page, session }, info) => {
|
||||
|
||||
@@ -94,7 +94,7 @@ test('[P7-08 notifications] failed mutation serializes activation and read-only
|
||||
});
|
||||
|
||||
test('[P7-08 notifications] labelled internal-link GET projection respects existing dirty-workspace Cancel before explicit navigation', async ({ page, session }, info) => {
|
||||
test.setTimeout(150_000); const seeded = await terminal(page, session.owner, info); const company = await json<{ id: number }>(await page.request.post(`${apiUrl}/companies`, { headers: session.owner.headers, data: { name: 'Notification context company — æøå' } }), 201); const job = await json<{ id: number }>(await page.request.post(`${apiUrl}/jobapplications`, { headers: session.owner.headers, data: { companyId: company.id, jobTitle: 'Notification dirty-context role', status: 'Saved' } }), 201); const original = { text: 'Persisted letter before notification navigation — æøå', source: 'manual' }; await json(await page.request.put(`${apiUrl}/jobapplications/${job.id}/cover-letter`, { headers: session.owner.headers, data: original }));
|
||||
test.setTimeout(150_000); const seeded = await terminal(page, session.owner, info); const company = await json<{ id: number }>(await page.request.post(`${apiUrl}/companies`, { headers: session.owner.headers, data: { name: 'Notification context company — æøå' } }), 201); const job = await json<{ id: number }>(await page.request.post(`${apiUrl}/jobapplications`, { headers: session.owner.headers, data: { companyId: company.id, jobTitle: 'Notification dirty-context role', status: 'Saved' } }), 201); const original = { text: 'Persisted letter before notification navigation — æøå', source: 'manual' }; await json(await page.request.put(`${apiUrl}/jobapplications/${job.id}/cover-letter`, { headers: session.owner.headers, data: { ...original, expectedVersion: (await (await page.request.get(`${apiUrl}/jobapplications/${job.id}/cover-letter`)).json()).revision } }));
|
||||
await page.route(`${apiUrl}/notifications?limit=10`, async route => { const response = await route.fetch(); const values = await response.json() as UserNotification[]; expect(values.find(item => item.id === seeded.notice.id)?.linkPath).toBeNull(); return route.fulfill({ response, json: values.map(item => item.id === seeded.notice.id ? { ...item, linkPath: '/documents' } : item) }); });
|
||||
const source = `/jobs/${job.id}?section=application&sub=letter`; await enter(page, session.owner, desktop, source); const field = page.getByRole('textbox', { name: copy('jobDetailsCoverLetter'), exact: true }); await expect(field).toHaveValue(original.text); const dirty = 'Unsaved reviewed letter survives notification navigation Cancel — æøå'; await field.fill(dirty); await open(page); const read = waitResponse(page, 'POST', `/notifications/${seeded.notice.id}/read`); await keyboard(page, panel(page).getByRole('button', { name: copy('notificationsOpen'), exact: true })); expect((await read).status()).toBe(204); const confirmation = page.getByRole('dialog', { name: copy('workspaceUnsavedTitle'), exact: true }); await expect(confirmation).toBeVisible(); await keyboard(page, confirmation.getByRole('button', { name: copy('workspaceKeepEditing'), exact: true })); await expect(confirmation).toHaveCount(0); await expect(field).toHaveValue(dirty); expect(new URL(page.url()).pathname + new URL(page.url()).search).toBe(source); const stored = await json<{ text: string }>(await page.request.get(`${apiUrl}/jobapplications/${job.id}/cover-letter`)); expect(stored.text).toBe(original.text); expect(await operation(page.request, seeded.id)).toEqual(seeded.value); expect((await notices(page.request)).find(item => item.id === seeded.notice.id)?.linkPath).toBeNull();
|
||||
await open(page); await capture(page, info, desktop, 'controlled-internal-link-dirty-cancel', 'Only linkPath=/documents is projected on an actual owned notice; actual producer API is asserted linkPath=null before and after. Existing unsaved-workspace guard is exercised.'); await close(page); expect(count(session, 'POST', `/notifications/${seeded.notice.id}/read`)).toHaveLength(1); expect(session.requests.filter(value => value.method !== 'GET' && !value.path.endsWith(`/${seeded.notice.id}/read`))).toEqual([]);
|
||||
|
||||
@@ -121,7 +121,7 @@ for (const sample of matrix) {
|
||||
userName: owner.userName, firstName: 'Åse', lastName: 'Oppsett', displayName: 'Åse Oppsett',
|
||||
} })).status()).toBe(204);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers,
|
||||
data: { profile: { version: '1', contact: { fullName: 'Canonical career name' }, skills: ['Manually entered C#'] } } })).status()).toBe(200);
|
||||
data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile: { version: '1', contact: { fullName: 'Canonical career name' }, skills: ['Manually entered C#'] } } })).status()).toBe(200);
|
||||
const company = await page.request.post(`${apiUrl}/companies`, { headers: owner.headers, data: { name: 'Oppsett AS' } });
|
||||
expect(company.status()).toBe(201);
|
||||
expect((await page.request.post(`${apiUrl}/jobapplications`, { headers: owner.headers,
|
||||
@@ -256,7 +256,7 @@ test('[P7-02 PRES-S02 E08] recovery to four complete setup steps removes guidanc
|
||||
userName: owner.userName, firstName: 'Åse', lastName: 'Oppsett', displayName: 'Åse Oppsett',
|
||||
} })).status()).toBe(204);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers,
|
||||
data: { profile: { version: '1', skills: ['Manually entered SQL'] } } })).status()).toBe(200);
|
||||
data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile: { version: '1', skills: ['Manually entered SQL'] } } })).status()).toBe(200);
|
||||
const company = await page.request.post(`${apiUrl}/companies`, { headers: owner.headers, data: { name: 'Complete setup AS' } });
|
||||
expect(company.status()).toBe(201);
|
||||
expect((await page.request.post(`${apiUrl}/jobapplications`, { headers: owner.headers,
|
||||
|
||||
@@ -62,7 +62,7 @@ async function seed(owner: Owner, long = false): Promise<Documents> {
|
||||
'Recorded review and maintenance of existing services, preserving factual source material without inventing metrics or qualifications.'] : ['Recorded owner-reviewed source evidence.'], skills: ['C#'] })),
|
||||
};
|
||||
await json(await owner.api.put(`${apiUrl}/career/profile`, { headers: owner.headers,
|
||||
data: { profile, cvText: 'Exact owner-reviewed original source — æøå\nRecorded work and qualifications.\n' } }));
|
||||
data: { expectedVersion: (await (await owner.api.get(`${apiUrl}/career/profile`)).json()).revision, profile, cvText: 'Exact owner-reviewed original source — æøå\nRecorded work and qualifications.\n' } }));
|
||||
const variant = await json<CvVariant>(await owner.api.post(`${apiUrl}/cv/variants`, { headers: owner.headers,
|
||||
data: { name: 'Recorded shared CV', settings: { themeId: 'minimal', language: 'en', showPhoto: false, showPageNumbers: false, showIcons: true,
|
||||
sections: [], customSections: [], headline: 'Recorded CV-specific shared presentation',
|
||||
|
||||
@@ -34,7 +34,7 @@ const test = base.extend<{ headers: Record<string, string> }>({
|
||||
? route.continue() : route.abort();
|
||||
});
|
||||
await context.addInitScript(() => localStorage.setItem("uiLanguage", "en"));
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { profile: canonical } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile: canonical } })).status()).toBe(200);
|
||||
await provide(headers);
|
||||
} finally { await admin.dispose(); }
|
||||
},
|
||||
@@ -86,8 +86,8 @@ test("[PRES-W01 PRES-W05 PRES-W06 PRES-W07 PRES-W08] legacy workspace preserves
|
||||
const sibling = await siblingResponse.json();
|
||||
const jobUrl = `${apiUrl}/jobapplications/${job.id}`;
|
||||
expect((await page.request.put(`${jobUrl}/cv`, { headers, data: { variantId: variant.id } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${jobUrl}/cover-letter`, { headers, data: { text: "Original reviewed letter", source: "manual" } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${jobUrl}/application-drafts`, { headers, data: { applicationAnswerDraft: "Original answer — æøå", recruiterMessageDraft: "Original recruiter message" } })).status()).toBe(204);
|
||||
expect((await page.request.put(`${jobUrl}/cover-letter`, { headers, data: { expectedVersion: (await (await page.request.get(`${jobUrl}/cover-letter`)).json()).revision, text: "Original reviewed letter", source: "manual" } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${jobUrl}/application-drafts`, { headers, data: { expectedVersion: (await (await page.request.get(`${jobUrl}`)).json()).version, applicationAnswerDraft: "Original answer — æøå", recruiterMessageDraft: "Original recruiter message" } })).status()).toBe(204);
|
||||
expect((await page.request.get(`${jobUrl}/tailoring`)).status()).toBe(200);
|
||||
const bytes = Buffer.from("Original attachment — æøå\n", "utf8");
|
||||
const upload = await page.request.post(`${apiUrl}/attachments`, { headers, multipart: { jobId: String(job.id), files: { name: "evidence.txt", mimeType: "text/plain", buffer: bytes } } });
|
||||
@@ -111,10 +111,10 @@ test("[PRES-W01 PRES-W05 PRES-W06 PRES-W07 PRES-W08] legacy workspace preserves
|
||||
expect(snapshot.cvVariantVersion).toBe(variant.version);
|
||||
expect(before.applicationMaterialJson).toContain("Original answer");
|
||||
expect(snapshot.attachments).toHaveLength(1);
|
||||
expect((await page.request.put(`${jobUrl}/cover-letter`, { headers, data: { text: "Later letter", source: "manual" } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${jobUrl}/cover-letter`, { headers, data: { expectedVersion: (await (await page.request.get(`${jobUrl}/cover-letter`)).json()).revision, text: "Later letter", source: "manual" } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${apiUrl}/cv/variants/${variant.id}`, { headers, data: { name: "Later CV", settings: { ...variant.settings, headline: "Later headline" }, expectedVersion: variant.version } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { profile: { ...canonical, contact: { ...canonical.contact, headline: "Later canonical headline" } } } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${jobUrl}/application-drafts`, { headers, data: { applicationAnswerDraft: "Later answer", recruiterMessageDraft: "Later recruiter message" } })).status()).toBe(204);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile: { ...canonical, contact: { ...canonical.contact, headline: "Later canonical headline" } } } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${jobUrl}/application-drafts`, { headers, data: { expectedVersion: (await (await page.request.get(`${jobUrl}`)).json()).version, applicationAnswerDraft: "Later answer", recruiterMessageDraft: "Later recruiter message" } })).status()).toBe(204);
|
||||
expect((await page.request.delete(`${apiUrl}/attachments/${attachment.id}`, { headers })).status()).toBe(204);
|
||||
expect(await (await page.request.get(`${jobUrl}/submitted-packages/${snapshot.id}`)).json()).toEqual(before);
|
||||
const archived = await page.request.get(`${jobUrl}/submitted-packages/${snapshot.id}/attachments/${snapshot.attachments[0].id}`);
|
||||
@@ -123,7 +123,7 @@ test("[PRES-W01 PRES-W05 PRES-W06 PRES-W07 PRES-W08] legacy workspace preserves
|
||||
expect((await page.request.put(`${jobUrl}/cv`, { headers, data: { variantId: null } })).status()).toBe(200);
|
||||
expect((await page.request.get(`${apiUrl}/cv/variants/${variant.id}`)).status()).toBe(200);
|
||||
expect(await (await page.request.get(`${apiUrl}/cv/variants/${sibling.id}`)).json()).toEqual(sibling);
|
||||
const restored = await page.request.post(`${jobUrl}/cover-letter/versions/1/restore`, { headers });
|
||||
const restored = await page.request.post(`${jobUrl}/cover-letter/versions/1/restore`, { data: { expectedVersion: (await (await page.request.get(`${jobUrl}/cover-letter`)).json()).revision }, headers });
|
||||
expect(restored.status()).toBe(200);
|
||||
expect((await restored.json()).currentVersion).toBe(3);
|
||||
expect((await (await page.request.get(`${jobUrl}/cover-letter`)).json()).text).toBe("Original reviewed letter");
|
||||
|
||||
@@ -41,7 +41,7 @@ const test = base.extend<{ owner: Owner }>({
|
||||
summary: [sentence], jobs: Array.from({ length: 3 }, (_, index) => ({ id: `audit-role-${index}`, title: `Platform developer ${index + 1}`, company: 'Nordlys Audit AS', startDate: '2020-01', endDate: '2025-12', bullets: Array.from({ length: 4 }, () => sentence.repeat(3)) })),
|
||||
education: [{ id: 'audit-education', qualification: 'Computer science', institution: 'Fixture University', startDate: '2016-08', endDate: '2019-06' }],
|
||||
projects: [{ id: 'audit-project', name: 'Recorded accessibility project', bullets: [sentence] }], skills: ['C#', 'React', 'SQL'], languages: [{ language: 'Norsk', proficiency: 'Fluent' }] };
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { profile } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile } })).status()).toBe(200);
|
||||
const company = await page.request.post(`${apiUrl}/companies`, { headers, data: { name: 'Nordlys samfunnskritiske plattformer og inkluderende digitale tjenester æøå' } });
|
||||
expect(company.status()).toBe(201);
|
||||
const companyId = (await company.json()).id;
|
||||
@@ -193,7 +193,7 @@ test('[P6-04] measured native route, long CV and complete large Board workflows'
|
||||
const sentence = 'Documented accessible services and reliable user workflows. Dokumenterte tilgjengelige tjenester og pålitelige arbeidsflyter. ';
|
||||
profile.jobs = Array.from({ length: 8 }, (_, index) => ({ id: `performance-role-${index}`, title: `Measured platform developer ${index + 1}`, company: 'Nordlys Audit AS', startDate: '2020-01', endDate: '2025-12',
|
||||
bullets: Array.from({ length: 4 }, (_, bullet) => `Evidence ${index + 1}.${bullet + 1}: ${sentence.repeat(3)}`) }));
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: { profile } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile } })).status()).toBe(200);
|
||||
await context.addInitScript(() => {
|
||||
const observations = { longTasks: [] as number[], layoutShift: 0 };
|
||||
Object.assign(window, { phase6Performance: observations });
|
||||
|
||||
@@ -28,7 +28,7 @@ const test = base.extend<{ owner: { headers: Record<string, string>; companyId:
|
||||
});
|
||||
const company = await page.request.post(`${apiUrl}/companies`, { headers, data: { name: "Nordlys æøå AS", location: "Tromsø" } });
|
||||
expect(company.status()).toBe(201);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { profile: {
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile: {
|
||||
version: "1", contact: { fullName: "Åse Eksempel", headline: "Engineer" },
|
||||
jobs: [{ title: "Engineer", company: "Nordlys AS", bullets: ["Built reliable APIs"] }], skills: ["C#", "Norsk"],
|
||||
} } })).status()).toBe(200);
|
||||
|
||||
@@ -28,7 +28,7 @@ const test = base.extend<{ owner: { headers: Record<string, string>; jobId: numb
|
||||
const companyId = (await company.json()).id;
|
||||
const job = await page.request.post(`${apiUrl}/jobapplications`, { headers, data: { companyId, jobTitle: "Tilgjengelig plattformutvikler", status: "Saved" } });
|
||||
expect(job.ok()).toBe(true);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { profile: { version: "1", contact: { fullName: "Åse Eksempel" }, skills: ["Norsk", "C#"] } } })).status()).toBe(200);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile: { version: "1", contact: { fullName: "Åse Eksempel" }, skills: ["Norsk", "C#"] } } })).status()).toBe(200);
|
||||
const cv = await page.request.post(`${apiUrl}/cv/variants`, { headers, data: { name: "Readiness CV" } });
|
||||
expect(cv.status()).toBe(200);
|
||||
await provide({ headers, jobId: (await job.json()).id, companyId, cvId: (await cv.json()).id });
|
||||
|
||||
@@ -493,7 +493,7 @@ test("the Code template exports a long structured CV as searchable multi-page PD
|
||||
|
||||
const profileResponse = await page.request.put(`${apiUrl}/career/profile`, {
|
||||
headers,
|
||||
data: { profile, cvText: "Long sanitized CV fixture for PDF verification." },
|
||||
data: { expectedVersion: (await (await page.request.get(`${apiUrl}/career/profile`)).json()).revision, profile, cvText: "Long sanitized CV fixture for PDF verification." },
|
||||
});
|
||||
expect(profileResponse.ok()).toBeTruthy();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user