fix(ui): preserve compose draft identity and delivery safeguards
This commit is contained in:
@@ -66,6 +66,42 @@ function HistoryBackControl() {
|
||||
return <button onClick={() => navigate(-1)}>History Back</button>;
|
||||
}
|
||||
|
||||
const composeUuid = '00000000-0000-4000-8000-000000000101';
|
||||
const replacementUuid = '00000000-0000-4000-8000-000000000202';
|
||||
|
||||
function savedComposeDraft(overrides: Record<string, unknown> = {}) {
|
||||
return { id: 'compose-owned', revision: 4, jobApplicationId: 42, provider: 'gmail', to: 'maria@acme.test', subject: 'Owned compose draft', bodyText: 'Owned saved body', threadId: 'thread-1', clientRequestId: 'server-owned-attempt', createdAtUtc: '2026-02-13T11:30:00Z', updatedAtUtc: '2026-02-13T11:30:00Z', ...overrides };
|
||||
}
|
||||
|
||||
function composeReads(stored: ReturnType<typeof savedComposeDraft>[] = []) {
|
||||
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);
|
||||
if (url === '/email/drafts') return Promise.resolve({ data: stored } as any);
|
||||
if (url === '/jobapplications/choices') return Promise.resolve({ data: [{ id: 42, jobTitle: 'Backend Engineer', companyName: 'Acme Systems' }] } as any);
|
||||
return original!(url, config);
|
||||
});
|
||||
const uuid = globalThis.crypto.randomUUID as jest.Mock;
|
||||
uuid.mockReset().mockReturnValueOnce(composeUuid).mockReturnValue(replacementUuid);
|
||||
return uuid;
|
||||
}
|
||||
|
||||
function composeField(key: string) {
|
||||
return within(screen.getByRole('region', { name: copy('emailInboxDraftTitle') })).getByRole('textbox', { name: copy(key) });
|
||||
}
|
||||
|
||||
async function openReply(bodyText = '') {
|
||||
fireEvent.click(await screen.findByRole('button', { name: copy('emailInboxViewMessage') }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: copy('emailInboxReply') }));
|
||||
if (bodyText) fireEvent.change(composeField('emailInboxMessage'), { target: { value: bodyText } });
|
||||
}
|
||||
|
||||
async function closeConfirmation(actionKey: string) {
|
||||
const dialog = await screen.findByRole('dialog');
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: copy(actionKey) }));
|
||||
await waitFor(() => expect(screen.queryByRole('dialog', { hidden: true })).not.toBeInTheDocument());
|
||||
}
|
||||
|
||||
function renderPage(initialPath = '/') {
|
||||
return render(
|
||||
<ToastProvider>
|
||||
@@ -834,4 +870,299 @@ describe('CorrespondenceInboxPage', () => {
|
||||
expect(mockedApi.patch).not.toHaveBeenCalled();
|
||||
expect(mockedApi.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('held draft Save captures one write and locks edits, Send, Discard and every draft replacement entry', async () => {
|
||||
composeReads([savedComposeDraft()]);
|
||||
const save = deferred<any>();
|
||||
mockedApi.post.mockReturnValue(save.promise);
|
||||
renderPage();
|
||||
await openReply('Captured save body');
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: copy('emailInboxCompose') })).toBeEnabled());
|
||||
const button = screen.getByRole('button', { name: copy('emailInboxSaveDraft') });
|
||||
act(() => { fireEvent.click(button); fireEvent.click(button); });
|
||||
expect(mockedApi.post).toHaveBeenCalledTimes(1);
|
||||
expect(mockedApi.post).toHaveBeenCalledWith('/email/drafts', { jobApplicationId: 42, provider: 'gmail', to: 'Maria Recruiter <maria@acme.test>', subject: 'Re: Interview invite', bodyText: 'Captured save body', threadId: 'thread-1' });
|
||||
for (const key of ['emailInboxRecipient', 'emailInboxSubject', 'emailInboxMessage']) expect(composeField(key)).toBeDisabled();
|
||||
for (const key of ['emailInboxReviewSend', 'emailInboxDiscardDraft', 'emailInboxCompose', 'emailInboxReply']) expect(screen.getByRole('button', { name: copy(key) })).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: copy('emailInboxResumeDraft', { name: 'Owned compose draft' }) })).toBeDisabled();
|
||||
await act(async () => { save.resolve({ data: savedComposeDraft({ to: 'Maria Recruiter <maria@acme.test>', subject: 'Re: Interview invite', bodyText: 'Captured save body' }) }); await save.promise; });
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: copy('profileSaveChanges') })).toBeEnabled());
|
||||
expect(composeField('emailInboxMessage')).toHaveValue('Captured save body');
|
||||
expect(mockedApi.post).toHaveBeenCalledTimes(1);
|
||||
expect(mockedApi.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('double Review creates one confirmation, Cancel sends nothing and explicit double Send owns one captured attempt', async () => {
|
||||
const uuid = composeReads();
|
||||
const send = deferred<any>();
|
||||
mockedApi.post.mockReturnValue(send.promise);
|
||||
renderPage();
|
||||
await openReply('Captured confirmed reply');
|
||||
const review = screen.getByRole('button', { name: copy('emailInboxReviewSend') });
|
||||
review.focus();
|
||||
act(() => { fireEvent.click(review); fireEvent.click(review); });
|
||||
expect(await screen.findAllByRole('dialog')).toHaveLength(1);
|
||||
expect(mockedApi.post).not.toHaveBeenCalled();
|
||||
await closeConfirmation('cancel');
|
||||
expect(mockedApi.post).not.toHaveBeenCalled();
|
||||
await waitFor(() => expect(review).toHaveFocus());
|
||||
expect(composeField('emailInboxMessage')).toHaveValue('Captured confirmed reply');
|
||||
fireEvent.click(review);
|
||||
const dialog = await screen.findByRole('dialog');
|
||||
const confirm = within(dialog).getByRole('button', { name: copy('emailInboxSendEmail') });
|
||||
act(() => { fireEvent.click(confirm); fireEvent.click(confirm); });
|
||||
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledTimes(1));
|
||||
expect(mockedApi.post).toHaveBeenCalledWith('/email/send', { jobApplicationId: 42, provider: 'gmail', clientRequestId: composeUuid, to: 'Maria Recruiter <maria@acme.test>', subject: 'Re: Interview invite', bodyText: 'Captured confirmed reply', threadId: 'thread-1', confirmed: true });
|
||||
await waitFor(() => expect(screen.queryByRole('dialog', { hidden: true })).not.toBeInTheDocument());
|
||||
expect(screen.getByRole('button', { name: copy('emailInboxSending') })).toBeDisabled();
|
||||
expect(uuid).toHaveBeenCalledTimes(1);
|
||||
await act(async () => { send.resolve({ data: { attemptId: 'server-pending', status: 'pending', duplicate: false } }); await send.promise; });
|
||||
expect(await screen.findByText(copy('emailInboxDeliveryUncertain'))).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: copy('emailInboxReviewSend') })).toBeDisabled();
|
||||
expect(mockedApi.post).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('recipient, subject and body edits cannot erase any recorded delivery state or rotate its attempt identity', async () => {
|
||||
for (const status of ['uncertain', 'pending', 'sending', 'sent', 'failed'] as const) {
|
||||
const uuid = composeReads();
|
||||
mockedApi.post.mockReset();
|
||||
const result = { attemptId: `recorded-${status}`, status, duplicate: false, failureCategory: status === 'failed' ? 'provider_rejected' : null };
|
||||
mockedApi.post.mockImplementation(() => status === 'failed' ? Promise.reject({ response: { status: 502, data: result } }) : Promise.resolve({ data: result } as any));
|
||||
const mounted = renderPage();
|
||||
await openReply('Recorded attempt body');
|
||||
fireEvent.click(screen.getByRole('button', { name: copy('emailInboxReviewSend') }));
|
||||
await closeConfirmation('emailInboxSendEmail');
|
||||
const stateKey = status === 'sent' ? 'emailInboxSentSuccess' : status === 'failed' ? 'emailInboxDeliveryFailed' : 'emailInboxDeliveryUncertain';
|
||||
await screen.findByText(copy(stateKey));
|
||||
for (const [key, value] of [['emailInboxRecipient', 'changed@acme.test'], ['emailInboxSubject', 'Changed subject'], ['emailInboxMessage', 'Changed body']]) {
|
||||
fireEvent.change(composeField(key), { target: { value } });
|
||||
expect(screen.getByText(copy(stateKey))).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: copy('emailInboxReviewSend') })).toBeDisabled();
|
||||
expect(uuid).toHaveBeenCalledTimes(1);
|
||||
expect(mockedApi.post).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
if (status === 'failed') expect(screen.getByRole('button', { name: copy('emailInboxPrepareAttempt') })).toBeInTheDocument();
|
||||
else expect(screen.queryByRole('button', { name: copy('emailInboxPrepareAttempt') })).not.toBeInTheDocument();
|
||||
mounted.unmount();
|
||||
}
|
||||
});
|
||||
|
||||
test('generic 409 cannot prepare a new attempt while an authoritative failed result requires one explicit server rotation', async () => {
|
||||
let uuid = composeReads([savedComposeDraft()]);
|
||||
mockedApi.post.mockRejectedValue({ response: { status: 409, data: { detail: 'Draft or delivery conflict' } } });
|
||||
let mounted = renderPage();
|
||||
fireEvent.click(await screen.findByRole('button', { name: copy('emailInboxResumeDraft', { name: 'Owned compose draft' }) }));
|
||||
fireEvent.click(screen.getByRole('button', { name: copy('emailInboxReviewSend') }));
|
||||
await closeConfirmation('emailInboxSendEmail');
|
||||
await screen.findByText(copy('emailInboxDeliveryUncertain'));
|
||||
expect(screen.queryByRole('button', { name: copy('emailInboxPrepareAttempt') })).not.toBeInTheDocument();
|
||||
expect(uuid).not.toHaveBeenCalled();
|
||||
expect(mockedApi.post).toHaveBeenCalledTimes(1);
|
||||
expect(mockedApi.post).toHaveBeenCalledWith('/email/send', expect.objectContaining({ clientRequestId: 'server-owned-attempt' }));
|
||||
mounted.unmount();
|
||||
|
||||
uuid = composeReads([savedComposeDraft()]);
|
||||
mockedApi.post.mockReset();
|
||||
const rotate = deferred<any>();
|
||||
let sends = 0;
|
||||
mockedApi.post.mockImplementation((url: string) => {
|
||||
if (url === '/email/send') return ++sends === 1 ? Promise.reject({ response: { status: 502, data: { attemptId: 'failed-server-attempt', status: 'failed', duplicate: false, failureCategory: 'provider_rejected' } } }) : Promise.resolve({ data: { attemptId: 'new-server-attempt', status: 'pending', duplicate: false } } as any);
|
||||
if (url === '/email/drafts/compose-owned/new-attempt') return rotate.promise;
|
||||
return Promise.reject(new Error(`Unexpected POST ${url}`));
|
||||
});
|
||||
mounted = renderPage();
|
||||
fireEvent.click(await screen.findByRole('button', { name: copy('emailInboxResumeDraft', { name: 'Owned compose draft' }) }));
|
||||
fireEvent.click(screen.getByRole('button', { name: copy('emailInboxReviewSend') }));
|
||||
await closeConfirmation('emailInboxSendEmail');
|
||||
const prepare = await screen.findByRole('button', { name: copy('emailInboxPrepareAttempt') });
|
||||
expect(mockedApi.post).toHaveBeenCalledTimes(1);
|
||||
act(() => { fireEvent.click(prepare); fireEvent.click(prepare); });
|
||||
expect(mockedApi.post).toHaveBeenCalledTimes(2);
|
||||
expect(mockedApi.post).toHaveBeenLastCalledWith('/email/drafts/compose-owned/new-attempt', { revision: 4 });
|
||||
expect(uuid).not.toHaveBeenCalled();
|
||||
await act(async () => { rotate.resolve({ data: savedComposeDraft({ revision: 5, clientRequestId: 'server-rotated-attempt' }) }); await rotate.promise; });
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: copy('emailInboxReviewSend') })).toBeEnabled());
|
||||
fireEvent.click(screen.getByRole('button', { name: copy('emailInboxReviewSend') }));
|
||||
await closeConfirmation('emailInboxSendEmail');
|
||||
expect(mockedApi.post).toHaveBeenLastCalledWith('/email/send', expect.objectContaining({ clientRequestId: 'server-rotated-attempt', confirmed: true }));
|
||||
expect(uuid).not.toHaveBeenCalled();
|
||||
mounted.unmount();
|
||||
});
|
||||
|
||||
test('recipient-only and subject-only unsaved changes require replacement and discard confirmation whose Cancel retains the draft', async () => {
|
||||
for (const [key, value] of [['emailInboxRecipient', 'edited-recipient@acme.test'], ['emailInboxSubject', 'Edited subject only']]) {
|
||||
composeReads([savedComposeDraft()]);
|
||||
const mounted = renderPage();
|
||||
await openReply();
|
||||
fireEvent.change(composeField(key), { target: { value } });
|
||||
fireEvent.click(screen.getByRole('button', { name: copy('emailInboxResumeDraft', { name: 'Owned compose draft' }) }));
|
||||
expect(await screen.findByRole('dialog', { name: copy('emailInboxReplaceDraftTitle') })).toBeInTheDocument();
|
||||
await closeConfirmation('cancel');
|
||||
expect(composeField(key)).toHaveValue(value);
|
||||
expect(composeField('emailInboxMessage')).toHaveValue('');
|
||||
fireEvent.click(screen.getByRole('button', { name: copy('emailInboxDiscardDraft') }));
|
||||
expect(await screen.findByRole('dialog', { name: copy('emailInboxDiscardDraftTitle') })).toBeInTheDocument();
|
||||
await closeConfirmation('cancel');
|
||||
expect(composeField(key)).toHaveValue(value);
|
||||
expect(composeField('emailInboxMessage')).toHaveValue('');
|
||||
expect(mockedApi.post).not.toHaveBeenCalled();
|
||||
expect(mockedApi.put).not.toHaveBeenCalled();
|
||||
expect(mockedApi.delete).not.toHaveBeenCalled();
|
||||
mounted.unmount();
|
||||
}
|
||||
});
|
||||
|
||||
test('saved-draft initial read and post-save refresh failures recover with GET-only Retry without replaying the successful Save', async () => {
|
||||
composeReads();
|
||||
const original = mockedApi.get.getMockImplementation();
|
||||
const retry = deferred<any>();
|
||||
let draftReads = 0;
|
||||
mockedApi.get.mockImplementation((url: string, config?: any) => {
|
||||
if (url === '/email/drafts') {
|
||||
draftReads += 1;
|
||||
if (draftReads === 1 || draftReads === 3) return Promise.reject({ response: { status: 503 } });
|
||||
if (draftReads === 2) return retry.promise;
|
||||
return Promise.resolve({ data: [savedComposeDraft({ to: 'Maria Recruiter <maria@acme.test>', subject: 'Re: Interview invite', bodyText: 'New captured save' })] } as any);
|
||||
}
|
||||
return original!(url, config);
|
||||
});
|
||||
mockedApi.post.mockResolvedValue({ data: savedComposeDraft({ to: 'Maria Recruiter <maria@acme.test>', subject: 'Re: Interview invite', bodyText: 'New captured save' }) } as any);
|
||||
renderPage();
|
||||
await screen.findByText(copy('emailInboxDraftsLoadFailed'));
|
||||
const button = retryNotice('emailInboxDraftsLoadFailed').getByRole('button', { name: copy('retry') });
|
||||
act(() => { fireEvent.click(button); fireEvent.click(button); });
|
||||
expect(draftReads).toBe(2);
|
||||
expect(mockedApi.post).not.toHaveBeenCalled();
|
||||
await act(async () => { retry.resolve({ data: [] }); await retry.promise; });
|
||||
await openReply('New captured save');
|
||||
fireEvent.click(screen.getByRole('button', { name: copy('emailInboxSaveDraft') }));
|
||||
await waitFor(() => expect(draftReads).toBe(3));
|
||||
expect(mockedApi.post).toHaveBeenCalledTimes(1);
|
||||
expect(composeField('emailInboxMessage')).toHaveValue('New captured save');
|
||||
expect(screen.getByRole('button', { name: copy('profileSaveChanges') })).toBeInTheDocument();
|
||||
fireEvent.click(retryNotice('emailInboxDraftsLoadFailed').getByRole('button', { name: copy('retry') }));
|
||||
await waitFor(() => expect(draftReads).toBe(4));
|
||||
expect(mockedApi.post).toHaveBeenCalledTimes(1);
|
||||
expect(mockedApi.put).not.toHaveBeenCalled();
|
||||
expect(mockedApi.delete).not.toHaveBeenCalled();
|
||||
expect(screen.getByRole('button', { name: copy('emailInboxResumeDraft', { name: 'Re: Interview invite' }) })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('a late Save after inbox unmount cannot refresh drafts or attach its result to another page', async () => {
|
||||
composeReads();
|
||||
const save = deferred<any>();
|
||||
mockedApi.post.mockReturnValue(save.promise);
|
||||
const mounted = renderPage();
|
||||
await openReply('Old owner save');
|
||||
fireEvent.click(screen.getByRole('button', { name: copy('emailInboxSaveDraft') }));
|
||||
expect(mockedApi.post).toHaveBeenCalledTimes(1);
|
||||
await waitFor(() => expect(mockedApi.get).toHaveBeenCalledWith('/jobapplications/choices', expect.anything()));
|
||||
const reads = mockedApi.get.mock.calls.length;
|
||||
mounted.unmount();
|
||||
await act(async () => { save.resolve({ data: savedComposeDraft({ bodyText: 'Old owner save' }) }); await save.promise; });
|
||||
expect(mockedApi.get).toHaveBeenCalledTimes(reads);
|
||||
expect(screen.queryByRole('region', { name: copy('emailInboxDraftTitle') })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(copy('emailInboxDraftSaved'))).not.toBeInTheDocument();
|
||||
expect(mockedApi.post).toHaveBeenCalledTimes(1);
|
||||
expect(mockedApi.put).not.toHaveBeenCalled();
|
||||
expect(mockedApi.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('a held managed-draft 409 retains recipient, subject, body and revision with current-language original Save recovery', async () => {
|
||||
const uuid = composeReads([savedComposeDraft()]);
|
||||
const save = deferred<any>();
|
||||
mockedApi.put.mockReturnValueOnce(save.promise).mockRejectedValueOnce({ response: { status: 409 } });
|
||||
renderPage();
|
||||
fireEvent.click(await screen.findByRole('button', { name: copy('emailInboxResumeDraft', { name: 'Owned compose draft' }) }));
|
||||
fireEvent.change(composeField('emailInboxRecipient'), { target: { value: 'local@acme.test' } });
|
||||
fireEvent.change(composeField('emailInboxSubject'), { target: { value: 'Local conflict subject' } });
|
||||
fireEvent.change(composeField('emailInboxMessage'), { target: { value: 'Local conflict body' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: copy('profileSaveChanges') }));
|
||||
expect(mockedApi.put).toHaveBeenCalledWith('/email/drafts/compose-owned', { revision: 4, to: 'local@acme.test', subject: 'Local conflict subject', bodyText: 'Local conflict body' });
|
||||
await act(async () => { save.reject({ response: { status: 409 } }); await save.promise.catch(() => undefined); });
|
||||
await screen.findByText(copy('emailInboxDraftChangedElsewhere'));
|
||||
const reads = mockedApi.get.mock.calls.length;
|
||||
await changeLanguage('nb');
|
||||
expect(mockedApi.get).toHaveBeenCalledTimes(reads);
|
||||
expect(screen.getByText(copy('emailInboxDraftChangedElsewhere'))).toBeInTheDocument();
|
||||
expect(composeField('emailInboxRecipient')).toHaveValue('local@acme.test');
|
||||
expect(composeField('emailInboxSubject')).toHaveValue('Local conflict subject');
|
||||
expect(composeField('emailInboxMessage')).toHaveValue('Local conflict body');
|
||||
expect(uuid).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole('button', { name: copy('profileSaveChanges') }));
|
||||
await waitFor(() => expect(mockedApi.put).toHaveBeenCalledTimes(2));
|
||||
expect(mockedApi.put).toHaveBeenLastCalledWith('/email/drafts/compose-owned', { revision: 4, to: 'local@acme.test', subject: 'Local conflict subject', bodyText: 'Local conflict body' });
|
||||
expect(mockedApi.post).not.toHaveBeenCalled();
|
||||
expect(mockedApi.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('server attempt rotation preserves three unsaved fields and their dirty state while the next send uses the new identity', async () => {
|
||||
const uuid = composeReads([savedComposeDraft()]);
|
||||
const original = mockedApi.get.getMockImplementation();
|
||||
let serverDraft = savedComposeDraft();
|
||||
mockedApi.get.mockImplementation((url: string, config?: any) => url === '/email/drafts' ? Promise.resolve({ data: [serverDraft] } as any) : original!(url, config));
|
||||
let sends = 0;
|
||||
mockedApi.post.mockImplementation((url: string) => {
|
||||
if (url === '/email/send') return ++sends === 1
|
||||
? Promise.reject({ response: { status: 502, data: { attemptId: 'failed-owned-attempt', status: 'failed', duplicate: false, failureCategory: 'provider_rejected' } } })
|
||||
: Promise.resolve({ data: { attemptId: 'rotated-owned-attempt', status: 'pending', duplicate: false } } as any);
|
||||
if (url === '/email/drafts/compose-owned/new-attempt') {
|
||||
serverDraft = savedComposeDraft({ revision: 5, clientRequestId: 'server-rotated-owned-key' });
|
||||
return Promise.resolve({ data: serverDraft } as any);
|
||||
}
|
||||
return Promise.reject(new Error(`Unexpected POST ${url}`));
|
||||
});
|
||||
renderPage();
|
||||
fireEvent.click(await screen.findByRole('button', { name: copy('emailInboxResumeDraft', { name: 'Owned compose draft' }) }));
|
||||
fireEvent.change(composeField('emailInboxRecipient'), { target: { value: 'edited-recipient@acme.test' } });
|
||||
fireEvent.change(composeField('emailInboxSubject'), { target: { value: 'Unsaved edited subject' } });
|
||||
fireEvent.change(composeField('emailInboxMessage'), { target: { value: 'Unsaved edited message' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: copy('emailInboxReviewSend') }));
|
||||
await closeConfirmation('emailInboxSendEmail');
|
||||
fireEvent.click(await screen.findByRole('button', { name: copy('emailInboxPrepareAttempt') }));
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: copy('emailInboxReviewSend') })).toBeEnabled());
|
||||
expect(mockedApi.post).toHaveBeenCalledWith('/email/drafts/compose-owned/new-attempt', { revision: 4 });
|
||||
expect(composeField('emailInboxRecipient')).toHaveValue('edited-recipient@acme.test');
|
||||
expect(composeField('emailInboxSubject')).toHaveValue('Unsaved edited subject');
|
||||
expect(composeField('emailInboxMessage')).toHaveValue('Unsaved edited message');
|
||||
fireEvent.click(screen.getByRole('button', { name: copy('emailInboxResumeDraft', { name: 'Owned compose draft' }) }));
|
||||
const dialog = await screen.findByRole('dialog', { name: copy('emailInboxReplaceDraftTitle') });
|
||||
expect(within(dialog).getByText(copy('emailInboxUnsavedDraftLost'))).toBeInTheDocument();
|
||||
await closeConfirmation('cancel');
|
||||
expect(composeField('emailInboxMessage')).toHaveValue('Unsaved edited message');
|
||||
fireEvent.click(screen.getByRole('button', { name: copy('emailInboxReviewSend') }));
|
||||
await closeConfirmation('emailInboxSendEmail');
|
||||
expect(mockedApi.post).toHaveBeenLastCalledWith('/email/send', { jobApplicationId: 42, provider: 'gmail', clientRequestId: 'server-rotated-owned-key', to: 'edited-recipient@acme.test', subject: 'Unsaved edited subject', bodyText: 'Unsaved edited message', threadId: 'thread-1', confirmed: true });
|
||||
expect(mockedApi.post).toHaveBeenCalledTimes(3);
|
||||
expect(uuid).not.toHaveBeenCalled();
|
||||
expect(mockedApi.put).not.toHaveBeenCalled();
|
||||
expect(mockedApi.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('a committed Save starts a fresh draft-list read despite an older pending GET and rejects the late old snapshot', async () => {
|
||||
composeReads();
|
||||
const original = mockedApi.get.getMockImplementation();
|
||||
const oldRead = deferred<any>();
|
||||
const freshRead = deferred<any>();
|
||||
let reads = 0;
|
||||
const created = savedComposeDraft({ id: 'newly-saved-owned', revision: 1, clientRequestId: 'newly-saved-server-key', to: 'Maria Recruiter <maria@acme.test>', subject: 'Re: Interview invite', bodyText: 'Freshly saved message' });
|
||||
mockedApi.get.mockImplementation((url: string, config?: any) => url === '/email/drafts' ? ++reads === 1 ? oldRead.promise : freshRead.promise : original!(url, config));
|
||||
mockedApi.post.mockResolvedValue({ data: created } as any);
|
||||
renderPage();
|
||||
await openReply('Freshly saved message');
|
||||
expect(reads).toBe(1);
|
||||
fireEvent.click(screen.getByRole('button', { name: copy('emailInboxSaveDraft') }));
|
||||
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledTimes(1));
|
||||
await waitFor(() => expect(reads).toBe(2));
|
||||
expect(mockedApi.get).toHaveBeenLastCalledWith('/email/drafts');
|
||||
await act(async () => { freshRead.resolve({ data: [created] }); await freshRead.promise; });
|
||||
expect(await screen.findByRole('button', { name: copy('emailInboxResumeDraft', { name: 'Re: Interview invite' }) })).toBeInTheDocument();
|
||||
await act(async () => { oldRead.resolve({ data: [savedComposeDraft({ id: 'outdated-owned', subject: 'Stale old draft' })] }); await oldRead.promise; });
|
||||
expect(screen.getByRole('button', { name: copy('emailInboxResumeDraft', { name: 'Re: Interview invite' }) })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: copy('emailInboxResumeDraft', { name: 'Stale old draft' }) })).not.toBeInTheDocument();
|
||||
expect(composeField('emailInboxMessage')).toHaveValue('Freshly saved message');
|
||||
expect(mockedApi.post).toHaveBeenCalledTimes(1);
|
||||
expect(mockedApi.put).not.toHaveBeenCalled();
|
||||
expect(mockedApi.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user