feat(email): persist and resume reply drafts
CI and Deploy / test (pull_request) Successful in 4m36s
CI and Deploy / deploy (pull_request) Has been skipped

This commit is contained in:
cesnimda
2026-08-10 10:16:40 +02:00
parent 050b99ca36
commit d3d2b6783a
2 changed files with 181 additions and 1 deletions
@@ -65,6 +65,7 @@ describe('CorrespondenceInboxPage', () => {
attachmentCount: 1,
},
] } as any);
if (url === '/email/drafts') return Promise.resolve({ data: [] } as any);
if (url === '/email/message') return Promise.resolve({ data: {
id: 'message-1', threadId: 'thread-1', subject: 'Interview invite', from: 'Maria Recruiter <maria@acme.test>', to: 'user@example.test',
date: new Date().toISOString(), snippet: 'Interview', bodyText: 'Please choose an interview time.', labels: ['INBOX'], attachments: [{ fileName: 'agenda.pdf' }],
@@ -223,6 +224,69 @@ describe('CorrespondenceInboxPage', () => {
expect(screen.getByRole('button', { name: /review and send/i })).toBeDisabled();
});
test('saves an incomplete reply with its server delivery identity', async () => {
const original = mockedApi.get.getMockImplementation();
mockedApi.get.mockImplementation((url: string, config?: any) => {
if (url === '/email/providers') return Promise.resolve({ data: [
{ provider: 'gmail', displayName: 'Gmail', connected: true, address: 'owner@gmail.test', canRead: true, canSend: true },
] } as any);
return original!(url, config);
});
mockedApi.post.mockImplementation((url: string) => {
if (url === '/email/drafts') return Promise.resolve({ data: {
id: 'draft-1', jobApplicationId: 42, provider: 'gmail', to: 'Maria Recruiter <maria@acme.test>', subject: 'Re: Interview invite', bodyText: '',
threadId: 'thread-1', clientRequestId: 'server-request-id', revision: 1, createdAtUtc: new Date().toISOString(), updatedAtUtc: new Date().toISOString(),
} } as any);
return Promise.reject(new Error(`Unexpected POST ${url}`));
});
renderPage();
fireEvent.click(await screen.findByRole('button', { name: /view message/i }));
fireEvent.click(await screen.findByRole('button', { name: /reply with connected provider/i }));
fireEvent.click(screen.getByRole('button', { name: /^save draft$/i }));
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/email/drafts', {
jobApplicationId: 42,
provider: 'gmail',
to: 'Maria Recruiter <maria@acme.test>',
subject: 'Re: Interview invite',
bodyText: '',
threadId: 'thread-1',
}));
expect(await screen.findByText(/email draft saved/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /^save changes$/i })).toBeInTheDocument();
});
test('resumes a saved draft and surfaces revision conflicts without overwriting it', async () => {
const original = mockedApi.get.getMockImplementation();
mockedApi.get.mockImplementation((url: string, config?: any) => {
if (url === '/email/drafts') return Promise.resolve({ data: [{
id: 'draft-1', jobApplicationId: 42, provider: 'gmail', to: 'maria@acme.test', subject: 'Saved subject', bodyText: 'Saved private draft.',
threadId: 'thread-1', clientRequestId: 'server-request-id', revision: 4, createdAtUtc: new Date().toISOString(), updatedAtUtc: new Date().toISOString(),
}] } as any);
if (url === '/email/providers') return Promise.resolve({ data: [
{ provider: 'gmail', displayName: 'Gmail', connected: true, address: 'owner@gmail.test', canRead: true, canSend: true },
] } as any);
return original!(url, config);
});
mockedApi.put.mockRejectedValue({ response: { status: 409 } });
renderPage();
fireEvent.click(await screen.findByRole('button', { name: /resume saved subject/i }));
expect(screen.getByLabelText(/message/i)).toHaveValue('Saved private draft.');
fireEvent.change(screen.getByLabelText(/message/i), { target: { value: 'Conflicting local edit.' } });
fireEvent.click(screen.getByRole('button', { name: /^save changes$/i }));
await waitFor(() => expect(mockedApi.put).toHaveBeenCalledWith('/email/drafts/draft-1', {
revision: 4,
to: 'maria@acme.test',
subject: 'Saved subject',
bodyText: 'Conflicting local edit.',
}));
expect(await screen.findByText(/changed in another tab/i)).toBeInTheDocument();
expect(screen.getByLabelText(/message/i)).toHaveValue('Conflicting local edit.');
});
test('does not offer retry when delivery is uncertain', async () => {
const original = mockedApi.get.getMockImplementation();
mockedApi.get.mockImplementation((url: string, config?: any) => {