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