From 58a63408c792cbebfbf3b96afa470a7aad3b1ebb Mon Sep 17 00:00:00 2001 From: Celes Renata Date: Tue, 21 Apr 2026 07:20:29 +0000 Subject: [PATCH] feat: add Ops and Watchlists tests (144 total frontend tests) --- frontend/src/test/ops.test.tsx | 167 ++++++++++++++++++++++++++ frontend/src/test/watchlists.test.tsx | 51 ++++++++ 2 files changed, 218 insertions(+) create mode 100644 frontend/src/test/ops.test.tsx create mode 100644 frontend/src/test/watchlists.test.tsx diff --git a/frontend/src/test/ops.test.tsx b/frontend/src/test/ops.test.tsx new file mode 100644 index 0000000..8452d17 --- /dev/null +++ b/frontend/src/test/ops.test.tsx @@ -0,0 +1,167 @@ +import { describe, it, expect } from 'vitest'; +import { screen, waitFor } from '@testing-library/react'; +import { renderRoute } from './render'; + +describe('Pipeline Health page', () => { + it('renders page title', async () => { + renderRoute('/ops/pipeline'); + await waitFor(() => { + expect(screen.getByRole('heading', { name: 'Pipeline Health' })).toBeInTheDocument(); + }); + }); + + it('renders queue depths section', async () => { + renderRoute('/ops/pipeline'); + await waitFor(() => { + expect(screen.getByText('Queue Depths')).toBeInTheDocument(); + }); + }); + + it('renders document stages section', async () => { + renderRoute('/ops/pipeline'); + await waitFor(() => { + expect(screen.getByText('Document Stages')).toBeInTheDocument(); + }); + // Mock has { status: 'extracted', doc_count: 5 } + expect(screen.getByText('5')).toBeInTheDocument(); + }); + + it('renders parsing quality section', async () => { + renderRoute('/ops/pipeline'); + await waitFor(() => { + expect(screen.getByText('Parsing Quality')).toBeInTheDocument(); + }); + }); + + it('renders extraction validation section', async () => { + renderRoute('/ops/pipeline'); + await waitFor(() => { + expect(screen.getByText('Extraction Validation')).toBeInTheDocument(); + }); + }); + + it('renders trend generation section', async () => { + renderRoute('/ops/pipeline'); + await waitFor(() => { + expect(screen.getByText('Trend Generation')).toBeInTheDocument(); + }); + }); + + it('renders pipeline toggle button', async () => { + renderRoute('/ops/pipeline'); + await waitFor(() => { + expect(screen.getByText('Pipeline ON')).toBeInTheDocument(); + }); + }); + + it('renders queue labels', async () => { + renderRoute('/ops/pipeline'); + await waitFor(() => { + expect(screen.getByText('Queue Depths')).toBeInTheDocument(); + }); + // Queue labels are rendered inside the Queue Depths card + expect(screen.getByText('Aggregation')).toBeInTheDocument(); + expect(screen.getByText('Recommendation')).toBeInTheDocument(); + }); +}); + +describe('Ingestion Monitor page', () => { + it('renders page title', async () => { + renderRoute('/ops/ingestion'); + await waitFor(() => { + expect(screen.getByRole('heading', { name: 'Ingestion Monitor' })).toBeInTheDocument(); + }); + }); + + it('renders summary stat cards', async () => { + renderRoute('/ops/ingestion'); + await waitFor(() => { + expect(screen.getByText('Total Runs')).toBeInTheDocument(); + }); + expect(screen.getByText('Items Fetched')).toBeInTheDocument(); + expect(screen.getByText('New Items')).toBeInTheDocument(); + }); + + it('renders summary values from mock data', async () => { + renderRoute('/ops/ingestion'); + await waitFor(() => { + // mock: total_runs: 10, completed: 8, failed: 2, total_items_fetched: 50, total_items_new: 12 + expect(screen.getByText('10')).toBeInTheDocument(); + }); + expect(screen.getByText('8')).toBeInTheDocument(); + expect(screen.getByText('50')).toBeInTheDocument(); + expect(screen.getByText('12')).toBeInTheDocument(); + }); + + it('renders throughput chart section', async () => { + renderRoute('/ops/ingestion'); + await waitFor(() => { + expect(screen.getByText('Throughput')).toBeInTheDocument(); + }); + }); + + it('renders bucket selector buttons', async () => { + renderRoute('/ops/ingestion'); + await waitFor(() => { + expect(screen.getByText('Ingestion Monitor')).toBeInTheDocument(); + }); + // Bucket selector has 15m, 1h, 6h, 1d — just verify the group exists + expect(screen.getByText('15m')).toBeInTheDocument(); + }); +}); + +describe('Model Performance page', () => { + it('renders page title', async () => { + renderRoute('/ops/model'); + await waitFor(() => { + expect(screen.getByRole('heading', { name: 'Model Performance' })).toBeInTheDocument(); + }); + }); + + it('renders key metric cards', async () => { + renderRoute('/ops/model'); + await waitFor(() => { + expect(screen.getByText('Total Extractions')).toBeInTheDocument(); + }); + expect(screen.getByText('Success Rate')).toBeInTheDocument(); + expect(screen.getByText('Avg Latency')).toBeInTheDocument(); + expect(screen.getByText('Retry Rate')).toBeInTheDocument(); + expect(screen.getByText('Avg Confidence')).toBeInTheDocument(); + }); + + it('renders metric values from mock data', async () => { + renderRoute('/ops/model'); + await waitFor(() => { + // mock: total_extractions: 20, success_rate: 0.9, avg_duration_ms: 1500, retry_rate: 0.05, avg_confidence: 0.8 + expect(screen.getByText('20')).toBeInTheDocument(); + }); + expect(screen.getByText('90.0%')).toBeInTheDocument(); + expect(screen.getByText('1500ms')).toBeInTheDocument(); + expect(screen.getByText('5.0%')).toBeInTheDocument(); + expect(screen.getByText('80%')).toBeInTheDocument(); + }); + + it('renders recent failures section', async () => { + renderRoute('/ops/model'); + await waitFor(() => { + expect(screen.getByText('Recent Failures (0)')).toBeInTheDocument(); + }); + expect(screen.getByText('No recent failures')).toBeInTheDocument(); + }); +}); + +describe('Source Coverage page', () => { + it('renders page title', async () => { + renderRoute('/ops/coverage'); + await waitFor(() => { + expect(screen.getByRole('heading', { name: 'Source Coverage' })).toBeInTheDocument(); + }); + }); + + it('renders coverage matrix section', async () => { + renderRoute('/ops/coverage'); + await waitFor(() => { + expect(screen.getByText('Company × Source Type Matrix')).toBeInTheDocument(); + }); + }); +}); diff --git a/frontend/src/test/watchlists.test.tsx b/frontend/src/test/watchlists.test.tsx new file mode 100644 index 0000000..692761a --- /dev/null +++ b/frontend/src/test/watchlists.test.tsx @@ -0,0 +1,51 @@ +import { describe, it, expect } from 'vitest'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { renderRoute } from './render'; + +describe('Watchlists page', () => { + it('renders page title', async () => { + renderRoute('/watchlists'); + await waitFor(() => { + expect(screen.getByRole('heading', { name: 'Watchlists' })).toBeInTheDocument(); + }); + }); + + it('renders New Watchlist button', async () => { + renderRoute('/watchlists'); + await waitFor(() => { + expect(screen.getByText('New Watchlist')).toBeInTheDocument(); + }); + }); + + it('shows create form on button click', async () => { + const user = userEvent.setup(); + renderRoute('/watchlists'); + await waitFor(() => { + expect(screen.getByText('New Watchlist')).toBeInTheDocument(); + }); + await user.click(screen.getByText('New Watchlist')); + await waitFor(() => { + expect(screen.getByLabelText('Watchlist name')).toBeInTheDocument(); + }); + expect(screen.getByLabelText('Watchlist description')).toBeInTheDocument(); + expect(screen.getByText('Create')).toBeInTheDocument(); + expect(screen.getByText('Cancel')).toBeInTheDocument(); + }); + + it('cancel closes the create form', async () => { + const user = userEvent.setup(); + renderRoute('/watchlists'); + await waitFor(() => { + expect(screen.getByText('New Watchlist')).toBeInTheDocument(); + }); + await user.click(screen.getByText('New Watchlist')); + await waitFor(() => { + expect(screen.getByText('Cancel')).toBeInTheDocument(); + }); + await user.click(screen.getByText('Cancel')); + await waitFor(() => { + expect(screen.queryByLabelText('Watchlist name')).not.toBeInTheDocument(); + }); + }); +});