test(frontend): veraltete Testerwartungen an aktuelle UI angepasst — Tasks 3-Spalten-Layout mit Toolbar-Store statt Inline-Button, MiniAppBlock async-Fetch + DOMPurify-Attributentfernung, Router-QueryClientProvider, Toast-Mocks auf flache echte API-Signatur, automation-Mocks mockResolvedValue

This commit is contained in:
Agent Zero
2026-08-27 08:26:27 +02:00
parent 1a24e3e999
commit 1c52d3e502
6 changed files with 65 additions and 36 deletions
+13 -7
View File
@@ -3,7 +3,7 @@
*/ */
import { describe, it, expect, vi } from 'vitest'; 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 { TasksPage } from '@/pages/Tasks';
import * as tasksApi from '@/api/tasks'; import * as tasksApi from '@/api/tasks';
@@ -12,6 +12,8 @@ vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: (key: string) => key }), useTranslation: () => ({ t: (key: string) => key }),
})); }));
import { usePluginToolbarStore } from '@/store/pluginToolbarStore';
// Mock toast // Mock toast
vi.mock('@/components/ui/Toast', () => ({ vi.mock('@/components/ui/Toast', () => ({
useToast: () => ({ useToast: () => ({
@@ -114,20 +116,24 @@ vi.mock('@/api/tasks', () => ({
describe('TasksPage', () => { describe('TasksPage', () => {
it('renders the tasks page with header', () => { it('renders the tasks page with header', () => {
render(<TasksPage />); render(<TasksPage />);
expect(screen.getByTestId('tasks-page')).toBeInTheDocument(); expect(screen.getAllByTestId('tasks-page')[0]).toBeInTheDocument();
expect(screen.getByText('tasks.title')).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', () => { it('renders task items from API', () => {
render(<TasksPage />); render(<TasksPage />);
expect(screen.getByText('Test Task')).toBeInTheDocument(); expect(screen.getAllByText('Test Task').length).toBeGreaterThan(0);
expect(screen.getByText('Test description')).toBeInTheDocument(); expect(screen.getAllByText('Test description').length).toBeGreaterThan(0);
}); });
it('opens create modal when create button is clicked', () => { it('opens create modal when create button is clicked', () => {
render(<TasksPage />); render(<TasksPage />);
const createBtn = screen.getByText('tasks.create'); // Seit UI-Overhaul Phase 4 lebt der Create-Button im PluginToolbarStore,
fireEvent.click(createBtn); // 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('modal')).toBeInTheDocument();
expect(screen.getByTestId('task-title-input')).toBeInTheDocument(); expect(screen.getByTestId('task-title-input')).toBeInTheDocument();
}); });
@@ -19,6 +19,14 @@ import ContactCardBlock from '@/components/comm/blocks/ContactCardBlock';
import MiniAppBlock from '@/components/comm/blocks/MiniAppBlock'; import MiniAppBlock from '@/components/comm/blocks/MiniAppBlock';
import type { MessageBlock } from '@/store/commStore'; 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 ─── // ─── Mock Data Helpers ───
function makeBlock( function makeBlock(
@@ -138,10 +146,11 @@ describe('BlockRenderer', () => {
expect(screen.getByText('John Doe')).toBeInTheDocument(); 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' })]; const blocks = [makeBlock('b1', 'miniapp', { app_id: 'my-app' })];
render(<BlockRenderer blocks={blocks} />); render(<BlockRenderer blocks={blocks} />);
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(<HtmlBlock block={block} />); const { container } = render(<HtmlBlock block={block} />);
const link = container.querySelector('a'); const link = container.querySelector('a');
expect(link?.getAttribute('href')).not.toContain('javascript:'); expect(link?.getAttribute('href') ?? '').not.toContain('javascript:');
}); });
it('renders div with dangerouslySetInnerHTML', () => { it('renders div with dangerouslySetInnerHTML', () => {
@@ -573,42 +582,40 @@ describe('ContactCardBlock', () => {
// ─── MiniAppBlock Tests ─── // ─── MiniAppBlock Tests ───
describe('MiniAppBlock', () => { 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' }); const block = makeBlock('b1', 'miniapp', { app_id: 'my-mini-app' });
render(<MiniAppBlock block={block} />); render(<MiniAppBlock block={block} />);
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(<MiniAppBlock block={block} />);
expect(await screen.findByText('some-other-app')).toBeInTheDocument();
});
it('shows a placeholder label when app_id missing', () => {
const block = makeBlock('b1', 'miniapp', {}); const block = makeBlock('b1', 'miniapp', {});
render(<MiniAppBlock block={block} />); render(<MiniAppBlock block={block} />);
expect(screen.getByText(/Mini-App: Unbekannt/)).toBeInTheDocument(); expect(screen.getByText('Unbekannt')).toBeInTheDocument();
}); });
it('renders info message about mini-apps', () => { it('shows config key-value pairs when no schema exists', async () => {
const block = makeBlock('b1', 'miniapp', { app_id: 'test' });
render(<MiniAppBlock block={block} />);
expect(screen.getByText(/Mini-Apps werden in Zukunft/)).toBeInTheDocument();
});
it('shows config details when config is provided', () => {
const block = makeBlock('b1', 'miniapp', { const block = makeBlock('b1', 'miniapp', {
app_id: 'test', app_id: 'test',
config: { key: 'value', nested: { data: 123 } }, config: { environment: 'production', region: 'eu-central' },
}); });
render(<MiniAppBlock block={block} />); render(<MiniAppBlock block={block} />);
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', () => { it('shows empty hint when no config', async () => {
const block = makeBlock('b1', 'miniapp', { app_id: 'test', config: {} }); const block = makeBlock('b1', 'miniapp', { app_id: 'test' });
render(<MiniAppBlock block={block} />); render(<MiniAppBlock block={block} />);
expect(screen.queryByText('Konfiguration')).not.toBeInTheDocument(); await screen.findByText('test');
}); expect(screen.queryByText('environment')).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(<MiniAppBlock block={block} />);
expect(screen.queryByText('Konfiguration')).not.toBeInTheDocument();
}); });
}); });
+8 -2
View File
@@ -4,6 +4,12 @@ import { render, screen, act } from '@testing-library/react';
import { MemoryRouter, Route, Routes, Navigate } from 'react-router-dom'; import { MemoryRouter, Route, Routes, Navigate } from 'react-router-dom';
import { ProtectedRoute } from '@/routes/ProtectedRoute'; import { ProtectedRoute } from '@/routes/ProtectedRoute';
import { useAuthStore } from '@/store/authStore'; 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 <QueryClientProvider client={client}>{ui}</QueryClientProvider>;
}
function ProtectedTest() { function ProtectedTest() {
return ( return (
@@ -23,7 +29,7 @@ function ProtectedTest() {
describe('Router & ProtectedRoute', () => { describe('Router & ProtectedRoute', () => {
it('redirects to /login when not authenticated', () => { it('redirects to /login when not authenticated', () => {
useAuthStore.setState({ isAuthenticated: false, user: null }); useAuthStore.setState({ isAuthenticated: false, user: null });
render(<ProtectedTest />); render(withProviders(<ProtectedTest />));
expect(screen.getByTestId('login-page')).toBeInTheDocument(); expect(screen.getByTestId('login-page')).toBeInTheDocument();
expect(screen.queryByTestId('protected-content')).not.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' }] }, 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' }, currentTenant: { id: 't1', name: 'Test Tenant', slug: 'test' },
}); });
render(<ProtectedTest />); render(withProviders(<ProtectedTest />));
expect(screen.getByTestId('protected-content')).toBeInTheDocument(); expect(screen.getByTestId('protected-content')).toBeInTheDocument();
expect(screen.queryByTestId('login-page')).not.toBeInTheDocument(); expect(screen.queryByTestId('login-page')).not.toBeInTheDocument();
useAuthStore.setState({ isAuthenticated: false, user: null }); useAuthStore.setState({ isAuthenticated: false, user: null });
@@ -45,6 +45,7 @@ describe('Automation API Hooks', () => {
return { data: [], isLoading: false }; return { data: [], isLoading: false };
}); });
mockApiGet.mockResolvedValue([]);
useAutomations(); useAutomations();
expect(mockApiGet).toHaveBeenCalledWith('/automation'); expect(mockApiGet).toHaveBeenCalledWith('/automation');
}); });
@@ -61,6 +62,7 @@ describe('Automation API Hooks', () => {
return { data: [], isLoading: false }; return { data: [], isLoading: false };
}); });
mockApiGet.mockResolvedValue([]);
useAgents(); useAgents();
expect(mockApiGet).toHaveBeenCalledWith('/agents'); expect(mockApiGet).toHaveBeenCalledWith('/agents');
}); });
@@ -40,7 +40,11 @@ vi.mock('react-i18next', () => ({
// Mock toast // Mock toast
vi.mock('@/components/ui/Toast', () => ({ vi.mock('@/components/ui/Toast', () => ({
useToast: () => ({ useToast: () => ({
toast: { success: vi.fn(), error: vi.fn() }, success: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
remove: vi.fn(),
}), }),
})); }));
@@ -38,7 +38,11 @@ vi.mock('react-i18next', () => ({
// Mock toast // Mock toast
vi.mock('@/components/ui/Toast', () => ({ vi.mock('@/components/ui/Toast', () => ({
useToast: () => ({ useToast: () => ({
toast: { success: vi.fn(), error: vi.fn() }, success: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
remove: vi.fn(),
}), }),
})); }));