diff --git a/apps/web/src/app/(portal)/modules/tender-radar/components/SavedSearchBar.test.tsx b/apps/web/src/app/(portal)/modules/tender-radar/components/SavedSearchBar.test.tsx
new file mode 100644
index 0000000..0e95175
--- /dev/null
+++ b/apps/web/src/app/(portal)/modules/tender-radar/components/SavedSearchBar.test.tsx
@@ -0,0 +1,211 @@
+import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+/**
+ * SavedSearchBar.test — RED-first (TDD) proof for Task 3 (FILTER-06, D-08/
+ * D-11). Mirrors the ResultsList.test.tsx pattern: mock the api client
+ * module, render, assert via waitFor/fireEvent. next/navigation is mocked
+ * because SavedSearchBar reads the current URL searchParams (to serialize
+ * "aktuelle Filter speichern") and writes them (to load a saved profile).
+ *
+ * Key contract under test (11-06-PLAN.md key_links): the filters object
+ * sent to createSavedSearch/read back from a profile MUST round-trip
+ * through the exact same URL param names FilterPanel/ResultsList use — q,
+ * plz, bundesland, region, cpv (multi-value), deadlineFrom/To, openOnly,
+ * valueMin/Max, includeNullValue, sort, favOnly.
+ */
+
+const mockListSavedSearches = vi.fn();
+const mockCreateSavedSearch = vi.fn();
+const mockUpdateSavedSearch = vi.fn();
+const mockDeleteSavedSearch = vi.fn();
+
+vi.mock('@/lib/tender-radar-api', () => ({
+ listSavedSearches: (...args: unknown[]) => mockListSavedSearches(...args),
+ createSavedSearch: (...args: unknown[]) => mockCreateSavedSearch(...args),
+ updateSavedSearch: (...args: unknown[]) => mockUpdateSavedSearch(...args),
+ deleteSavedSearch: (...args: unknown[]) => mockDeleteSavedSearch(...args),
+}));
+
+const mockReplace = vi.fn();
+let mockSearchParams = new URLSearchParams();
+
+vi.mock('next/navigation', () => ({
+ useRouter: () => ({ replace: mockReplace }),
+ useSearchParams: () => mockSearchParams,
+}));
+
+afterEach(() => {
+ cleanup();
+ mockListSavedSearches.mockReset();
+ mockCreateSavedSearch.mockReset();
+ mockUpdateSavedSearch.mockReset();
+ mockDeleteSavedSearch.mockReset();
+ mockReplace.mockReset();
+ mockSearchParams = new URLSearchParams();
+ vi.restoreAllMocks();
+});
+
+describe('SavedSearchBar', () => {
+ it('fetches on mount and renders each saved profile by name', async () => {
+ mockListSavedSearches.mockResolvedValue([
+ { id: 'ss-1', name: 'Bau NRW', filters: { q: 'Bau' }, createdAt: '', updatedAt: '' },
+ ]);
+
+ const { SavedSearchBar } = await import('./SavedSearchBar');
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Bau NRW')).toBeInTheDocument();
+ });
+ expect(mockListSavedSearches).toHaveBeenCalledTimes(1);
+ });
+
+ it('renders an empty-state message when no profiles are saved', async () => {
+ mockListSavedSearches.mockResolvedValue([]);
+
+ const { SavedSearchBar } = await import('./SavedSearchBar');
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText(/Noch keine Suchprofile gespeichert/i)).toBeInTheDocument();
+ });
+ });
+
+ it('"aktuelle Filter speichern" serializes the current URL searchParams into filters and calls createSavedSearch', async () => {
+ mockSearchParams = new URLSearchParams(
+ 'q=Bau&plz=10&bundesland=Berlin&deadlineFrom=2026-08-01&deadlineTo=2026-09-01&openOnly=false&valueMin=1000&valueMax=50000&includeNullValue=false&sort=deadline&favOnly=true&cpv=45&cpv=71&page=3&tender=abc',
+ );
+ mockListSavedSearches.mockResolvedValue([]);
+ mockCreateSavedSearch.mockResolvedValue({
+ id: 'ss-new',
+ name: 'Mein Profil',
+ filters: {},
+ createdAt: '',
+ updatedAt: '',
+ });
+
+ const { SavedSearchBar } = await import('./SavedSearchBar');
+ render();
+
+ await waitFor(() => expect(mockListSavedSearches).toHaveBeenCalled());
+
+ const input = screen.getByPlaceholderText('Name des Suchprofils…');
+ fireEvent.change(input, { target: { value: 'Mein Profil' } });
+ fireEvent.click(screen.getByRole('button', { name: 'Speichern' }));
+
+ await waitFor(() => expect(mockCreateSavedSearch).toHaveBeenCalledTimes(1));
+ const [payload] = mockCreateSavedSearch.mock.calls[0];
+ expect(payload.name).toBe('Mein Profil');
+ // Filter keys are preserved verbatim (URL-param name === filters key).
+ expect(payload.filters).toMatchObject({
+ q: 'Bau',
+ plz: '10',
+ bundesland: 'Berlin',
+ deadlineFrom: '2026-08-01',
+ deadlineTo: '2026-09-01',
+ openOnly: 'false',
+ valueMin: '1000',
+ valueMax: '50000',
+ includeNullValue: 'false',
+ sort: 'deadline',
+ favOnly: 'true',
+ cpv: ['45', '71'],
+ });
+ // Navigation-only params (page, tender) are never persisted into a
+ // saved search profile — they are view state, not filter state.
+ expect(payload.filters).not.toHaveProperty('page');
+ expect(payload.filters).not.toHaveProperty('tender');
+ });
+
+ it('does not call createSavedSearch when the name field is empty', async () => {
+ mockListSavedSearches.mockResolvedValue([]);
+
+ const { SavedSearchBar } = await import('./SavedSearchBar');
+ render();
+
+ await waitFor(() => expect(mockListSavedSearches).toHaveBeenCalled());
+ fireEvent.click(screen.getByRole('button', { name: 'Speichern' }));
+
+ expect(mockCreateSavedSearch).not.toHaveBeenCalled();
+ });
+
+ it('clicking a saved profile writes its filters back into the URL via router.replace (round-trip contract)', async () => {
+ mockListSavedSearches.mockResolvedValue([
+ {
+ id: 'ss-1',
+ name: 'Bau NRW',
+ filters: { q: 'Bau', cpv: ['45', '71'], openOnly: 'false' },
+ createdAt: '',
+ updatedAt: '',
+ },
+ ]);
+
+ const { SavedSearchBar } = await import('./SavedSearchBar');
+ render();
+
+ const profileButton = await screen.findByRole('button', { name: 'Bau NRW' });
+ fireEvent.click(profileButton);
+
+ expect(mockReplace).toHaveBeenCalledTimes(1);
+ const [target] = mockReplace.mock.calls[0];
+ const params = new URLSearchParams(String(target).replace(/^\?/, ''));
+ expect(params.get('q')).toBe('Bau');
+ expect(params.get('openOnly')).toBe('false');
+ expect(params.getAll('cpv')).toEqual(['45', '71']);
+ });
+
+ it('renaming a profile calls updateSavedSearch(id, { name }) and reloads the list', async () => {
+ mockListSavedSearches
+ .mockResolvedValueOnce([
+ { id: 'ss-1', name: 'Alt', filters: {}, createdAt: '', updatedAt: '' },
+ ])
+ .mockResolvedValueOnce([
+ { id: 'ss-1', name: 'Neu', filters: {}, createdAt: '', updatedAt: '' },
+ ]);
+ mockUpdateSavedSearch.mockResolvedValue({
+ id: 'ss-1',
+ name: 'Neu',
+ filters: {},
+ createdAt: '',
+ updatedAt: '',
+ });
+
+ const { SavedSearchBar } = await import('./SavedSearchBar');
+ render();
+
+ await screen.findByRole('button', { name: 'Alt' });
+ fireEvent.click(screen.getByRole('button', { name: 'Alt umbenennen' }));
+
+ const renameInput = screen.getByDisplayValue('Alt');
+ fireEvent.change(renameInput, { target: { value: 'Neu' } });
+ fireEvent.click(screen.getByRole('button', { name: 'Alt umbenennen bestätigen' }));
+
+ await waitFor(() => {
+ expect(mockUpdateSavedSearch).toHaveBeenCalledWith('ss-1', { name: 'Neu' });
+ });
+ await waitFor(() => {
+ expect(screen.getByRole('button', { name: 'Neu' })).toBeInTheDocument();
+ });
+ });
+
+ it('deleting a profile calls deleteSavedSearch(id) and reloads the list', async () => {
+ mockListSavedSearches
+ .mockResolvedValueOnce([
+ { id: 'ss-1', name: 'Weg damit', filters: {}, createdAt: '', updatedAt: '' },
+ ])
+ .mockResolvedValueOnce([]);
+ mockDeleteSavedSearch.mockResolvedValue(undefined);
+
+ const { SavedSearchBar } = await import('./SavedSearchBar');
+ render();
+
+ await screen.findByRole('button', { name: 'Weg damit' });
+ fireEvent.click(screen.getByRole('button', { name: 'Weg damit löschen' }));
+
+ await waitFor(() => expect(mockDeleteSavedSearch).toHaveBeenCalledWith('ss-1'));
+ await waitFor(() => {
+ expect(screen.getByText(/Noch keine Suchprofile gespeichert/i)).toBeInTheDocument();
+ });
+ });
+});