@semiont/react-ui 0.4.16 → 0.4.18
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/dist/index.d.mts +44 -15
- package/dist/index.mjs +10295 -353
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/modals/ResourceSearchModal.tsx +69 -57
- package/src/components/modals/SearchModal.tsx +54 -51
- package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +59 -82
- package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +221 -0
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +46 -45
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +23 -29
package/package.json
CHANGED
|
@@ -1,9 +1,38 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
4
|
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
|
|
5
|
-
import {
|
|
5
|
+
import { map } from 'rxjs/operators';
|
|
6
|
+
import type { components } from '@semiont/core';
|
|
7
|
+
import { getResourceId, getPrimaryRepresentation } from '@semiont/api-client';
|
|
8
|
+
import { useApiClient } from '../../contexts/ApiClientContext';
|
|
9
|
+
import { useObservable } from '../../hooks/useObservable';
|
|
6
10
|
import { useSearchAnnouncements } from '../../hooks/useSearchAnnouncements';
|
|
11
|
+
import { createSearchPipeline } from '../../lib/search-pipeline';
|
|
12
|
+
|
|
13
|
+
type ResourceDescriptor = components['schemas']['ResourceDescriptor'];
|
|
14
|
+
|
|
15
|
+
type SearchResult = {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
content?: string;
|
|
19
|
+
mediaType?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const SEARCH_DEBOUNCE_MS = 300;
|
|
23
|
+
const SEARCH_LIMIT = 50;
|
|
24
|
+
|
|
25
|
+
function toSearchResult(resource: ResourceDescriptor & { content?: string }): SearchResult | null {
|
|
26
|
+
const id = getResourceId(resource);
|
|
27
|
+
if (!id) return null;
|
|
28
|
+
const primary = getPrimaryRepresentation(resource);
|
|
29
|
+
return {
|
|
30
|
+
id,
|
|
31
|
+
name: resource.name,
|
|
32
|
+
content: resource.content,
|
|
33
|
+
mediaType: primary?.mediaType,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
7
36
|
|
|
8
37
|
interface ResourceSearchModalProps {
|
|
9
38
|
isOpen: boolean;
|
|
@@ -27,8 +56,7 @@ export function ResourceSearchModal({
|
|
|
27
56
|
translations = {}
|
|
28
57
|
}: ResourceSearchModalProps) {
|
|
29
58
|
const { announceSearchResults, announceSearching, announceNavigation } = useSearchAnnouncements();
|
|
30
|
-
const
|
|
31
|
-
const [debouncedSearch, setDebouncedSearch] = useState(searchTerm);
|
|
59
|
+
const semiont = useApiClient();
|
|
32
60
|
|
|
33
61
|
const t = {
|
|
34
62
|
title: translations.title || 'Search Resources',
|
|
@@ -38,63 +66,47 @@ export function ResourceSearchModal({
|
|
|
38
66
|
close: translations.close || '✕',
|
|
39
67
|
};
|
|
40
68
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
69
|
+
// ── Search pipeline ─────────────────────────────────────────────────────
|
|
70
|
+
const [pipeline] = useState(() =>
|
|
71
|
+
createSearchPipeline<SearchResult>(
|
|
72
|
+
(q) =>
|
|
73
|
+
semiont.browse.resources({ search: q, limit: SEARCH_LIMIT }).pipe(
|
|
74
|
+
map((resources) => {
|
|
75
|
+
if (resources === undefined) return undefined;
|
|
76
|
+
return resources
|
|
77
|
+
.map(toSearchResult)
|
|
78
|
+
.filter((r): r is SearchResult => r !== null);
|
|
79
|
+
}),
|
|
80
|
+
),
|
|
81
|
+
{ debounceMs: SEARCH_DEBOUNCE_MS, initialQuery: searchTerm },
|
|
82
|
+
),
|
|
54
83
|
);
|
|
84
|
+
useEffect(() => () => pipeline.dispose(), [pipeline]);
|
|
55
85
|
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const mediaType = Array.isArray(reps) && reps.length > 0 && reps[0]
|
|
61
|
-
? reps[0].mediaType
|
|
62
|
-
: undefined;
|
|
63
|
-
|
|
64
|
-
return {
|
|
65
|
-
id: resource['@id'],
|
|
66
|
-
name: resource.name,
|
|
67
|
-
content: resource.content,
|
|
68
|
-
mediaType
|
|
69
|
-
};
|
|
70
|
-
}) || [];
|
|
71
|
-
|
|
72
|
-
// Announce search results
|
|
73
|
-
useEffect(() => {
|
|
74
|
-
if (!loading && debouncedSearch) {
|
|
75
|
-
announceSearchResults(results.length, debouncedSearch);
|
|
76
|
-
}
|
|
77
|
-
}, [loading, results.length, debouncedSearch]);
|
|
86
|
+
const search = useObservable(pipeline.query$) ?? '';
|
|
87
|
+
const searchState = useObservable(pipeline.state$);
|
|
88
|
+
const results = searchState?.results ?? [];
|
|
89
|
+
const loading = searchState?.isSearching ?? false;
|
|
78
90
|
|
|
79
|
-
//
|
|
91
|
+
// Re-seed when modal re-opens with a different searchTerm prop. The
|
|
92
|
+
// initialQuery option only applies to the first construction; subsequent
|
|
93
|
+
// prop changes need an explicit setQuery.
|
|
80
94
|
useEffect(() => {
|
|
81
|
-
if (
|
|
82
|
-
|
|
95
|
+
if (isOpen && searchTerm) {
|
|
96
|
+
pipeline.setQuery(searchTerm);
|
|
83
97
|
}
|
|
84
|
-
}, [
|
|
98
|
+
}, [isOpen, searchTerm, pipeline]);
|
|
85
99
|
|
|
86
|
-
//
|
|
100
|
+
// Accessibility announcements for search lifecycle.
|
|
87
101
|
useEffect(() => {
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
102
|
+
if (!search.trim()) return;
|
|
103
|
+
if (loading) {
|
|
104
|
+
announceSearching();
|
|
105
|
+
} else {
|
|
106
|
+
announceSearchResults(results.length, search);
|
|
91
107
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const handleSearch = (e: React.FormEvent) => {
|
|
95
|
-
e.preventDefault();
|
|
96
|
-
// Search is handled by React Query hook
|
|
97
|
-
};
|
|
108
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
109
|
+
}, [loading, results.length]);
|
|
98
110
|
|
|
99
111
|
const handleSelect = (resourceId: string, resourceName: string) => {
|
|
100
112
|
announceNavigation(resourceName, 'resource');
|
|
@@ -142,16 +154,16 @@ export function ResourceSearchModal({
|
|
|
142
154
|
</button>
|
|
143
155
|
</div>
|
|
144
156
|
|
|
145
|
-
<
|
|
157
|
+
<div className="semiont-search-modal__search-form">
|
|
146
158
|
<input
|
|
147
159
|
type="text"
|
|
148
160
|
value={search}
|
|
149
|
-
onChange={(e) =>
|
|
161
|
+
onChange={(e) => pipeline.setQuery(e.target.value)}
|
|
150
162
|
placeholder={t.placeholder}
|
|
151
163
|
className="semiont-search-modal__search-input"
|
|
152
164
|
autoFocus
|
|
153
165
|
/>
|
|
154
|
-
</
|
|
166
|
+
</div>
|
|
155
167
|
|
|
156
168
|
<div className="semiont-search-modal__results">
|
|
157
169
|
{loading && (
|
|
@@ -168,7 +180,7 @@ export function ResourceSearchModal({
|
|
|
168
180
|
|
|
169
181
|
{!loading && results.length > 0 && (
|
|
170
182
|
<div className="semiont-search-modal__resource-list">
|
|
171
|
-
{results.map((resource
|
|
183
|
+
{results.map((resource) => {
|
|
172
184
|
const isImage = resource.mediaType?.startsWith('image/');
|
|
173
185
|
|
|
174
186
|
return (
|
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect } from 'react';
|
|
4
4
|
import { Dialog, DialogPanel, Transition, TransitionChild } from '@headlessui/react';
|
|
5
|
-
|
|
6
|
-
import { useSearchAnnouncements } from '../../hooks/useSearchAnnouncements';
|
|
5
|
+
import { map } from 'rxjs/operators';
|
|
7
6
|
import { getResourceId } from '@semiont/api-client';
|
|
7
|
+
import { useSearchAnnouncements } from '../../hooks/useSearchAnnouncements';
|
|
8
|
+
import { useApiClient } from '../../contexts/ApiClientContext';
|
|
9
|
+
import { useObservable } from '../../hooks/useObservable';
|
|
10
|
+
import { createSearchPipeline } from '../../lib/search-pipeline';
|
|
8
11
|
import './SearchModal.css';
|
|
9
12
|
|
|
13
|
+
const SEARCH_DEBOUNCE_MS = 300;
|
|
14
|
+
const SEARCH_LIMIT = 5;
|
|
15
|
+
|
|
10
16
|
interface SearchModalProps {
|
|
11
17
|
isOpen: boolean;
|
|
12
18
|
onClose: () => void;
|
|
@@ -39,9 +45,7 @@ export function SearchModal({
|
|
|
39
45
|
translations = {}
|
|
40
46
|
}: SearchModalProps) {
|
|
41
47
|
const { announceSearchResults, announceSearching } = useSearchAnnouncements();
|
|
42
|
-
const
|
|
43
|
-
const [debouncedQuery, setDebouncedQuery] = useState('');
|
|
44
|
-
const [results, setResults] = useState<SearchResult[]>([]);
|
|
48
|
+
const semiont = useApiClient();
|
|
45
49
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
46
50
|
|
|
47
51
|
const t = {
|
|
@@ -56,63 +60,62 @@ export function SearchModal({
|
|
|
56
60
|
esc: translations.esc || 'ESC',
|
|
57
61
|
};
|
|
58
62
|
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
63
|
+
// ── Search pipeline ─────────────────────────────────────────────────────
|
|
64
|
+
// The fetch closure maps ResourceDescriptor → SearchResult inside the
|
|
65
|
+
// map operator. The helper handles debounce, switchMap, and loading state.
|
|
66
|
+
const [pipeline] = useState(() =>
|
|
67
|
+
createSearchPipeline<SearchResult>(
|
|
68
|
+
(q) =>
|
|
69
|
+
semiont.browse.resources({ search: q, limit: SEARCH_LIMIT }).pipe(
|
|
70
|
+
map((resources) => {
|
|
71
|
+
if (resources === undefined) return undefined;
|
|
72
|
+
return resources
|
|
73
|
+
.map((resource): SearchResult | null => {
|
|
74
|
+
const id = getResourceId(resource);
|
|
75
|
+
if (!id) return null;
|
|
76
|
+
return {
|
|
77
|
+
type: 'resource',
|
|
78
|
+
id,
|
|
79
|
+
name: resource.name,
|
|
80
|
+
content: (resource as { content?: string }).content?.substring(0, 150),
|
|
81
|
+
};
|
|
82
|
+
})
|
|
83
|
+
.filter((r): r is SearchResult => r !== null);
|
|
84
|
+
}),
|
|
85
|
+
),
|
|
86
|
+
{ debounceMs: SEARCH_DEBOUNCE_MS },
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
useEffect(() => () => pipeline.dispose(), [pipeline]);
|
|
73
90
|
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
const
|
|
91
|
+
const query = useObservable(pipeline.query$) ?? '';
|
|
92
|
+
const searchState = useObservable(pipeline.state$);
|
|
93
|
+
const results = searchState?.results ?? [];
|
|
94
|
+
const loading = searchState?.isSearching ?? false;
|
|
77
95
|
|
|
78
|
-
// Reset
|
|
96
|
+
// Reset query and selection when modal opens.
|
|
79
97
|
useEffect(() => {
|
|
80
98
|
if (isOpen) {
|
|
81
|
-
setQuery('');
|
|
82
|
-
setDebouncedQuery('');
|
|
83
|
-
setResults([]);
|
|
99
|
+
pipeline.setQuery('');
|
|
84
100
|
setSelectedIndex(0);
|
|
85
101
|
}
|
|
86
|
-
}, [isOpen]);
|
|
102
|
+
}, [isOpen, pipeline]);
|
|
87
103
|
|
|
88
|
-
//
|
|
104
|
+
// Reset selection cursor when results change.
|
|
89
105
|
useEffect(() => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
106
|
+
setSelectedIndex(0);
|
|
107
|
+
}, [results.length]);
|
|
94
108
|
|
|
109
|
+
// Accessibility announcements for search lifecycle.
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (!query.trim()) return;
|
|
95
112
|
if (loading) {
|
|
96
113
|
announceSearching();
|
|
97
|
-
} else
|
|
98
|
-
|
|
99
|
-
.filter((resource: any) => getResourceId(resource) !== undefined)
|
|
100
|
-
.map((resource: any) => ({
|
|
101
|
-
type: 'resource' as const,
|
|
102
|
-
id: getResourceId(resource)!,
|
|
103
|
-
name: resource.name,
|
|
104
|
-
content: resource.content?.substring(0, 150)
|
|
105
|
-
}));
|
|
106
|
-
|
|
107
|
-
// TODO: Add entities search when API is ready
|
|
108
|
-
const entityResults: SearchResult[] = [];
|
|
109
|
-
|
|
110
|
-
const allResults = [...resourceResults, ...entityResults];
|
|
111
|
-
setResults(allResults);
|
|
112
|
-
setSelectedIndex(0);
|
|
113
|
-
announceSearchResults(allResults.length, debouncedQuery);
|
|
114
|
+
} else {
|
|
115
|
+
announceSearchResults(results.length, query);
|
|
114
116
|
}
|
|
115
|
-
|
|
117
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
118
|
+
}, [loading, results.length]);
|
|
116
119
|
|
|
117
120
|
// Handle keyboard navigation
|
|
118
121
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
@@ -170,7 +173,7 @@ export function SearchModal({
|
|
|
170
173
|
<input
|
|
171
174
|
type="text"
|
|
172
175
|
value={query}
|
|
173
|
-
onChange={(e) => setQuery(e.target.value)}
|
|
176
|
+
onChange={(e) => pipeline.setQuery(e.target.value)}
|
|
174
177
|
onKeyDown={handleKeyDown}
|
|
175
178
|
placeholder={t.placeholder}
|
|
176
179
|
className="semiont-search-modal__input"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
|
4
|
+
import { BehaviorSubject } from 'rxjs';
|
|
4
5
|
import { renderWithProviders } from '../../../test-utils';
|
|
5
6
|
import '@testing-library/jest-dom';
|
|
6
7
|
import { ResourceSearchModal } from '../ResourceSearchModal';
|
|
@@ -14,19 +15,24 @@ vi.mock('@headlessui/react', () => ({
|
|
|
14
15
|
TransitionChild: ({ children }: any) => <>{children}</>,
|
|
15
16
|
}));
|
|
16
17
|
|
|
17
|
-
// Mock api-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
vi.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
18
|
+
// Mock the api-client Observable surface.
|
|
19
|
+
// Note: useApiClient is called on every render. The real ApiClientProvider
|
|
20
|
+
// holds a single instance — the mock must do the same, otherwise useMemo deps
|
|
21
|
+
// invalidate on every render and RxJS pipelines restart from their initial
|
|
22
|
+
// value on each keystroke.
|
|
23
|
+
const browseResourcesSubject = new BehaviorSubject<any[] | undefined>(undefined);
|
|
24
|
+
const browseResourcesMock = vi.fn(() => browseResourcesSubject.asObservable());
|
|
25
|
+
const stableMockClient = { browse: { resources: browseResourcesMock } };
|
|
26
|
+
|
|
27
|
+
vi.mock('../../../contexts/ApiClientContext', async () => {
|
|
28
|
+
const actual = await vi.importActual<typeof import('../../../contexts/ApiClientContext')>(
|
|
29
|
+
'../../../contexts/ApiClientContext'
|
|
30
|
+
);
|
|
31
|
+
return {
|
|
32
|
+
...actual,
|
|
33
|
+
useApiClient: () => stableMockClient,
|
|
34
|
+
};
|
|
35
|
+
});
|
|
30
36
|
|
|
31
37
|
// Mock search announcements
|
|
32
38
|
vi.mock('../../../hooks/useSearchAnnouncements', () => ({
|
|
@@ -37,6 +43,10 @@ vi.mock('../../../hooks/useSearchAnnouncements', () => ({
|
|
|
37
43
|
})),
|
|
38
44
|
}));
|
|
39
45
|
|
|
46
|
+
function setBrowseResults(resources: any[] | undefined) {
|
|
47
|
+
browseResourcesSubject.next(resources);
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
describe('ResourceSearchModal', () => {
|
|
41
51
|
const defaultProps = {
|
|
42
52
|
isOpen: true,
|
|
@@ -46,7 +56,15 @@ describe('ResourceSearchModal', () => {
|
|
|
46
56
|
|
|
47
57
|
beforeEach(() => {
|
|
48
58
|
vi.clearAllMocks();
|
|
49
|
-
|
|
59
|
+
browseResourcesSubject.next(undefined);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const buildResource = (id: string, name: string, mediaType = 'text/plain', content?: string) => ({
|
|
63
|
+
'@context': 'https://www.w3.org/ns/anno.jsonld',
|
|
64
|
+
'@id': id,
|
|
65
|
+
name,
|
|
66
|
+
content,
|
|
67
|
+
representations: [{ mediaType, isPrimary: true }],
|
|
50
68
|
});
|
|
51
69
|
|
|
52
70
|
it('renders modal with title when open', () => {
|
|
@@ -77,63 +95,27 @@ describe('ResourceSearchModal', () => {
|
|
|
77
95
|
expect(screen.getByPlaceholderText('Type here...')).toBeInTheDocument();
|
|
78
96
|
});
|
|
79
97
|
|
|
80
|
-
it('shows loading state
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
expect(screen.getByText('Searching...')).toBeInTheDocument();
|
|
98
|
+
it('shows loading state while the Observable has not emitted', async () => {
|
|
99
|
+
renderWithProviders(<ResourceSearchModal {...defaultProps} searchTerm="something" />);
|
|
100
|
+
await waitFor(() => expect(screen.getByText('Searching...')).toBeInTheDocument(), { timeout: 1000 });
|
|
84
101
|
});
|
|
85
102
|
|
|
86
|
-
it('shows no results message when search
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
renderWithProviders(
|
|
93
|
-
<ResourceSearchModal {...defaultProps} searchTerm="xyz" />
|
|
94
|
-
);
|
|
95
|
-
expect(screen.getByText('No documents found')).toBeInTheDocument();
|
|
103
|
+
it('shows no results message when search returns an empty array', async () => {
|
|
104
|
+
renderWithProviders(<ResourceSearchModal {...defaultProps} searchTerm="xyz" />);
|
|
105
|
+
setBrowseResults([]);
|
|
106
|
+
await waitFor(() => expect(screen.getByText('No documents found')).toBeInTheDocument(), { timeout: 1000 });
|
|
96
107
|
});
|
|
97
108
|
|
|
98
|
-
it('renders search results', () => {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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();
|
|
109
|
+
it('renders search results', async () => {
|
|
110
|
+
renderWithProviders(<ResourceSearchModal {...defaultProps} searchTerm="test" />);
|
|
111
|
+
setBrowseResults([buildResource('res-1', 'Test Document', 'text/plain', 'Some content here')]);
|
|
112
|
+
await waitFor(() => expect(screen.getByText('Test Document')).toBeInTheDocument(), { timeout: 1000 });
|
|
117
113
|
});
|
|
118
114
|
|
|
119
|
-
it('calls onSelect and onClose when a result is clicked', () => {
|
|
115
|
+
it('calls onSelect and onClose when a result is clicked', async () => {
|
|
120
116
|
const onSelect = vi.fn();
|
|
121
117
|
const onClose = vi.fn();
|
|
122
118
|
|
|
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
119
|
renderWithProviders(
|
|
138
120
|
<ResourceSearchModal
|
|
139
121
|
{...defaultProps}
|
|
@@ -142,31 +124,18 @@ describe('ResourceSearchModal', () => {
|
|
|
142
124
|
onClose={onClose}
|
|
143
125
|
/>
|
|
144
126
|
);
|
|
127
|
+
setBrowseResults([buildResource('res-1', 'Test Document', 'text/plain', 'Some content')]);
|
|
128
|
+
await waitFor(() => expect(screen.getByText('Test Document')).toBeInTheDocument(), { timeout: 1000 });
|
|
145
129
|
|
|
146
130
|
fireEvent.click(screen.getByText('Test Document'));
|
|
147
131
|
expect(onSelect).toHaveBeenCalledWith('res-1');
|
|
148
132
|
expect(onClose).toHaveBeenCalled();
|
|
149
133
|
});
|
|
150
134
|
|
|
151
|
-
it('shows media type for image results', () => {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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();
|
|
135
|
+
it('shows media type for image results', async () => {
|
|
136
|
+
renderWithProviders(<ResourceSearchModal {...defaultProps} searchTerm="photo" />);
|
|
137
|
+
setBrowseResults([buildResource('res-img', 'Photo', 'image/png')]);
|
|
138
|
+
await waitFor(() => expect(screen.getByText('image/png')).toBeInTheDocument(), { timeout: 1000 });
|
|
170
139
|
});
|
|
171
140
|
|
|
172
141
|
it('calls onClose when close button is clicked', () => {
|
|
@@ -177,4 +146,12 @@ describe('ResourceSearchModal', () => {
|
|
|
177
146
|
fireEvent.click(screen.getByLabelText('✕'));
|
|
178
147
|
expect(onClose).toHaveBeenCalled();
|
|
179
148
|
});
|
|
149
|
+
|
|
150
|
+
it('passes the search term through to browse.resources', async () => {
|
|
151
|
+
renderWithProviders(<ResourceSearchModal {...defaultProps} searchTerm="hello" />);
|
|
152
|
+
await waitFor(
|
|
153
|
+
() => expect(browseResourcesMock).toHaveBeenCalledWith({ search: 'hello', limit: 50 }),
|
|
154
|
+
{ timeout: 1000 }
|
|
155
|
+
);
|
|
156
|
+
});
|
|
180
157
|
});
|