feat(messaging-ui): attachments in inbox mail — reply + compose upload

InboxAdapter gains uploadAttachment + attachment params on mailReply/
composeInternal/composeExternal. MailReader reply and ComposeModal grow an
attach affordance (paperclip + pending chips); mail history renders sent
attachments. MockInboxAdapter implements upload. 67 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 01:05:14 +05:30
parent 2f24a95ef4
commit 2aa2893973
8 changed files with 214 additions and 34 deletions
@@ -34,6 +34,24 @@ describe('mock inbox adapter', () => {
await a.mailReply('mt_welcome', 'thanks!');
expect((await a.mailHistory('mt_welcome')).some((m) => m.text === 'thanks!')).toBe(true);
});
it('reply carries an uploaded attachment', async () => {
const a = new MockInboxAdapter();
const ref = await a.uploadAttachment(new File(['x'], 'plan.pdf', { type: 'application/pdf' }));
expect(ref).toMatchObject({ filename: 'plan.pdf', mimeType: 'application/pdf' });
await a.mailReply('mt_welcome', '', ref);
const last = (await a.mailHistory('mt_welcome')).at(-1)!;
expect(last.attachment?.filename).toBe('plan.pdf');
});
it('composeInternal carries a first attachment onto the new thread', async () => {
const a = new MockInboxAdapter();
const ref = await a.uploadAttachment(new File(['x'], 'quote.png', { type: 'image/png' }));
await a.composeInternal('pp_sofia', 'Quote', 'see attached', [ref]);
const open = await a.listInbox('OPEN');
const row = open.find((i) => i.title === 'Quote')!;
expect((await a.mailHistory(row.threadId!)).at(-1)?.attachment?.filename).toBe('quote.png');
});
});
describe('<Inbox /> (rendered)', () => {
@@ -55,6 +73,17 @@ describe('<Inbox /> (rendered)', () => {
await waitFor(() => expect(screen.getByText('got it')).toBeTruthy());
});
it('attaches a file into a mail reply', async () => {
mount();
fireEvent.click(await screen.findByText('Welcome to the Founders Club'));
const file = new File(['data'], 'roof.pdf', { type: 'application/pdf' });
fireEvent.change(await screen.findByLabelText('Attach file'), { target: { files: [file] } });
// The pending chip shows the file, then Reply sends it.
await screen.findByText(/roof\.pdf/);
fireEvent.click(screen.getByText('Reply'));
await waitFor(() => expect(screen.getAllByText(/roof\.pdf/).length).toBeGreaterThan(0));
});
it('composes an in-app message and it shows in the inbox', async () => {
mount();
fireEvent.click(await screen.findByText('New message'));