@semiont/react-ui 0.2.36 → 0.2.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/index.d.mts +8 -0
  2. package/dist/index.mjs +252 -166
  3. package/dist/index.mjs.map +1 -1
  4. package/package.json +1 -1
  5. package/src/components/CodeMirrorRenderer.tsx +71 -203
  6. package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +142 -0
  7. package/src/components/__tests__/LiveRegion.hooks.test.tsx +79 -0
  8. package/src/components/__tests__/ResizeHandle.test.tsx +165 -0
  9. package/src/components/__tests__/SessionExpiryBanner.test.tsx +123 -0
  10. package/src/components/__tests__/StatusDisplay.test.tsx +160 -0
  11. package/src/components/__tests__/Toolbar.test.tsx +110 -0
  12. package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +285 -0
  13. package/src/components/annotation-popups/__tests__/SharedPopupElements.test.tsx +273 -0
  14. package/src/components/modals/__tests__/KeyboardShortcutsHelpModal.test.tsx +90 -0
  15. package/src/components/modals/__tests__/ProposeEntitiesModal.test.tsx +129 -0
  16. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +180 -0
  17. package/src/components/navigation/__tests__/ObservableLink.test.tsx +90 -0
  18. package/src/components/navigation/__tests__/SimpleNavigation.test.tsx +169 -0
  19. package/src/components/navigation/__tests__/SortableResourceTab.test.tsx +371 -0
  20. package/src/components/resource/AnnotateView.tsx +27 -153
  21. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +349 -0
  22. package/src/components/resource/__tests__/HistoryEvent.test.tsx +492 -0
  23. package/src/components/resource/__tests__/event-formatting.test.ts +273 -0
  24. package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +226 -0
  25. package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +188 -0
  26. package/src/components/resource/panels/__tests__/PanelHeader.test.tsx +69 -0
  27. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +445 -0
  28. package/src/components/resource/panels/__tests__/StatisticsPanel.test.tsx +271 -0
  29. package/src/components/resource/panels/__tests__/TagEntry.test.tsx +210 -0
  30. package/src/components/settings/__tests__/SettingsPanel.test.tsx +190 -0
  31. package/src/components/viewers/__tests__/ImageViewer.test.tsx +63 -0
  32. package/src/integrations/__tests__/css-modules-helper.test.tsx +225 -0
  33. package/src/integrations/__tests__/styled-components-theme.test.ts +179 -0
@@ -0,0 +1,129 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import React from 'react';
3
+ import { screen, fireEvent } from '@testing-library/react';
4
+ import { renderWithProviders } from '../../../test-utils';
5
+ import '@testing-library/jest-dom';
6
+ import { ProposeEntitiesModal } from '../ProposeEntitiesModal';
7
+
8
+ // Mock HeadlessUI to avoid jsdom OOM issues
9
+ vi.mock('@headlessui/react', () => ({
10
+ Dialog: ({ children, onClose, ...props }: any) => <div role="dialog" {...props}>{typeof children === 'function' ? children({ open: true }) : children}</div>,
11
+ DialogPanel: ({ children, ...props }: any) => <div {...props}>{children}</div>,
12
+ DialogTitle: ({ children, ...props }: any) => <h2 {...props}>{children}</h2>,
13
+ DialogDescription: ({ children, ...props }: any) => <p {...props}>{children}</p>,
14
+ Transition: ({ show, children }: any) => show ? <>{children}</> : null,
15
+ TransitionChild: ({ children }: any) => <>{children}</>,
16
+ }));
17
+
18
+ // Stable entity types array to avoid infinite re-render loops
19
+ const mockEntityTypes = ['Person', 'Organization', 'Location'];
20
+ const stableQueryResult = { data: { entityTypes: mockEntityTypes } };
21
+
22
+ vi.mock('../../../lib/api-hooks', () => ({
23
+ useEntityTypes: vi.fn(() => ({
24
+ list: {
25
+ useQuery: () => stableQueryResult,
26
+ },
27
+ })),
28
+ }));
29
+
30
+ describe('ProposeEntitiesModal', () => {
31
+ const defaultProps = {
32
+ isOpen: true,
33
+ onConfirm: vi.fn(),
34
+ onCancel: vi.fn(),
35
+ };
36
+
37
+ beforeEach(() => {
38
+ vi.clearAllMocks();
39
+ sessionStorage.clear();
40
+ });
41
+
42
+ it('renders modal title when open', () => {
43
+ renderWithProviders(<ProposeEntitiesModal {...defaultProps} />);
44
+ expect(screen.getByText('Detect Entity References')).toBeInTheDocument();
45
+ });
46
+
47
+ it('does not render when closed', () => {
48
+ renderWithProviders(
49
+ <ProposeEntitiesModal {...defaultProps} isOpen={false} />
50
+ );
51
+ expect(screen.queryByText('Detect Entity References')).not.toBeInTheDocument();
52
+ });
53
+
54
+ it('renders available entity type buttons', () => {
55
+ renderWithProviders(<ProposeEntitiesModal {...defaultProps} />);
56
+ expect(screen.getByText('Person')).toBeInTheDocument();
57
+ expect(screen.getByText('Organization')).toBeInTheDocument();
58
+ expect(screen.getByText('Location')).toBeInTheDocument();
59
+ });
60
+
61
+ it('toggles entity type selection on click', () => {
62
+ renderWithProviders(<ProposeEntitiesModal {...defaultProps} />);
63
+
64
+ fireEvent.click(screen.getByText('Person'));
65
+ expect(screen.getByText('1 type selected')).toBeInTheDocument();
66
+
67
+ fireEvent.click(screen.getByText('Organization'));
68
+ expect(screen.getByText('2 types selected')).toBeInTheDocument();
69
+
70
+ // Deselect
71
+ fireEvent.click(screen.getByText('Person'));
72
+ expect(screen.getByText('1 type selected')).toBeInTheDocument();
73
+ });
74
+
75
+ it('disables confirm button when no types selected', () => {
76
+ renderWithProviders(<ProposeEntitiesModal {...defaultProps} />);
77
+ const buttons = screen.getAllByRole('button');
78
+ const confirmButton = buttons.find(b => b.textContent?.includes('Detect Entity'));
79
+ expect(confirmButton).toBeDisabled();
80
+ });
81
+
82
+ it('enables confirm button when types are selected', () => {
83
+ renderWithProviders(<ProposeEntitiesModal {...defaultProps} />);
84
+ fireEvent.click(screen.getByText('Person'));
85
+
86
+ const buttons = screen.getAllByRole('button');
87
+ const confirmButton = buttons.find(b => b.textContent?.includes('Detect Entity'));
88
+ expect(confirmButton).not.toBeDisabled();
89
+ });
90
+
91
+ it('calls onConfirm with selected types', () => {
92
+ const onConfirm = vi.fn();
93
+ renderWithProviders(
94
+ <ProposeEntitiesModal {...defaultProps} onConfirm={onConfirm} />
95
+ );
96
+
97
+ fireEvent.click(screen.getByText('Person'));
98
+ fireEvent.click(screen.getByText('Location'));
99
+
100
+ const buttons = screen.getAllByRole('button');
101
+ const confirmButton = buttons.find(b => b.textContent?.includes('Detect Entity'));
102
+ fireEvent.click(confirmButton!);
103
+
104
+ expect(onConfirm).toHaveBeenCalledWith(['Person', 'Location']);
105
+ });
106
+
107
+ it('calls onCancel when cancel button is clicked', () => {
108
+ const onCancel = vi.fn();
109
+ renderWithProviders(
110
+ <ProposeEntitiesModal {...defaultProps} onCancel={onCancel} />
111
+ );
112
+ fireEvent.click(screen.getByText('Cancel'));
113
+ expect(onCancel).toHaveBeenCalled();
114
+ });
115
+
116
+ it('saves preferences to sessionStorage on confirm', () => {
117
+ renderWithProviders(<ProposeEntitiesModal {...defaultProps} />);
118
+
119
+ fireEvent.click(screen.getByText('Person'));
120
+
121
+ const buttons = screen.getAllByRole('button');
122
+ const confirmButton = buttons.find(b => b.textContent?.includes('Detect Entity'));
123
+ fireEvent.click(confirmButton!);
124
+
125
+ expect(sessionStorage.getItem('userPreferredEntityTypes')).toBe(
126
+ JSON.stringify(['Person'])
127
+ );
128
+ });
129
+ });
@@ -0,0 +1,180 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import React from 'react';
3
+ import { screen, fireEvent } from '@testing-library/react';
4
+ import { renderWithProviders } from '../../../test-utils';
5
+ import '@testing-library/jest-dom';
6
+ import { ResourceSearchModal } from '../ResourceSearchModal';
7
+
8
+ // Mock HeadlessUI to avoid jsdom OOM issues
9
+ vi.mock('@headlessui/react', () => ({
10
+ Dialog: ({ children, onClose, ...props }: any) => <div role="dialog" {...props}>{typeof children === 'function' ? children({ open: true }) : children}</div>,
11
+ DialogPanel: ({ children, ...props }: any) => <div {...props}>{children}</div>,
12
+ DialogTitle: ({ children, ...props }: any) => <h2 {...props}>{children}</h2>,
13
+ Transition: ({ show, children }: any) => show ? <>{children}</> : null,
14
+ TransitionChild: ({ children }: any) => <>{children}</>,
15
+ }));
16
+
17
+ // Mock api-hooks
18
+ const mockUseQuery = vi.fn(() => ({
19
+ data: null,
20
+ isFetching: false,
21
+ }));
22
+
23
+ vi.mock('../../../lib/api-hooks', () => ({
24
+ useResources: vi.fn(() => ({
25
+ search: {
26
+ useQuery: mockUseQuery,
27
+ },
28
+ })),
29
+ }));
30
+
31
+ // Mock search announcements
32
+ vi.mock('../../../hooks/useSearchAnnouncements', () => ({
33
+ useSearchAnnouncements: vi.fn(() => ({
34
+ announceSearchResults: vi.fn(),
35
+ announceSearching: vi.fn(),
36
+ announceNavigation: vi.fn(),
37
+ })),
38
+ }));
39
+
40
+ describe('ResourceSearchModal', () => {
41
+ const defaultProps = {
42
+ isOpen: true,
43
+ onClose: vi.fn(),
44
+ onSelect: vi.fn(),
45
+ };
46
+
47
+ beforeEach(() => {
48
+ vi.clearAllMocks();
49
+ mockUseQuery.mockReturnValue({ data: null, isFetching: false });
50
+ });
51
+
52
+ it('renders modal with title when open', () => {
53
+ renderWithProviders(<ResourceSearchModal {...defaultProps} />);
54
+ expect(screen.getByText('Search Resources')).toBeInTheDocument();
55
+ });
56
+
57
+ it('does not render when closed', () => {
58
+ renderWithProviders(
59
+ <ResourceSearchModal {...defaultProps} isOpen={false} />
60
+ );
61
+ expect(screen.queryByText('Search Resources')).not.toBeInTheDocument();
62
+ });
63
+
64
+ it('renders search input with placeholder', () => {
65
+ renderWithProviders(<ResourceSearchModal {...defaultProps} />);
66
+ expect(screen.getByPlaceholderText('Search for resources...')).toBeInTheDocument();
67
+ });
68
+
69
+ it('uses custom translations', () => {
70
+ renderWithProviders(
71
+ <ResourceSearchModal
72
+ {...defaultProps}
73
+ translations={{ title: 'Find Docs', placeholder: 'Type here...' }}
74
+ />
75
+ );
76
+ expect(screen.getByText('Find Docs')).toBeInTheDocument();
77
+ expect(screen.getByPlaceholderText('Type here...')).toBeInTheDocument();
78
+ });
79
+
80
+ it('shows loading state when fetching', () => {
81
+ mockUseQuery.mockReturnValue({ data: null, isFetching: true });
82
+ renderWithProviders(<ResourceSearchModal {...defaultProps} />);
83
+ expect(screen.getByText('Searching...')).toBeInTheDocument();
84
+ });
85
+
86
+ it('shows no results message when search has no matches', () => {
87
+ mockUseQuery.mockReturnValue({
88
+ data: { resources: [] },
89
+ isFetching: false,
90
+ });
91
+
92
+ renderWithProviders(
93
+ <ResourceSearchModal {...defaultProps} searchTerm="xyz" />
94
+ );
95
+ expect(screen.getByText('No documents found')).toBeInTheDocument();
96
+ });
97
+
98
+ it('renders search results', () => {
99
+ mockUseQuery.mockReturnValue({
100
+ data: {
101
+ resources: [
102
+ {
103
+ '@id': 'res-1',
104
+ name: 'Test Document',
105
+ content: 'Some content here',
106
+ representations: [{ mediaType: 'text/plain' }],
107
+ },
108
+ ],
109
+ },
110
+ isFetching: false,
111
+ });
112
+
113
+ renderWithProviders(
114
+ <ResourceSearchModal {...defaultProps} searchTerm="test" />
115
+ );
116
+ expect(screen.getByText('Test Document')).toBeInTheDocument();
117
+ });
118
+
119
+ it('calls onSelect and onClose when a result is clicked', () => {
120
+ const onSelect = vi.fn();
121
+ const onClose = vi.fn();
122
+
123
+ mockUseQuery.mockReturnValue({
124
+ data: {
125
+ resources: [
126
+ {
127
+ '@id': 'res-1',
128
+ name: 'Test Document',
129
+ content: 'Some content',
130
+ representations: [{ mediaType: 'text/plain' }],
131
+ },
132
+ ],
133
+ },
134
+ isFetching: false,
135
+ });
136
+
137
+ renderWithProviders(
138
+ <ResourceSearchModal
139
+ {...defaultProps}
140
+ searchTerm="test"
141
+ onSelect={onSelect}
142
+ onClose={onClose}
143
+ />
144
+ );
145
+
146
+ fireEvent.click(screen.getByText('Test Document'));
147
+ expect(onSelect).toHaveBeenCalledWith('res-1');
148
+ expect(onClose).toHaveBeenCalled();
149
+ });
150
+
151
+ it('shows media type for image results', () => {
152
+ mockUseQuery.mockReturnValue({
153
+ data: {
154
+ resources: [
155
+ {
156
+ '@id': 'res-img',
157
+ name: 'Photo',
158
+ content: 'image data',
159
+ representations: [{ mediaType: 'image/png' }],
160
+ },
161
+ ],
162
+ },
163
+ isFetching: false,
164
+ });
165
+
166
+ renderWithProviders(
167
+ <ResourceSearchModal {...defaultProps} searchTerm="photo" />
168
+ );
169
+ expect(screen.getByText('image/png')).toBeInTheDocument();
170
+ });
171
+
172
+ it('calls onClose when close button is clicked', () => {
173
+ const onClose = vi.fn();
174
+ renderWithProviders(
175
+ <ResourceSearchModal {...defaultProps} onClose={onClose} />
176
+ );
177
+ fireEvent.click(screen.getByLabelText('✕'));
178
+ expect(onClose).toHaveBeenCalled();
179
+ });
180
+ });
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import React from 'react';
3
+ import { screen, fireEvent } from '@testing-library/react';
4
+ import '@testing-library/jest-dom';
5
+ import { renderWithProviders, resetEventBusForTesting } from '../../../test-utils';
6
+ import { ObservableLink } from '../ObservableLink';
7
+
8
+ describe('ObservableLink', () => {
9
+ beforeEach(() => {
10
+ resetEventBusForTesting();
11
+ });
12
+
13
+ it('renders anchor with href', () => {
14
+ renderWithProviders(
15
+ <ObservableLink href="/discover">Discover</ObservableLink>
16
+ );
17
+
18
+ const link = screen.getByRole('link', { name: 'Discover' });
19
+ expect(link).toHaveAttribute('href', '/discover');
20
+ });
21
+
22
+ it('renders children', () => {
23
+ renderWithProviders(
24
+ <ObservableLink href="/test">
25
+ <span data-testid="child">Click me</span>
26
+ </ObservableLink>
27
+ );
28
+
29
+ expect(screen.getByTestId('child')).toBeInTheDocument();
30
+ expect(screen.getByText('Click me')).toBeInTheDocument();
31
+ });
32
+
33
+ it('emits browse:link-clicked with href and label on click', () => {
34
+ const handler = vi.fn();
35
+
36
+ const { eventBus } = renderWithProviders(
37
+ <ObservableLink href="/discover" label="Discover">
38
+ Discover Resources
39
+ </ObservableLink>,
40
+ { returnEventBus: true }
41
+ );
42
+
43
+ const subscription = eventBus!.get('browse:link-clicked').subscribe(handler);
44
+
45
+ const link = screen.getByRole('link');
46
+ fireEvent.click(link);
47
+
48
+ expect(handler).toHaveBeenCalledWith({
49
+ href: '/discover',
50
+ label: 'Discover',
51
+ });
52
+
53
+ subscription.unsubscribe();
54
+ });
55
+
56
+ it('calls original onClick handler if provided', () => {
57
+ const onClick = vi.fn();
58
+
59
+ renderWithProviders(
60
+ <ObservableLink href="/test" onClick={onClick}>
61
+ Click
62
+ </ObservableLink>
63
+ );
64
+
65
+ const link = screen.getByRole('link');
66
+ fireEvent.click(link);
67
+
68
+ expect(onClick).toHaveBeenCalled();
69
+ });
70
+
71
+ it('passes through additional anchor props', () => {
72
+ renderWithProviders(
73
+ <ObservableLink
74
+ href="/external"
75
+ className="custom-link"
76
+ target="_blank"
77
+ rel="noopener noreferrer"
78
+ data-testid="my-link"
79
+ >
80
+ External
81
+ </ObservableLink>
82
+ );
83
+
84
+ const link = screen.getByTestId('my-link');
85
+ expect(link).toHaveClass('custom-link');
86
+ expect(link).toHaveAttribute('target', '_blank');
87
+ expect(link).toHaveAttribute('rel', 'noopener noreferrer');
88
+ expect(link).toHaveAttribute('href', '/external');
89
+ });
90
+ });
@@ -0,0 +1,169 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import React from 'react';
3
+ import { screen, fireEvent } from '@testing-library/react';
4
+ import '@testing-library/jest-dom';
5
+ import { renderWithProviders, resetEventBusForTesting } from '../../../test-utils';
6
+ import { SimpleNavigation } from '../SimpleNavigation';
7
+ import type { SimpleNavigationItem } from '../SimpleNavigation';
8
+
9
+ const MockChevronLeft = (props: any) => <span data-testid="chevron" {...props} />;
10
+ const MockBars = (props: any) => <span data-testid="bars" {...props} />;
11
+
12
+ const MockLink = ({ href, children, ...props }: any) => (
13
+ <a href={href} {...props}>{children}</a>
14
+ );
15
+
16
+ const MockIcon1 = (props: any) => <span data-testid="icon-1" {...props} />;
17
+ const MockIcon2 = (props: any) => <span data-testid="icon-2" {...props} />;
18
+
19
+ const defaultItems: SimpleNavigationItem[] = [
20
+ { name: 'Dashboard', href: '/admin/dashboard', icon: MockIcon1 },
21
+ { name: 'Users', href: '/admin/users', icon: MockIcon2, description: 'Manage users' },
22
+ ];
23
+
24
+ const defaultProps = {
25
+ title: 'Administration',
26
+ items: defaultItems,
27
+ currentPath: '/admin/dashboard',
28
+ LinkComponent: MockLink,
29
+ isCollapsed: false,
30
+ icons: {
31
+ chevronLeft: MockChevronLeft,
32
+ bars: MockBars,
33
+ },
34
+ collapseSidebarLabel: 'Collapse sidebar',
35
+ expandSidebarLabel: 'Expand sidebar',
36
+ };
37
+
38
+ describe('SimpleNavigation', () => {
39
+ beforeEach(() => {
40
+ resetEventBusForTesting();
41
+ });
42
+
43
+ describe('title visibility', () => {
44
+ it('renders title when not collapsed', () => {
45
+ renderWithProviders(<SimpleNavigation {...defaultProps} />);
46
+
47
+ expect(screen.getByText('Administration')).toBeInTheDocument();
48
+ });
49
+
50
+ it('hides title when collapsed', () => {
51
+ renderWithProviders(
52
+ <SimpleNavigation {...defaultProps} isCollapsed={true} />
53
+ );
54
+
55
+ expect(screen.queryByText('Administration')).not.toBeInTheDocument();
56
+ });
57
+ });
58
+
59
+ describe('navigation items', () => {
60
+ it('renders navigation items', () => {
61
+ renderWithProviders(<SimpleNavigation {...defaultProps} />);
62
+
63
+ expect(screen.getByText('Dashboard')).toBeInTheDocument();
64
+ expect(screen.getByText('Users')).toBeInTheDocument();
65
+ });
66
+
67
+ it('marks active item with aria-current="page"', () => {
68
+ renderWithProviders(<SimpleNavigation {...defaultProps} />);
69
+
70
+ const dashboardLink = screen.getByText('Dashboard').closest('a');
71
+ expect(dashboardLink).toHaveAttribute('aria-current', 'page');
72
+
73
+ const usersLink = screen.getByText('Users').closest('a');
74
+ expect(usersLink).not.toHaveAttribute('aria-current');
75
+ });
76
+
77
+ it('shows item text when not collapsed', () => {
78
+ renderWithProviders(<SimpleNavigation {...defaultProps} />);
79
+
80
+ expect(screen.getByText('Dashboard')).toBeInTheDocument();
81
+ expect(screen.getByText('Users')).toBeInTheDocument();
82
+ });
83
+
84
+ it('shows only icons when collapsed', () => {
85
+ renderWithProviders(
86
+ <SimpleNavigation {...defaultProps} isCollapsed={true} />
87
+ );
88
+
89
+ // Text spans should not be present in collapsed mode
90
+ expect(screen.queryByText('Dashboard')).not.toBeInTheDocument();
91
+ expect(screen.queryByText('Users')).not.toBeInTheDocument();
92
+
93
+ // Icons should still be rendered
94
+ const icons = screen.getAllByTestId(/^icon-/);
95
+ expect(icons.length).toBe(2);
96
+ });
97
+ });
98
+
99
+ describe('sidebar toggle', () => {
100
+ it('emits browse:sidebar-toggle on collapse button click', () => {
101
+ const handler = vi.fn();
102
+
103
+ const { eventBus } = renderWithProviders(
104
+ <SimpleNavigation {...defaultProps} />,
105
+ { returnEventBus: true }
106
+ );
107
+
108
+ const subscription = eventBus!.get('browse:sidebar-toggle').subscribe(handler);
109
+
110
+ const collapseButton = screen.getByLabelText('Collapse sidebar');
111
+ fireEvent.click(collapseButton);
112
+
113
+ expect(handler).toHaveBeenCalledWith(undefined);
114
+
115
+ subscription.unsubscribe();
116
+ });
117
+
118
+ it('shows expand label when collapsed', () => {
119
+ renderWithProviders(
120
+ <SimpleNavigation {...defaultProps} isCollapsed={true} />
121
+ );
122
+
123
+ expect(screen.getByLabelText('Expand sidebar')).toBeInTheDocument();
124
+ });
125
+ });
126
+
127
+ describe('dropdown', () => {
128
+ it('opens dropdown when header button clicked if dropdownContent provided', () => {
129
+ const dropdownContent = (onClose: () => void) => (
130
+ <div data-testid="dropdown-content">
131
+ <button onClick={onClose}>Close</button>
132
+ </div>
133
+ );
134
+
135
+ renderWithProviders(
136
+ <SimpleNavigation {...defaultProps} dropdownContent={dropdownContent} />
137
+ );
138
+
139
+ // Dropdown should not be visible initially
140
+ expect(screen.queryByTestId('dropdown-content')).not.toBeInTheDocument();
141
+
142
+ // Click header button to open dropdown
143
+ const headerButton = screen.getByRole('button', { name: /Administration/i });
144
+ fireEvent.click(headerButton);
145
+
146
+ expect(screen.getByTestId('dropdown-content')).toBeInTheDocument();
147
+ });
148
+
149
+ it('closes dropdown on outside click', () => {
150
+ const dropdownContent = (onClose: () => void) => (
151
+ <div data-testid="dropdown-content">Dropdown</div>
152
+ );
153
+
154
+ renderWithProviders(
155
+ <SimpleNavigation {...defaultProps} dropdownContent={dropdownContent} />
156
+ );
157
+
158
+ // Open dropdown
159
+ const headerButton = screen.getByRole('button', { name: /Administration/i });
160
+ fireEvent.click(headerButton);
161
+ expect(screen.getByTestId('dropdown-content')).toBeInTheDocument();
162
+
163
+ // Click outside
164
+ fireEvent.mouseDown(document.body);
165
+
166
+ expect(screen.queryByTestId('dropdown-content')).not.toBeInTheDocument();
167
+ });
168
+ });
169
+ });