test(10-06): SourceConfigForm component test — fetch, save, bounds
- 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
This commit is contained in:
+136
@@ -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(<SourceConfigForm />);
|
||||||
|
|
||||||
|
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(<SourceConfigForm />);
|
||||||
|
|
||||||
|
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(<SourceConfigForm />);
|
||||||
|
|
||||||
|
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(<SourceConfigForm />);
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user