diff --git a/frontend/src/__tests__/ai/SessionList.test.tsx b/frontend/src/__tests__/ai/SessionList.test.tsx
index 2dfa3b5..523cec1 100644
--- a/frontend/src/__tests__/ai/SessionList.test.tsx
+++ b/frontend/src/__tests__/ai/SessionList.test.tsx
@@ -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(
+ {ui}
+ );
+}
+
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();
+ renderWithProvider();
await waitFor(() => {
expect(screen.getByText('First Chat')).toBeInTheDocument();
});
});
it('renders folder names', async () => {
- render();
+ renderWithProvider();
await waitFor(() => {
expect(screen.getByText('My Folder')).toBeInTheDocument();
});
});
it('renders sessions inside folders', async () => {
- render();
+ renderWithProvider();
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();
+ renderWithProvider();
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();
+ renderWithProvider();
expect(screen.getByText('Laden...')).toBeInTheDocument();
});
it('displays error when fetch fails', async () => {
fetchSessionsMock.mockRejectedValueOnce(new Error('Failed to load'));
- render();
+ renderWithProvider();
await waitFor(() => {
expect(screen.getByText('Failed to load')).toBeInTheDocument();
});
diff --git a/frontend/src/__tests__/calendar/CalendarPage.test.tsx b/frontend/src/__tests__/calendar/CalendarPage.test.tsx
index d2c5ce5..6458dff 100644
--- a/frontend/src/__tests__/calendar/CalendarPage.test.tsx
+++ b/frontend/src/__tests__/calendar/CalendarPage.test.tsx
@@ -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(
+
+
+
+ );
+}
+
describe('CalendarPage', () => {
it('renders calendar page container', async () => {
- render();
+ renderPage();
expect(screen.getByTestId('calendar-page')).toBeInTheDocument();
});
it('renders calendar tree pane', async () => {
- render();
+ renderPage();
await waitFor(() => {
expect(screen.getByTestId('calendar-tree-pane')).toBeInTheDocument();
});
});
it('renders calendar view pane', async () => {
- render();
+ renderPage();
await waitFor(() => {
expect(screen.getByTestId('calendar-view-pane')).toBeInTheDocument();
});
});
it('renders calendar detail pane', async () => {
- render();
+ renderPage();
await waitFor(() => {
expect(screen.getByTestId('calendar-detail-pane')).toBeInTheDocument();
});
});
it('renders CalendarTree component', async () => {
- render();
+ 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();
+ renderPage();
await waitFor(() => {
expect(screen.getByTestId('month-view-stub')).toBeInTheDocument();
});
});
it('renders mobile calendar tree pane', async () => {
- render();
+ renderPage();
expect(screen.getByTestId('mobile-calendar-tree-pane')).toBeInTheDocument();
});
it('renders mobile calendar view pane only after navigation', async () => {
- render();
+ renderPage();
// Mobile view starts on 'tree', so view pane should not be visible initially
expect(screen.queryByTestId('mobile-calendar-view-pane')).not.toBeInTheDocument();
});
diff --git a/frontend/src/__tests__/dashboard/Dashboard.test.tsx b/frontend/src/__tests__/dashboard/Dashboard.test.tsx
index df4a2a3..14954da 100644
--- a/frontend/src/__tests__/dashboard/Dashboard.test.tsx
+++ b/frontend/src/__tests__/dashboard/Dashboard.test.tsx
@@ -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(
+
+
+
+
+
+ );
+}
+
describe('DashboardPage', () => {
it('renders dashboard page', () => {
- render();
+ renderPage();
expect(screen.getByTestId('dashboard-page')).toBeInTheDocument();
});
it('renders page title', () => {
- render();
+ renderPage();
expect(screen.getByText('Dashboard')).toBeInTheDocument();
});
it('renders stat card for companies', () => {
- render();
+ renderPage();
expect(screen.getByTestId('stat-companies')).toBeInTheDocument();
});
it('renders stat card for contacts', () => {
- render();
+ renderPage();
expect(screen.getByTestId('stat-contacts')).toBeInTheDocument();
});
it('renders stat card for active this week', () => {
- render();
+ renderPage();
expect(screen.getByTestId('stat-active-week')).toBeInTheDocument();
});
it('renders stat card for new this month', () => {
- render();
+ renderPage();
expect(screen.getByTestId('stat-new-month')).toBeInTheDocument();
});
it('renders correct company count in stat card', () => {
- render();
+ renderPage();
const statCompanies = screen.getByTestId('stat-companies');
expect(statCompanies).toHaveTextContent('24');
});
it('renders correct contact count in stat card', () => {
- render();
+ renderPage();
const statContacts = screen.getByTestId('stat-contacts');
expect(statContacts).toHaveTextContent('156');
});
it('renders activity feed', () => {
- render();
+ renderPage();
expect(screen.getByTestId('activity-feed')).toBeInTheDocument();
});
it('renders activity feed with user names from audit log', () => {
- render();
+ renderPage();
expect(screen.getByText('anna.schmidt')).toBeInTheDocument();
expect(screen.getByText('max.mustermann')).toBeInTheDocument();
});
it('renders activity feed with action descriptions', () => {
- render();
+ renderPage();
expect(screen.getByText(/create/)).toBeInTheDocument();
expect(screen.getByText(/update/)).toBeInTheDocument();
expect(screen.getByText(/delete/)).toBeInTheDocument();
diff --git a/frontend/src/__tests__/dms/DmsPage.test.tsx b/frontend/src/__tests__/dms/DmsPage.test.tsx
index f3eca79..c438122 100644
--- a/frontend/src/__tests__/dms/DmsPage.test.tsx
+++ b/frontend/src/__tests__/dms/DmsPage.test.tsx
@@ -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();
+ return render(
+
+
+
+ );
}
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
});
diff --git a/frontend/src/__tests__/dms/UploadDropzone.test.tsx b/frontend/src/__tests__/dms/UploadDropzone.test.tsx
index ce61f0c..fe52303 100644
--- a/frontend/src/__tests__/dms/UploadDropzone.test.tsx
+++ b/frontend/src/__tests__/dms/UploadDropzone.test.tsx
@@ -26,7 +26,7 @@ describe('UploadDropzone', () => {
it('renders drag-and-drop instruction text', () => {
render();
- 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', () => {
diff --git a/frontend/src/__tests__/mail/MailPage.test.tsx b/frontend/src/__tests__/mail/MailPage.test.tsx
index 91736a2..f0fed88 100644
--- a/frontend/src/__tests__/mail/MailPage.test.tsx
+++ b/frontend/src/__tests__/mail/MailPage.test.tsx
@@ -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();
+ return render(
+
+
+
+ );
}
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.
});
diff --git a/frontend/src/__tests__/permissions/ShareDialog.test.tsx b/frontend/src/__tests__/permissions/ShareDialog.test.tsx
index 40002ae..84c1250 100644
--- a/frontend/src/__tests__/permissions/ShareDialog.test.tsx
+++ b/frontend/src/__tests__/permissions/ShareDialog.test.tsx
@@ -93,7 +93,7 @@ describe('ShareDialog', () => {
render();
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' });
diff --git a/frontend/src/__tests__/settings/SettingsSystem.test.tsx b/frontend/src/__tests__/settings/SettingsSystem.test.tsx
index 727cd62..57f11f6 100644
--- a/frontend/src/__tests__/settings/SettingsSystem.test.tsx
+++ b/frontend/src/__tests__/settings/SettingsSystem.test.tsx
@@ -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: () =>
,
+}));
+
+vi.mock('@/pages/SettingsTheme', () => ({
+ SettingsThemePage: () => ,
+}));
+
+vi.mock('@/pages/SettingsPlugins', () => ({
+ SettingsPluginsPage: () => ,
+}));
+
function renderPage() {
- return render();
+ const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+ });
+ return render(
+
+
+
+ );
}
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
});
diff --git a/frontend/src/components/comm/blocks/HtmlBlock.tsx b/frontend/src/components/comm/blocks/HtmlBlock.tsx
index 184ee56..0927e84 100644
--- a/frontend/src/components/comm/blocks/HtmlBlock.tsx
+++ b/frontend/src/components/comm/blocks/HtmlBlock.tsx
@@ -13,7 +13,13 @@ const HtmlBlock: React.FC = ({ 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 (
{
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', () => {
);
- 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', () => {
diff --git a/frontend/src/pages/SettingsPlugins.tsx b/frontend/src/pages/SettingsPlugins.tsx
index 1e23156..171ef24 100644
--- a/frontend/src/pages/SettingsPlugins.tsx
+++ b/frontend/src/pages/SettingsPlugins.tsx
@@ -180,7 +180,7 @@ export function SettingsPluginsPage() {
const [confirmUninstall, setConfirmUninstall] = useState
(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 {
diff --git a/frontend/src/pages/__tests__/Reports.test.tsx b/frontend/src/pages/__tests__/Reports.test.tsx
index 2761447..d3d0ad8 100644
--- a/frontend/src/pages/__tests__/Reports.test.tsx
+++ b/frontend/src/pages/__tests__/Reports.test.tsx
@@ -65,6 +65,7 @@ vi.mock('lucide-react', () => {
Building2: Icon,
Calendar: Icon,
CalendarDays: Icon,
+ ChevronDown: Icon,
Download: Icon,
FileText: Icon,
Loader2: Icon,