fix(rd21): retain draft revisions and qualify conflict recovery

This commit is contained in:
cesnimda
2026-10-23 12:34:34 +02:00
parent 12f342c09c
commit 05a738693e
53 changed files with 813 additions and 192 deletions
@@ -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);