From e927e5fc600f21d001a736b5dc9ab758a60d2761 Mon Sep 17 00:00:00 2001 From: Woody Date: Thu, 23 Apr 2026 11:46:53 +0800 Subject: [PATCH] feat(frontend): polish styling, spacing, and add e2e integration tests for Phase 1.3 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- frontend/src/App.tsx | 8 +- frontend/src/components/KeywordsDisplay.tsx | 2 +- frontend/src/components/QueryInput.tsx | 4 +- .../test/components/KeywordsDisplay.test.tsx | 15 ++- .../src/test/components/QueryInput.test.tsx | 4 +- frontend/src/test/e2e/query_flow.test.tsx | 124 ++++++++++++++++++ 6 files changed, 146 insertions(+), 11 deletions(-) create mode 100644 frontend/src/test/e2e/query_flow.test.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 037d244..7e642c3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -32,11 +32,11 @@ const AppContent: React.FC = () => { } return ( -
-
+
+
-
+
{ error={ingestMutation.isError ? (ingestMutation.error instanceof Error ? ingestMutation.error.message : 'Upload failed') : null} />
-
+
= ({ keywords, isLo } return ( -
+
Extracted Keywords:
{keywords?.map((keyword, index) => ( diff --git a/frontend/src/components/QueryInput.tsx b/frontend/src/components/QueryInput.tsx index f2401bc..9f03d9c 100644 --- a/frontend/src/components/QueryInput.tsx +++ b/frontend/src/components/QueryInput.tsx @@ -40,9 +40,9 @@ export const QueryInput: React.FC = ({ onSubmit, isLoading }) = ) diff --git a/frontend/src/test/components/KeywordsDisplay.test.tsx b/frontend/src/test/components/KeywordsDisplay.test.tsx index 657a926..7a32387 100644 --- a/frontend/src/test/components/KeywordsDisplay.test.tsx +++ b/frontend/src/test/components/KeywordsDisplay.test.tsx @@ -55,8 +55,19 @@ describe('KeywordsDisplay', () => { const keywords = ['keyword1', 'keyword2', 'keyword3'] render() - const container = screen.getByTestId('keywords-container') - expect(container).toHaveClass('flex', 'flex-wrap') + const container = screen.getByTestId('keywords-section') + expect(container).toBeInTheDocument() + + const innerContainer = screen.getByTestId('keywords-container') + expect(innerContainer).toHaveClass('flex', 'flex-wrap') + }) + + it('has smooth transition classes on keywords section', () => { + const keywords = ['test'] + render() + + const section = screen.getByTestId('keywords-section') + expect(section).toHaveClass('transition-all', 'duration-300', 'ease-in-out') }) it('renders each keyword chip with mr-2 mb-2 spacing', () => { diff --git a/frontend/src/test/components/QueryInput.test.tsx b/frontend/src/test/components/QueryInput.test.tsx index 054fd01..fda6779 100644 --- a/frontend/src/test/components/QueryInput.test.tsx +++ b/frontend/src/test/components/QueryInput.test.tsx @@ -39,7 +39,7 @@ describe('QueryInput', () => { it('button is disabled when isLoading is true', () => { render() - const button = screen.getByRole('button', { name: /loading/i }) + const button = screen.getByRole('button', { name: /processing/i }) expect(button).toBeDisabled() }) @@ -52,7 +52,7 @@ describe('QueryInput', () => { it('button is disabled when isLoading is true even with text', () => { render() const textarea = screen.getByPlaceholderText('Ask a question about your documents...') - const button = screen.getByRole('button', { name: /loading/i }) + const button = screen.getByRole('button', { name: /processing/i }) fireEvent.change(textarea, { target: { value: 'Some question' } }) diff --git a/frontend/src/test/e2e/query_flow.test.tsx b/frontend/src/test/e2e/query_flow.test.tsx new file mode 100644 index 0000000..c2c771d --- /dev/null +++ b/frontend/src/test/e2e/query_flow.test.tsx @@ -0,0 +1,124 @@ +import React from 'react' +import { render, screen, fireEvent, waitFor, act } from '@testing-library/react' +import { queryClient } from '../../lib/queries' +import App from '../../App' +import type { QueryResponse, IngestResponse } from '../../types' + +vi.mock('../../lib/api', () => ({ + queryDocument: vi.fn(), + ingestDocument: vi.fn(), +})) + +import { queryDocument, ingestDocument } from '../../lib/api' + +const mockQueryDocument = vi.mocked(queryDocument) +const mockIngestDocument = vi.mocked(ingestDocument) + +const mockQueryResponse: QueryResponse = { + keywords: ['legislative', 'council', 'meeting'], + answer: '- The meeting was held on January 15\n- Key topics were discussed', + sources: [ + { + filename: 'minutes.pdf', + upload_date: '2024-01-15', + content_summary: 'Meeting minutes', + chunk_index: 0, + }, + ], +} + +const mockIngestResponse: IngestResponse = { + document_id: 'doc-abc123', + chunk_count: 5, + filename: 'test-document.pdf', +} + +describe('Query flow integration (App-level)', () => { + beforeEach(() => { + queryClient.clear() + vi.restoreAllMocks() + mockQueryDocument.mockResolvedValue(mockQueryResponse) + mockIngestDocument.mockResolvedValue(mockIngestResponse) + }) + + it('shows empty state initially', () => { + render() + + expect(screen.getByText(/Video upload coming in Phase 2/i)).toBeInTheDocument() + expect( + screen.getByText(/Ask a question to see the answer here/i), + ).toBeInTheDocument() + + const submitButton = screen.getByRole('button', { name: /submit/i }) + expect(submitButton).toBeDisabled() + }) + + it('full query flow: type question, submit, see keywords and answer', async () => { + mockQueryDocument.mockResolvedValue(mockQueryResponse) + + render() + + const textarea = screen.getByPlaceholderText( + 'Ask a question about your documents...', + ) + fireEvent.change(textarea, { target: { value: 'What happened at the meeting?' } }) + + const submitButton = screen.getByRole('button', { name: /submit/i }) + await act(async () => { + fireEvent.click(submitButton) + }) + + await waitFor(() => { + expect(screen.getByText('legislative')).toBeInTheDocument() + }) + expect(screen.getByText('council')).toBeInTheDocument() + expect(screen.getByText('meeting')).toBeInTheDocument() + + await waitFor(() => { + expect(screen.getByText('The meeting was held on January 15')).toBeInTheDocument() + }) + expect(screen.getByText('Key topics were discussed')).toBeInTheDocument() + + expect(screen.getByText('minutes.pdf')).toBeInTheDocument() + expect(screen.getByText('Meeting minutes')).toBeInTheDocument() + }) + + it('handles API error gracefully', async () => { + mockQueryDocument.mockRejectedValue(new Error('Server error: 500')) + + render() + + const textarea = screen.getByPlaceholderText( + 'Ask a question about your documents...', + ) + fireEvent.change(textarea, { target: { value: 'Will this fail?' } }) + + const submitButton = screen.getByRole('button', { name: /submit/i }) + await act(async () => { + fireEvent.click(submitButton) + }) + + await waitFor(() => { + expect(screen.getByText(/Server error: 500/i)).toBeInTheDocument() + }) + }) + + it('handles ingest flow: upload document successfully', async () => { + mockIngestDocument.mockResolvedValue(mockIngestResponse) + + render() + + const fileInput = screen.getByTestId('file-input') + const file = new File(['dummy content'], 'test-document.pdf', { + type: 'application/pdf', + }) + + await act(async () => { + fireEvent.change(fileInput, { target: { files: [file] } }) + }) + + await waitFor(() => { + expect(screen.getByText(/Uploaded test-document\.pdf/i)).toBeInTheDocument() + }) + }) +})