From 8e3dfda64db5085746250a6759b679fff3f9df5e Mon Sep 17 00:00:00 2001 From: Schalli Date: Tue, 21 Jul 2026 11:25:43 +0200 Subject: [PATCH] =?UTF-8?q?test(10-06):=20SourceConfigForm=20component=20t?= =?UTF-8?q?est=20=E2=80=94=20fetch,=20save,=20bounds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fetch-on-mount populates pollIntervalMin input - Speichern calls saveSourceConfig with edited interval + isActive - interval below 5 or above 1440 rejected client-side, no save call - automated proof for the INGEST-06 admin-UI slice --- .../components/SourceConfigForm.test.tsx | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 apps/web/src/app/(portal)/modules/tender-radar/settings/components/SourceConfigForm.test.tsx diff --git a/apps/web/src/app/(portal)/modules/tender-radar/settings/components/SourceConfigForm.test.tsx b/apps/web/src/app/(portal)/modules/tender-radar/settings/components/SourceConfigForm.test.tsx new file mode 100644 index 0000000..0849d05 --- /dev/null +++ b/apps/web/src/app/(portal)/modules/tender-radar/settings/components/SourceConfigForm.test.tsx @@ -0,0 +1,136 @@ +import { cleanup, render, screen, waitFor, fireEvent } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +// Mock @/lib/tender-radar-api +const mockFetchSourceConfig = vi.fn(); +const mockSaveSourceConfig = vi.fn(); + +vi.mock('@/lib/tender-radar-api', () => ({ + fetchSourceConfig: (...args: unknown[]) => mockFetchSourceConfig(...args), + saveSourceConfig: (...args: unknown[]) => mockSaveSourceConfig(...args), +})); + +afterEach(() => { + cleanup(); + // Reset call history AND implementations between tests — restoreAllMocks + // alone does not clear vi.fn() call counts for these module-level mocks, + // which would otherwise leak a prior test's saveSourceConfig call into a + // later "not.toHaveBeenCalled()" assertion. + mockFetchSourceConfig.mockReset(); + mockSaveSourceConfig.mockReset(); + vi.restoreAllMocks(); +}); + +describe('SourceConfigForm', () => { + it('fetches config on mount and renders the returned pollIntervalMin in the interval input', async () => { + mockFetchSourceConfig.mockResolvedValue({ + sourceType: 'doe-opendata', + pollIntervalMin: 90, + isActive: true, + lastIngestedDay: null, + }); + + const { SourceConfigForm } = await import('./SourceConfigForm'); + render(); + + await waitFor(() => { + expect( + screen.getByLabelText(/Abrufintervall/i) as HTMLInputElement, + ).toHaveValue(90); + }); + + expect(mockFetchSourceConfig).toHaveBeenCalledTimes(1); + }); + + it('editing the interval and clicking Speichern calls saveSourceConfig with the new pollIntervalMin and isActive', async () => { + mockFetchSourceConfig.mockResolvedValue({ + sourceType: 'doe-opendata', + pollIntervalMin: 60, + isActive: false, + lastIngestedDay: '2026-07-20T00:00:00.000Z', + }); + mockSaveSourceConfig.mockResolvedValue({ + sourceType: 'doe-opendata', + pollIntervalMin: 120, + isActive: true, + lastIngestedDay: '2026-07-20T00:00:00.000Z', + }); + + const { SourceConfigForm } = await import('./SourceConfigForm'); + render(); + + await waitFor(() => { + expect(screen.getByLabelText(/Abrufintervall/i)).toHaveValue(60); + }); + + fireEvent.change(screen.getByLabelText(/Abrufintervall/i), { + target: { value: '120' }, + }); + fireEvent.click(screen.getByRole('switch')); + + fireEvent.click(screen.getByRole('button', { name: 'Speichern' })); + + await waitFor(() => { + expect(mockSaveSourceConfig).toHaveBeenCalledWith({ + pollIntervalMin: 120, + isActive: true, + }); + }); + }); + + it('rejects an interval below 5 client-side without calling saveSourceConfig', async () => { + mockFetchSourceConfig.mockResolvedValue({ + sourceType: 'doe-opendata', + pollIntervalMin: 60, + isActive: true, + lastIngestedDay: null, + }); + + const { SourceConfigForm } = await import('./SourceConfigForm'); + render(); + + await waitFor(() => { + expect(screen.getByLabelText(/Abrufintervall/i)).toHaveValue(60); + }); + + fireEvent.change(screen.getByLabelText(/Abrufintervall/i), { + target: { value: '2' }, + }); + fireEvent.click(screen.getByRole('button', { name: 'Speichern' })); + + await waitFor(() => { + expect( + screen.getByText(/Intervall muss zwischen 5 und 1440 Minuten liegen/i), + ).toBeInTheDocument(); + }); + expect(mockSaveSourceConfig).not.toHaveBeenCalled(); + }); + + it('rejects an interval above 1440 client-side without calling saveSourceConfig', async () => { + mockFetchSourceConfig.mockResolvedValue({ + sourceType: 'doe-opendata', + pollIntervalMin: 60, + isActive: true, + lastIngestedDay: null, + }); + + const { SourceConfigForm } = await import('./SourceConfigForm'); + render(); + + await waitFor(() => { + expect(screen.getByLabelText(/Abrufintervall/i)).toHaveValue(60); + }); + + fireEvent.change(screen.getByLabelText(/Abrufintervall/i), { + target: { value: '1441' }, + }); + fireEvent.click(screen.getByRole('button', { name: 'Speichern' })); + + await waitFor(() => { + expect( + screen.getByText(/Intervall muss zwischen 5 und 1440 Minuten liegen/i), + ).toBeInTheDocument(); + }); + expect(mockSaveSourceConfig).not.toHaveBeenCalled(); + }); +});