attachOnDraftChat.test.tsx2.9 KBView on GitHub
import { fireEvent, render, screen } from '@testing-library/react';

import { ChatDropZone } from '@/modules/files/components/chat/ChatDropZone';

/**
 * Attaching a file works on a chat that does not exist yet.
 *
 * The chat's resting state is the DRAFT: no thread id, in the frontend or the backend, until
 * something needs one (`ensureOpenChatThread`). Both attach paths used to demand a thread they
 * were never going to be given — the paperclip took `disabled={!mainThreadId}` and sat greyed out,
 * and this drop zone returned early on a null `threadId` — so on every fresh chat the only way to
 * enable attaching was to type something first. Picking or dropping a file IS a first use.
 */

// `mock`-prefixed so the factory may close over it — jest hoists the factory above this file.
const mockUploadToChat = jest.fn();
jest.mock('@/modules/files/upload/useFileUpload', () => ({
  useFileUpload: () => ({ uploadToChat: mockUploadToChat }),
}));

/** A drop carrying one real file, in the shape `readDroppedItems` walks. */
function fileDrop(file: File) {
  return {
    dataTransfer: {
      types: ['Files'],
      files: [file],
      items: [{ kind: 'file', type: file.type, getAsFile: () => file, webkitGetAsEntry: () => null }],
    },
  };
}

describe('attaching to the draft chat', () => {
  beforeEach(() => mockUploadToChat.mockClear());

  it('mints the thread on drop instead of refusing it', async () => {
    // No thread yet — exactly the state a fresh chat is in. Resolving is what mints one.
    const resolveThreadId = jest.fn(() => 'thread_minted_on_use');

    const { container } = render(
      <ChatDropZone resolveThreadId={resolveThreadId}>
        <div>composer</div>
      </ChatDropZone>,
    );
    const zone = container.firstElementChild!;

    // The drag affordance no longer waits on a thread either.
    fireEvent.dragEnter(zone, fileDrop(new File(['x'], 'a.png', { type: 'image/png' })));
    expect(await screen.findByText('Drop to attach to message')).toBeInTheDocument();

    const file = new File(['x'], 'a.png', { type: 'image/png' });
    fireEvent.drop(zone, fileDrop(file));

    await screen.findByText('composer');
    expect(resolveThreadId).toHaveBeenCalled();
    expect(mockUploadToChat).toHaveBeenCalledWith({ threadId: 'thread_minted_on_use', file });
  });

  it('does not mint a thread for a drop that carries nothing', async () => {
    const resolveThreadId = jest.fn(() => 'thread_1');
    const { container } = render(
      <ChatDropZone resolveThreadId={resolveThreadId}>
        <div>composer</div>
      </ChatDropZone>,
    );

    fireEvent.drop(container.firstElementChild!, {
      dataTransfer: { types: ['Files'], files: [], items: [] },
    });

    await screen.findByText('composer');
    // A stray empty chat is worse than a dropped no-op: nothing to attach, nothing to create.
    expect(resolveThreadId).not.toHaveBeenCalled();
    expect(mockUploadToChat).not.toHaveBeenCalled();
  });
});