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
+4 -4
View File
@@ -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) => {