diff --git a/frontend/src/__tests__/dms/BulkActions.test.tsx b/frontend/src/__tests__/dms/BulkActions.test.tsx
new file mode 100644
index 0000000..81a83f7
--- /dev/null
+++ b/frontend/src/__tests__/dms/BulkActions.test.tsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import { describe, it, expect, vi } from 'vitest';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { BulkActions } from '@/components/dms/BulkActions';
+import type { DmsFolder } from '@/api/dms';
+
+vi.mock('@/api/dms', () => ({
+ bulkMoveFiles: vi.fn().mockResolvedValue(undefined),
+ bulkDeleteFiles: vi.fn().mockResolvedValue(undefined),
+}));
+
+vi.mock('@/components/ui/Toast', () => ({
+ useToast: () => ({ success: vi.fn(), error: vi.fn(), info: vi.fn() }),
+}));
+
+const mockFolders: DmsFolder[] = [
+ { id: 'folder1', name: 'Documents', parent_id: null, created_by: 'user1', created_at: '2024-01-01T00:00:00Z' },
+ { id: 'folder2', name: 'Images', parent_id: null, created_by: 'user1', created_at: '2024-01-01T00:00:00Z' },
+];
+
+const defaultProps = {
+ selectedFileIds: new Set(['f1', 'f2', 'f3']),
+ folders: mockFolders,
+ onClear: vi.fn(),
+ onActionComplete: vi.fn(),
+};
+
+describe('BulkActions', () => {
+ it('renders bulk actions bar when files are selected', () => {
+ render();
+ expect(screen.getByTestId('bulk-actions-bar')).toBeInTheDocument();
+ });
+
+ it('does not render when no files selected', () => {
+ render();
+ expect(screen.queryByTestId('bulk-actions-bar')).not.toBeInTheDocument();
+ });
+
+ it('renders selected file count', () => {
+ render();
+ expect(screen.getByText(/3/)).toBeInTheDocument();
+ });
+
+ it('renders move button', () => {
+ render();
+ expect(screen.getByText(/move|verschieben/i)).toBeInTheDocument();
+ });
+
+ it('renders delete button', () => {
+ render();
+ expect(screen.getByText(/delete|löschen/i)).toBeInTheDocument();
+ });
+
+ it('renders cancel/clear button', () => {
+ render();
+ expect(screen.getByText(/cancel|abbrechen/i)).toBeInTheDocument();
+ });
+
+ it('calls onClear when cancel button clicked', () => {
+ render();
+ fireEvent.click(screen.getByText(/cancel|abbrechen/i));
+ expect(defaultProps.onClear).toHaveBeenCalled();
+ });
+});
diff --git a/frontend/src/__tests__/dms/FileDetails.test.tsx b/frontend/src/__tests__/dms/FileDetails.test.tsx
new file mode 100644
index 0000000..4780325
--- /dev/null
+++ b/frontend/src/__tests__/dms/FileDetails.test.tsx
@@ -0,0 +1,78 @@
+import React from 'react';
+import { describe, it, expect, vi } from 'vitest';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { FileDetails } from '@/components/dms/FileDetails';
+import type { DmsFile } from '@/api/dms';
+
+const mockFile: DmsFile = {
+ id: 'f1',
+ folder_id: 'folder1',
+ name: 'document.pdf',
+ mime_type: 'application/pdf',
+ size_bytes: 1024,
+ storage_path: '/path/f1',
+ deleted_at: null,
+ uploaded_by: 'user1',
+ created_at: '2024-01-01T10:00:00Z',
+ updated_at: '2024-01-02T12:00:00Z',
+ access_level: 'read',
+};
+
+const defaultProps = {
+ file: mockFile,
+ onPreview: vi.fn(),
+ onShare: vi.fn(),
+ onDelete: vi.fn(),
+ onClose: vi.fn(),
+};
+
+describe('FileDetails', () => {
+ it('renders file details container', () => {
+ render();
+ expect(screen.getByTestId('file-details')).toBeInTheDocument();
+ });
+
+ it('renders file name in header', () => {
+ render();
+ expect(screen.getAllByText('document.pdf').length).toBeGreaterThan(0);
+ });
+
+ it('renders empty state when no file selected', () => {
+ render();
+ expect(screen.getByTestId('file-details-empty')).toBeInTheDocument();
+ });
+
+ it('renders close button', () => {
+ render();
+ expect(screen.getByLabelText(/close|schließen/i)).toBeInTheDocument();
+ });
+
+ it('calls onClose when close button clicked', () => {
+ render();
+ fireEvent.click(screen.getByLabelText(/close|schließen/i));
+ expect(defaultProps.onClose).toHaveBeenCalled();
+ });
+
+ it('renders preview button', () => {
+ render();
+ const previewBtn = screen.getByText(/preview|vorschau/i);
+ expect(previewBtn).toBeInTheDocument();
+ });
+
+ it('calls onPreview when preview button clicked', () => {
+ render();
+ fireEvent.click(screen.getByText(/preview|vorschau/i));
+ expect(defaultProps.onPreview).toHaveBeenCalledWith(mockFile);
+ });
+
+ it('renders delete button', () => {
+ render();
+ expect(screen.getByText(/delete|löschen/i)).toBeInTheDocument();
+ });
+
+ it('calls onDelete when delete button clicked', () => {
+ render();
+ fireEvent.click(screen.getByText(/delete|löschen/i));
+ expect(defaultProps.onDelete).toHaveBeenCalledWith(mockFile);
+ });
+});
diff --git a/frontend/src/__tests__/dms/FileExplorer.test.tsx b/frontend/src/__tests__/dms/FileExplorer.test.tsx
new file mode 100644
index 0000000..0c3e43d
--- /dev/null
+++ b/frontend/src/__tests__/dms/FileExplorer.test.tsx
@@ -0,0 +1,71 @@
+import React from 'react';
+import { describe, it, expect, vi } from 'vitest';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { FileExplorer } from '@/components/dms/FileExplorer';
+import type { DmsFile } from '@/api/dms';
+
+const mockFiles: DmsFile[] = [
+ { id: 'f1', folder_id: null, name: 'document.pdf', mime_type: 'application/pdf', size_bytes: 1024, storage_path: '/path/f1', deleted_at: null, uploaded_by: 'user1', created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-02T00:00:00Z' },
+ { id: 'f2', folder_id: null, name: 'image.png', mime_type: 'image/png', size_bytes: 2048, storage_path: '/path/f2', deleted_at: null, uploaded_by: 'user2', created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-03T00:00:00Z' },
+];
+
+const defaultProps = {
+ files: mockFiles,
+ selectedFileIds: new Set(),
+ selectedFile: null,
+ viewMode: 'list' as const,
+ onToggleSelect: vi.fn(),
+ onFileClick: vi.fn(),
+ onFileDoubleClick: vi.fn(),
+ onFileShare: vi.fn(),
+ onFileDelete: vi.fn(),
+ onFilePreview: vi.fn(),
+ sortBy: 'name' as const,
+ sortOrder: 'asc' as const,
+ onSortChange: vi.fn(),
+};
+
+describe('FileExplorer', () => {
+ it('renders file list in list view', () => {
+ render();
+ expect(screen.getByTestId('file-explorer-list')).toBeInTheDocument();
+ });
+
+ it('renders file rows for each file', () => {
+ render();
+ expect(screen.getByTestId('file-row-f1')).toBeInTheDocument();
+ expect(screen.getByTestId('file-row-f2')).toBeInTheDocument();
+ });
+
+ it('renders file names', () => {
+ render();
+ expect(screen.getByText('document.pdf')).toBeInTheDocument();
+ expect(screen.getByText('image.png')).toBeInTheDocument();
+ });
+
+ it('shows loading state', () => {
+ render();
+ expect(screen.getByTestId('file-explorer-loading')).toBeInTheDocument();
+ });
+
+ it('shows empty state when no files', () => {
+ render();
+ expect(screen.getByTestId('file-explorer-empty')).toBeInTheDocument();
+ });
+
+ it('calls onFileClick when file row clicked', () => {
+ render();
+ fireEvent.click(screen.getByTestId('file-row-f1'));
+ expect(defaultProps.onFileClick).toHaveBeenCalledWith(mockFiles[0]);
+ });
+
+ it('renders table view when viewMode is table', () => {
+ render();
+ expect(screen.getByTestId('file-explorer-table')).toBeInTheDocument();
+ });
+
+ it('renders icons view when viewMode is icons-md', () => {
+ render();
+ expect(screen.getByTestId('file-explorer-icons-md')).toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/__tests__/dms/FileGrid.test.tsx b/frontend/src/__tests__/dms/FileGrid.test.tsx
new file mode 100644
index 0000000..fc8970f
--- /dev/null
+++ b/frontend/src/__tests__/dms/FileGrid.test.tsx
@@ -0,0 +1,61 @@
+import React from 'react';
+import { describe, it, expect, vi } from 'vitest';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { FileGrid } from '@/components/dms/FileGrid';
+import type { DmsFile } from '@/api/dms';
+
+const mockFiles: DmsFile[] = [
+ { id: 'f1', folder_id: null, name: 'report.pdf', mime_type: 'application/pdf', size_bytes: 1024, storage_path: '/path/f1', deleted_at: null, uploaded_by: 'user1', created_at: '2024-01-01T00:00:00Z' },
+ { id: 'f2', folder_id: null, name: 'photo.jpg', mime_type: 'image/jpeg', size_bytes: 4096, storage_path: '/path/f2', deleted_at: null, uploaded_by: 'user2', created_at: '2024-01-01T00:00:00Z' },
+];
+
+const defaultProps = {
+ files: mockFiles,
+ selectedFileIds: new Set(),
+ onToggleSelect: vi.fn(),
+ onFileClick: vi.fn(),
+ onFileShare: vi.fn(),
+ onFileDelete: vi.fn(),
+ onFilePreview: vi.fn(),
+};
+
+describe('FileGrid', () => {
+ it('renders file grid container', () => {
+ render();
+ expect(screen.getByTestId('file-grid')).toBeInTheDocument();
+ });
+
+ it('renders file cards for each file', () => {
+ render();
+ expect(screen.getByTestId('file-card-f1')).toBeInTheDocument();
+ expect(screen.getByTestId('file-card-f2')).toBeInTheDocument();
+ });
+
+ it('renders file names in cards', () => {
+ render();
+ expect(screen.getByText('report.pdf')).toBeInTheDocument();
+ expect(screen.getByText('photo.jpg')).toBeInTheDocument();
+ });
+
+ it('shows loading state', () => {
+ render();
+ expect(screen.getByTestId('file-grid-loading')).toBeInTheDocument();
+ });
+
+ it('shows empty state when no files', () => {
+ render();
+ expect(screen.getByTestId('file-grid-empty')).toBeInTheDocument();
+ });
+
+ it('calls onFileClick when file card clicked', () => {
+ render();
+ fireEvent.click(screen.getByTestId('file-card-f1'));
+ expect(defaultProps.onFileClick).toHaveBeenCalledWith(mockFiles[0]);
+ });
+
+ it('renders checkboxes for file selection', () => {
+ render();
+ const checkboxes = screen.getAllByRole('checkbox');
+ expect(checkboxes.length).toBeGreaterThanOrEqual(2);
+ });
+});
diff --git a/frontend/src/__tests__/dms/SourceTree.test.tsx b/frontend/src/__tests__/dms/SourceTree.test.tsx
new file mode 100644
index 0000000..995dd32
--- /dev/null
+++ b/frontend/src/__tests__/dms/SourceTree.test.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import { describe, it, expect, vi } from 'vitest';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { SourceTree } from '@/components/dms/SourceTree';
+import type { DmsFolder, DmsFile } from '@/api/dms';
+
+const mockFolders: DmsFolder[] = [
+ { id: 'folder1', name: 'Documents', parent_id: null, created_by: 'user1', created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', file_count: 5 },
+ { id: 'folder2', name: 'Images', parent_id: null, created_by: 'user1', created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', file_count: 3 },
+];
+
+const mockSharedFiles: DmsFile[] = [
+ { id: 'sf1', folder_id: null, name: 'shared.doc', mime_type: 'application/msword', size_bytes: 512, storage_path: '/path/sf1', deleted_at: null, uploaded_by: 'other', created_at: '2024-01-01T00:00:00Z' },
+];
+
+const defaultProps = {
+ folders: mockFolders,
+ sharedFiles: mockSharedFiles,
+ selectedFolderId: null,
+ selectedSource: null as string | null,
+ onSelectFolder: vi.fn(),
+ onSelectSource: vi.fn(),
+ onDropFiles: vi.fn(),
+ onDropFolder: vi.fn(),
+};
+
+describe('SourceTree', () => {
+ it('renders source tree container', () => {
+ render();
+ expect(screen.getByTestId('source-tree')).toBeInTheDocument();
+ });
+
+ it('renders folder names', () => {
+ render();
+ expect(screen.getByText('Documents')).toBeInTheDocument();
+ expect(screen.getByText('Images')).toBeInTheDocument();
+ });
+
+ it('shows loading state', () => {
+ render();
+ expect(screen.getByTestId('source-tree-loading')).toBeInTheDocument();
+ });
+
+ it('calls onSelectFolder when folder clicked', () => {
+ render();
+ fireEvent.click(screen.getByText('Documents'));
+ expect(defaultProps.onSelectFolder).toHaveBeenCalled();
+ });
+
+ it('calls onSelectSource when source clicked', () => {
+ render();
+ // Click on "All Files" entry which calls onSelectSource('local')
+ const allFilesEntry = screen.getByText(/all files|alle dateien/i);
+ if (allFilesEntry) {
+ fireEvent.click(allFilesEntry);
+ expect(defaultProps.onSelectSource).toHaveBeenCalled();
+ }
+ });
+});