@semiont/react-ui 0.5.5 → 0.5.6
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.
- package/README.md +59 -55
- package/dist/{PdfAnnotationCanvas.client-CN3C3S55.js → PdfAnnotationCanvas.client-NIMALXNZ.js} +7 -27
- package/dist/PdfAnnotationCanvas.client-NIMALXNZ.js.map +1 -0
- package/dist/index.css +6 -0
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +243 -99
- package/dist/index.js +539 -405
- package/dist/index.js.map +1 -1
- package/package.json +16 -12
- package/src/components/Button/__tests__/Button.test.tsx +0 -2
- package/src/components/CodeMirrorRenderer.tsx +2 -0
- package/src/components/ErrorBoundary.tsx +0 -9
- package/src/components/ProtectedErrorBoundary.tsx +6 -2
- package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +0 -1
- package/src/components/__tests__/ErrorBoundary.test.tsx +20 -13
- package/src/components/__tests__/LiveRegion.hooks.test.tsx +1 -1
- package/src/components/__tests__/ProtectedErrorBoundary.test.tsx +2 -1
- package/src/components/__tests__/ResizeHandle.test.tsx +0 -1
- package/src/components/__tests__/SessionExpiryBanner.test.tsx +0 -1
- package/src/components/__tests__/StatusDisplay.test.tsx +0 -1
- package/src/components/__tests__/Toast.test.tsx +2 -3
- package/src/components/__tests__/Toolbar.test.tsx +0 -1
- package/src/components/annotation/annotations.css +14 -0
- package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +3 -5
- package/src/components/annotation-popups/__tests__/SharedPopupElements.test.tsx +0 -1
- package/src/components/branding/__tests__/SemiontBranding.test.tsx +1 -2
- package/src/components/layout/__tests__/LeftSidebar.test.tsx +5 -6
- package/src/components/layout/__tests__/PageLayout.test.tsx +1 -3
- package/src/components/layout/__tests__/SkipLinks.a11y.test.tsx +8 -8
- package/src/components/layout/__tests__/UnifiedHeader.test.tsx +12 -1
- package/src/components/modals/__tests__/KeyboardShortcutsHelpModal.test.tsx +0 -1
- package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +3 -4
- package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +0 -1
- package/src/components/modals/__tests__/SearchModal.basic.test.tsx +1 -1
- package/src/components/modals/__tests__/SearchModal.keyboard.test.tsx +0 -5
- package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +0 -1
- package/src/components/modals/__tests__/SearchModal.visual.test.tsx +2 -2
- package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +0 -1
- package/src/components/navigation/NavigationMenu.tsx +1 -1
- package/src/components/navigation/__tests__/Footer.a11y.test.tsx +4 -0
- package/src/components/navigation/__tests__/Footer.test.tsx +3 -6
- package/src/components/navigation/__tests__/NavigationMenu.a11y.test.tsx +1 -1
- package/src/components/navigation/__tests__/NavigationMenu.test.tsx +7 -9
- package/src/components/navigation/__tests__/ObservableLink.test.tsx +0 -1
- package/src/components/navigation/__tests__/SimpleNavigation.test.tsx +1 -2
- package/src/components/navigation/__tests__/SortableResourceTab.test.tsx +0 -1
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +6 -4
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +10 -19
- package/src/components/resource/__tests__/BrowseView.test.tsx +8 -6
- package/src/components/resource/__tests__/HistoryEvent.test.tsx +0 -4
- package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +4 -6
- package/src/components/resource/panels/ReferencesPanel.tsx +1 -1
- package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +3 -4
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +7 -6
- package/src/components/resource/panels/__tests__/AssistSection.test.tsx +14 -10
- package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +0 -1
- package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +30 -17
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +6 -5
- package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +4 -5
- package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +19 -13
- package/src/components/resource/panels/__tests__/JsonLdPanel.test.tsx +1 -3
- package/src/components/resource/panels/__tests__/PanelHeader.test.tsx +0 -1
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +5 -5
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +40 -7
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +3 -3
- package/src/components/resource/panels/__tests__/StatisticsPanel.test.tsx +30 -32
- package/src/components/resource/panels/__tests__/TagEntry.test.tsx +5 -5
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +6 -5
- package/src/components/settings/__tests__/SettingsPanel.test.tsx +0 -1
- package/src/components/viewers/__tests__/ImageViewer.test.tsx +0 -1
- package/src/features/auth/__tests__/SignInForm.a11y.test.tsx +2 -0
- package/src/features/auth/__tests__/SignUpForm.a11y.test.tsx +11 -12
- package/src/features/auth/__tests__/SignUpForm.test.tsx +3 -3
- package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -0
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +2 -1
- package/src/features/resource-discovery/__tests__/ResourceCard.test.tsx +0 -1
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +33 -35
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +12 -11
- package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +204 -11
- package/src/features/resource-discovery/state/discover-state-unit.ts +70 -11
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +2 -2
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +3 -2
- package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +0 -1
- package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +5 -2
- package/src/integrations/__tests__/css-modules-helper.test.tsx +2 -3
- package/src/integrations/__tests__/styled-components-theme.test.ts +1 -3
- package/dist/PdfAnnotationCanvas.client-CN3C3S55.js.map +0 -1
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
2
|
import type { MockedFunction } from 'vitest';
|
|
3
3
|
import React from 'react';
|
|
4
|
-
import { render, screen,
|
|
4
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
5
5
|
import userEvent from '@testing-library/user-event';
|
|
6
6
|
import '@testing-library/jest-dom';
|
|
7
7
|
import { of } from 'rxjs';
|
|
8
8
|
import { CacheObservable } from '@semiont/sdk';
|
|
9
9
|
import { TaggingPanel } from '../TaggingPanel';
|
|
10
|
-
import type {
|
|
10
|
+
import type { EventBus, TagSchema } from '@semiont/core';
|
|
11
11
|
import { createTestSemiontWrapper } from '../../../../test-utils';
|
|
12
12
|
|
|
13
|
-
import type { Annotation } from '@semiont/core';
|
|
13
|
+
import type { Annotation, AnnotationId } from '@semiont/core';
|
|
14
14
|
|
|
15
15
|
// Composition-based event tracker
|
|
16
16
|
interface TrackedEvent {
|
|
@@ -124,7 +124,7 @@ vi.mock('@semiont/core', async () => {
|
|
|
124
124
|
|
|
125
125
|
// Mock TagEntry component to simplify testing
|
|
126
126
|
vi.mock('../TagEntry', () => ({
|
|
127
|
-
TagEntry: ({ tag
|
|
127
|
+
TagEntry: ({ tag }: any) => (
|
|
128
128
|
<div data-testid={`tag-${tag.id}`}>
|
|
129
129
|
<div>{tag.id}</div>
|
|
130
130
|
</div>
|
|
@@ -138,10 +138,11 @@ const mockGetTargetSelector = getTargetSelector as MockedFunction<typeof getTarg
|
|
|
138
138
|
// Test data fixtures
|
|
139
139
|
const createMockTag = (id: string, start: number, end: number, tagName: string = 'Issue'): Annotation => ({
|
|
140
140
|
'@context': 'http://www.w3.org/ns/anno.jsonld',
|
|
141
|
-
id,
|
|
141
|
+
id: id as AnnotationId,
|
|
142
142
|
type: 'Annotation',
|
|
143
143
|
motivation: 'tagging',
|
|
144
144
|
creator: {
|
|
145
|
+
'@type': 'Person',
|
|
145
146
|
name: `user${id}@example.com`,
|
|
146
147
|
},
|
|
147
148
|
created: `2024-01-0${id.slice(-1)}T10:00:00Z`,
|
|
@@ -45,6 +45,8 @@ const mockTranslations = {
|
|
|
45
45
|
errorEmailRequired: 'Email is required',
|
|
46
46
|
errorPasswordRequired: 'Password is required',
|
|
47
47
|
errorBackendUrlRequired: 'Backend URL is required',
|
|
48
|
+
errorBackendUrlInvalid: 'Backend URL is not valid',
|
|
49
|
+
errorBackendUrlUnreachable: 'Backend URL is unreachable',
|
|
48
50
|
tagline: 'make meaning',
|
|
49
51
|
};
|
|
50
52
|
|
|
@@ -54,7 +54,7 @@ describe('SignUpForm - Accessibility', () => {
|
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
it('should have no accessibility violations during loading state', async () => {
|
|
57
|
-
const onSignUp = vi.fn(() => new Promise(() => {})); // Never resolves
|
|
57
|
+
const onSignUp = vi.fn<() => Promise<void>>(() => new Promise<void>(() => {})); // Never resolves
|
|
58
58
|
|
|
59
59
|
const { container } = render(
|
|
60
60
|
<SignUpForm
|
|
@@ -138,7 +138,7 @@ describe('SignUpForm - Accessibility', () => {
|
|
|
138
138
|
});
|
|
139
139
|
|
|
140
140
|
it('should disable button during loading state', async () => {
|
|
141
|
-
const onSignUp = vi.fn(() => new Promise(() => {}));
|
|
141
|
+
const onSignUp = vi.fn<() => Promise<void>>(() => new Promise<void>(() => {}));
|
|
142
142
|
|
|
143
143
|
render(
|
|
144
144
|
<SignUpForm
|
|
@@ -197,13 +197,12 @@ describe('SignUpForm - Accessibility', () => {
|
|
|
197
197
|
it('should have Google icon inside button', () => {
|
|
198
198
|
const onSignUp = vi.fn();
|
|
199
199
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
);
|
|
200
|
+
render(
|
|
201
|
+
<SignUpForm
|
|
202
|
+
onSignUp={onSignUp}
|
|
203
|
+
Link={MockLink}
|
|
204
|
+
translations={mockTranslations} />
|
|
205
|
+
);
|
|
207
206
|
|
|
208
207
|
const button = screen.getByRole('button');
|
|
209
208
|
const svg = button.querySelector('svg');
|
|
@@ -214,7 +213,7 @@ describe('SignUpForm - Accessibility', () => {
|
|
|
214
213
|
});
|
|
215
214
|
|
|
216
215
|
it('should show loading spinner during sign-up', async () => {
|
|
217
|
-
const onSignUp = vi.fn(() => new Promise(() => {}));
|
|
216
|
+
const onSignUp = vi.fn<() => Promise<void>>(() => new Promise<void>(() => {}));
|
|
218
217
|
|
|
219
218
|
const { container } = render(
|
|
220
219
|
<SignUpForm
|
|
@@ -349,7 +348,7 @@ describe('SignUpForm - Accessibility', () => {
|
|
|
349
348
|
|
|
350
349
|
describe('Loading State Accessibility', () => {
|
|
351
350
|
it('should announce loading state to screen readers', async () => {
|
|
352
|
-
const onSignUp = vi.fn(() => new Promise(() => {}));
|
|
351
|
+
const onSignUp = vi.fn<() => Promise<void>>(() => new Promise<void>(() => {}));
|
|
353
352
|
|
|
354
353
|
render(
|
|
355
354
|
<SignUpForm
|
|
@@ -368,7 +367,7 @@ describe('SignUpForm - Accessibility', () => {
|
|
|
368
367
|
});
|
|
369
368
|
|
|
370
369
|
it('should maintain button focus during loading', async () => {
|
|
371
|
-
const onSignUp = vi.fn(() => new Promise(() => {}));
|
|
370
|
+
const onSignUp = vi.fn<() => Promise<void>>(() => new Promise<void>(() => {}));
|
|
372
371
|
|
|
373
372
|
render(
|
|
374
373
|
<SignUpForm
|
|
@@ -53,7 +53,7 @@ describe('SignUpForm', () => {
|
|
|
53
53
|
|
|
54
54
|
describe('Sign-Up Interaction', () => {
|
|
55
55
|
it('calls onSignUp when button is clicked', async () => {
|
|
56
|
-
const onSignUp = vi.fn<
|
|
56
|
+
const onSignUp = vi.fn<() => Promise<void>>().mockResolvedValue(undefined);
|
|
57
57
|
render(<SignUpForm onSignUp={onSignUp} Link={MockLink} translations={mockTranslations} />);
|
|
58
58
|
|
|
59
59
|
const button = screen.getByRole('button', { name: /Continue with Google/i });
|
|
@@ -63,7 +63,7 @@ describe('SignUpForm', () => {
|
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
it('shows loading state while signing up', async () => {
|
|
66
|
-
const onSignUp = vi.fn<
|
|
66
|
+
const onSignUp = vi.fn<() => Promise<void>>(() => new Promise<void>((resolve) => setTimeout(resolve, 100)));
|
|
67
67
|
render(<SignUpForm onSignUp={onSignUp} Link={MockLink} translations={mockTranslations} />);
|
|
68
68
|
|
|
69
69
|
const button = screen.getByRole('button');
|
|
@@ -83,7 +83,7 @@ describe('SignUpForm', () => {
|
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
it('disables button during loading', async () => {
|
|
86
|
-
const onSignUp = vi.fn<
|
|
86
|
+
const onSignUp = vi.fn<() => Promise<void>>(() => new Promise<void>((resolve) => setTimeout(resolve, 100)));
|
|
87
87
|
render(<SignUpForm onSignUp={onSignUp} Link={MockLink} translations={mockTranslations} />);
|
|
88
88
|
|
|
89
89
|
const button = screen.getByRole('button');
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from '@heroicons/react/24/outline';
|
|
14
14
|
import { COMMON_PANELS, type ToolbarPanelType } from '../../../state/shell-state-unit';
|
|
15
15
|
import type { TagSchema } from '@semiont/sdk';
|
|
16
|
+
export type { TagSchema };
|
|
16
17
|
|
|
17
18
|
export interface TagSchemasPageProps {
|
|
18
19
|
// Data props
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { describe, it, expect, vi } from 'vitest';
|
|
9
9
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
10
10
|
import { ResourceComposePage } from '../components/ResourceComposePage';
|
|
11
|
-
import type { ResourceComposePageProps
|
|
11
|
+
import type { ResourceComposePageProps } from '../components/ResourceComposePage';
|
|
12
12
|
import { createTestSemiontWrapper } from '../../../test-utils';
|
|
13
13
|
|
|
14
14
|
// Mock CodeMirrorRenderer to avoid CodeMirror dependencies
|
|
@@ -63,6 +63,7 @@ const createMockProps = (overrides?: Partial<ResourceComposePageProps>): Resourc
|
|
|
63
63
|
initialLocale: 'en',
|
|
64
64
|
theme: 'light',
|
|
65
65
|
showLineNumbers: false,
|
|
66
|
+
hoverDelayMs: 0,
|
|
66
67
|
activePanel: null,
|
|
67
68
|
onSaveResource: vi.fn().mockResolvedValue(undefined),
|
|
68
69
|
onCancel: vi.fn(),
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
import { describe, it, expect, vi } from 'vitest';
|
|
8
8
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
9
9
|
import { ResourceCard } from '../components/ResourceCard';
|
|
10
|
-
import type { ResourceCardProps } from '../components/ResourceCard';
|
|
11
10
|
|
|
12
11
|
const createMockResource = (overrides?: any) => ({
|
|
13
12
|
'@context': 'https://www.w3.org/ns/anno.jsonld',
|
|
@@ -10,10 +10,12 @@ import { render, screen, fireEvent } from '@testing-library/react';
|
|
|
10
10
|
import { ResourceDiscoveryPage } from '../components/ResourceDiscoveryPage';
|
|
11
11
|
import type { ResourceDiscoveryPageProps } from '../components/ResourceDiscoveryPage';
|
|
12
12
|
import { createTestSemiontWrapper } from '../../../test-utils';
|
|
13
|
+
import { resourceId } from '@semiont/core';
|
|
14
|
+
import type { ResourceDescriptor } from '@semiont/core';
|
|
13
15
|
|
|
14
|
-
const createMockResource = (id: string, name: string, entityTypes: string[] = []) => ({
|
|
16
|
+
const createMockResource = (id: string, name: string, entityTypes: string[] = []): ResourceDescriptor => ({
|
|
15
17
|
'@context': 'https://www.w3.org/ns/anno.jsonld',
|
|
16
|
-
'@id': id,
|
|
18
|
+
'@id': resourceId(id),
|
|
17
19
|
'@type': 'schema:DigitalDocument',
|
|
18
20
|
name,
|
|
19
21
|
description: `Description for ${name}`,
|
|
@@ -31,6 +33,8 @@ const createMockProps = (overrides?: Partial<ResourceDiscoveryPageProps>): Resou
|
|
|
31
33
|
isSearching: false,
|
|
32
34
|
searchQuery: '',
|
|
33
35
|
onSearchQueryChange: vi.fn(),
|
|
36
|
+
selectedEntityType: '',
|
|
37
|
+
onSelectedEntityTypeChange: vi.fn(),
|
|
34
38
|
theme: 'light',
|
|
35
39
|
showLineNumbers: false,
|
|
36
40
|
activePanel: null,
|
|
@@ -254,65 +258,59 @@ describe('ResourceDiscoveryPage', () => {
|
|
|
254
258
|
expect(screen.getByRole('button', { name: 'Report' })).toBeInTheDocument();
|
|
255
259
|
});
|
|
256
260
|
|
|
257
|
-
it('
|
|
261
|
+
it('calls onSelectedEntityTypeChange when a filter chip is clicked', () => {
|
|
262
|
+
const onSelectedEntityTypeChange = vi.fn();
|
|
258
263
|
const props = createMockProps({
|
|
259
|
-
recentDocuments: [
|
|
260
|
-
createMockResource('1', 'Doc 1', ['Document']),
|
|
261
|
-
createMockResource('2', 'Doc 2', ['Article']),
|
|
262
|
-
createMockResource('3', 'Doc 3', ['Document']),
|
|
263
|
-
],
|
|
264
264
|
entityTypes: ['Document', 'Article'],
|
|
265
|
+
onSelectedEntityTypeChange,
|
|
265
266
|
});
|
|
266
267
|
renderWithProviders(<ResourceDiscoveryPage {...props} />);
|
|
267
268
|
|
|
268
|
-
|
|
269
|
-
expect(
|
|
270
|
-
expect(screen.getByText('Doc 2')).toBeInTheDocument();
|
|
271
|
-
expect(screen.getByText('Doc 3')).toBeInTheDocument();
|
|
272
|
-
|
|
273
|
-
// Filter by Document
|
|
274
|
-
const documentButton = screen.getByRole('button', { name: 'Document' });
|
|
275
|
-
fireEvent.click(documentButton);
|
|
269
|
+
fireEvent.click(screen.getByRole('button', { name: 'Document' }));
|
|
270
|
+
expect(onSelectedEntityTypeChange).toHaveBeenCalledWith('Document');
|
|
276
271
|
|
|
277
|
-
|
|
278
|
-
expect(
|
|
279
|
-
expect(screen.getByText('Doc 3')).toBeInTheDocument();
|
|
272
|
+
fireEvent.click(screen.getByRole('button', { name: 'Article' }));
|
|
273
|
+
expect(onSelectedEntityTypeChange).toHaveBeenCalledWith('Article');
|
|
280
274
|
});
|
|
281
275
|
|
|
282
|
-
it('shows filtered heading when
|
|
276
|
+
it('shows filtered heading when selectedEntityType prop is set', () => {
|
|
283
277
|
const props = createMockProps({
|
|
284
278
|
recentDocuments: [createMockResource('1', 'Doc 1', ['Document'])],
|
|
285
279
|
entityTypes: ['Document'],
|
|
280
|
+
selectedEntityType: 'Document',
|
|
286
281
|
});
|
|
287
282
|
renderWithProviders(<ResourceDiscoveryPage {...props} />);
|
|
288
283
|
|
|
289
|
-
const documentButton = screen.getByRole('button', { name: 'Document' });
|
|
290
|
-
fireEvent.click(documentButton);
|
|
291
|
-
|
|
292
284
|
expect(screen.getByText('Documents tagged with Document')).toBeInTheDocument();
|
|
293
285
|
});
|
|
294
286
|
|
|
295
|
-
it('
|
|
287
|
+
it('calls onSelectedEntityTypeChange with empty string when "All" is clicked', () => {
|
|
288
|
+
const onSelectedEntityTypeChange = vi.fn();
|
|
289
|
+
const props = createMockProps({
|
|
290
|
+
entityTypes: ['Document', 'Article'],
|
|
291
|
+
selectedEntityType: 'Document',
|
|
292
|
+
onSelectedEntityTypeChange,
|
|
293
|
+
});
|
|
294
|
+
renderWithProviders(<ResourceDiscoveryPage {...props} />);
|
|
295
|
+
|
|
296
|
+
fireEvent.click(screen.getByRole('button', { name: 'All' }));
|
|
297
|
+
expect(onSelectedEntityTypeChange).toHaveBeenCalledWith('');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('renders the recentDocuments prop as-is without applying any post-filter', () => {
|
|
301
|
+
// The component is now controlled — backend filtering means
|
|
302
|
+
// `recentDocuments` already contains only the resources matching the
|
|
303
|
+
// active `selectedEntityType`. The component must not re-filter.
|
|
296
304
|
const props = createMockProps({
|
|
297
305
|
recentDocuments: [
|
|
298
306
|
createMockResource('1', 'Doc 1', ['Document']),
|
|
299
307
|
createMockResource('2', 'Doc 2', ['Article']),
|
|
300
308
|
],
|
|
301
309
|
entityTypes: ['Document', 'Article'],
|
|
310
|
+
selectedEntityType: 'Document',
|
|
302
311
|
});
|
|
303
312
|
renderWithProviders(<ResourceDiscoveryPage {...props} />);
|
|
304
313
|
|
|
305
|
-
// Filter by Document
|
|
306
|
-
const documentButton = screen.getByRole('button', { name: 'Document' });
|
|
307
|
-
fireEvent.click(documentButton);
|
|
308
|
-
|
|
309
|
-
expect(screen.getByText('Doc 1')).toBeInTheDocument();
|
|
310
|
-
expect(screen.queryByText('Doc 2')).not.toBeInTheDocument();
|
|
311
|
-
|
|
312
|
-
// Click All
|
|
313
|
-
const allButton = screen.getByRole('button', { name: 'All' });
|
|
314
|
-
fireEvent.click(allButton);
|
|
315
|
-
|
|
316
314
|
expect(screen.getByText('Doc 1')).toBeInTheDocument();
|
|
317
315
|
expect(screen.getByText('Doc 2')).toBeInTheDocument();
|
|
318
316
|
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* All dependencies passed as props - no Next.js hooks!
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import React, {
|
|
8
|
+
import React, { useCallback, useRef } from 'react';
|
|
9
9
|
import { getResourceId } from '@semiont/core';
|
|
10
10
|
import { COMMON_PANELS, type ToolbarPanelType } from '../../../state/shell-state-unit';
|
|
11
11
|
import { useRovingTabIndex } from '../../../hooks/useRovingTabIndex';
|
|
@@ -26,6 +26,11 @@ export interface ResourceDiscoveryPageProps {
|
|
|
26
26
|
searchQuery: string;
|
|
27
27
|
onSearchQueryChange: (query: string) => void;
|
|
28
28
|
|
|
29
|
+
// Controlled entity-type filter — owned by the state unit so filtering
|
|
30
|
+
// pushes to the backend rather than running as a post-fetch array filter.
|
|
31
|
+
selectedEntityType: string;
|
|
32
|
+
onSelectedEntityTypeChange: (entityType: string) => void;
|
|
33
|
+
|
|
29
34
|
// UI state props
|
|
30
35
|
theme: 'light' | 'dark';
|
|
31
36
|
showLineNumbers: boolean;
|
|
@@ -67,6 +72,8 @@ export function ResourceDiscoveryPage({
|
|
|
67
72
|
isSearching,
|
|
68
73
|
searchQuery,
|
|
69
74
|
onSearchQueryChange,
|
|
75
|
+
selectedEntityType,
|
|
76
|
+
onSelectedEntityTypeChange,
|
|
70
77
|
theme,
|
|
71
78
|
showLineNumbers,
|
|
72
79
|
activePanel,
|
|
@@ -75,17 +82,11 @@ export function ResourceDiscoveryPage({
|
|
|
75
82
|
translations: t,
|
|
76
83
|
ToolbarPanels,
|
|
77
84
|
}: ResourceDiscoveryPageProps) {
|
|
78
|
-
const [selectedEntityType, setSelectedEntityType] = useState<string>('');
|
|
79
|
-
|
|
80
85
|
const hasSearchQuery = searchQuery.trim() !== '';
|
|
81
86
|
|
|
82
87
|
// When searching, render search results; otherwise render recent.
|
|
83
|
-
|
|
84
|
-
const filteredResources =
|
|
85
|
-
? baseDocuments
|
|
86
|
-
: baseDocuments.filter((resource: ResourceDescriptor) =>
|
|
87
|
-
resource.entityTypes && resource.entityTypes.includes(selectedEntityType)
|
|
88
|
-
);
|
|
88
|
+
// Both already arrive entity-type-filtered from the backend — no post-filter here.
|
|
89
|
+
const filteredResources = hasSearchQuery ? searchDocuments : recentDocuments;
|
|
89
90
|
|
|
90
91
|
// Roving tabindex for entity type filters
|
|
91
92
|
const entityFilterRoving = useRovingTabIndex<HTMLDivElement>(
|
|
@@ -105,8 +106,8 @@ export function ResourceDiscoveryPage({
|
|
|
105
106
|
|
|
106
107
|
// Memoized callbacks
|
|
107
108
|
const handleEntityTypeFilter = useCallback((entityType: string) => {
|
|
108
|
-
|
|
109
|
-
}, []);
|
|
109
|
+
onSelectedEntityTypeChange(entityType);
|
|
110
|
+
}, [onSelectedEntityTypeChange]);
|
|
110
111
|
|
|
111
112
|
const openResource = useCallback((resource: ResourceDescriptor) => {
|
|
112
113
|
const resourceId = getResourceId(resource);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
2
|
import { BehaviorSubject, firstValueFrom } from 'rxjs';
|
|
3
|
-
import { filter } from 'rxjs/operators';
|
|
3
|
+
import { filter, skip, take, toArray } from 'rxjs/operators';
|
|
4
4
|
import type { SemiontClient } from '@semiont/sdk';
|
|
5
5
|
import type { ShellStateUnit } from '../../../../state/shell-state-unit';
|
|
6
6
|
import { createDiscoverStateUnit } from '../discover-state-unit';
|
|
@@ -9,23 +9,43 @@ function mockBrowse(): ShellStateUnit {
|
|
|
9
9
|
return { dispose: vi.fn() } as unknown as ShellStateUnit;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
interface BrowseFilters {
|
|
13
|
+
limit?: number;
|
|
14
|
+
archived?: boolean;
|
|
15
|
+
search?: string;
|
|
16
|
+
entityType?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
function mockClient(overrides: {
|
|
13
20
|
resources$?: BehaviorSubject<unknown[] | undefined>;
|
|
14
21
|
entityTypes$?: BehaviorSubject<string[] | undefined>;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
22
|
+
resourcesFn?: (filters: BrowseFilters) => BehaviorSubject<unknown[] | undefined>;
|
|
23
|
+
} = {}): { client: SemiontClient; resourceCalls: BrowseFilters[] } {
|
|
24
|
+
const resourceCalls: BrowseFilters[] = [];
|
|
25
|
+
const defaultResources$ =
|
|
26
|
+
overrides.resources$ ?? new BehaviorSubject<unknown[] | undefined>([{ '@id': 'r1' }]);
|
|
27
|
+
const entityTypes$ =
|
|
28
|
+
overrides.entityTypes$ ?? new BehaviorSubject<string[] | undefined>(['Person']);
|
|
29
|
+
|
|
30
|
+
const resourcesFn = overrides.resourcesFn ?? (() => defaultResources$);
|
|
31
|
+
|
|
32
|
+
const client = {
|
|
19
33
|
browse: {
|
|
20
|
-
resources: () =>
|
|
34
|
+
resources: (filters: BrowseFilters = {}) => {
|
|
35
|
+
resourceCalls.push(filters);
|
|
36
|
+
return resourcesFn(filters).asObservable();
|
|
37
|
+
},
|
|
21
38
|
entityTypes: () => entityTypes$.asObservable(),
|
|
22
39
|
},
|
|
23
40
|
} as unknown as SemiontClient;
|
|
41
|
+
|
|
42
|
+
return { client, resourceCalls };
|
|
24
43
|
}
|
|
25
44
|
|
|
26
45
|
describe('createDiscoverStateUnit', () => {
|
|
27
46
|
it('exposes recent resources from browse namespace', async () => {
|
|
28
|
-
const
|
|
47
|
+
const { client } = mockClient();
|
|
48
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
29
49
|
|
|
30
50
|
const recent = await firstValueFrom(stateUnit.recentResources$);
|
|
31
51
|
expect(recent).toEqual([{ '@id': 'r1' }]);
|
|
@@ -34,7 +54,8 @@ describe('createDiscoverStateUnit', () => {
|
|
|
34
54
|
});
|
|
35
55
|
|
|
36
56
|
it('exposes entity types from browse namespace', async () => {
|
|
37
|
-
const
|
|
57
|
+
const { client } = mockClient();
|
|
58
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
38
59
|
|
|
39
60
|
const types = await firstValueFrom(stateUnit.entityTypes$);
|
|
40
61
|
expect(types).toEqual(['Person']);
|
|
@@ -42,9 +63,21 @@ describe('createDiscoverStateUnit', () => {
|
|
|
42
63
|
stateUnit.dispose();
|
|
43
64
|
});
|
|
44
65
|
|
|
66
|
+
it('falls back to [] when entityTypes() emits undefined', async () => {
|
|
67
|
+
const entityTypes$ = new BehaviorSubject<string[] | undefined>(undefined);
|
|
68
|
+
const { client } = mockClient({ entityTypes$ });
|
|
69
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
70
|
+
|
|
71
|
+
const types = await firstValueFrom(stateUnit.entityTypes$);
|
|
72
|
+
expect(types).toEqual([]);
|
|
73
|
+
|
|
74
|
+
stateUnit.dispose();
|
|
75
|
+
});
|
|
76
|
+
|
|
45
77
|
it('reports loading when resources are undefined', async () => {
|
|
46
78
|
const resources$ = new BehaviorSubject<unknown[] | undefined>(undefined);
|
|
47
|
-
const
|
|
79
|
+
const { client } = mockClient({ resources$ });
|
|
80
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
48
81
|
|
|
49
82
|
const loading = await firstValueFrom(stateUnit.isLoadingRecent$);
|
|
50
83
|
expect(loading).toBe(true);
|
|
@@ -57,7 +90,8 @@ describe('createDiscoverStateUnit', () => {
|
|
|
57
90
|
});
|
|
58
91
|
|
|
59
92
|
it('exposes a search pipeline', () => {
|
|
60
|
-
const
|
|
93
|
+
const { client } = mockClient();
|
|
94
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
61
95
|
|
|
62
96
|
expect(stateUnit.search).toBeDefined();
|
|
63
97
|
expect(typeof stateUnit.search.setQuery).toBe('function');
|
|
@@ -68,9 +102,168 @@ describe('createDiscoverStateUnit', () => {
|
|
|
68
102
|
|
|
69
103
|
it('disposes browse and search on dispose', () => {
|
|
70
104
|
const browse = mockBrowse();
|
|
71
|
-
const
|
|
105
|
+
const { client } = mockClient();
|
|
106
|
+
const stateUnit = createDiscoverStateUnit(client, browse);
|
|
72
107
|
stateUnit.dispose();
|
|
73
108
|
|
|
74
109
|
expect(browse.dispose).toHaveBeenCalled();
|
|
75
110
|
});
|
|
111
|
+
|
|
112
|
+
it('initial selectedEntityType$ is empty and recent fetch carries no entityType', async () => {
|
|
113
|
+
const { client, resourceCalls } = mockClient();
|
|
114
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
115
|
+
|
|
116
|
+
await firstValueFrom(stateUnit.recentResources$);
|
|
117
|
+
|
|
118
|
+
expect(resourceCalls).toHaveLength(1);
|
|
119
|
+
expect(resourceCalls[0]).toEqual({ limit: 10, archived: false });
|
|
120
|
+
expect(await firstValueFrom(stateUnit.selectedEntityType$)).toBe('');
|
|
121
|
+
|
|
122
|
+
stateUnit.dispose();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('setSelectedEntityType drives a refetch with the entityType filter', async () => {
|
|
126
|
+
const { client, resourceCalls } = mockClient();
|
|
127
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
128
|
+
|
|
129
|
+
// Prime the first subscription so the switchMap is live.
|
|
130
|
+
const sub = stateUnit.recentResources$.subscribe();
|
|
131
|
+
|
|
132
|
+
stateUnit.setSelectedEntityType('Person');
|
|
133
|
+
|
|
134
|
+
expect(await firstValueFrom(stateUnit.selectedEntityType$)).toBe('Person');
|
|
135
|
+
// Two calls expected: initial '' then 'Person'.
|
|
136
|
+
expect(resourceCalls.length).toBeGreaterThanOrEqual(2);
|
|
137
|
+
expect(resourceCalls.at(-1)).toEqual({ limit: 10, archived: false, entityType: 'Person' });
|
|
138
|
+
|
|
139
|
+
sub.unsubscribe();
|
|
140
|
+
stateUnit.dispose();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('search with an empty query yields no results without hitting the wire', async () => {
|
|
144
|
+
vi.useFakeTimers();
|
|
145
|
+
try {
|
|
146
|
+
const { client, resourceCalls } = mockClient();
|
|
147
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
148
|
+
|
|
149
|
+
const collected: Array<{ results: unknown[]; isSearching: boolean }> = [];
|
|
150
|
+
const sub = stateUnit.search.state$.subscribe((s) => collected.push(s));
|
|
151
|
+
|
|
152
|
+
await vi.advanceTimersByTimeAsync(300);
|
|
153
|
+
|
|
154
|
+
expect(collected.at(-1)).toEqual({ results: [], isSearching: false });
|
|
155
|
+
const searchCalls = resourceCalls.filter((c) => c.search !== undefined);
|
|
156
|
+
expect(searchCalls).toHaveLength(0);
|
|
157
|
+
|
|
158
|
+
sub.unsubscribe();
|
|
159
|
+
stateUnit.dispose();
|
|
160
|
+
} finally {
|
|
161
|
+
vi.useRealTimers();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('search with a non-empty query and selected entityType pushes both into the filter', async () => {
|
|
166
|
+
vi.useFakeTimers();
|
|
167
|
+
try {
|
|
168
|
+
const results$ = new BehaviorSubject<unknown[] | undefined>([{ '@id': 'hit' }]);
|
|
169
|
+
const { client, resourceCalls } = mockClient({
|
|
170
|
+
resourcesFn: (filters) => (filters.search ? results$ : new BehaviorSubject<unknown[] | undefined>([])),
|
|
171
|
+
});
|
|
172
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
173
|
+
|
|
174
|
+
const sub = stateUnit.search.state$.subscribe();
|
|
175
|
+
stateUnit.setSelectedEntityType('Person');
|
|
176
|
+
stateUnit.search.setQuery('lincoln');
|
|
177
|
+
|
|
178
|
+
await vi.advanceTimersByTimeAsync(300);
|
|
179
|
+
|
|
180
|
+
const searchCalls = resourceCalls.filter((c) => c.search !== undefined);
|
|
181
|
+
expect(searchCalls.length).toBeGreaterThanOrEqual(1);
|
|
182
|
+
expect(searchCalls.at(-1)).toEqual({
|
|
183
|
+
search: 'lincoln',
|
|
184
|
+
limit: 20,
|
|
185
|
+
entityType: 'Person',
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
sub.unsubscribe();
|
|
189
|
+
stateUnit.dispose();
|
|
190
|
+
} finally {
|
|
191
|
+
vi.useRealTimers();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('search results flow through state$ once the debounced query fetches', async () => {
|
|
196
|
+
vi.useFakeTimers();
|
|
197
|
+
try {
|
|
198
|
+
const results$ = new BehaviorSubject<unknown[] | undefined>([{ '@id': 'hit' }]);
|
|
199
|
+
const { client } = mockClient({
|
|
200
|
+
resourcesFn: () => results$,
|
|
201
|
+
});
|
|
202
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
203
|
+
|
|
204
|
+
const collected: Array<{ results: unknown[]; isSearching: boolean }> = [];
|
|
205
|
+
const sub = stateUnit.search.state$.subscribe((s) => collected.push(s));
|
|
206
|
+
|
|
207
|
+
stateUnit.search.setQuery('lincoln');
|
|
208
|
+
await vi.advanceTimersByTimeAsync(300);
|
|
209
|
+
|
|
210
|
+
expect(collected.at(-1)).toEqual({ results: [{ '@id': 'hit' }], isSearching: false });
|
|
211
|
+
|
|
212
|
+
sub.unsubscribe();
|
|
213
|
+
stateUnit.dispose();
|
|
214
|
+
} finally {
|
|
215
|
+
vi.useRealTimers();
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('search reports isSearching while the fetch is in flight', async () => {
|
|
220
|
+
vi.useFakeTimers();
|
|
221
|
+
try {
|
|
222
|
+
const inflight$ = new BehaviorSubject<unknown[] | undefined>(undefined);
|
|
223
|
+
const { client } = mockClient({
|
|
224
|
+
resourcesFn: () => inflight$,
|
|
225
|
+
});
|
|
226
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
227
|
+
|
|
228
|
+
const collected: Array<{ results: unknown[]; isSearching: boolean }> = [];
|
|
229
|
+
const sub = stateUnit.search.state$.subscribe((s) => collected.push(s));
|
|
230
|
+
|
|
231
|
+
stateUnit.search.setQuery('lincoln');
|
|
232
|
+
await vi.advanceTimersByTimeAsync(300);
|
|
233
|
+
|
|
234
|
+
expect(collected.at(-1)).toEqual({ results: [], isSearching: true });
|
|
235
|
+
|
|
236
|
+
sub.unsubscribe();
|
|
237
|
+
stateUnit.dispose();
|
|
238
|
+
} finally {
|
|
239
|
+
vi.useRealTimers();
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('search query observable echoes the latest setQuery value', async () => {
|
|
244
|
+
const { client } = mockClient();
|
|
245
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
246
|
+
|
|
247
|
+
const queries = stateUnit.search.query$.pipe(skip(1), take(1), toArray()).toPromise();
|
|
248
|
+
stateUnit.search.setQuery('alpha');
|
|
249
|
+
|
|
250
|
+
expect(await queries).toEqual(['alpha']);
|
|
251
|
+
|
|
252
|
+
stateUnit.dispose();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('omits entityType from the filter when the empty sentinel is selected', async () => {
|
|
256
|
+
const { client, resourceCalls } = mockClient();
|
|
257
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
258
|
+
|
|
259
|
+
const sub = stateUnit.recentResources$.subscribe();
|
|
260
|
+
stateUnit.setSelectedEntityType('Person');
|
|
261
|
+
stateUnit.setSelectedEntityType('');
|
|
262
|
+
|
|
263
|
+
const last = resourceCalls.at(-1)!;
|
|
264
|
+
expect(last.entityType).toBeUndefined();
|
|
265
|
+
|
|
266
|
+
sub.unsubscribe();
|
|
267
|
+
stateUnit.dispose();
|
|
268
|
+
});
|
|
76
269
|
});
|