diff --git a/frontend/src/__tests__/Tasks.test.tsx b/frontend/src/__tests__/Tasks.test.tsx index 008b136..0b947f7 100644 --- a/frontend/src/__tests__/Tasks.test.tsx +++ b/frontend/src/__tests__/Tasks.test.tsx @@ -3,7 +3,7 @@ */ import { describe, it, expect, vi } from 'vitest'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; import { TasksPage } from '@/pages/Tasks'; import * as tasksApi from '@/api/tasks'; @@ -12,6 +12,8 @@ vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key }), })); +import { usePluginToolbarStore } from '@/store/pluginToolbarStore'; + // Mock toast vi.mock('@/components/ui/Toast', () => ({ useToast: () => ({ @@ -114,20 +116,24 @@ vi.mock('@/api/tasks', () => ({ describe('TasksPage', () => { it('renders the tasks page with header', () => { render(); - expect(screen.getByTestId('tasks-page')).toBeInTheDocument(); - expect(screen.getByText('tasks.title')).toBeInTheDocument(); + expect(screen.getAllByTestId('tasks-page')[0]).toBeInTheDocument(); + // Header + Detail-Karte beide nutzen tasks.title seit 3-Spalten-Layout + expect(screen.getAllByText('tasks.title').length).toBeGreaterThan(0); }); it('renders task items from API', () => { render(); - expect(screen.getByText('Test Task')).toBeInTheDocument(); - expect(screen.getByText('Test description')).toBeInTheDocument(); + expect(screen.getAllByText('Test Task').length).toBeGreaterThan(0); + expect(screen.getAllByText('Test description').length).toBeGreaterThan(0); }); it('opens create modal when create button is clicked', () => { render(); - const createBtn = screen.getByText('tasks.create'); - fireEvent.click(createBtn); + // Seit UI-Overhaul Phase 4 lebt der Create-Button im PluginToolbarStore, + // nicht mehr als inline gerenderter Button. + const item = usePluginToolbarStore.getState().items.find((i) => i.id === 'new-task'); + expect(item).toBeDefined(); + act(() => { item!.onClick(); }); expect(screen.getByTestId('modal')).toBeInTheDocument(); expect(screen.getByTestId('task-title-input')).toBeInTheDocument(); }); diff --git a/frontend/src/__tests__/comm/BlockRenderer.test.tsx b/frontend/src/__tests__/comm/BlockRenderer.test.tsx index b5c80cd..20637d2 100644 --- a/frontend/src/__tests__/comm/BlockRenderer.test.tsx +++ b/frontend/src/__tests__/comm/BlockRenderer.test.tsx @@ -19,6 +19,14 @@ import ContactCardBlock from '@/components/comm/blocks/ContactCardBlock'; import MiniAppBlock from '@/components/comm/blocks/MiniAppBlock'; import type { MessageBlock } from '@/store/commStore'; +// Mock apiClient für MiniAppBlock (fetcht Mini-App-Definitionen on mount) +const miniappsMock = [ + { app_id: 'my-mini-app', name: 'My Mini App', icon: 'grid', description: 'Demo', plugin_name: 'demo', render_schema: {} }, +]; +vi.mock('@/api/client', () => ({ + apiClient: { get: vi.fn(() => Promise.resolve({ data: miniappsMock })) }, +})); + // ─── Mock Data Helpers ─── function makeBlock( @@ -138,10 +146,11 @@ describe('BlockRenderer', () => { expect(screen.getByText('John Doe')).toBeInTheDocument(); }); - it('renders miniapp block via BlockRenderer', () => { + it('renders miniapp block via BlockRenderer', async () => { const blocks = [makeBlock('b1', 'miniapp', { app_id: 'my-app' })]; render(); - expect(screen.getByText(/Mini-App: my-app/)).toBeInTheDocument(); + // Async-Fetch: unresolvierte app_id faellt auf den Rohtext zurueck + expect(await screen.findByText('my-app')).toBeInTheDocument(); }); }); @@ -239,7 +248,7 @@ describe('HtmlBlock', () => { }); const { container } = render(); const link = container.querySelector('a'); - expect(link?.getAttribute('href')).not.toContain('javascript:'); + expect(link?.getAttribute('href') ?? '').not.toContain('javascript:'); }); it('renders div with dangerouslySetInnerHTML', () => { @@ -573,42 +582,40 @@ describe('ContactCardBlock', () => { // ─── MiniAppBlock Tests ─── describe('MiniAppBlock', () => { - it('renders app_id in label', () => { + it('shows the registered app name when app_id resolves', async () => { const block = makeBlock('b1', 'miniapp', { app_id: 'my-mini-app' }); render(); - expect(screen.getByText(/Mini-App: my-mini-app/)).toBeInTheDocument(); + expect(await screen.findByText('My Mini App')).toBeInTheDocument(); }); - it('uses default app_id when not provided', () => { + it('falls back to the raw app_id when unknown', async () => { + const block = makeBlock('b1', 'miniapp', { app_id: 'some-other-app' }); + render(); + expect(await screen.findByText('some-other-app')).toBeInTheDocument(); + }); + + it('shows a placeholder label when app_id missing', () => { const block = makeBlock('b1', 'miniapp', {}); render(); - expect(screen.getByText(/Mini-App: Unbekannt/)).toBeInTheDocument(); + expect(screen.getByText('Unbekannt')).toBeInTheDocument(); }); - it('renders info message about mini-apps', () => { - const block = makeBlock('b1', 'miniapp', { app_id: 'test' }); - render(); - expect(screen.getByText(/Mini-Apps werden in Zukunft/)).toBeInTheDocument(); - }); - - it('shows config details when config is provided', () => { + it('shows config key-value pairs when no schema exists', async () => { const block = makeBlock('b1', 'miniapp', { app_id: 'test', - config: { key: 'value', nested: { data: 123 } }, + config: { environment: 'production', region: 'eu-central' }, }); render(); - expect(screen.getByText('Konfiguration')).toBeInTheDocument(); + expect(await screen.findByText('environment')).toBeInTheDocument(); + expect(screen.getByText('production')).toBeInTheDocument(); + expect(screen.getByText('region')).toBeInTheDocument(); + expect(screen.getByText('eu-central')).toBeInTheDocument(); }); - it('does not show config details when config is empty', () => { - const block = makeBlock('b1', 'miniapp', { app_id: 'test', config: {} }); + it('shows empty hint when no config', async () => { + const block = makeBlock('b1', 'miniapp', { app_id: 'test' }); render(); - expect(screen.queryByText('Konfiguration')).not.toBeInTheDocument(); - }); - - it('does not show config details when config is not an object', () => { - const block = makeBlock('b1', 'miniapp', { app_id: 'test', config: 'not-an-object' }); - render(); - expect(screen.queryByText('Konfiguration')).not.toBeInTheDocument(); + await screen.findByText('test'); + expect(screen.queryByText('environment')).not.toBeInTheDocument(); }); }); diff --git a/frontend/src/__tests__/shell/Router.test.tsx b/frontend/src/__tests__/shell/Router.test.tsx index 2c4c491..c2512aa 100644 --- a/frontend/src/__tests__/shell/Router.test.tsx +++ b/frontend/src/__tests__/shell/Router.test.tsx @@ -4,6 +4,12 @@ import { render, screen, act } from '@testing-library/react'; import { MemoryRouter, Route, Routes, Navigate } from 'react-router-dom'; import { ProtectedRoute } from '@/routes/ProtectedRoute'; import { useAuthStore } from '@/store/authStore'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +function withProviders(ui: React.ReactElement) { + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + return {ui}; +} function ProtectedTest() { return ( @@ -23,7 +29,7 @@ function ProtectedTest() { describe('Router & ProtectedRoute', () => { it('redirects to /login when not authenticated', () => { useAuthStore.setState({ isAuthenticated: false, user: null }); - render(); + render(withProviders()); expect(screen.getByTestId('login-page')).toBeInTheDocument(); expect(screen.queryByTestId('protected-content')).not.toBeInTheDocument(); }); @@ -34,7 +40,7 @@ describe('Router & ProtectedRoute', () => { user: { id: '1', email: 'test@test.de', first_name: 'Test', last_name: 'User', role: 'admin', avatar_url: null, tenants: [{ id: 't1', name: 'Test Tenant', slug: 'test' }] }, currentTenant: { id: 't1', name: 'Test Tenant', slug: 'test' }, }); - render(); + render(withProviders()); expect(screen.getByTestId('protected-content')).toBeInTheDocument(); expect(screen.queryByTestId('login-page')).not.toBeInTheDocument(); useAuthStore.setState({ isAuthenticated: false, user: null }); diff --git a/frontend/src/api/__tests__/automation.test.ts b/frontend/src/api/__tests__/automation.test.ts index f9189e8..6ad6538 100644 --- a/frontend/src/api/__tests__/automation.test.ts +++ b/frontend/src/api/__tests__/automation.test.ts @@ -45,6 +45,7 @@ describe('Automation API Hooks', () => { return { data: [], isLoading: false }; }); + mockApiGet.mockResolvedValue([]); useAutomations(); expect(mockApiGet).toHaveBeenCalledWith('/automation'); }); @@ -61,6 +62,7 @@ describe('Automation API Hooks', () => { return { data: [], isLoading: false }; }); + mockApiGet.mockResolvedValue([]); useAgents(); expect(mockApiGet).toHaveBeenCalledWith('/agents'); }); diff --git a/frontend/src/pages/__tests__/AgentDashboard.test.tsx b/frontend/src/pages/__tests__/AgentDashboard.test.tsx index e7f46ee..0afbff1 100644 --- a/frontend/src/pages/__tests__/AgentDashboard.test.tsx +++ b/frontend/src/pages/__tests__/AgentDashboard.test.tsx @@ -40,7 +40,11 @@ vi.mock('react-i18next', () => ({ // Mock toast vi.mock('@/components/ui/Toast', () => ({ useToast: () => ({ - toast: { success: vi.fn(), error: vi.fn() }, + success: vi.fn(), + error: vi.fn(), + warning: vi.fn(), + info: vi.fn(), + remove: vi.fn(), }), })); diff --git a/frontend/src/pages/__tests__/AutomationDashboard.test.tsx b/frontend/src/pages/__tests__/AutomationDashboard.test.tsx index 4aa93e7..1fc0ff5 100644 --- a/frontend/src/pages/__tests__/AutomationDashboard.test.tsx +++ b/frontend/src/pages/__tests__/AutomationDashboard.test.tsx @@ -38,7 +38,11 @@ vi.mock('react-i18next', () => ({ // Mock toast vi.mock('@/components/ui/Toast', () => ({ useToast: () => ({ - toast: { success: vi.fn(), error: vi.fn() }, + success: vi.fn(), + error: vi.fn(), + warning: vi.fn(), + info: vi.fn(), + remove: vi.fn(), }), }));