import * as React from 'react' import { render, screen, fireEvent } from '@testing-library/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { DeckBuilderWorkspace } from './DeckBuilderWorkspace' import type { DeckBuilder } from './types' /** * The composer is pure: it renders whatever the injected `builder` exposes and * lights up optional affordances (style chip, reference uploader) only when the * builder declares them. These tests drive it with hand-rolled builders — no app * import — proving the surface is app-agnostic. */ function makeBuilder(overrides: Partial = {}): DeckBuilder { return { deckTitle: '', phase: 'empty', messages: [], slides: [], setActiveId: jest.fn(), sendMessage: jest.fn(), regenerate: jest.fn(), ...overrides, } } function renderWithClient(ui: React.ReactElement) { const client = new QueryClient({ defaultOptions: { queries: { retry: false } }, }) return render({ui}) } describe('DeckBuilderWorkspace', () => { it('renders the dual-pane surface from a minimal injected builder', () => { renderWithClient() // Toolbar shows the placeholder title + the empty-outline hint. expect(screen.getByText('New presentation')).toBeInTheDocument() expect(screen.getByText('Paste an outline to begin')).toBeInTheDocument() // Empty deck pane surfaces its empty-state copy. expect( screen.getByText(/Your slides will render here as a live deck/i) ).toBeInTheDocument() // Phase-empty composer uses the outline placeholder. expect( screen.getByPlaceholderText(/Paste your outline/i) ).toBeInTheDocument() }) it('routes composer submissions to builder.sendMessage', () => { const builder = makeBuilder() renderWithClient() const textarea = screen.getByPlaceholderText(/Paste your outline/i) // Single-line content submits on bare Enter. fireEvent.change(textarea, { target: { value: 'My deck outline' } }) fireEvent.keyDown(textarea, { key: 'Enter' }) expect(builder.sendMessage).toHaveBeenCalledWith('My deck outline') }) it('shows the derived-style chip only when the builder provides deckStyle', () => { const { rerender } = renderWithClient( ) expect(screen.queryByTestId('deck-style-chip')).not.toBeInTheDocument() const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }) rerender( ) const chip = screen.getByTestId('deck-style-chip') expect(chip).toBeInTheDocument() expect(chip).toHaveTextContent('Modern Professional') // `source: 'reference'` marks it as cloned. expect(chip).toHaveTextContent(/cloned/i) }) it('wires the reference uploader only when the builder supports attachments', () => { // Demo-style builder: no attachments capability -> no attach affordance. const { unmount } = renderWithClient( ) expect(screen.queryByTestId('chat-attach-input')).not.toBeInTheDocument() unmount() // Backend-style builder: supportsAttachments + an injected client -> uploader on. const attachments = { listAttachments: jest.fn().mockResolvedValue([]), uploadAttachment: jest.fn(), deleteAttachment: jest.fn(), } renderWithClient( ) expect(screen.getByTestId('chat-attach-input')).toBeInTheDocument() }) })