diff --git a/frontend/src/__tests__/pages/ingest-csv.test.tsx b/frontend/src/__tests__/pages/ingest-csv.test.tsx index 2df551d2a..1d3619bc3 100644 --- a/frontend/src/__tests__/pages/ingest-csv.test.tsx +++ b/frontend/src/__tests__/pages/ingest-csv.test.tsx @@ -278,8 +278,10 @@ describe('CSV import results on /ingest', () => { expect(await screen.findByText('API key could not be saved. Please try again.')).toBeInTheDocument(); expect(screen.getByRole('alert')).toHaveTextContent('API key could not be saved. Please try again.'); - expect(screen.getByText('Normalized document viewer')).toBeInTheDocument(); - expect(screen.getByText(`Receipt: ${jobId}`)).toBeInTheDocument(); + expect(screen.getByText('Ingestion request completed')).toBeInTheDocument(); + // No DEMO-DOC-ID / job_id fallback: completed panel shows empty state, not a fake viewer receipt. + expect(screen.getByText('No normalized document is available for this ingest.')).toBeInTheDocument(); + expect(screen.queryByText('Normalized document viewer')).not.toBeInTheDocument(); } finally { vi.useRealTimers(); } diff --git a/frontend/src/__tests__/pages/ingest-normalized-viewer.test.tsx b/frontend/src/__tests__/pages/ingest-normalized-viewer.test.tsx new file mode 100644 index 000000000..19d91e38e --- /dev/null +++ b/frontend/src/__tests__/pages/ingest-normalized-viewer.test.tsx @@ -0,0 +1,93 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { fireEvent, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import IngestPage from '@/app/ingest/page'; + +const mocks = vi.hoisted(() => ({ + ingestURL: vi.fn(), + ingestFile: vi.fn(), + ingestEpcisEvent: vi.fn(), + ingestEpcisBatch: vi.fn(), + ingestEpcisXml: vi.fn(), + validateEpcis: vi.fn(), + getIngestionJob: vi.fn(), + refresh: vi.fn(), + storeApiKey: vi.fn(), + urlData: undefined as { job_id?: string; document_id?: string } | undefined, +})); + +vi.mock('@/lib/auth-context', () => ({ + useAuth: () => ({ apiKey: 'cookie-managed', setApiKey: mocks.storeApiKey }), // pragma: allowlist secret +})); +vi.mock('@/hooks/use-api', () => ({ + useIngestURL: () => ({ mutateAsync: mocks.ingestURL, data: mocks.urlData, reset: vi.fn(), isPending: false, isError: false }), + useIngestFile: () => ({ mutateAsync: mocks.ingestFile, reset: vi.fn(), isPending: false, isError: false }), + useIngestEpcisEvent: () => ({ mutateAsync: mocks.ingestEpcisEvent, reset: vi.fn(), isPending: false }), + useIngestEpcisBatch: () => ({ mutateAsync: mocks.ingestEpcisBatch, reset: vi.fn(), isPending: false }), + useIngestEpcisXml: () => ({ mutateAsync: mocks.ingestEpcisXml, reset: vi.fn(), isPending: false }), + useValidateEpcisEvent: () => ({ mutateAsync: mocks.validateEpcis, reset: vi.fn(), isPending: false }), +})); +vi.mock('@/lib/api-client', () => ({ + apiClient: { getIngestionJob: mocks.getIngestionJob }, +})); +vi.mock('@/hooks/use-dashboard-refresh', () => ({ + notifyDashboardRefresh: mocks.refresh, +})); +vi.mock('@/app/ingest/NormalizedDocumentViewer', () => ({ + NormalizedDocumentViewer: ({ documentId }: { documentId: string }) => ( +
{documentId}
+ ), +})); + +const EMPTY_STATE = 'No normalized document is available for this ingest.'; + +function mockCompletedUrlIngest(response: { job_id: string; document_id?: string }) { + mocks.ingestURL.mockImplementation(async () => { + mocks.urlData = response; + return response; + }); + mocks.getIngestionJob.mockResolvedValue({ status: 'completed' }); +} + +async function submitUrlIngest() { + render(); + fireEvent.change(screen.getByPlaceholderText(/https:\/\//i), { + target: { value: `https://example.org/document-${crypto.randomUUID()}.pdf` }, + }); + await userEvent.click(screen.getByRole('button', { name: /^ingest document$/i })); + // The page polls the job every 2s; the mocked poll resolves completed. + expect(await screen.findByText('Ingestion request completed', {}, { timeout: 5000 })).toBeInTheDocument(); +} + +describe('completed URL ingest on /ingest', () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.urlData = undefined; + mocks.storeApiKey.mockResolvedValue(undefined); + }); + + it('shows an empty state and no viewer when document_id is absent', async () => { + mockCompletedUrlIngest({ job_id: crypto.randomUUID() }); + await submitUrlIngest(); + + expect(screen.getByText(EMPTY_STATE)).toBeInTheDocument(); + expect(screen.queryByTestId('normalized-viewer')).not.toBeInTheDocument(); + }); + + it('shows an empty state and no viewer when document_id is an empty string', async () => { + mockCompletedUrlIngest({ job_id: crypto.randomUUID(), document_id: '' }); + await submitUrlIngest(); + + expect(screen.getByText(EMPTY_STATE)).toBeInTheDocument(); + expect(screen.queryByTestId('normalized-viewer')).not.toBeInTheDocument(); + }); + + it('passes the real document id to the viewer when present', async () => { + const documentId = crypto.randomUUID(); + mockCompletedUrlIngest({ job_id: crypto.randomUUID(), document_id: documentId }); + await submitUrlIngest(); + + expect(screen.queryByText(EMPTY_STATE)).not.toBeInTheDocument(); + expect(screen.getByTestId('normalized-viewer')).toHaveTextContent(documentId); + }); +}); diff --git a/frontend/src/app/ingest/page.tsx b/frontend/src/app/ingest/page.tsx index 25e4b520c..d381cdf23 100644 --- a/frontend/src/app/ingest/page.tsx +++ b/frontend/src/app/ingest/page.tsx @@ -565,6 +565,9 @@ export default function IngestPage() { ? 'success' : 'warning' : 'neutral'; + // No fallback id: without a real document_id the completed panel shows an + // empty state instead of the viewer. + const completedDocumentId = ingestMutation.data?.document_id; const activeSaveError = activeTab === 'file' ? csvError : activeTab === 'epcis' ? epcisError : urlError; return ( @@ -1071,9 +1074,13 @@ export default function IngestPage() { )} - + {typeof completedDocumentId === 'string' && completedDocumentId.length > 0 ? ( + + ) : ( +

+ No normalized document is available for this ingest. +

+ )} )}