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 <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
f6618fd57e
commit
e927e5fc60
|
|
@ -32,11 +32,11 @@ const AppContent: React.FC = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen grid grid-rows-[1fr_auto] grid-cols-2">
|
||||
<div className="border-r border-b border-gray-200 p-4">
|
||||
<div className="h-screen grid grid-rows-[1fr_auto] grid-cols-2 bg-gray-50">
|
||||
<div className="border-r border-b border-gray-200 p-4 min-h-0">
|
||||
<VideoPlaceholder />
|
||||
</div>
|
||||
<div className="border-b border-gray-200 p-4 flex flex-col gap-4 overflow-y-auto">
|
||||
<div className="border-b border-gray-200 p-6 flex flex-col gap-4 overflow-y-auto min-h-0">
|
||||
<QueryInput onSubmit={handleQuerySubmit} isLoading={queryMutation.isPending} />
|
||||
<KeywordsDisplay keywords={queryMutation.data?.keywords} isLoading={queryMutation.isPending} />
|
||||
<IngestPanel
|
||||
|
|
@ -46,7 +46,7 @@ const AppContent: React.FC = () => {
|
|||
error={ingestMutation.isError ? (ingestMutation.error instanceof Error ? ingestMutation.error.message : 'Upload failed') : null}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 p-4 border-t border-gray-200 overflow-y-auto">
|
||||
<div className="col-span-2 p-6 border-t border-gray-200 overflow-y-auto min-h-0">
|
||||
<ResponsePanel
|
||||
answer={queryMutation.data?.answer ?? null}
|
||||
sources={queryMutation.data?.sources ?? []}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export const KeywordsDisplay: React.FC<KeywordsDisplayProps> = ({ keywords, isLo
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div data-testid="keywords-section" className="space-y-2 transition-all duration-300 ease-in-out">
|
||||
<span className="text-xs text-gray-500 uppercase tracking-wide">Extracted Keywords:</span>
|
||||
<div data-testid="keywords-container" className="flex flex-wrap">
|
||||
{keywords?.map((keyword, index) => (
|
||||
|
|
|
|||
|
|
@ -40,9 +40,9 @@ export const QueryInput: React.FC<QueryInputProps> = ({ onSubmit, isLoading }) =
|
|||
<button
|
||||
type="submit"
|
||||
disabled={isDisabled}
|
||||
className="px-4 py-2 bg-blue-600 text-white font-medium rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-blue-600"
|
||||
className="px-4 py-2 bg-blue-600 text-white font-medium rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-blue-600 transition-all duration-200"
|
||||
>
|
||||
{isLoading ? 'Loading...' : 'Submit'}
|
||||
{isLoading ? 'Processing...' : 'Submit'}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -55,8 +55,19 @@ describe('KeywordsDisplay', () => {
|
|||
const keywords = ['keyword1', 'keyword2', 'keyword3']
|
||||
render(<KeywordsDisplay keywords={keywords} isLoading={false} />)
|
||||
|
||||
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(<KeywordsDisplay keywords={keywords} isLoading={false} />)
|
||||
|
||||
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', () => {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ describe('QueryInput', () => {
|
|||
|
||||
it('button is disabled when isLoading is true', () => {
|
||||
render(<QueryInput onSubmit={mockOnSubmit} isLoading={true} />)
|
||||
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(<QueryInput onSubmit={mockOnSubmit} isLoading={true} />)
|
||||
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' } })
|
||||
|
||||
|
|
|
|||
|
|
@ -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(<App />)
|
||||
|
||||
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(<App />)
|
||||
|
||||
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(<App />)
|
||||
|
||||
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(<App />)
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue