feat(email): persist and resume reply drafts
This commit is contained in:
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user