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:
Agent Zero
2026-08-04 19:25:44 +02:00
parent 2bacadabc2
commit 17765e47b4
12 changed files with 190 additions and 131 deletions
+17 -6
View File
@@ -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();
+32 -30
View File
@@ -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', () => {
+22 -48
View File
@@ -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', () => {
+1 -1
View File
@@ -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,