import { render, screen, fireEvent, waitFor } from '@testing-library/react' import { describe, test, expect, vi, beforeEach } from 'vitest' import React from 'react' // Mock the dependencies vi.mock('../../../src/contexts/ToastContext', () => ({ useToast: () => ({ showToast: vi.fn() }) })) vi.mock('../../../src/services/projectService', () => ({ projectService: { getProjectDocuments: vi.fn().mockResolvedValue([]), deleteDocument: vi.fn().mockResolvedValue(undefined), updateDocument: vi.fn().mockResolvedValue({ id: 'doc-1', title: 'Updated' }), getDocument: vi.fn().mockResolvedValue({ id: 'doc-1', title: 'Document 1' }) } })) vi.mock('../../../src/services/knowledgeBaseService', () => ({ knowledgeBaseService: { getItems: vi.fn().mockResolvedValue([]) } })) // Create a minimal DocsTab component for testing const DocsTabTest = () => { const [documents, setDocuments] = React.useState([ { id: 'doc-1', title: 'Document 1', content: { type: 'prp' }, document_type: 'prp', updated_at: '2025-07-30T12:00:00Z' }, { id: 'doc-2', title: 'Document 2', content: { type: 'technical' }, document_type: 'technical', updated_at: '2025-07-30T13:00:00Z' }, { id: 'doc-3', title: 'Document 3', content: { type: 'business' }, document_type: 'business', updated_at: '2025-07-30T14:00:00Z' } ]) const [selectedDocument, setSelectedDocument] = React.useState(documents[0]) const { showToast } = { showToast: vi.fn() } return (
{documents.map(doc => (
setSelectedDocument(doc)} >
{doc.document_type}

{doc.title}

{selectedDocument?.id !== doc.id && ( )}
))}
console.log('New document')} > New Document
{selectedDocument && (
Selected: {selectedDocument.title}
)}
) } describe('DocsTab Document Cards Integration', () => { beforeEach(() => { vi.clearAllMocks() }) test('renders all document cards', () => { render() expect(screen.getByTestId('document-card-doc-1')).toBeInTheDocument() expect(screen.getByTestId('document-card-doc-2')).toBeInTheDocument() expect(screen.getByTestId('document-card-doc-3')).toBeInTheDocument() expect(screen.getByTestId('new-document-card')).toBeInTheDocument() }) test('shows active state on selected document', () => { render() const doc1 = screen.getByTestId('document-card-doc-1') expect(doc1.className).toContain('border-blue-500') const doc2 = screen.getByTestId('document-card-doc-2') expect(doc2.className).not.toContain('border-blue-500') }) test('switches between documents', () => { render() // Initially doc-1 is selected expect(screen.getByTestId('selected-document')).toHaveTextContent('Selected: Document 1') // Click on doc-2 fireEvent.click(screen.getByTestId('document-card-doc-2')) // Now doc-2 should be selected expect(screen.getByTestId('selected-document')).toHaveTextContent('Selected: Document 2') // Check active states expect(screen.getByTestId('document-card-doc-1').className).not.toContain('border-blue-500') expect(screen.getByTestId('document-card-doc-2').className).toContain('border-blue-500') }) test('deletes document with confirmation', () => { const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true) render() // Click delete on doc-2 const deleteButton = screen.getByTestId('delete-doc-2') fireEvent.click(deleteButton) expect(confirmSpy).toHaveBeenCalledWith('Delete "Document 2"?') // Document should be removed expect(screen.queryByTestId('document-card-doc-2')).not.toBeInTheDocument() confirmSpy.mockRestore() }) test('cancels delete when user declines', () => { const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false) render() // Click delete on doc-2 const deleteButton = screen.getByTestId('delete-doc-2') fireEvent.click(deleteButton) // Document should still be there expect(screen.getByTestId('document-card-doc-2')).toBeInTheDocument() confirmSpy.mockRestore() }) test('selects next document when deleting active document', () => { const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true) render() // doc-1 is initially selected expect(screen.getByTestId('selected-document')).toHaveTextContent('Selected: Document 1') // Switch to doc-2 fireEvent.click(screen.getByTestId('document-card-doc-2')) expect(screen.getByTestId('selected-document')).toHaveTextContent('Selected: Document 2') // Switch to doc-1 to delete a non-selected document fireEvent.click(screen.getByTestId('document-card-doc-1')) // Delete doc-2 (not currently selected - it should have delete button) const deleteButton = screen.getByTestId('delete-doc-2') fireEvent.click(deleteButton) // Should automatically select another document expect(screen.getByTestId('selected-document')).toHaveTextContent('Selected: Document') expect(screen.queryByTestId('document-card-doc-2')).not.toBeInTheDocument() confirmSpy.mockRestore() }) test('does not show delete button on active card', () => { render() // doc-1 is active, should not have delete button expect(screen.queryByTestId('delete-doc-1')).not.toBeInTheDocument() // doc-2 is not active, should have delete button expect(screen.getByTestId('delete-doc-2')).toBeInTheDocument() }) test('horizontal scroll container has correct classes', () => { const { container } = render() const scrollContainer = container.querySelector('.overflow-x-auto') expect(scrollContainer).toBeInTheDocument() expect(scrollContainer?.className).toContain('scrollbar-thin') expect(scrollContainer?.className).toContain('scrollbar-thumb-gray-300') }) test('document cards maintain fixed width', () => { render() const cards = screen.getAllByTestId(/document-card-doc-/) cards.forEach(card => { expect(card.className).toContain('flex-shrink-0') expect(card.className).toContain('w-48') }) }) }) describe('DocsTab Document API Integration', () => { test('calls deleteDocument API when deleting a document', async () => { const { projectService } = await import('../../../src/services/projectService') const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true) // Create a test component that uses the actual deletion logic const DocsTabWithAPI = () => { const [documents, setDocuments] = React.useState([ { id: 'doc-1', title: 'Document 1', content: {}, document_type: 'prp', updated_at: '2025-07-30' }, { id: 'doc-2', title: 'Document 2', content: {}, document_type: 'spec', updated_at: '2025-07-30' } ]) const [selectedDocument, setSelectedDocument] = React.useState(documents[0]) const project = { id: 'proj-123', title: 'Test Project' } const { showToast } = { showToast: vi.fn() } const handleDelete = async (docId: string) => { try { // This mirrors the actual DocsTab deletion logic await projectService.deleteDocument(project.id, docId) setDocuments(prev => prev.filter(d => d.id !== docId)) if (selectedDocument?.id === docId) { setSelectedDocument(documents.find(d => d.id !== docId) || null) } showToast('Document deleted', 'success') } catch (error) { console.error('Failed to delete document:', error) showToast('Failed to delete document', 'error') } } return (
{documents.map(doc => (
{doc.title}
))}
) } render() // Click delete button fireEvent.click(screen.getByTestId('delete-doc-2')) // Wait for async operations await waitFor(() => { expect(projectService.deleteDocument).toHaveBeenCalledWith('proj-123', 'doc-2') }) // Verify document is removed from UI expect(screen.queryByTestId('doc-doc-2')).not.toBeInTheDocument() confirmSpy.mockRestore() }) test('handles deletion API errors gracefully', async () => { const { projectService } = await import('../../../src/services/projectService') const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true) const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) // Make deleteDocument reject projectService.deleteDocument = vi.fn().mockRejectedValue(new Error('API Error')) const DocsTabWithError = () => { const [documents, setDocuments] = React.useState([ { id: 'doc-1', title: 'Document 1', content: {}, document_type: 'prp', updated_at: '2025-07-30' } ]) const project = { id: 'proj-123', title: 'Test Project' } const showToast = vi.fn() const handleDelete = async (docId: string) => { try { await projectService.deleteDocument(project.id, docId) setDocuments(prev => prev.filter(d => d.id !== docId)) showToast('Document deleted', 'success') } catch (error) { console.error('Failed to delete document:', error) showToast('Failed to delete document', 'error') } } return (
{documents.map(doc => (
))}
) } render() // Click delete button fireEvent.click(screen.getByTestId('delete-doc-1')) // Wait for async operations await waitFor(() => { expect(projectService.deleteDocument).toHaveBeenCalledWith('proj-123', 'doc-1') }) // Document should still be in UI due to error expect(screen.getByTestId('doc-doc-1')).toBeInTheDocument() // Error should be logged expect(consoleSpy).toHaveBeenCalledWith('Failed to delete document:', expect.any(Error)) confirmSpy.mockRestore() consoleSpy.mockRestore() }) test('deletion persists after page refresh', async () => { const { projectService } = await import('../../../src/services/projectService') // Simulate documents before deletion let mockDocuments = [ { id: 'doc-1', title: 'Document 1', content: {}, document_type: 'prp', updated_at: '2025-07-30' }, { id: 'doc-2', title: 'Document 2', content: {}, document_type: 'spec', updated_at: '2025-07-30' } ] // First render - before deletion const { rerender } = render(
{mockDocuments.length}
) expect(screen.getByTestId('docs-count')).toHaveTextContent('2') // Mock deleteDocument to also update the mock data projectService.deleteDocument = vi.fn().mockImplementation(async (projectId, docId) => { mockDocuments = mockDocuments.filter(d => d.id !== docId) return Promise.resolve() }) // Mock the list function to return current state projectService.listProjectDocuments = vi.fn().mockImplementation(async () => { return mockDocuments }) // Perform deletion await projectService.deleteDocument('proj-123', 'doc-2') // Simulate page refresh by re-fetching documents const refreshedDocs = await projectService.listProjectDocuments('proj-123') // Re-render with refreshed data rerender(
{refreshedDocs.length}
) // Should only have 1 document after refresh expect(screen.getByTestId('docs-count')).toHaveTextContent('1') expect(refreshedDocs).toHaveLength(1) expect(refreshedDocs[0].id).toBe('doc-1') }) })