channelAttachments.test.tsx4.0 KBView on GitHub import { render, screen } from '@testing-library/react';
// The sender avatar reaches for tRPC (BIMI lookup) and a canvas, neither of which exists here and
// neither of which is what these cases are about.
jest.mock('@/components/ui/bimi-avatar', () => ({
BimiAvatar: ({ name }: { name: string }) => <span>{name}</span>,
}));
import {
ChannelMessageList,
type ThreadMessage,
} from '@/modules/inbox/components/ChannelMessageList';
/**
* Slack file attachments in the unibox's channel views (design: slack-images.md).
*
* Same shape of gap as reactions before it: storage, presigning and the `SlackAttachments`
* renderer all existed and were wired into the CONVERSATION timeline, so a customer's screenshot
* appeared on the deal and rendered nowhere in the unibox — the surface people actually read
* Slack in. The missing pieces were the read (which selected Slack's link-unfurl `attachments`
* column and not the `attachment_refs` one holding real files) and this row.
*
* The cases worth pinning are the ones a screenshot would not catch: a ref whose presign failed,
* which must degrade to a chip rather than a broken image, and the empty case, which must cost
* no vertical space at all.
*/
const message = (overrides: Partial<ThreadMessage> = {}): ThreadMessage => ({
id: 'evt-1',
senderName: 'Zach',
outbound: false,
when: '2026-08-16T20:24:12.000Z',
body: 'It happens a lot',
...overrides,
});
describe('ChannelMessageList attachments', () => {
it('renders an inbound image inline, linked to its full-size URL', () => {
render(
<ChannelMessageList
messages={[
message({
attachments: [
{
key=[redacted],
name: 'screenshot.png',
mime: 'image/png',
isImage: true,
url: 'https://files.example/presigned/screenshot.png',
},
],
}),
]}
/>,
);
const image = screen.getByAltText('screenshot.png');
expect(image).toHaveAttribute('src', 'https://files.example/presigned/screenshot.png');
expect(image.closest('a')).toHaveAttribute(
'href',
'https://files.example/presigned/screenshot.png',
);
});
it('degrades an image whose presign failed to a chip, not a broken <img>', () => {
// `url` absent is exactly what a failed presign leaves behind — best-effort by design, so
// the read returns the ref regardless. Rendering it as an <img> would draw a broken icon.
render(
<ChannelMessageList
messages={[
message({
attachments: [
{ key=[redacted], name: 'diagram.png', isImage: true },
],
}),
]}
/>,
);
expect(screen.queryByRole('img')).not.toBeInTheDocument();
expect(screen.getByText('diagram.png')).toBeInTheDocument();
});
it('renders a non-image upload as a downloadable chip', () => {
render(
<ChannelMessageList
messages={[
message({
attachments: [
{
key=[redacted],
name: 'contract.pdf',
mime: 'application/pdf',
isImage: false,
url: 'https://files.example/presigned/contract.pdf',
},
],
}),
]}
/>,
);
expect(screen.getByText('contract.pdf').closest('a')).toHaveAttribute(
'href',
'https://files.example/presigned/contract.pdf',
);
});
it('draws nothing for a message with no attachments', () => {
// Every message in a channel takes this path, so "nothing" has to mean no element at all —
// an empty container would space out an entire channel of ordinary messages.
const { container } = render(<ChannelMessageList messages={[message()]} />);
expect(screen.queryByRole('img')).not.toBeInTheDocument();
expect(container.querySelectorAll('a')).toHaveLength(0);
});
});