fix: Fix all 68 frontend test failures
P1 Code Bugs: - SettingsPlugins.tsx: Array.isArray guard for plugins.map (9 tests) - HtmlBlock.tsx: javascript: URL sanitization in href attributes (1 test, security fix) P2 Test-Setup (QueryClientProvider): - Dashboard.test.tsx: Add QueryClientProvider + dashboard mock (11 tests) - CalendarPage.test.tsx: Add QueryClientProvider + savedFilters mock (8 tests) - SessionList.test.tsx: Add QueryClientProvider (6 tests) - MailPage.test.tsx: Add QueryClientProvider + savedFilters mock (8 tests fixed) - DmsPage.test.tsx: Add QueryClientProvider + DMS API mocks (2 tests fixed) - SettingsSystem.test.tsx: Add QueryClientProvider + sub-page mocks (1 test fixed) P2 Test-Setup (ChevronDown Mock): - Reports.test.tsx: Add ChevronDown to lucide-react mock (5 tests) P3 Text Fix: - UploadDropzone.test.tsx: Fix umlaut Auswaehlen -> Auswahlen (1 test) Pre-existing Test Fixes (18 tests): - MailPage.test.tsx: Remove 6 obsolete tests (compose-btn, shared-mailbox-selector, etc. — now plugin toolbar actions) - DmsPage.test.tsx: Adapt 3 tests to new testids, remove 3 obsolete tests (upload/folder/search now plugin toolbar actions) - SettingsSystem.test.tsx: Remove 4 obsolete tests (form fields moved to sub-pages) - PluginRouteRenderer.test.tsx: Adapt test to loading spinner behavior - ShareDialog.test.tsx: Fix button text i18n mismatch
This commit is contained in:
@@ -1,8 +1,19 @@
|
||||
import React from 'react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { SessionList } from '@/components/ai/SessionList';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
function renderWithProvider(ui: React.ReactElement) {
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const mockSessions = [
|
||||
{ id: 's1', title: 'First Chat', folder_id: null, agent_id: null, is_sidebar: false, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z' },
|
||||
{ id: 's2', title: 'Second Chat', folder_id: 'f1', agent_id: null, is_sidebar: false, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z' },
|
||||
@@ -46,21 +57,21 @@ beforeEach(() => {
|
||||
|
||||
describe('SessionList', () => {
|
||||
it('renders session list with sessions', async () => {
|
||||
render(<SessionList activeSessionId="s1" onSelectSession={vi.fn()} />);
|
||||
renderWithProvider(<SessionList activeSessionId="s1" onSelectSession={vi.fn()} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('First Chat')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders folder names', async () => {
|
||||
render(<SessionList activeSessionId="s1" onSelectSession={vi.fn()} />);
|
||||
renderWithProvider(<SessionList activeSessionId="s1" onSelectSession={vi.fn()} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('My Folder')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders sessions inside folders', async () => {
|
||||
render(<SessionList activeSessionId="s1" onSelectSession={vi.fn()} />);
|
||||
renderWithProvider(<SessionList activeSessionId="s1" onSelectSession={vi.fn()} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Second Chat')).toBeInTheDocument();
|
||||
});
|
||||
@@ -68,7 +79,7 @@ describe('SessionList', () => {
|
||||
|
||||
it('calls onSelectSession when session clicked', async () => {
|
||||
const onSelect = vi.fn();
|
||||
render(<SessionList activeSessionId={null} onSelectSession={onSelect} />);
|
||||
renderWithProvider(<SessionList activeSessionId={null} onSelectSession={onSelect} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('First Chat')).toBeInTheDocument();
|
||||
});
|
||||
@@ -79,13 +90,13 @@ describe('SessionList', () => {
|
||||
it('shows loading state initially', () => {
|
||||
fetchSessionsMock.mockReturnValue(new Promise(() => {}));
|
||||
fetchFoldersMock.mockReturnValue(new Promise(() => {}));
|
||||
render(<SessionList activeSessionId={null} onSelectSession={vi.fn()} />);
|
||||
renderWithProvider(<SessionList activeSessionId={null} onSelectSession={vi.fn()} />);
|
||||
expect(screen.getByText('Laden...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays error when fetch fails', async () => {
|
||||
fetchSessionsMock.mockRejectedValueOnce(new Error('Failed to load'));
|
||||
render(<SessionList activeSessionId={null} onSelectSession={vi.fn()} />);
|
||||
renderWithProvider(<SessionList activeSessionId={null} onSelectSession={vi.fn()} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Failed to load')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
vi.mock('@/api/calendar', () => ({
|
||||
fetchCalendars: vi.fn().mockResolvedValue([
|
||||
@@ -52,37 +53,55 @@ vi.mock('@/components/ui/Toast', () => ({
|
||||
useToast: () => ({ success: vi.fn(), error: vi.fn(), info: vi.fn() }),
|
||||
}));
|
||||
|
||||
vi.mock('@/api/savedFilters', () => ({
|
||||
useSavedFilters: () => ({ data: [], isLoading: false, isError: false }),
|
||||
useCreateSavedFilter: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
||||
useDeleteSavedFilter: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
||||
}));
|
||||
|
||||
import { CalendarPage } from '@/pages/Calendar';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
function renderPage() {
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CalendarPage />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('CalendarPage', () => {
|
||||
it('renders calendar page container', async () => {
|
||||
render(<CalendarPage />);
|
||||
renderPage();
|
||||
expect(screen.getByTestId('calendar-page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders calendar tree pane', async () => {
|
||||
render(<CalendarPage />);
|
||||
renderPage();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('calendar-tree-pane')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders calendar view pane', async () => {
|
||||
render(<CalendarPage />);
|
||||
renderPage();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('calendar-view-pane')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders calendar detail pane', async () => {
|
||||
render(<CalendarPage />);
|
||||
renderPage();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('calendar-detail-pane')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders CalendarTree component', async () => {
|
||||
render(<CalendarPage />);
|
||||
renderPage();
|
||||
// CalendarTree is rendered inside both desktop and mobile panes
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTestId('calendar-tree-stub').length).toBeGreaterThan(0);
|
||||
@@ -90,19 +109,19 @@ describe('CalendarPage', () => {
|
||||
});
|
||||
|
||||
it('renders MonthView by default', async () => {
|
||||
render(<CalendarPage />);
|
||||
renderPage();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('month-view-stub')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders mobile calendar tree pane', async () => {
|
||||
render(<CalendarPage />);
|
||||
renderPage();
|
||||
expect(screen.getByTestId('mobile-calendar-tree-pane')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders mobile calendar view pane only after navigation', async () => {
|
||||
render(<CalendarPage />);
|
||||
renderPage();
|
||||
// Mobile view starts on 'tree', so view pane should not be visible initially
|
||||
expect(screen.queryByTestId('mobile-calendar-view-pane')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { DashboardPage } from '@/pages/Dashboard';
|
||||
|
||||
vi.mock('@/api/hooks', () => ({
|
||||
@@ -22,62 +23,83 @@ vi.mock('@/api/hooks', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/api/dashboard', () => ({
|
||||
useDashboardWidgets: () => ({
|
||||
data: { items: [], total: 0 },
|
||||
isError: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
function renderPage() {
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<DashboardPage />
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('DashboardPage', () => {
|
||||
it('renders dashboard page', () => {
|
||||
render(<MemoryRouter><DashboardPage /></MemoryRouter>);
|
||||
renderPage();
|
||||
expect(screen.getByTestId('dashboard-page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders page title', () => {
|
||||
render(<MemoryRouter><DashboardPage /></MemoryRouter>);
|
||||
renderPage();
|
||||
expect(screen.getByText('Dashboard')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders stat card for companies', () => {
|
||||
render(<MemoryRouter><DashboardPage /></MemoryRouter>);
|
||||
renderPage();
|
||||
expect(screen.getByTestId('stat-companies')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders stat card for contacts', () => {
|
||||
render(<MemoryRouter><DashboardPage /></MemoryRouter>);
|
||||
renderPage();
|
||||
expect(screen.getByTestId('stat-contacts')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders stat card for active this week', () => {
|
||||
render(<MemoryRouter><DashboardPage /></MemoryRouter>);
|
||||
renderPage();
|
||||
expect(screen.getByTestId('stat-active-week')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders stat card for new this month', () => {
|
||||
render(<MemoryRouter><DashboardPage /></MemoryRouter>);
|
||||
renderPage();
|
||||
expect(screen.getByTestId('stat-new-month')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders correct company count in stat card', () => {
|
||||
render(<MemoryRouter><DashboardPage /></MemoryRouter>);
|
||||
renderPage();
|
||||
const statCompanies = screen.getByTestId('stat-companies');
|
||||
expect(statCompanies).toHaveTextContent('24');
|
||||
});
|
||||
|
||||
it('renders correct contact count in stat card', () => {
|
||||
render(<MemoryRouter><DashboardPage /></MemoryRouter>);
|
||||
renderPage();
|
||||
const statContacts = screen.getByTestId('stat-contacts');
|
||||
expect(statContacts).toHaveTextContent('156');
|
||||
});
|
||||
|
||||
it('renders activity feed', () => {
|
||||
render(<MemoryRouter><DashboardPage /></MemoryRouter>);
|
||||
renderPage();
|
||||
expect(screen.getByTestId('activity-feed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders activity feed with user names from audit log', () => {
|
||||
render(<MemoryRouter><DashboardPage /></MemoryRouter>);
|
||||
renderPage();
|
||||
expect(screen.getByText('anna.schmidt')).toBeInTheDocument();
|
||||
expect(screen.getByText('max.mustermann')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders activity feed with action descriptions', () => {
|
||||
render(<MemoryRouter><DashboardPage /></MemoryRouter>);
|
||||
renderPage();
|
||||
expect(screen.getByText(/create/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/update/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/delete/)).toBeInTheDocument();
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
vi.mock('@/api/dms', () => ({
|
||||
fetchFolders: vi.fn().mockResolvedValue([
|
||||
@@ -11,16 +12,36 @@ vi.mock('@/api/dms', () => ({
|
||||
createFolder: vi.fn().mockResolvedValue({ id: 'f3', name: 'Neuer Ordner', parent_id: null, created_by: 'u1' }),
|
||||
deleteFile: vi.fn().mockResolvedValue(undefined),
|
||||
searchFiles: vi.fn().mockResolvedValue({ files: [], total: 0 }),
|
||||
fetchFilesByFolder: vi.fn().mockResolvedValue([]),
|
||||
fetchAllFiles: vi.fn().mockResolvedValue([]),
|
||||
getSharedWithMe: vi.fn().mockResolvedValue([]),
|
||||
bulkMoveFiles: vi.fn().mockResolvedValue(undefined),
|
||||
bulkDeleteFiles: vi.fn().mockResolvedValue(undefined),
|
||||
updateFolder: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/Toast', () => ({
|
||||
useToast: () => ({ success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn() }),
|
||||
}));
|
||||
|
||||
vi.mock('@/api/savedFilters', () => ({
|
||||
useSavedFilters: () => ({ data: [], isLoading: false, isError: false }),
|
||||
useCreateSavedFilter: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
||||
useDeleteSavedFilter: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
||||
}));
|
||||
|
||||
import { DmsPage } from '@/pages/Dms';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
function renderWithRouter() {
|
||||
return render(<MemoryRouter><DmsPage /></MemoryRouter>);
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter><DmsPage /></MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -33,55 +54,36 @@ describe('DmsPage', () => {
|
||||
expect(screen.getByTestId('dms-page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders folder tree container', async () => {
|
||||
it('renders source tree pane', async () => {
|
||||
renderWithRouter();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('folder-tree-container')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('dms-source-pane')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders file grid container', async () => {
|
||||
it('renders file explorer pane', async () => {
|
||||
renderWithRouter();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('file-grid-container')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('dms-explorer-pane')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders folder tree with folders after loading', async () => {
|
||||
renderWithRouter();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Vertraege')).toBeInTheDocument();
|
||||
expect(screen.getByText('Rechnungen')).toBeInTheDocument();
|
||||
// Use getAllByText because folder names appear in both desktop and mobile views
|
||||
expect(screen.getAllByText('Vertraege').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('Rechnungen').length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows upload button and toggles upload dropzone', async () => {
|
||||
renderWithRouter();
|
||||
const uploadBtn = screen.getByText('Hochladen');
|
||||
expect(uploadBtn).toBeInTheDocument();
|
||||
fireEvent.click(uploadBtn);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('upload-dropzone')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows new folder button and toggles form', async () => {
|
||||
renderWithRouter();
|
||||
const folderBtn = screen.getByText('Neuer Ordner');
|
||||
expect(folderBtn).toBeInTheDocument();
|
||||
fireEvent.click(folderBtn);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('new-folder-form')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
// REMOVED: 'shows upload button and toggles upload dropzone' — upload is now a plugin toolbar action, not a direct button with text 'Hochladen'
|
||||
// REMOVED: 'shows new folder button and toggles form' — new folder is now a plugin toolbar action, not a direct button with text 'Neuer Ordner'
|
||||
|
||||
it('renders trash link', async () => {
|
||||
renderWithRouter();
|
||||
expect(screen.getByText('Papierkorb')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders search input', async () => {
|
||||
renderWithRouter();
|
||||
expect(screen.getByLabelText('Dateien durchsuchen')).toBeInTheDocument();
|
||||
});
|
||||
// REMOVED: 'renders search input' — search is now a plugin toolbar action, not a labeled input directly in DmsPage
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('UploadDropzone', () => {
|
||||
|
||||
it('renders drag-and-drop instruction text', () => {
|
||||
render(<UploadDropzone folderId={null} onUploaded={vi.fn()} />);
|
||||
expect(screen.getByText('Dateien hierher ziehen oder klicken zum Auswaehlen')).toBeInTheDocument();
|
||||
expect(screen.getByText('Dateien hierher ziehen oder klicken zum Auswählen')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens file picker on click', () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
vi.mock('@/api/mail', () => ({
|
||||
decodeMimeHeader: (s: string) => s,
|
||||
@@ -38,10 +39,24 @@ vi.mock('@/components/ui/Toast', () => ({
|
||||
useToast: () => ({ success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn() }),
|
||||
}));
|
||||
|
||||
vi.mock('@/api/savedFilters', () => ({
|
||||
useSavedFilters: () => ({ data: [], isLoading: false, isError: false }),
|
||||
useCreateSavedFilter: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
||||
useDeleteSavedFilter: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
||||
}));
|
||||
|
||||
import { MailPage } from '@/pages/Mail';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
function renderWithRouter() {
|
||||
return render(<MemoryRouter><MailPage /></MemoryRouter>);
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter><MailPage /></MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -77,26 +92,9 @@ describe('MailPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('renders compose button', async () => {
|
||||
renderWithRouter();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('compose-btn')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders shared mailbox selector', async () => {
|
||||
renderWithRouter();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('shared-mailbox-selector')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders mail search bar', async () => {
|
||||
renderWithRouter();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('mail-search-bar')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
// REMOVED: 'renders compose button' — compose is now a plugin toolbar action, not a direct page element
|
||||
// REMOVED: 'renders shared mailbox selector' — account selection integrated into folder tree, selector no longer exists
|
||||
// REMOVED: 'renders mail search bar' — search is now a plugin toolbar action
|
||||
|
||||
it('renders folders after loading', async () => {
|
||||
renderWithRouter();
|
||||
@@ -114,23 +112,8 @@ describe('MailPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('shows compose modal when compose button is clicked', async () => {
|
||||
renderWithRouter();
|
||||
await waitFor(() => {
|
||||
const btn = screen.getByTestId('compose-btn');
|
||||
fireEvent.click(btn);
|
||||
expect(screen.getByTestId('compose-modal')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows compose toolbar with bold and italic buttons', async () => {
|
||||
renderWithRouter();
|
||||
await waitFor(() => {
|
||||
const btn = screen.getByTestId('compose-btn');
|
||||
fireEvent.click(btn);
|
||||
expect(screen.getByTestId('compose-toolbar')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
// REMOVED: 'shows compose modal when compose button is clicked' — compose now opens as a window via windowStore, not a modal in the page
|
||||
// REMOVED: 'shows compose toolbar with bold and italic buttons' — compose form is in a separate window, not directly in MailPage
|
||||
|
||||
it('renders mail detail empty state initially', async () => {
|
||||
renderWithRouter();
|
||||
@@ -150,14 +133,5 @@ describe('MailPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('shows reply and forward buttons in mail detail', async () => {
|
||||
renderWithRouter();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Test Subject')).toBeInTheDocument();
|
||||
});
|
||||
fireEvent.click(screen.getByText('Test Subject'));
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTestId('mail-detail-toolbar').length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
// REMOVED: 'shows reply and forward buttons in mail detail' — reply/forward are now plugin toolbar actions, not elements within mail-detail. mail-detail-toolbar testid no longer exists.
|
||||
});
|
||||
|
||||
@@ -93,7 +93,7 @@ describe('ShareDialog', () => {
|
||||
render(<ShareDialog open={true} file={mockFile} onClose={vi.fn()} onShared={vi.fn()} />);
|
||||
const userIdInput = screen.getByLabelText('Benutzer-ID');
|
||||
fireEvent.change(userIdInput, { target: { value: 'u5' } });
|
||||
const addBtn = screen.getByRole('button', { name: 'Freigabe hinzufuegen' });
|
||||
const addBtn = screen.getByRole('button', { name: 'Freigabe hinzufügen' });
|
||||
fireEvent.click(addBtn);
|
||||
await waitFor(() => {
|
||||
expect(shareFile).toHaveBeenCalledWith('file1', { user_id: 'u5', permission: 'read' });
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { SettingsSystemPage } from '@/pages/SettingsSystem';
|
||||
|
||||
const mockSettings = {
|
||||
@@ -36,8 +37,47 @@ vi.mock('@/components/ui/Toast', () => ({
|
||||
useToast: () => ({ success: vi.fn(), error: vi.fn(), info: vi.fn() }),
|
||||
}));
|
||||
|
||||
vi.mock('@/api/users', () => ({
|
||||
useMenuOrder: () => ({ data: { items: [], order: [] }, isLoading: false, isError: false }),
|
||||
useUpdateMenuOrder: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
||||
}));
|
||||
|
||||
vi.mock('@/api/settings', () => ({
|
||||
useSystemSettings: () => ({ data: mockSettings, isLoading: false }),
|
||||
useUpdateSystemSettings: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
||||
}));
|
||||
|
||||
vi.mock('@/api/plugins', () => ({
|
||||
usePlugins: () => ({ data: [], isLoading: false, isError: false, error: null, refetch: vi.fn() }),
|
||||
useInstallPlugin: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
||||
useActivatePlugin: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
||||
useDeactivatePlugin: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
||||
useUninstallPlugin: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
||||
useUploadPlugin: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
||||
useInstallPluginFromUrl: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
||||
}));
|
||||
|
||||
vi.mock('@/pages/SettingsMenuOrder', () => ({
|
||||
SettingsMenuOrderPage: () => <div data-testid="menu-order-stub" />,
|
||||
}));
|
||||
|
||||
vi.mock('@/pages/SettingsTheme', () => ({
|
||||
SettingsThemePage: () => <div data-testid="theme-stub" />,
|
||||
}));
|
||||
|
||||
vi.mock('@/pages/SettingsPlugins', () => ({
|
||||
SettingsPluginsPage: () => <div data-testid="plugins-stub" />,
|
||||
}));
|
||||
|
||||
function renderPage() {
|
||||
return render(<MemoryRouter><SettingsSystemPage /></MemoryRouter>);
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter><SettingsSystemPage /></MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('SettingsSystemPage', () => {
|
||||
@@ -46,25 +86,8 @@ describe('SettingsSystemPage', () => {
|
||||
expect(screen.getByTestId('settings-system-page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders company name input with loaded value', () => {
|
||||
renderPage();
|
||||
expect(screen.getByTestId('settings-company-name')).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue('Test GmbH')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders save button', () => {
|
||||
renderPage();
|
||||
expect(screen.getByTestId('settings-system-save')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders currency and tax selects', () => {
|
||||
renderPage();
|
||||
const selects = screen.getAllByRole('combobox');
|
||||
expect(selects.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('renders payment terms input', () => {
|
||||
renderPage();
|
||||
expect(screen.getByDisplayValue('14')).toBeInTheDocument();
|
||||
});
|
||||
// REMOVED: 'renders company name input with loaded value' — form fields moved to sub-pages (SettingsStammdaten etc.), no longer directly in SettingsSystem
|
||||
// REMOVED: 'renders save button' — save button moved to sub-pages
|
||||
// REMOVED: 'renders currency and tax selects' — selects moved to sub-pages
|
||||
// REMOVED: 'renders payment terms input' — input moved to sub-pages
|
||||
});
|
||||
|
||||
@@ -13,7 +13,13 @@ const HtmlBlock: React.FC<HtmlBlockProps> = ({ block }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sanitized = DOMPurify.sanitize(rawHtml);
|
||||
// Replace javascript: URLs in href attributes before sanitizing
|
||||
const safeHtml = rawHtml.replace(
|
||||
/href\s*=\s*(["'])\s*javascript:[^"']*\1/gi,
|
||||
'href=$1#$1'
|
||||
);
|
||||
|
||||
const sanitized = DOMPurify.sanitize(safeHtml);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -94,7 +94,7 @@ describe('PluginRouteRenderer', () => {
|
||||
expect(screen.getByText(/unknown/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders nothing when manifests have not loaded yet', () => {
|
||||
it('renders loading spinner when manifests have not loaded yet', () => {
|
||||
usePluginStore.setState({
|
||||
manifests: [],
|
||||
loaded: false,
|
||||
@@ -104,7 +104,8 @@ describe('PluginRouteRenderer', () => {
|
||||
<PluginRouteRenderer />
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(container.innerHTML).toBe('');
|
||||
// Component now shows a loading spinner instead of empty HTML
|
||||
expect(container.querySelector('[role="status"]')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('matches the first route when multiple match', () => {
|
||||
|
||||
@@ -180,7 +180,7 @@ export function SettingsPluginsPage() {
|
||||
const [confirmUninstall, setConfirmUninstall] = useState<Plugin | null>(null);
|
||||
const [confirmRemoveData, setConfirmRemoveData] = useState(false);
|
||||
|
||||
const plugins: Plugin[] = data ?? [];
|
||||
const plugins: Plugin[] = Array.isArray(data) ? data : (data?.plugins ?? []);
|
||||
|
||||
const handleInstall = async (plugin: Plugin) => {
|
||||
try {
|
||||
|
||||
@@ -65,6 +65,7 @@ vi.mock('lucide-react', () => {
|
||||
Building2: Icon,
|
||||
Calendar: Icon,
|
||||
CalendarDays: Icon,
|
||||
ChevronDown: Icon,
|
||||
Download: Icon,
|
||||
FileText: Icon,
|
||||
Loader2: Icon,
|
||||
|
||||
Reference in New Issue
Block a user