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);
  });
});