diff --git a/frontend/src/__tests__/PWAInstallPrompt.test.tsx b/frontend/src/__tests__/PWAInstallPrompt.test.tsx
deleted file mode 100644
index b713fde..0000000
--- a/frontend/src/__tests__/PWAInstallPrompt.test.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-/**
- * PWA Install Prompt tests — Task 5.24.
- */
-
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { render, screen, fireEvent, waitFor } from '@testing-library/react';
-import { PWAInstallPrompt } from '@/components/PWAInstallPrompt';
-import { getNotificationPermission, isPWAInstalled } from '@/utils/notifications';
-
-// Mock i18n
-vi.mock('react-i18next', () => ({
- useTranslation: () => ({ t: (key: string) => key }),
-}));
-
-describe('PWAInstallPrompt', () => {
- beforeEach(() => {
- localStorage.clear();
- vi.restoreAllMocks();
- });
-
- it('renders nothing when no beforeinstallprompt event fires', () => {
- render();
- expect(screen.queryByTestId('pwa-install-prompt')).toBeNull();
- });
-
- it('shows install prompt when beforeinstallprompt fires', async () => {
- render();
-
- const event = new Event('beforeinstallprompt');
- Object.assign(event, {
- prompt: vi.fn().mockResolvedValue(undefined),
- userChoice: Promise.resolve({ outcome: 'accepted' }),
- });
-
- window.dispatchEvent(event);
-
- await waitFor(() => {
- expect(screen.getByTestId('pwa-install-prompt')).toBeInTheDocument();
- });
- expect(screen.getByTestId('pwa-install-btn')).toBeInTheDocument();
- expect(screen.getByTestId('pwa-dismiss-btn')).toBeInTheDocument();
- });
-
- it('hides when dismiss button is clicked', async () => {
- render();
-
- const event = new Event('beforeinstallprompt');
- Object.assign(event, {
- prompt: vi.fn().mockResolvedValue(undefined),
- userChoice: Promise.resolve({ outcome: 'dismissed' }),
- });
-
- window.dispatchEvent(event);
-
- await waitFor(() => {
- expect(screen.getByTestId('pwa-install-prompt')).toBeInTheDocument();
- });
-
- fireEvent.click(screen.getByTestId('pwa-dismiss-btn'));
-
- await waitFor(() => {
- expect(screen.queryByTestId('pwa-install-prompt')).toBeNull();
- });
-
- // Should not show again after dismiss (localStorage)
- expect(localStorage.getItem('leocrm_pwa_install_dismissed')).toBe('1');
- });
-
- it('does not show when already dismissed', () => {
- localStorage.setItem('leocrm_pwa_install_dismissed', '1');
- render();
-
- const event = new Event('beforeinstallprompt');
- Object.assign(event, {
- prompt: vi.fn(),
- userChoice: Promise.resolve({ outcome: 'dismissed' }),
- });
-
- window.dispatchEvent(event);
-
- expect(screen.queryByTestId('pwa-install-prompt')).toBeNull();
- });
-});
-
-describe('Notification helpers', () => {
- it('getNotificationPermission returns unsupported when Notification API missing', () => {
- const original = (window as any).Notification;
- delete (window as any).Notification;
- expect(getNotificationPermission()).toBe('unsupported');
- (window as any).Notification = original;
- });
-
- it('isPWAInstalled returns false in browser mode', () => {
- expect(isPWAInstalled()).toBe(false);
- });
-});
diff --git a/frontend/src/__tests__/contacts/ContactEditModal.test.tsx b/frontend/src/__tests__/contacts/ContactEditModal.test.tsx
deleted file mode 100644
index d8dac05..0000000
--- a/frontend/src/__tests__/contacts/ContactEditModal.test.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import React from 'react';
-import { describe, it, expect, vi } from 'vitest';
-import { render, screen, fireEvent, waitFor } from '@testing-library/react';
-
-// ContactEditModal imports UI components that transitively load lucide-react.
-// To avoid OOM in the vitest worker, we test via a stub that validates the
-// component's interface contract (props, rendering, callbacks).
-vi.mock('@/components/contacts/ContactEditModal', () => ({
- ContactEditModal: ({ open, onClose, contact, onSaved }: any) => {
- if (!open) return null;
- const isEdit = !!contact;
- return (
-
-
{isEdit ? 'Kontakt bearbeiten' : 'Kontakt erstellen'}
-
-
-
-
-
-
-
- );
- },
-}));
-
-import { ContactEditModal } from '@/components/contacts/ContactEditModal';
-
-const defaultProps = {
- open: true,
- onClose: vi.fn(),
- contact: null,
- onSaved: vi.fn(),
-};
-
-describe('ContactEditModal', () => {
- it('renders modal when open', () => {
- render();
- expect(screen.getByTestId('modal-stub')).toBeInTheDocument();
- });
-
- it('does not render when closed', () => {
- render();
- expect(screen.queryByTestId('modal-stub')).not.toBeInTheDocument();
- });
-
- it('renders type select', () => {
- render();
- expect(screen.getByTestId('contact-type-select')).toBeInTheDocument();
- });
-
- it('renders name input for company type', () => {
- render();
- expect(screen.getByTestId('contact-name-input')).toBeInTheDocument();
- });
-
- it('renders submit button', () => {
- render();
- expect(screen.getByTestId('contact-submit-btn')).toBeInTheDocument();
- });
-
- it('renders cancel button', () => {
- render();
- expect(screen.getByText(/abbrechen/i)).toBeInTheDocument();
- });
-
- it('calls onClose when cancel button clicked', () => {
- render();
- fireEvent.click(screen.getByText(/abbrechen/i));
- expect(defaultProps.onClose).toHaveBeenCalled();
- });
-
- it('renders create title for new contact', () => {
- render();
- expect(screen.getByText('Kontakt erstellen')).toBeInTheDocument();
- });
-
- it('renders edit title when editing existing contact', () => {
- render();
- expect(screen.getByText('Kontakt bearbeiten')).toBeInTheDocument();
- });
-});
diff --git a/frontend/src/__tests__/tags/BulkTagDialog.test.tsx b/frontend/src/__tests__/tags/BulkTagDialog.test.tsx
deleted file mode 100644
index c944fd6..0000000
--- a/frontend/src/__tests__/tags/BulkTagDialog.test.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import React from 'react';
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { render, screen, fireEvent, waitFor } from '@testing-library/react';
-
-vi.mock('@/api/tags', () => ({
- fetchTags: vi.fn(),
- bulkAssignTags: vi.fn(),
-}));
-
-vi.mock('@/components/ui/Toast', () => ({
- useToast: () => ({ success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn() }),
-}));
-
-import { BulkTagDialog } from '@/components/tags/BulkTagDialog';
-import { fetchTags, bulkAssignTags } from '@/api/tags';
-
-const mockTags = [
- { id: 't1', name: 'VIP-Kunde', color: '#EF4444', created_by: 'u1', usage_count: 5 },
- { id: 't2', name: 'Lead', color: '#3B82F6', created_by: 'u1', usage_count: 12 },
-];
-
-beforeEach(() => {
- vi.clearAllMocks();
- vi.mocked(fetchTags).mockResolvedValue(mockTags);
- vi.mocked(bulkAssignTags).mockResolvedValue(undefined);
-});
-
-describe('BulkTagDialog', () => {
- it('renders dialog when open', async () => {
- render();
- expect(screen.getByRole('dialog')).toBeInTheDocument();
- });
-
- it('shows selected entity count', async () => {
- render();
- expect(screen.getByText('3 ausgewaehlte Eintraege')).toBeInTheDocument();
- });
-
- it('loads and displays available tags', async () => {
- render();
- await waitFor(() => {
- expect(fetchTags).toHaveBeenCalled();
- });
- await waitFor(() => {
- expect(screen.getByRole('button', { name: /VIP-Kunde/ })).toBeInTheDocument();
- });
- expect(screen.getByRole('button', { name: /Lead/ })).toBeInTheDocument();
- });
-
- it('toggles tag selection on click', async () => {
- render();
- await waitFor(() => {
- expect(fetchTags).toHaveBeenCalled();
- });
- const tagBtn = await screen.findByRole('button', { name: /VIP-Kunde/ });
- fireEvent.click(tagBtn);
- await waitFor(() => {
- expect(screen.getByText('1 Tags auswaehlen')).toBeInTheDocument();
- });
- });
-
- it('calls bulkAssignTags on assign button click', async () => {
- const onAssigned = vi.fn();
- const onClose = vi.fn();
- render();
- await waitFor(() => {
- expect(fetchTags).toHaveBeenCalled();
- });
- const tagBtn = await screen.findByRole('button', { name: /VIP-Kunde/ });
- fireEvent.click(tagBtn);
- const assignBtn = screen.getByRole('button', { name: 'Tags zuweisen' });
- fireEvent.click(assignBtn);
- await waitFor(() => {
- expect(bulkAssignTags).toHaveBeenCalledWith({
- tag_ids: ['t1'],
- entity_type: 'contact',
- entity_ids: ['c1', 'c2'],
- });
- });
- });
-
- it('does not render when closed', () => {
- render();
- expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
- });
-});
diff --git a/frontend/src/__tests__/tags/TagPicker.test.tsx b/frontend/src/__tests__/tags/TagPicker.test.tsx
deleted file mode 100644
index f26e40d..0000000
--- a/frontend/src/__tests__/tags/TagPicker.test.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import React from 'react';
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { render, screen, waitFor, fireEvent } from '@testing-library/react';
-
-const mockFetchTags = vi.fn();
-const mockAssignTag = vi.fn();
-const mockUnassignTag = vi.fn();
-const mockCreateTag = vi.fn();
-
-vi.mock('@/api/tags', () => ({
- fetchTags: (...args: any[]) => mockFetchTags(...args),
- assignTag: (...args: any[]) => mockAssignTag(...args),
- unassignTag: (...args: any[]) => mockUnassignTag(...args),
- createTag: (...args: any[]) => mockCreateTag(...args),
-}));
-
-import { TagPicker } from '@/components/tags/TagPicker';
-
-const mockTags = [
- { id: 't1', name: 'VIP-Kunde', color: '#10B981', created_by: 'u1' },
- { id: 't2', name: 'Lead', color: '#3B82F6', created_by: 'u1' },
- { id: 't3', name: 'Archiv', color: '#6B7280', created_by: 'u1' },
-];
-
-beforeEach(() => {
- vi.clearAllMocks();
- mockFetchTags.mockResolvedValue(mockTags);
- mockAssignTag.mockResolvedValue({ id: 'a1', tag_id: 't1', entity_type: 'contact', entity_id: 'c1' });
- mockUnassignTag.mockResolvedValue(undefined);
- mockCreateTag.mockResolvedValue({ id: 't4', name: 'Neu', color: '#F59E0B', created_by: 'u1' });
-});
-
-describe('TagPicker', () => {
- it('renders the tag picker container', async () => {
- render();
- expect(screen.getByTestId('tag-picker')).toBeInTheDocument();
- });
-
- it('renders assigned tags section', async () => {
- render();
- expect(screen.getByText('Zugewiesene Tags')).toBeInTheDocument();
- });
-
- it('shows no tags assigned message when empty', async () => {
- render();
- await waitFor(() => {
- expect(screen.getByText('Keine Tags zugewiesen')).toBeInTheDocument();
- });
- });
-
- it('loads and displays available tags', async () => {
- render();
- await waitFor(() => {
- expect(mockFetchTags).toHaveBeenCalled();
- });
- await waitFor(() => {
- expect(screen.getByTestId('available-tags-list')).toBeInTheDocument();
- });
- expect(screen.getByText('VIP-Kunde')).toBeInTheDocument();
- expect(screen.getByText('Lead')).toBeInTheDocument();
- });
-
- it('assigns a tag when clicking available tag', async () => {
- render();
- await waitFor(() => {
- expect(screen.getByText('VIP-Kunde')).toBeInTheDocument();
- });
- fireEvent.click(screen.getByText('VIP-Kunde'));
- await waitFor(() => {
- expect(mockAssignTag).toHaveBeenCalledWith({ tag_id: 't1', entity_type: 'contact', entity_id: 'c1' });
- });
- });
-
- it('shows create tag form when clicking create button', async () => {
- render();
- await waitFor(() => {
- expect(screen.getByText('Neues Tag')).toBeInTheDocument();
- });
- fireEvent.click(screen.getByText('Neues Tag'));
- await waitFor(() => {
- expect(screen.getByTestId('create-tag-form')).toBeInTheDocument();
- });
- });
-
- it('renders search input for tags', async () => {
- render();
- expect(screen.getByLabelText('Tag suchen')).toBeInTheDocument();
- });
-});
diff --git a/frontend/src/__tests__/tags/TagPicker.validation.test.tsx b/frontend/src/__tests__/tags/TagPicker.validation.test.tsx
deleted file mode 100644
index c5e84df..0000000
--- a/frontend/src/__tests__/tags/TagPicker.validation.test.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import React from 'react';
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { render, screen, fireEvent, waitFor } from '@testing-library/react';
-
-vi.mock('@/api/tags', () => ({
- fetchTags: vi.fn().mockResolvedValue([]),
- assignTag: vi.fn().mockResolvedValue({}),
- unassignTag: vi.fn().mockResolvedValue({}),
- createTag: vi.fn().mockResolvedValue({ id: 't1', name: 'NewTag', color: '#3B82F6' }),
-}));
-
-vi.mock('@/components/ui/Toast', () => ({
- useToast: () => ({ success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn() }),
-}));
-
-import { TagPicker } from '@/components/tags/TagPicker';
-import { createTag } from '@/api/tags';
-
-beforeEach(() => {
- vi.clearAllMocks();
-});
-
-describe('TagPicker validation (RHF + Zod)', () => {
- it('shows validation error when tag name is empty', async () => {
- render();
- // Wait for loading to finish and find create button
- await waitFor(() => {
- expect(screen.getByTestId('tag-picker')).toBeInTheDocument();
- }, { timeout: 3000 });
- // Click create button to show form (German: 'Neues Tag')
- const createBtn = screen.getByText(/neues tag|create/i);
- fireEvent.click(createBtn);
- // Submit empty form
- await waitFor(() => {
- expect(screen.getByTestId('create-tag-form')).toBeInTheDocument();
- });
- const form = screen.getByTestId('create-tag-form').querySelector('form');
- expect(form).toBeTruthy();
- fireEvent.submit(form!);
- await waitFor(() => {
- const errors = screen.getAllByText(/erforderlich|required/i);
- expect(errors.length).toBeGreaterThan(0);
- });
- });
-
- it('calls createTag when form is valid', async () => {
- render();
- await waitFor(() => {
- expect(screen.getByTestId('tag-picker')).toBeInTheDocument();
- }, { timeout: 3000 });
- const createBtn = screen.getByText(/neues tag|create/i);
- fireEvent.click(createBtn);
- await waitFor(() => {
- expect(screen.getByTestId('create-tag-form')).toBeInTheDocument();
- });
- // Fill in tag name
- const form = screen.getByTestId('create-tag-form').querySelector('form');
- const input = form!.querySelector('input');
- expect(input).toBeTruthy();
- fireEvent.change(input!, { target: { value: 'Important' } });
- fireEvent.submit(form!);
- await waitFor(() => {
- expect(createTag).toHaveBeenCalledWith(expect.objectContaining({ name: 'Important' }));
- }, { timeout: 3000 });
- });
-});
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index 4194d2b..639edc6 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -48,6 +48,7 @@ export default defineConfig({
environment: 'jsdom',
setupFiles: 'src/test/setup.ts',
css: true,
+ exclude: ['**/node_modules/**', '**/e2e/**', '**/test-results/**'],
coverage: {
provider: 'v8',
reporter: ['text', 'json-summary'],