From bd036ef0f0a11f3bec7f89f078d054036fc564d0 Mon Sep 17 00:00:00 2001 From: Christopher Sellers <1@christophersellers.com> Date: Mon, 26 Oct 2026 07:29:06 -0700 Subject: [PATCH] fix(ingest): replace DEMO-DOC-ID fallback with empty state Signed-off-by: Christopher Sellers <1@christophersellers.com> --- .../pages/ingest-normalized-viewer.test.tsx | 93 +++++++++++++++++++ frontend/src/app/ingest/page.tsx | 13 ++- 2 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 frontend/src/__tests__/pages/ingest-normalized-viewer.test.tsx 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 b30ade7ea..0fb1d4b45 100644 --- a/frontend/src/app/ingest/page.tsx +++ b/frontend/src/app/ingest/page.tsx @@ -411,6 +411,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; return ( @@ -893,9 +896,13 @@ export default function IngestPage() { - + {typeof completedDocumentId === 'string' && completedDocumentId.length > 0 ? ( + + ) : ( +

+ No normalized document is available for this ingest. +

+ )} )}