fix(rd21): expose committed revisions and refresh answer sources

This commit is contained in:
cesnimda
2026-10-23 12:51:36 +02:00
parent 05a738693e
commit 1b89a779d0
4 changed files with 141 additions and 6 deletions
@@ -89,6 +89,72 @@ async function createJob(request: APIRequestContext, owner: Owner) {
data: { companyId: company.id, jobTitle: 'Original job title', status: 'Saved', notes: 'Retain original notes', location: 'Trondheim' } }), 201);
}
test('RD21 cross-origin browser exposes committed revisions and native Answers saves advance consecutively', async ({ page, owner }, info) => {
const job = await createJob(page.request, owner);
await page.goto(`/jobs/${job.id}?section=application&sub=answers`);
const answer = page.getByRole('textbox', { name: t.assetsApplicationAnswer, exact: true });
await expect(answer).toBeEditable();
const browserCommit = await page.evaluate(async ({ api, job, headers }) => {
const response = await fetch(`${api}/jobapplications/${job.id}/application-drafts`, { method: 'PUT', credentials: 'include',
headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ applicationAnswerDraft: 'Browser-visible commit', recruiterMessageDraft: '', expectedVersion: job.version }) });
return { status: response.status, revision: response.headers.get('X-Job-Version') };
}, { api, job, headers: owner.headers });
expect(browserCommit.status).toBe(204);
expect(db(info, owner, job.id).job).toMatchObject({ Version: job.version + 1, Notes: expect.stringContaining('<<<APPLICATION_ANSWER_DRAFT>>>\nBrowser-visible commit\n<<<END_APPLICATION_ANSWER_DRAFT>>>') });
expect(browserCommit.revision).toBe(String(job.version + 1));
await page.reload();
await expect(answer).toHaveValue('Browser-visible commit');
for (const [index, text] of ['First native answer', 'Second native answer'].entries()) {
await answer.fill(text);
const responsePromise = page.waitForResponse(response => response.url().endsWith(`/jobapplications/${job.id}/application-drafts`) && response.request().method() === 'PUT');
await page.getByRole('button', { name: t.assetsSaveDrafts, exact: true }).click();
const response = await responsePromise;
expect(response.status()).toBe(204);
expect(Number(response.headers()['x-job-version'])).toBe(job.version + index + 2);
await expect(page.getByRole('button', { name: t.assetsSaveDrafts, exact: true })).toBeDisabled();
await expect(page.getByText('Missing committed job revision.', { exact: true })).toHaveCount(0);
expect(db(info, owner, job.id).job).toMatchObject({ Version: job.version + index + 2, Notes: expect.stringContaining(`<<<APPLICATION_ANSWER_DRAFT>>>\n${text}\n<<<END_APPLICATION_ANSWER_DRAFT>>>`) });
}
await attach(info, 'rd21-cross-origin-native-answers', { browserCommit, database: db(info, owner, job.id) });
});
test('RD21 cover-letter save then Answers entry waits for fresh coherent source before a native save', async ({ page, owner }, info) => {
const job = await createJob(page.request, owner);
await page.goto(`/jobs/${job.id}?section=application&sub=letter`);
const letter = page.getByRole('textbox', { name: t.jobDetailsCoverLetter, exact: true });
await expect(letter).toBeEditable();
await letter.fill('Acknowledged cover letter retained');
const saved = page.waitForResponse(response => response.url().endsWith(`/jobapplications/${job.id}/cover-letter`) && response.request().method() === 'PUT');
await page.getByRole('button', { name: t.save, exact: true }).click();
const letterResponse = await saved;
expect(letterResponse.status()).toBe(200);
const letterRevision = (await letterResponse.json()).revision;
await expect(page.getByRole('button', { name: t.save, exact: true })).toBeDisabled();
expect(db(info, owner, job.id).job).toMatchObject({ Version: letterRevision, CoverLetterText: 'Acknowledged cover letter retained' });
let release!: () => void;
const gate = new Promise<void>(resolve => { release = resolve; });
let started!: () => void;
const requested = new Promise<void>(resolve => { started = resolve; });
await page.route(`**/api/jobapplications/${job.id}/workspace`, async route => { started(); await gate; await route.continue(); });
await page.getByRole('tab', { name: t.workspaceAnswers, exact: true }).click();
await expect(page.getByRole('tab', { name: t.workspaceAnswers, exact: true })).toHaveAttribute('aria-selected', 'true');
const answer = page.getByRole('textbox', { name: t.assetsApplicationAnswer, exact: true });
try {
await expect(answer).toHaveCount(0);
await requested;
} finally { release(); }
await expect(answer).toBeEditable();
await answer.fill('Answer after cover letter');
const answerSaved = page.waitForResponse(response => response.url().endsWith(`/jobapplications/${job.id}/application-drafts`) && response.request().method() === 'PUT');
await page.getByRole('button', { name: t.assetsSaveDrafts, exact: true }).click();
const response = await answerSaved;
expect(response.request().postDataJSON().expectedVersion).toBe(letterRevision);
expect(response.status()).toBe(204);
await expect(page.getByRole('button', { name: t.assetsSaveDrafts, exact: true })).toBeDisabled();
expect(db(info, owner, job.id).job).toMatchObject({ Version: letterRevision + 1, CoverLetterText: 'Acknowledged cover letter retained', Notes: expect.stringContaining('<<<APPLICATION_ANSWER_DRAFT>>>\nAnswer after cover letter\n<<<END_APPLICATION_ANSWER_DRAFT>>>') });
await attach(info, 'rd21-cover-letter-answers-entry', { letterRevision, committedRevision: response.headers()['x-job-version'], database: db(info, owner, job.id) });
});
test('RD21 original two-session ordering rejects stale snapshots and preserves each acknowledged SQLite commit across navigation and a fresh session', async ({ page, owner, second, playwright, email }, info) => {
await seedCareer(page.request, owner);
const careerA = await json<Career>(await page.request.get(`${api}/career/profile`));