@semiont/react-ui 0.5.27 → 0.5.29

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 (192) hide show
  1. package/README.md +3 -3
  2. package/dist/{PdfAnnotationCanvas.client-EVUPTUWZ.js → PdfAnnotationCanvas.client-LSH3PZPY.js} +32 -5
  3. package/dist/{ar-OU3DQGC6.js → ar-AZBRADEB.js} +43 -23
  4. package/dist/{bn-PHWCR6AZ.js → bn-CCEW6AOS.js} +43 -23
  5. package/dist/{chunk-VXASXU4K.js → chunk-3ZBKCWUZ.js} +1 -1
  6. package/dist/{chunk-5VDTWRXJ.js → chunk-G4VHL4UA.js} +31 -31
  7. package/dist/{chunk-PCRRBU7H.js → chunk-SDKFZLEX.js} +43 -23
  8. package/dist/{cs-NEXYZ62I.js → cs-HXPF2CTE.js} +43 -23
  9. package/dist/{da-FAT43YCU.js → da-K5NTK7LH.js} +43 -23
  10. package/dist/{de-KRQ3WBCU.js → de-GMRQEDZI.js} +43 -23
  11. package/dist/{el-Q4IU3EKC.js → el-EUB4CI4G.js} +43 -23
  12. package/dist/{en-2KADE7N4.js → en-R5XA26NF.js} +2 -2
  13. package/dist/{es-2IO3R2MK.js → es-Z32DAZ6W.js} +43 -23
  14. package/dist/{fa-Q5GOQFXX.js → fa-G7HN6B32.js} +43 -23
  15. package/dist/{fi-R6NMRWBE.js → fi-3YBLZO7I.js} +43 -23
  16. package/dist/{fr-QY4D5E6J.js → fr-VUBH7BBS.js} +43 -23
  17. package/dist/{he-X6Y77XHY.js → he-WQQEY26B.js} +43 -23
  18. package/dist/{hi-XL7IOEQM.js → hi-HJZ6W4JB.js} +43 -23
  19. package/dist/{id-QZ4ZZS67.js → id-ZOGZ37MI.js} +43 -23
  20. package/dist/index.css +279 -74
  21. package/dist/index.d.ts +318 -393
  22. package/dist/index.js +2619 -2687
  23. package/dist/{it-ME2Q7OB6.js → it-DT6UW66F.js} +43 -23
  24. package/dist/{ja-HX7QASRI.js → ja-RUKQCOLO.js} +43 -23
  25. package/dist/{ko-EQO5AHAV.js → ko-ZH5ADKZM.js} +43 -23
  26. package/dist/{ms-UILFSF3I.js → ms-Y2AFPZTU.js} +43 -23
  27. package/dist/{nl-NNI45PMK.js → nl-OQEWIXNA.js} +43 -23
  28. package/dist/{no-MAL5F6P6.js → no-KPAYJZWY.js} +43 -23
  29. package/dist/{pl-4RMY6JAJ.js → pl-WSXK2UUD.js} +43 -23
  30. package/dist/{pt-VQH2X3QQ.js → pt-NWRJRUCK.js} +43 -23
  31. package/dist/{ro-XXCQO2OG.js → ro-3CGVMOC6.js} +43 -23
  32. package/dist/{sv-7Z6EJ6U4.js → sv-HGSEBA7S.js} +43 -23
  33. package/dist/test-utils.js +16 -7
  34. package/dist/{th-L3DXXYVQ.js → th-NZ4G352Z.js} +43 -23
  35. package/dist/{tr-P3DFVLFJ.js → tr-LGGS4DLW.js} +43 -23
  36. package/dist/{uk-7C5D3PXS.js → uk-AIX536L4.js} +43 -23
  37. package/dist/{vi-ZLTLEO7P.js → vi-V6CVQZLQ.js} +43 -23
  38. package/dist/{zh-5FHVWDIN.js → zh-AGKPGI3L.js} +43 -23
  39. package/package.json +16 -15
  40. package/src/components/AssistProgress.tsx +36 -2
  41. package/src/components/CodeMirrorRenderer.tsx +8 -8
  42. package/src/components/StatusDisplay.tsx +13 -13
  43. package/src/components/__tests__/AssistProgress.test.tsx +59 -0
  44. package/src/components/__tests__/StatusDisplay.test.tsx +9 -9
  45. package/src/components/modals/ComposeStep.tsx +233 -0
  46. package/src/components/modals/ConfigureGatherStep.tsx +10 -18
  47. package/src/components/modals/ConfigureGenerationStep.tsx +178 -34
  48. package/src/components/modals/ConfigureSearchStep.tsx +19 -0
  49. package/src/components/modals/ContextSummary.tsx +185 -72
  50. package/src/components/modals/CorpusPane.tsx +75 -0
  51. package/src/components/modals/DiscardPrompt.tsx +38 -0
  52. package/src/components/modals/GatherContextStep.tsx +237 -107
  53. package/src/components/modals/ReferenceWizardModal.tsx +252 -59
  54. package/src/components/modals/ResourceGenerateModal.tsx +225 -112
  55. package/src/components/modals/SearchModal.css +337 -101
  56. package/src/components/modals/SearchResultsStep.tsx +61 -68
  57. package/src/components/modals/WizardFooter.tsx +7 -1
  58. package/src/components/modals/__tests__/ComposeStep.test.tsx +152 -0
  59. package/src/components/modals/__tests__/ConfigureGatherStep.test.tsx +14 -26
  60. package/src/components/modals/__tests__/ConfigureGenerationStep.test.tsx +206 -8
  61. package/src/components/modals/__tests__/ConfigureSearchStep.test.tsx +24 -1
  62. package/src/components/modals/__tests__/GatherContextStep.resource.test.tsx +463 -25
  63. package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +442 -39
  64. package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +300 -86
  65. package/src/components/modals/__tests__/SearchResultsStep.test.tsx +61 -0
  66. package/src/components/modals/__tests__/WizardFooter.test.tsx +116 -9
  67. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +34 -1
  68. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +38 -1
  69. package/src/components/resource/AnnotateView.tsx +3 -3
  70. package/src/components/resource/BrowseView.tsx +8 -8
  71. package/src/components/resource/DownloadFileLink.tsx +1 -1
  72. package/src/components/resource/ResourceViewer.tsx +4 -4
  73. package/src/components/resource/__tests__/AnnotateView.embeddable.test.tsx +1 -1
  74. package/src/components/resource/__tests__/BrowseView.embeddable.test.tsx +1 -1
  75. package/src/components/resource/__tests__/BrowseView.reference-hover.test.tsx +1 -1
  76. package/src/components/resource/__tests__/BrowseView.test.tsx +11 -11
  77. package/src/components/resource/__tests__/download-link.test.tsx +6 -6
  78. package/src/components/resource/annotate-renderers.tsx +3 -3
  79. package/src/components/resource/panels/AssistSection.css +35 -0
  80. package/src/components/resource/panels/ReferenceEntry.tsx +11 -4
  81. package/src/components/resource/panels/ReferencesPanel.tsx +9 -1
  82. package/src/components/resource/panels/ResourceInfoPanel.tsx +89 -20
  83. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +7 -2
  84. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +41 -0
  85. package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +1 -1
  86. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +84 -3
  87. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +149 -7
  88. package/src/features/auth/__tests__/SignInForm.a11y.test.tsx +12 -12
  89. package/src/features/auth/components/SignInForm.tsx +53 -53
  90. package/src/features/auth-welcome/state/__tests__/welcome-state-unit.test.ts +2 -2
  91. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +0 -65
  92. package/src/features/resource-compose/components/ResourceComposePage.tsx +35 -133
  93. package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +1 -100
  94. package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -57
  95. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +1 -1
  96. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +2 -2
  97. package/src/features/resource-discovery/state/discover-state-unit.ts +1 -1
  98. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +73 -17
  99. package/src/features/resource-viewer/__tests__/generation-options.test.ts +53 -0
  100. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +148 -83
  101. package/src/features/resource-viewer/generation-options.ts +29 -0
  102. package/src/styles/core/forms.css +35 -8
  103. package/src/styles/features/resource-viewer.css +0 -22
  104. package/src/styles/features/resource.css +0 -4
  105. package/src/styles/index.css +0 -1
  106. package/translations/ar.json +42 -22
  107. package/translations/bn.json +42 -22
  108. package/translations/cs.json +42 -22
  109. package/translations/da.json +42 -22
  110. package/translations/de.json +42 -22
  111. package/translations/el.json +42 -22
  112. package/translations/en.json +42 -22
  113. package/translations/es.json +42 -22
  114. package/translations/fa.json +42 -22
  115. package/translations/fi.json +42 -22
  116. package/translations/fr.json +42 -22
  117. package/translations/he.json +42 -22
  118. package/translations/hi.json +42 -22
  119. package/translations/id.json +42 -22
  120. package/translations/it.json +42 -22
  121. package/translations/ja.json +42 -22
  122. package/translations/ko.json +42 -22
  123. package/translations/ms.json +42 -22
  124. package/translations/nl.json +42 -22
  125. package/translations/no.json +42 -22
  126. package/translations/pl.json +42 -22
  127. package/translations/pt.json +42 -22
  128. package/translations/ro.json +42 -22
  129. package/translations/sv.json +42 -22
  130. package/translations/th.json +42 -22
  131. package/translations/tr.json +42 -22
  132. package/translations/uk.json +42 -22
  133. package/translations/vi.json +42 -22
  134. package/translations/zh.json +42 -22
  135. package/dist/PdfAnnotationCanvas.client-EVUPTUWZ.js.map +0 -1
  136. package/dist/PdfAnnotationCanvas.client-TGDPPGXJ.css.map +0 -1
  137. package/dist/ar-OU3DQGC6.js.map +0 -1
  138. package/dist/bn-PHWCR6AZ.js.map +0 -1
  139. package/dist/chunk-5VDTWRXJ.js.map +0 -1
  140. package/dist/chunk-BTUG3VJJ.js.map +0 -1
  141. package/dist/chunk-O23TLLXW.js.map +0 -1
  142. package/dist/chunk-PCRRBU7H.js.map +0 -1
  143. package/dist/chunk-QT7LYM72.js.map +0 -1
  144. package/dist/chunk-VXASXU4K.js.map +0 -1
  145. package/dist/cs-NEXYZ62I.js.map +0 -1
  146. package/dist/da-FAT43YCU.js.map +0 -1
  147. package/dist/de-KRQ3WBCU.js.map +0 -1
  148. package/dist/el-Q4IU3EKC.js.map +0 -1
  149. package/dist/en-2KADE7N4.js.map +0 -1
  150. package/dist/es-2IO3R2MK.js.map +0 -1
  151. package/dist/fa-Q5GOQFXX.js.map +0 -1
  152. package/dist/fi-R6NMRWBE.js.map +0 -1
  153. package/dist/fr-QY4D5E6J.js.map +0 -1
  154. package/dist/he-X6Y77XHY.js.map +0 -1
  155. package/dist/hi-XL7IOEQM.js.map +0 -1
  156. package/dist/id-QZ4ZZS67.js.map +0 -1
  157. package/dist/index.css.map +0 -1
  158. package/dist/index.js.map +0 -1
  159. package/dist/integrations/css-modules-helper.js.map +0 -1
  160. package/dist/integrations/styled-components-theme.js.map +0 -1
  161. package/dist/it-ME2Q7OB6.js.map +0 -1
  162. package/dist/ja-HX7QASRI.js.map +0 -1
  163. package/dist/ko-EQO5AHAV.js.map +0 -1
  164. package/dist/ms-UILFSF3I.js.map +0 -1
  165. package/dist/nl-NNI45PMK.js.map +0 -1
  166. package/dist/no-MAL5F6P6.js.map +0 -1
  167. package/dist/pl-4RMY6JAJ.js.map +0 -1
  168. package/dist/pt-VQH2X3QQ.js.map +0 -1
  169. package/dist/ro-XXCQO2OG.js.map +0 -1
  170. package/dist/sv-7Z6EJ6U4.js.map +0 -1
  171. package/dist/test-utils.css.map +0 -1
  172. package/dist/test-utils.js.map +0 -1
  173. package/dist/th-L3DXXYVQ.js.map +0 -1
  174. package/dist/tr-P3DFVLFJ.js.map +0 -1
  175. package/dist/uk-7C5D3PXS.js.map +0 -1
  176. package/dist/vi-ZLTLEO7P.js.map +0 -1
  177. package/dist/zh-5FHVWDIN.js.map +0 -1
  178. package/src/components/modals/ResourceSearchModal.tsx +0 -224
  179. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +0 -163
  180. package/src/features/admin-exchange/__tests__/AdminExchangePage.test.tsx +0 -137
  181. package/src/features/admin-exchange/__tests__/ExportCard.test.tsx +0 -41
  182. package/src/features/admin-exchange/__tests__/ImportCard.test.tsx +0 -148
  183. package/src/features/admin-exchange/__tests__/ImportProgress.test.tsx +0 -117
  184. package/src/features/admin-exchange/components/AdminExchangePage.tsx +0 -117
  185. package/src/features/admin-exchange/components/ExportCard.tsx +0 -35
  186. package/src/features/admin-exchange/components/ImportCard.tsx +0 -183
  187. package/src/features/admin-exchange/components/ImportProgress.tsx +0 -80
  188. package/src/features/admin-exchange/state/__tests__/exchange-state-unit.test.ts +0 -185
  189. package/src/features/admin-exchange/state/exchange-state-unit.ts +0 -133
  190. package/src/features/moderation-linked-data/__tests__/LinkedDataPage.test.tsx +0 -118
  191. package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +0 -118
  192. package/src/styles/features/exchange.css +0 -374
@@ -1,224 +0,0 @@
1
- 'use client';
2
-
3
- import { useState, useEffect, useRef } from 'react';
4
- import { readyValue } from '@semiont/sdk';
5
- import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
6
- import { map } from 'rxjs/operators';
7
- import { getResourceId, getPrimaryRepresentation } from '@semiont/core';
8
- import { createSearchPipeline } from '@semiont/sdk';
9
- import { useSemiont } from '../../session/SemiontProvider';
10
- import { useObservable } from '../../hooks/useObservable';
11
- import { useSearchAnnouncements } from '../../hooks/useSearchAnnouncements';
12
-
13
- import type { ResourceDescriptor } from '@semiont/core';
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
- }
36
-
37
- interface ResourceSearchModalProps {
38
- isOpen: boolean;
39
- onClose: () => void;
40
- onSelect: (resourceId: string) => void;
41
- searchTerm?: string;
42
- translations?: {
43
- title?: string;
44
- placeholder?: string;
45
- searching?: string;
46
- noResults?: string;
47
- close?: string;
48
- };
49
- }
50
-
51
- export function ResourceSearchModal({
52
- isOpen,
53
- onClose,
54
- onSelect,
55
- searchTerm = '',
56
- translations = {}
57
- }: ResourceSearchModalProps) {
58
- const { announceSearchResults, announceSearching, announceNavigation } = useSearchAnnouncements();
59
- const semiont = useObservable(useSemiont().activeSession$)?.client;
60
- // Pipeline factory captures `semiont` once via useState; if semiont is
61
- // still loading at first render the captured value would be undefined.
62
- // Route through a ref so the fetch closure reads the latest client.
63
- const semiontRef = useRef(semiont);
64
- semiontRef.current = semiont;
65
-
66
- const t = {
67
- title: translations.title || 'Search Resources',
68
- placeholder: translations.placeholder || 'Search for resources...',
69
- searching: translations.searching || 'Searching...',
70
- noResults: translations.noResults || 'No documents found',
71
- close: translations.close || '✕',
72
- };
73
-
74
- // ── Search pipeline ─────────────────────────────────────────────────────
75
- const [pipeline] = useState(() =>
76
- createSearchPipeline<SearchResult>(
77
- (q) =>
78
- semiontRef.current!.browse.resources({ search: q, limit: SEARCH_LIMIT }).pipe(
79
- map((st) => {
80
- const resources = readyValue(st)?.resources;
81
- if (resources === undefined) return undefined;
82
- return resources
83
- .map(toSearchResult)
84
- .filter((r): r is SearchResult => r !== null);
85
- }),
86
- ),
87
- { debounceMs: SEARCH_DEBOUNCE_MS, initialQuery: searchTerm },
88
- ),
89
- );
90
- useEffect(() => () => pipeline.dispose(), [pipeline]);
91
-
92
- const search = useObservable(pipeline.query$) ?? '';
93
- const searchState = useObservable(pipeline.state$);
94
- const results = searchState?.results ?? [];
95
- const loading = searchState?.isSearching ?? false;
96
-
97
- // Re-seed when modal re-opens with a different searchTerm prop. The
98
- // initialQuery option only applies to the first construction; subsequent
99
- // prop changes need an explicit setQuery.
100
- useEffect(() => {
101
- if (isOpen && searchTerm) {
102
- pipeline.setQuery(searchTerm);
103
- }
104
- }, [isOpen, searchTerm, pipeline]);
105
-
106
- // Accessibility announcements for search lifecycle.
107
- useEffect(() => {
108
- if (!search.trim()) return;
109
- if (loading) {
110
- announceSearching();
111
- } else {
112
- announceSearchResults(results.length, search);
113
- }
114
- // eslint-disable-next-line react-hooks/exhaustive-deps
115
- }, [loading, results.length]);
116
-
117
- const handleSelect = (resourceId: string, resourceName: string) => {
118
- announceNavigation(resourceName, 'resource');
119
- onSelect(resourceId);
120
- onClose();
121
- };
122
-
123
- return (
124
- <Transition appear show={isOpen}>
125
- <Dialog as="div" className="semiont-search-modal semiont-search-modal--resource" onClose={onClose}>
126
- {/* Backdrop */}
127
- <TransitionChild
128
- enter="ease-out duration-200"
129
- enterFrom="opacity-0"
130
- enterTo="opacity-100"
131
- leave="ease-in duration-150"
132
- leaveFrom="opacity-100"
133
- leaveTo="opacity-0"
134
- >
135
- <div className="semiont-search-modal__backdrop" />
136
- </TransitionChild>
137
-
138
- {/* Modal panel */}
139
- <div className="semiont-search-modal__wrapper">
140
- <div className="semiont-search-modal__centering semiont-search-modal__centering--center">
141
- <TransitionChild
142
- enter="ease-out duration-200"
143
- enterFrom="opacity-0 scale-95"
144
- enterTo="opacity-100 scale-100"
145
- leave="ease-in duration-150"
146
- leaveFrom="opacity-100 scale-100"
147
- leaveTo="opacity-0 scale-95"
148
- >
149
- <DialogPanel className="semiont-search-modal__panel semiont-search-modal__panel--with-border">
150
- <div className="semiont-search-modal__header">
151
- <DialogTitle className="semiont-search-modal__title">
152
- {t.title}
153
- </DialogTitle>
154
- <button
155
- onClick={onClose}
156
- className="semiont-search-modal__close-button"
157
- aria-label={t.close}
158
- >
159
- {t.close}
160
- </button>
161
- </div>
162
-
163
- <div className="semiont-search-modal__search-form">
164
- <input
165
- type="text"
166
- value={search}
167
- onChange={(e) => pipeline.setQuery(e.target.value)}
168
- placeholder={t.placeholder}
169
- className="semiont-search-modal__search-input"
170
- autoFocus
171
- />
172
- </div>
173
-
174
- <div className="semiont-search-modal__results">
175
- {loading && (
176
- <div className="semiont-search-modal__empty">
177
- {t.searching}
178
- </div>
179
- )}
180
-
181
- {!loading && results.length === 0 && search && (
182
- <div className="semiont-search-modal__empty">
183
- {t.noResults}
184
- </div>
185
- )}
186
-
187
- {!loading && results.length > 0 && (
188
- <div className="semiont-search-modal__resource-list">
189
- {results.map((resource) => {
190
- const isImage = resource.mediaType?.startsWith('image/');
191
-
192
- return (
193
- <div
194
- key={resource.id}
195
- className="semiont-search-modal__resource-item"
196
- onClick={() => handleSelect(resource.id, resource.name)}
197
- >
198
- <h4 className="semiont-search-modal__resource-name">
199
- {resource.name}
200
- </h4>
201
- {resource.content && !isImage && (
202
- <p className="semiont-search-modal__resource-description">
203
- {resource.content.substring(0, 150)}...
204
- </p>
205
- )}
206
- {isImage && (
207
- <p className="semiont-search-modal__resource-media-type">
208
- {resource.mediaType}
209
- </p>
210
- )}
211
- </div>
212
- );
213
- })}
214
- </div>
215
- )}
216
- </div>
217
- </DialogPanel>
218
- </TransitionChild>
219
- </div>
220
- </div>
221
- </Dialog>
222
- </Transition>
223
- );
224
- }
@@ -1,163 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { asStates } from '../../../__tests__/test-client';
3
- import { screen, fireEvent, waitFor } from '@testing-library/react';
4
- import { BehaviorSubject, map } from 'rxjs';
5
- import { renderWithProviders } from '../../../test-utils';
6
- import '@testing-library/jest-dom';
7
- import { ResourceSearchModal } from '../ResourceSearchModal';
8
-
9
- // Mock HeadlessUI to avoid jsdom OOM issues
10
- vi.mock('@headlessui/react', () => ({
11
- Dialog: ({ children, onClose, ...props }: any) => <div role="dialog" {...props}>{typeof children === 'function' ? children({ open: true }) : children}</div>,
12
- DialogPanel: ({ children, ...props }: any) => <div {...props}>{children}</div>,
13
- DialogTitle: ({ children, ...props }: any) => <h2 {...props}>{children}</h2>,
14
- Transition: ({ show, children }: any) => show ? <>{children}</> : null,
15
- TransitionChild: ({ children }: any) => <>{children}</>,
16
- }));
17
-
18
- // Mock the http-transport Observable surface.
19
- // The session-based useSemiont path: useObservable(useSemiont().activeSession$)?.client
20
- // We mock useSemiont to return a stable browser whose activeSession$ emits a
21
- // session-shaped object that carries the mock client.
22
- const browseResourcesSubject = new BehaviorSubject<any[] | undefined>(undefined);
23
- const browseResourcesMock = vi.fn(() => asStates(browseResourcesSubject.asObservable().pipe(
24
- // Arrays get the list envelope `resources()` now emits; `undefined`
25
- // passes through as the pending state.
26
- map((v) => (Array.isArray(v) ? { resources: v, total: v.length, offset: 0, limit: 20, matchKind: 'lexical' } : v)),
27
- )));
28
- const stableMockClient = { browse: { resources: browseResourcesMock } };
29
- const stableMockSession = { client: stableMockClient };
30
- const stableActiveSession$ = new BehaviorSubject<any>(stableMockSession);
31
- const stableMockBrowser = { activeSession$: stableActiveSession$ };
32
-
33
- vi.mock('../../../session/SemiontProvider', async () => {
34
- const actual = await vi.importActual<typeof import('../../../session/SemiontProvider')>(
35
- '../../../session/SemiontProvider'
36
- );
37
- return {
38
- ...actual,
39
- useSemiont: () => stableMockBrowser,
40
- };
41
- });
42
-
43
- // Mock search announcements
44
- vi.mock('../../../hooks/useSearchAnnouncements', () => ({
45
- useSearchAnnouncements: vi.fn(() => ({
46
- announceSearchResults: vi.fn(),
47
- announceSearching: vi.fn(),
48
- announceNavigation: vi.fn(),
49
- })),
50
- }));
51
-
52
- function setBrowseResults(resources: any[] | undefined) {
53
- browseResourcesSubject.next(resources);
54
- }
55
-
56
- describe('ResourceSearchModal', () => {
57
- const defaultProps = {
58
- isOpen: true,
59
- onClose: vi.fn(),
60
- onSelect: vi.fn(),
61
- };
62
-
63
- beforeEach(() => {
64
- vi.clearAllMocks();
65
- browseResourcesSubject.next(undefined);
66
- });
67
-
68
- const buildResource = (id: string, name: string, mediaType = 'text/plain', content?: string) => ({
69
- '@context': 'https://www.w3.org/ns/anno.jsonld',
70
- '@id': id,
71
- name,
72
- content,
73
- representations: [{ mediaType, isPrimary: true }],
74
- });
75
-
76
- it('renders modal with title when open', () => {
77
- renderWithProviders(<ResourceSearchModal {...defaultProps} />);
78
- expect(screen.getByText('Search Resources')).toBeInTheDocument();
79
- });
80
-
81
- it('does not render when closed', () => {
82
- renderWithProviders(
83
- <ResourceSearchModal {...defaultProps} isOpen={false} />
84
- );
85
- expect(screen.queryByText('Search Resources')).not.toBeInTheDocument();
86
- });
87
-
88
- it('renders search input with placeholder', () => {
89
- renderWithProviders(<ResourceSearchModal {...defaultProps} />);
90
- expect(screen.getByPlaceholderText('Search for resources...')).toBeInTheDocument();
91
- });
92
-
93
- it('uses custom translations', () => {
94
- renderWithProviders(
95
- <ResourceSearchModal
96
- {...defaultProps}
97
- translations={{ title: 'Find Docs', placeholder: 'Type here...' }}
98
- />
99
- );
100
- expect(screen.getByText('Find Docs')).toBeInTheDocument();
101
- expect(screen.getByPlaceholderText('Type here...')).toBeInTheDocument();
102
- });
103
-
104
- it('shows loading state while the Observable has not emitted', async () => {
105
- renderWithProviders(<ResourceSearchModal {...defaultProps} searchTerm="something" />);
106
- await waitFor(() => expect(screen.getByText('Searching...')).toBeInTheDocument(), { timeout: 1000 });
107
- });
108
-
109
- it('shows no results message when search returns an empty array', async () => {
110
- renderWithProviders(<ResourceSearchModal {...defaultProps} searchTerm="xyz" />);
111
- setBrowseResults([]);
112
- await waitFor(() => expect(screen.getByText('No documents found')).toBeInTheDocument(), { timeout: 1000 });
113
- });
114
-
115
- it('renders search results', async () => {
116
- renderWithProviders(<ResourceSearchModal {...defaultProps} searchTerm="test" />);
117
- setBrowseResults([buildResource('res-1', 'Test Document', 'text/plain', 'Some content here')]);
118
- await waitFor(() => expect(screen.getByText('Test Document')).toBeInTheDocument(), { timeout: 1000 });
119
- });
120
-
121
- it('calls onSelect and onClose when a result is clicked', async () => {
122
- const onSelect = vi.fn();
123
- const onClose = vi.fn();
124
-
125
- renderWithProviders(
126
- <ResourceSearchModal
127
- {...defaultProps}
128
- searchTerm="test"
129
- onSelect={onSelect}
130
- onClose={onClose}
131
- />
132
- );
133
- setBrowseResults([buildResource('res-1', 'Test Document', 'text/plain', 'Some content')]);
134
- await waitFor(() => expect(screen.getByText('Test Document')).toBeInTheDocument(), { timeout: 1000 });
135
-
136
- fireEvent.click(screen.getByText('Test Document'));
137
- expect(onSelect).toHaveBeenCalledWith('res-1');
138
- expect(onClose).toHaveBeenCalled();
139
- });
140
-
141
- it('shows media type for image results', async () => {
142
- renderWithProviders(<ResourceSearchModal {...defaultProps} searchTerm="photo" />);
143
- setBrowseResults([buildResource('res-img', 'Photo', 'image/png')]);
144
- await waitFor(() => expect(screen.getByText('image/png')).toBeInTheDocument(), { timeout: 1000 });
145
- });
146
-
147
- it('calls onClose when close button is clicked', () => {
148
- const onClose = vi.fn();
149
- renderWithProviders(
150
- <ResourceSearchModal {...defaultProps} onClose={onClose} />
151
- );
152
- fireEvent.click(screen.getByLabelText('✕'));
153
- expect(onClose).toHaveBeenCalled();
154
- });
155
-
156
- it('passes the search term through to browse.resources', async () => {
157
- renderWithProviders(<ResourceSearchModal {...defaultProps} searchTerm="hello" />);
158
- await waitFor(
159
- () => expect(browseResourcesMock).toHaveBeenCalledWith({ search: 'hello', limit: 50 }),
160
- { timeout: 1000 }
161
- );
162
- });
163
- });
@@ -1,137 +0,0 @@
1
- /**
2
- * Tests for AdminExchangePage component
3
- */
4
-
5
- import { describe, it, expect, vi } from 'vitest';
6
- import { render, screen } from '@testing-library/react';
7
- import { AdminExchangePage } from '../components/AdminExchangePage';
8
- import type { AdminExchangePageProps } from '../components/AdminExchangePage';
9
-
10
- const createProps = (overrides?: Partial<AdminExchangePageProps>): AdminExchangePageProps => ({
11
- onExport: vi.fn(),
12
- isExporting: false,
13
- onFileSelected: vi.fn(),
14
- onImport: vi.fn(),
15
- onCancelImport: vi.fn(),
16
- selectedFile: null,
17
- preview: null,
18
- isImporting: false,
19
- importPhase: null,
20
- theme: 'light',
21
- activePanel: null,
22
- translations: {
23
- title: 'Backup & Restore',
24
- subtitle: 'Back up and restore your knowledge base',
25
- export: {
26
- title: 'Backup',
27
- description: 'Create a lossless backup',
28
- exportButton: 'Create Backup',
29
- exporting: 'Creating backup…',
30
- },
31
- import: {
32
- title: 'Restore',
33
- description: 'Restore from a backup',
34
- dropzoneLabel: 'Drop a file here',
35
- dropzoneActive: 'Drop to upload',
36
- detectedFormat: 'Format',
37
- statsPreview: 'Preview',
38
- importButton: 'Restore',
39
- importing: 'Restoring…',
40
- importConfirmTitle: 'Confirm',
41
- importConfirmMessage: 'Cannot undo.',
42
- confirmImport: 'Proceed',
43
- cancelImport: 'Cancel',
44
- },
45
- progress: {
46
- phaseStarted: 'Starting…',
47
- phaseComplete: 'Complete',
48
- phaseError: 'Failed',
49
- statsEventsReplayed: 'Events replayed',
50
- statsResourcesCreated: 'Resources created',
51
- statsAnnotationsCreated: 'Annotations created',
52
- statsEntityTypesAdded: 'Entity types added',
53
- },
54
- },
55
- ToolbarPanels: () => <div data-testid="toolbar-panels" />,
56
- Toolbar: () => <div data-testid="toolbar" />,
57
- ...overrides,
58
- });
59
-
60
- describe('AdminExchangePage', () => {
61
- it('renders page title and subtitle', () => {
62
- render(<AdminExchangePage {...createProps()} />);
63
- expect(screen.getByText('Backup & Restore')).toBeInTheDocument();
64
- expect(screen.getByText('Back up and restore your knowledge base')).toBeInTheDocument();
65
- });
66
-
67
- it('renders ExportCard', () => {
68
- render(<AdminExchangePage {...createProps()} />);
69
- expect(screen.getByRole('heading', { name: 'Backup' })).toBeInTheDocument();
70
- expect(screen.getByText('Create a lossless backup')).toBeInTheDocument();
71
- });
72
-
73
- it('renders ImportCard', () => {
74
- render(<AdminExchangePage {...createProps()} />);
75
- expect(screen.getByRole('heading', { name: 'Restore' })).toBeInTheDocument();
76
- expect(screen.getByText('Restore from a backup')).toBeInTheDocument();
77
- });
78
-
79
- it('renders toolbar components', () => {
80
- render(<AdminExchangePage {...createProps()} />);
81
- expect(screen.getByTestId('toolbar-panels')).toBeInTheDocument();
82
- expect(screen.getByTestId('toolbar')).toBeInTheDocument();
83
- });
84
-
85
- it('does not render ImportProgress when importPhase is null', () => {
86
- const { container } = render(<AdminExchangePage {...createProps()} />);
87
- expect(container.querySelector('.semiont-exchange__progress')).not.toBeInTheDocument();
88
- });
89
-
90
- it('renders ImportProgress when importPhase is set', () => {
91
- render(<AdminExchangePage {...createProps({ importPhase: 'started' })} />);
92
- expect(screen.getByText('Starting…')).toBeInTheDocument();
93
- });
94
-
95
- it('renders ImportProgress with backup result on completion', () => {
96
- render(<AdminExchangePage {...createProps({
97
- importPhase: 'complete',
98
- importResult: { stats: { eventsReplayed: 42, resourcesCreated: 5, annotationsCreated: 12, entityTypesAdded: 3 } },
99
- })} />);
100
- expect(screen.getByText('Complete')).toBeInTheDocument();
101
- expect(screen.getByText('42')).toBeInTheDocument();
102
- expect(screen.getByText('Events replayed')).toBeInTheDocument();
103
- });
104
-
105
- it('applies panel-open class when common panel is active', () => {
106
- const { container } = render(<AdminExchangePage {...createProps({ activePanel: 'user' })} />);
107
- expect(container.querySelector('.semiont-page--panel-open')).toBeInTheDocument();
108
- });
109
-
110
- it('does not apply panel-open class when no panel is active', () => {
111
- const { container } = render(<AdminExchangePage {...createProps({ activePanel: null })} />);
112
- expect(container.querySelector('.semiont-page--panel-open')).not.toBeInTheDocument();
113
- });
114
-
115
- it('passes theme to ToolbarPanels', () => {
116
- const ToolbarPanels = vi.fn(() => <div data-testid="toolbar-panels" />);
117
- render(<AdminExchangePage {...createProps({ theme: 'dark', ToolbarPanels })} />);
118
- expect(ToolbarPanels).toHaveBeenCalledWith(
119
- expect.objectContaining({ theme: 'dark' }),
120
- undefined,
121
- );
122
- });
123
-
124
- it('passes context to Toolbar', () => {
125
- const Toolbar = vi.fn(() => <div data-testid="toolbar" />);
126
- render(<AdminExchangePage {...createProps({ Toolbar })} />);
127
- expect(Toolbar).toHaveBeenCalledWith(
128
- expect.objectContaining({ context: 'simple' }),
129
- undefined,
130
- );
131
- });
132
-
133
- it('renders cards in grid layout', () => {
134
- const { container } = render(<AdminExchangePage {...createProps()} />);
135
- expect(container.querySelector('.semiont-exchange__cards')).toBeInTheDocument();
136
- });
137
- });
@@ -1,41 +0,0 @@
1
- /**
2
- * Tests for ExportCard component
3
- */
4
-
5
- import { describe, it, expect, vi } from 'vitest';
6
- import { render, screen, fireEvent } from '@testing-library/react';
7
- import { ExportCard } from '../components/ExportCard';
8
- import type { ExportCardProps } from '../components/ExportCard';
9
-
10
- const createProps = (overrides?: Partial<ExportCardProps>): ExportCardProps => ({
11
- onExport: vi.fn(),
12
- isExporting: false,
13
- translations: {
14
- title: 'Backup',
15
- description: 'Create a lossless backup',
16
- exportButton: 'Create Backup',
17
- exporting: 'Creating backup…',
18
- },
19
- ...overrides,
20
- });
21
-
22
- describe('ExportCard', () => {
23
- it('renders title and description', () => {
24
- render(<ExportCard {...createProps()} />);
25
- expect(screen.getByRole('heading', { name: 'Backup' })).toBeInTheDocument();
26
- expect(screen.getByText('Create a lossless backup')).toBeInTheDocument();
27
- });
28
-
29
- it('calls onExport when button is clicked', () => {
30
- const onExport = vi.fn();
31
- render(<ExportCard {...createProps({ onExport })} />);
32
- fireEvent.click(screen.getByRole('button', { name: 'Create Backup' }));
33
- expect(onExport).toHaveBeenCalled();
34
- });
35
-
36
- it('disables button and shows exporting text when isExporting', () => {
37
- render(<ExportCard {...createProps({ isExporting: true })} />);
38
- const button = screen.getByRole('button', { name: 'Creating backup…' });
39
- expect(button).toBeDisabled();
40
- });
41
- });