diff --git a/apps/web/src/app/(portal)/modules/tender-radar/components/TenderDetail.test.tsx b/apps/web/src/app/(portal)/modules/tender-radar/components/TenderDetail.test.tsx new file mode 100644 index 0000000..5e792bb --- /dev/null +++ b/apps/web/src/app/(portal)/modules/tender-radar/components/TenderDetail.test.tsx @@ -0,0 +1,125 @@ +import { cleanup, render, screen, waitFor } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +/** + * TenderDetail.test — RED-first (TDD) proof for Plan 11-04 (UI-02, D-07). + * Mirrors the SourceConfigForm.test.tsx pattern: mock the api client + * module (getTender), render with a tenderId prop, assert via waitFor. + * + * Covers the three must-have truths from the plan: + * (a) a source link is rendered and points at the tender's sourceUrl + * with a safe target/rel combination (T-11-09). + * (b) estimatedValue=null and deadlineAt=null render graceful German + * placeholders instead of blank/error output (D-05 applies equally + * to the detail view, not just the list). + * (c) the mandatory no-local-mirroring notice is always present — no + * Vergabeunterlagen are fetched or mirrored locally (rawPayload is + * 100% NULL in the live DB; research finding, no document URLs + * exist to mirror in the first place). + */ + +const mockGetTender = vi.fn(); + +vi.mock('@/lib/tender-radar-api', () => ({ + getTender: (...args: unknown[]) => mockGetTender(...args), +})); + +afterEach(() => { + cleanup(); + mockGetTender.mockReset(); + vi.restoreAllMocks(); +}); + +describe('TenderDetail', () => { + it('renders a source link pointing at the tender sourceUrl with safe target/rel', async () => { + mockGetTender.mockResolvedValue({ + id: 't1', + sourcePortal: 'doe-opendata', + title: 'Sanierung Rathaus', + buyerName: 'Stadt Musterhausen', + cpvCodes: ['45000000'], + region: 'Oberbayern', + plz: '80331', + bundesland: 'Bayern', + deadlineAt: '2026-09-01T00:00:00.000Z', + estimatedValue: '150000.00', + procedureType: 'Offenes Verfahren', + status: 'active', + sourceUrl: 'https://example-portal.de/ausschreibung/t1', + publishedAt: '2026-07-01T00:00:00.000Z', + }); + + const { TenderDetail } = await import('./TenderDetail'); + render( {}} />); + + await waitFor(() => { + expect(screen.getByText('Sanierung Rathaus')).toBeInTheDocument(); + }); + + const link = screen.getByRole('link', { name: /Quellportal/i }); + expect(link).toHaveAttribute( + 'href', + 'https://example-portal.de/ausschreibung/t1', + ); + expect(link).toHaveAttribute('target', '_blank'); + expect(link).toHaveAttribute('rel', 'noopener noreferrer'); + expect(mockGetTender).toHaveBeenCalledWith('t1'); + }); + + it('shows graceful placeholders when estimatedValue and deadlineAt are null', async () => { + mockGetTender.mockResolvedValue({ + id: 't2', + sourcePortal: 'doe-opendata', + title: 'Straßenbau Projekt X', + buyerName: null, + cpvCodes: [], + region: null, + plz: null, + bundesland: null, + deadlineAt: null, + estimatedValue: null, + procedureType: null, + status: 'active', + sourceUrl: null, + publishedAt: '2026-07-01T00:00:00.000Z', + }); + + const { TenderDetail } = await import('./TenderDetail'); + render( {}} />); + + await waitFor(() => { + expect(screen.getByText('Straßenbau Projekt X')).toBeInTheDocument(); + }); + expect(screen.getByText('keine Wertangabe')).toBeInTheDocument(); + expect(screen.getByText('keine Frist angegeben')).toBeInTheDocument(); + }); + + it('always shows the no-local-mirroring notice for Vergabeunterlagen', async () => { + mockGetTender.mockResolvedValue({ + id: 't3', + sourcePortal: 'doe-opendata', + title: 'Beschaffung IT-Ausstattung', + buyerName: 'Landkreis Beispiel', + cpvCodes: ['30200000'], + region: null, + plz: null, + bundesland: null, + deadlineAt: '2026-08-15T00:00:00.000Z', + estimatedValue: '25000.00', + procedureType: 'Verhandlungsverfahren', + status: 'active', + sourceUrl: 'https://example-portal.de/ausschreibung/t3', + publishedAt: '2026-07-01T00:00:00.000Z', + }); + + const { TenderDetail } = await import('./TenderDetail'); + render( {}} />); + + await waitFor(() => { + expect(screen.getByText('Beschaffung IT-Ausstattung')).toBeInTheDocument(); + }); + expect( + screen.getByText(/Vergabeunterlagen werden nicht lokal gespiegelt/i), + ).toBeInTheDocument(); + }); +});