@semiont/react-ui 0.5.6 → 0.5.7

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 (155) hide show
  1. package/README.md +1 -1
  2. package/dist/{ar-U2EXWUMQ.js → ar-SONK6MON.js} +3 -7
  3. package/dist/ar-SONK6MON.js.map +1 -0
  4. package/dist/{bn-DRJGV772.js → bn-ZKPRITNG.js} +3 -7
  5. package/dist/bn-ZKPRITNG.js.map +1 -0
  6. package/dist/{chunk-3Q3TUKWP.js → chunk-Y2EEAOMZ.js} +29 -29
  7. package/dist/{cs-PTWDM23V.js → cs-LPXQ7NHQ.js} +3 -7
  8. package/dist/cs-LPXQ7NHQ.js.map +1 -0
  9. package/dist/{da-KSNIKYSS.js → da-6TKY7MCY.js} +6 -10
  10. package/dist/da-6TKY7MCY.js.map +1 -0
  11. package/dist/{de-F2XBEWFY.js → de-C3GNII74.js} +3 -7
  12. package/dist/de-C3GNII74.js.map +1 -0
  13. package/dist/{el-DLD2GWAP.js → el-UBCXQDJ7.js} +3 -7
  14. package/dist/el-UBCXQDJ7.js.map +1 -0
  15. package/dist/{es-WLPYWGB5.js → es-BQ23TRI7.js} +11 -15
  16. package/dist/es-BQ23TRI7.js.map +1 -0
  17. package/dist/{fa-BAXHSDZG.js → fa-AFTBZB77.js} +3 -7
  18. package/dist/fa-AFTBZB77.js.map +1 -0
  19. package/dist/{fi-FCHSYVOT.js → fi-WOYNLZC2.js} +3 -7
  20. package/dist/fi-WOYNLZC2.js.map +1 -0
  21. package/dist/{fr-3UERBSL6.js → fr-NDSMIFJM.js} +3 -7
  22. package/dist/fr-NDSMIFJM.js.map +1 -0
  23. package/dist/{he-F6F3FV2K.js → he-VJXVRDOY.js} +3 -7
  24. package/dist/he-VJXVRDOY.js.map +1 -0
  25. package/dist/{hi-4BK6IK7Q.js → hi-BF6PHIE2.js} +3 -7
  26. package/dist/hi-BF6PHIE2.js.map +1 -0
  27. package/dist/{id-7ECCWP3J.js → id-GXG5QCZY.js} +3 -7
  28. package/dist/id-GXG5QCZY.js.map +1 -0
  29. package/dist/index.css +97 -0
  30. package/dist/index.css.map +1 -1
  31. package/dist/index.d.ts +29 -22
  32. package/dist/index.js +346 -301
  33. package/dist/index.js.map +1 -1
  34. package/dist/{it-234Z6XK6.js → it-XKHHCBAF.js} +3 -7
  35. package/dist/it-XKHHCBAF.js.map +1 -0
  36. package/dist/{ja-PJWQI4OQ.js → ja-TX7VM4XD.js} +3 -7
  37. package/dist/ja-TX7VM4XD.js.map +1 -0
  38. package/dist/{ko-APUEW2RS.js → ko-DNC7EQ7J.js} +3 -7
  39. package/dist/ko-DNC7EQ7J.js.map +1 -0
  40. package/dist/{ms-PJBZWZWD.js → ms-POZGBKPH.js} +3 -7
  41. package/dist/ms-POZGBKPH.js.map +1 -0
  42. package/dist/{nl-L4C3ZBCU.js → nl-IRMTKI7Z.js} +4 -11
  43. package/dist/nl-IRMTKI7Z.js.map +1 -0
  44. package/dist/{no-QE5N5KNG.js → no-ZUDJA4S6.js} +20 -24
  45. package/dist/no-ZUDJA4S6.js.map +1 -0
  46. package/dist/{pl-5Q2D23PD.js → pl-2NGAXL5U.js} +3 -7
  47. package/dist/pl-2NGAXL5U.js.map +1 -0
  48. package/dist/{pt-AIGUOIOC.js → pt-ABMCXZUM.js} +118 -122
  49. package/dist/pt-ABMCXZUM.js.map +1 -0
  50. package/dist/{ro-T56CSHTY.js → ro-VOJP6O5X.js} +3 -7
  51. package/dist/ro-VOJP6O5X.js.map +1 -0
  52. package/dist/{sv-L4TJQ2UH.js → sv-4HVFIIE5.js} +43 -47
  53. package/dist/sv-4HVFIIE5.js.map +1 -0
  54. package/dist/test-utils.js +2 -2
  55. package/dist/test-utils.js.map +1 -1
  56. package/dist/{th-6O7Y6O2Q.js → th-IFPZP3HQ.js} +3 -7
  57. package/dist/th-IFPZP3HQ.js.map +1 -0
  58. package/dist/{tr-D4CQCSNO.js → tr-2GYEAMJ4.js} +3 -7
  59. package/dist/tr-2GYEAMJ4.js.map +1 -0
  60. package/dist/{uk-2HMQG6ND.js → uk-XCJBVLLD.js} +3 -7
  61. package/dist/uk-XCJBVLLD.js.map +1 -0
  62. package/dist/{vi-XVJ4RUEJ.js → vi-4FR7CB2F.js} +3 -7
  63. package/dist/vi-4FR7CB2F.js.map +1 -0
  64. package/dist/{zh-K2KDPGHK.js → zh-NSKFOINB.js} +3 -7
  65. package/dist/zh-NSKFOINB.js.map +1 -0
  66. package/package.json +2 -2
  67. package/src/components/ProtectedErrorBoundary.css +119 -0
  68. package/src/components/ProtectedErrorBoundary.tsx +18 -13
  69. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +1 -1
  70. package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +1 -1
  71. package/src/components/resource/AnnotateView.tsx +35 -37
  72. package/src/components/resource/BrowseView.tsx +31 -31
  73. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +0 -1
  74. package/src/components/resource/__tests__/BrowseView.test.tsx +4 -8
  75. package/src/components/resource/__tests__/HistoryEvent.test.tsx +0 -1
  76. package/src/components/resource/__tests__/event-formatting.test.ts +1 -1
  77. package/src/components/resource/panels/CollaborationPanel.tsx +1 -1
  78. package/src/components/resource/panels/JsonLdPanel.tsx +33 -16
  79. package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +1 -1
  80. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +1 -1
  81. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +1 -1
  82. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +1 -1
  83. package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +1 -1
  84. package/src/components/resource/panels/__tests__/JsonLdPanel.test.tsx +95 -424
  85. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +1 -1
  86. package/src/components/resource/panels/__tests__/TagEntry.test.tsx +1 -1
  87. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +1 -1
  88. package/src/features/admin-exchange/__tests__/AdminExchangePage.test.tsx +7 -10
  89. package/src/features/admin-exchange/__tests__/ImportProgress.test.tsx +38 -27
  90. package/src/features/admin-exchange/components/ImportProgress.tsx +28 -34
  91. package/src/features/moderation-linked-data/__tests__/LinkedDataPage.test.tsx +11 -9
  92. package/src/features/resource-compose/components/ResourceComposePage.tsx +36 -9
  93. package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -8
  94. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +7 -5
  95. package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +37 -0
  96. package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +9 -5
  97. package/src/styles/features/exchange.css +0 -30
  98. package/src/styles/index.css +1 -0
  99. package/translations/ar.json +1 -3
  100. package/translations/bn.json +1 -3
  101. package/translations/cs.json +1 -3
  102. package/translations/da.json +4 -6
  103. package/translations/de.json +1 -3
  104. package/translations/el.json +1 -3
  105. package/translations/es.json +9 -11
  106. package/translations/fa.json +1 -3
  107. package/translations/fi.json +1 -3
  108. package/translations/fr.json +1 -3
  109. package/translations/he.json +1 -3
  110. package/translations/hi.json +1 -3
  111. package/translations/id.json +1 -3
  112. package/translations/it.json +1 -3
  113. package/translations/ja.json +1 -3
  114. package/translations/ko.json +1 -3
  115. package/translations/ms.json +1 -3
  116. package/translations/nl.json +2 -7
  117. package/translations/no.json +18 -20
  118. package/translations/pl.json +1 -3
  119. package/translations/pt.json +116 -118
  120. package/translations/ro.json +1 -3
  121. package/translations/sv.json +41 -43
  122. package/translations/th.json +1 -3
  123. package/translations/tr.json +1 -3
  124. package/translations/uk.json +1 -3
  125. package/translations/vi.json +1 -3
  126. package/translations/zh.json +1 -3
  127. package/dist/ar-U2EXWUMQ.js.map +0 -1
  128. package/dist/bn-DRJGV772.js.map +0 -1
  129. package/dist/cs-PTWDM23V.js.map +0 -1
  130. package/dist/da-KSNIKYSS.js.map +0 -1
  131. package/dist/de-F2XBEWFY.js.map +0 -1
  132. package/dist/el-DLD2GWAP.js.map +0 -1
  133. package/dist/es-WLPYWGB5.js.map +0 -1
  134. package/dist/fa-BAXHSDZG.js.map +0 -1
  135. package/dist/fi-FCHSYVOT.js.map +0 -1
  136. package/dist/fr-3UERBSL6.js.map +0 -1
  137. package/dist/he-F6F3FV2K.js.map +0 -1
  138. package/dist/hi-4BK6IK7Q.js.map +0 -1
  139. package/dist/id-7ECCWP3J.js.map +0 -1
  140. package/dist/it-234Z6XK6.js.map +0 -1
  141. package/dist/ja-PJWQI4OQ.js.map +0 -1
  142. package/dist/ko-APUEW2RS.js.map +0 -1
  143. package/dist/ms-PJBZWZWD.js.map +0 -1
  144. package/dist/nl-L4C3ZBCU.js.map +0 -1
  145. package/dist/no-QE5N5KNG.js.map +0 -1
  146. package/dist/pl-5Q2D23PD.js.map +0 -1
  147. package/dist/pt-AIGUOIOC.js.map +0 -1
  148. package/dist/ro-T56CSHTY.js.map +0 -1
  149. package/dist/sv-L4TJQ2UH.js.map +0 -1
  150. package/dist/th-6O7Y6O2Q.js.map +0 -1
  151. package/dist/tr-D4CQCSNO.js.map +0 -1
  152. package/dist/uk-2HMQG6ND.js.map +0 -1
  153. package/dist/vi-XVJ4RUEJ.js.map +0 -1
  154. package/dist/zh-K2KDPGHK.js.map +0 -1
  155. /package/dist/{chunk-3Q3TUKWP.js.map → chunk-Y2EEAOMZ.js.map} +0 -0
@@ -3,500 +3,171 @@ import { render, screen, waitFor } from '@testing-library/react';
3
3
  import userEvent from '@testing-library/user-event';
4
4
  import '@testing-library/jest-dom';
5
5
  import { JsonLdPanel } from '../JsonLdPanel';
6
+ import { resourceId } from '@semiont/core';
6
7
  import type { components } from '@semiont/core';
7
8
 
8
- type SemiontResource = components['schemas']['ResourceDescriptor'];
9
+ type GetResourceResponse = components['schemas']['GetResourceResponse'];
9
10
 
10
- // Mock CodeMirror modules
11
+ // Mock CodeMirror modules (the panel still renders the graph in CodeMirror)
11
12
  vi.mock('@codemirror/view', () => {
12
13
  class MockEditorView {
13
14
  destroy = vi.fn();
14
- constructor() {
15
- // Store config if needed for assertions
16
- }
17
15
  static editable = { of: vi.fn() };
18
16
  static theme = vi.fn(() => ({}));
19
17
  }
20
-
21
- return {
22
- EditorView: MockEditorView,
23
- lineNumbers: vi.fn(),
24
- };
18
+ return { EditorView: MockEditorView, lineNumbers: vi.fn() };
25
19
  });
26
-
27
20
  vi.mock('@codemirror/state', () => ({
28
- EditorState: {
29
- create: vi.fn(() => ({})),
30
- readOnly: { of: vi.fn() },
31
- },
32
- }));
33
-
34
- vi.mock('@codemirror/lang-json', () => ({
35
- json: vi.fn(),
21
+ EditorState: { create: vi.fn(() => ({})), readOnly: { of: vi.fn() } },
36
22
  }));
37
-
38
- vi.mock('@codemirror/theme-one-dark', () => ({
39
- oneDark: {},
40
- }));
41
-
23
+ vi.mock('@codemirror/lang-json', () => ({ json: vi.fn() }));
24
+ vi.mock('@codemirror/theme-one-dark', () => ({ oneDark: {} }));
42
25
  vi.mock('@codemirror/language', () => ({
43
26
  syntaxHighlighting: vi.fn(),
44
- HighlightStyle: {
45
- define: vi.fn(() => ({})),
46
- },
27
+ HighlightStyle: { define: vi.fn(() => ({})) },
47
28
  }));
48
-
49
29
  vi.mock('../../../lib/codemirror-json-theme', () => ({
50
30
  jsonLightTheme: {},
51
31
  jsonLightHighlightStyle: {},
52
32
  }));
53
33
 
54
- // Mock useLineNumbers hook (must be before import)
34
+ // Mock the hooks the panel consumes — NOT the session/client stack.
55
35
  vi.mock('@/hooks/useLineNumbers');
56
-
36
+ vi.mock('@/hooks/useResourceGraph');
57
37
  import { useLineNumbers } from '@/hooks/useLineNumbers';
58
-
59
- // Test data fixtures
60
- const createMockResource = (overrides?: Partial<SemiontResource>): SemiontResource => ({
61
- '@context': 'http://www.w3.org/ns/anno.jsonld',
62
- '@id': 'test-resource-1',
63
- id: 'test-resource-1',
64
- name: 'Test Resource',
65
- content: 'This is test content',
66
- format: 'text/plain',
67
- archived: false,
68
- entityTypes: ['Person', 'Organization'],
69
- locale: 'en-US',
70
- representations: [],
71
- created: '2024-01-01T10:00:00Z',
72
- modified: '2024-01-01T10:00:00Z',
73
- ...overrides,
74
- });
38
+ import { useResourceGraph } from '@/hooks/useResourceGraph';
39
+
40
+ const RID = resourceId('test-resource-1');
41
+
42
+ const MOCK_GRAPH: GetResourceResponse = {
43
+ resource: {
44
+ '@context': 'http://www.w3.org/ns/anno.jsonld',
45
+ '@id': 'test-resource-1',
46
+ id: 'test-resource-1',
47
+ name: 'Test Resource',
48
+ content: 'This is test content',
49
+ format: 'text/plain',
50
+ archived: false,
51
+ entityTypes: ['Person'],
52
+ locale: 'en-US',
53
+ representations: [],
54
+ created: '2024-01-01T10:00:00Z',
55
+ modified: '2024-01-01T10:00:00Z',
56
+ },
57
+ annotations: [],
58
+ entityReferences: [],
59
+ };
75
60
 
76
61
  describe('JsonLdPanel Component', () => {
77
- let mockClipboard: any;
62
+ let mockClipboard: { writeText: ReturnType<typeof vi.fn> };
78
63
 
79
64
  beforeEach(() => {
80
65
  vi.clearAllMocks();
81
66
 
82
- // Mock clipboard API
83
- mockClipboard = {
84
- writeText: vi.fn().mockResolvedValue(undefined),
85
- };
67
+ mockClipboard = { writeText: vi.fn().mockResolvedValue(undefined) };
86
68
  Object.defineProperty(navigator, 'clipboard', {
87
- value: mockClipboard,
88
- writable: true,
89
- configurable: true,
69
+ value: mockClipboard, writable: true, configurable: true,
90
70
  });
91
-
92
- // Mock document.documentElement for dark mode detection
93
71
  Object.defineProperty(document.documentElement, 'classList', {
94
- value: {
95
- contains: vi.fn().mockReturnValue(false),
96
- },
97
- writable: true,
98
- configurable: true,
72
+ value: { contains: vi.fn().mockReturnValue(false) },
73
+ writable: true, configurable: true,
99
74
  });
100
75
 
101
- // Mock useLineNumbers hook implementation
102
76
  vi.mocked(useLineNumbers).mockReturnValue({
103
- showLineNumbers: true,
104
- toggleLineNumbers: vi.fn()
77
+ showLineNumbers: true, toggleLineNumbers: vi.fn(),
78
+ });
79
+ // Default: graph loaded successfully.
80
+ vi.mocked(useResourceGraph).mockReturnValue({
81
+ graph: MOCK_GRAPH, loading: false, error: null,
105
82
  });
106
83
  });
107
84
 
108
- afterEach(() => {
109
- vi.restoreAllMocks();
110
- });
85
+ afterEach(() => { vi.restoreAllMocks(); });
111
86
 
112
87
  describe('Rendering', () => {
113
- it('should render panel with header', () => {
114
- const resource = createMockResource();
115
- render(<JsonLdPanel resource={resource} />);
116
-
88
+ it('renders the header, copy button, and editor container', () => {
89
+ const { container } = render(<JsonLdPanel resourceId={RID} />);
117
90
  expect(screen.getByText('JSON-LD')).toBeInTheDocument();
118
- });
119
-
120
- it('should render copy button', () => {
121
- const resource = createMockResource();
122
- render(<JsonLdPanel resource={resource} />);
123
-
124
91
  expect(screen.getByText(/Copy/)).toBeInTheDocument();
92
+ expect(container.querySelector('.semiont-jsonld-panel__editor')).toBeInTheDocument();
125
93
  });
126
94
 
127
- it('should render editor container', () => {
128
- const resource = createMockResource();
129
- const { container } = render(<JsonLdPanel resource={resource} />);
130
-
131
- const editorDiv = container.querySelector('.semiont-jsonld-panel__editor');
132
- expect(editorDiv).toBeInTheDocument();
95
+ it('fetches the graph for the given resourceId', () => {
96
+ render(<JsonLdPanel resourceId={RID} />);
97
+ expect(useResourceGraph).toHaveBeenCalledWith(RID);
133
98
  });
134
99
  });
135
100
 
136
- describe('CodeMirror Integration', () => {
137
- it('should initialize CodeMirror editor', () => {
138
- const resource = createMockResource();
139
- const { container } = render(<JsonLdPanel resource={resource} />);
140
-
141
- // Verify editor container is rendered
142
- const editorDiv = container.querySelector('.semiont-jsonld-panel__editor');
143
- expect(editorDiv).toBeInTheDocument();
144
- });
145
-
146
- it('should pass resource as JSON to editor', () => {
147
- const resource = createMockResource();
148
- const { container } = render(<JsonLdPanel resource={resource} />);
149
-
150
- // Verify component renders without errors
151
- expect(container.firstChild).toBeInTheDocument();
152
- });
153
-
154
- it('should format JSON with indentation', () => {
155
- const resource = createMockResource();
156
- const { container } = render(<JsonLdPanel resource={resource} />);
157
-
158
- // Verify component renders without errors
159
- expect(container.firstChild).toBeInTheDocument();
101
+ describe('Loading / error states', () => {
102
+ it('shows a loading state and disables Copy while loading', () => {
103
+ vi.mocked(useResourceGraph).mockReturnValue({ graph: null, loading: true, error: null });
104
+ render(<JsonLdPanel resourceId={RID} />);
105
+ expect(screen.getByText(/Loading JSON-LD/i)).toBeInTheDocument();
106
+ expect(screen.getByText(/Copy/).closest('button')).toBeDisabled();
160
107
  });
161
108
 
162
- it('should configure editor as read-only', () => {
163
- const resource = createMockResource();
164
- const { container } = render(<JsonLdPanel resource={resource} />);
165
-
166
- // Verify component renders without errors (read-only is configured internally)
167
- expect(container.firstChild).toBeInTheDocument();
168
- });
169
-
170
- it('should include line numbers when enabled', () => {
171
- vi.mocked(useLineNumbers).mockReturnValue({ showLineNumbers: true, toggleLineNumbers: vi.fn() });
172
-
173
- const resource = createMockResource();
174
- const { container } = render(<JsonLdPanel resource={resource} />);
175
-
176
- // Verify component renders without errors (line numbers configured internally)
177
- expect(container.firstChild).toBeInTheDocument();
178
- });
179
-
180
- it('should not include line numbers when disabled', () => {
181
- vi.mocked(useLineNumbers).mockReturnValue({ showLineNumbers: false, toggleLineNumbers: vi.fn() });
182
-
183
- const resource = createMockResource();
184
- const { container } = render(<JsonLdPanel resource={resource} />);
185
-
186
- // Verify component renders without errors (line numbers not included)
187
- expect(container.firstChild).toBeInTheDocument();
188
- });
189
-
190
- it('should cleanup editor on unmount', () => {
191
- const resource = createMockResource();
192
- const { unmount } = render(<JsonLdPanel resource={resource} />);
193
-
194
- // Should unmount without errors (cleanup happens internally)
195
- expect(() => unmount()).not.toThrow();
196
- });
197
- });
198
-
199
- describe('Dark Mode Support', () => {
200
- it('should use dark theme when dark mode is active', () => {
201
- Object.defineProperty(document.documentElement, 'classList', {
202
- value: {
203
- contains: vi.fn().mockReturnValue(true), // Dark mode active
204
- },
205
- writable: true,
206
- configurable: true,
207
- });
208
-
209
- const resource = createMockResource();
210
- const { container } = render(<JsonLdPanel resource={resource} />);
211
-
212
- // Verify component renders without errors (dark theme configured internally)
213
- expect(container.firstChild).toBeInTheDocument();
214
- });
215
-
216
- it('should use light theme when dark mode is not active', () => {
217
- Object.defineProperty(document.documentElement, 'classList', {
218
- value: {
219
- contains: vi.fn().mockReturnValue(false), // Light mode
220
- },
221
- writable: true,
222
- configurable: true,
109
+ it('shows an error state and disables Copy on failure', () => {
110
+ vi.mocked(useResourceGraph).mockReturnValue({
111
+ graph: null, loading: false, error: new Error('boom'),
223
112
  });
224
-
225
- const resource = createMockResource();
226
- const { container } = render(<JsonLdPanel resource={resource} />);
227
-
228
- // Verify component renders without errors (light theme configured internally)
229
- expect(container.firstChild).toBeInTheDocument();
113
+ render(<JsonLdPanel resourceId={RID} />);
114
+ expect(screen.getByText(/Failed to load JSON-LD/i)).toBeInTheDocument();
115
+ expect(screen.getByText(/Copy/).closest('button')).toBeDisabled();
230
116
  });
231
117
  });
232
118
 
233
- describe('Copy to Clipboard', () => {
234
- it('should copy JSON to clipboard when button clicked', async () => {
235
- const resource = createMockResource();
236
- render(<JsonLdPanel resource={resource} />);
237
-
238
- const copyButton = screen.getByText(/Copy/);
239
- await userEvent.click(copyButton);
119
+ describe('Copy to clipboard', () => {
120
+ it('copies the full graph not the bare descriptor', async () => {
121
+ render(<JsonLdPanel resourceId={RID} />);
122
+ await userEvent.click(screen.getByText(/Copy/));
240
123
 
241
124
  expect(mockClipboard.writeText).toHaveBeenCalledOnce();
125
+ const parsed = JSON.parse(mockClipboard.writeText.mock.calls[0][0]);
242
126
 
243
- const copiedText = mockClipboard.writeText.mock.calls[0][0];
244
- expect(copiedText).toContain('test-resource-1');
245
- expect(copiedText).toContain('Test Resource');
127
+ // The graph wraps the descriptor and adds annotation collections; a bare
128
+ // ResourceDescriptor would have `id` at the top level and no `resource`.
129
+ expect(parsed.resource.id).toBe('test-resource-1');
130
+ expect(Array.isArray(parsed.annotations)).toBe(true);
131
+ expect(Array.isArray(parsed.entityReferences)).toBe(true);
132
+ expect(parsed.id).toBeUndefined();
246
133
  });
247
134
 
248
- it('should copy formatted JSON', async () => {
249
- const resource = createMockResource();
250
- render(<JsonLdPanel resource={resource} />);
251
-
252
- const copyButton = screen.getByText(/Copy/);
253
- await userEvent.click(copyButton);
254
-
255
- const copiedText = mockClipboard.writeText.mock.calls[0][0];
256
-
257
- // Should be formatted with indentation
258
- expect(copiedText).toContain('\n');
259
- expect(copiedText).toContain(' ');
135
+ it('copies pretty-printed, valid JSON', async () => {
136
+ render(<JsonLdPanel resourceId={RID} />);
137
+ await userEvent.click(screen.getByText(/Copy/));
138
+ const copied = mockClipboard.writeText.mock.calls[0][0];
139
+ expect(copied).toContain('\n');
140
+ expect(copied).toContain(' ');
141
+ expect(() => JSON.parse(copied)).not.toThrow();
142
+ });
260
143
 
261
- // Should be valid JSON
262
- expect(() => JSON.parse(copiedText)).not.toThrow();
144
+ it('does not copy when there is no graph yet', async () => {
145
+ vi.mocked(useResourceGraph).mockReturnValue({ graph: null, loading: true, error: null });
146
+ render(<JsonLdPanel resourceId={RID} />);
147
+ const button = screen.getByText(/Copy/).closest('button')!;
148
+ await userEvent.click(button);
149
+ expect(mockClipboard.writeText).not.toHaveBeenCalled();
263
150
  });
264
151
 
265
- it('should handle clipboard API errors gracefully', async () => {
152
+ it('handles clipboard errors gracefully', async () => {
266
153
  const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
267
154
  mockClipboard.writeText.mockRejectedValue(new Error('Clipboard error'));
268
-
269
- const resource = createMockResource();
270
- render(<JsonLdPanel resource={resource} />);
271
-
272
- const copyButton = screen.getByText(/Copy/);
273
-
274
- await expect(async () => {
275
- await userEvent.click(copyButton);
276
- }).not.toThrow();
277
-
155
+ render(<JsonLdPanel resourceId={RID} />);
156
+ await userEvent.click(screen.getByText(/Copy/));
278
157
  await waitFor(() => {
279
- expect(consoleErrorSpy).toHaveBeenCalledWith(
280
- 'Failed to copy JSON-LD:',
281
- expect.any(Error)
282
- );
158
+ expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to copy JSON-LD:', expect.any(Error));
283
159
  });
284
-
285
160
  consoleErrorSpy.mockRestore();
286
161
  });
287
162
  });
288
163
 
289
- describe('Resource Updates', () => {
290
- it('should reinitialize editor when resource changes', () => {
291
- const resource1 = createMockResource({ id: 'resource-1', name: 'Resource 1' });
292
- const { rerender, container } = render(<JsonLdPanel resource={resource1} />);
293
-
294
- expect(container.firstChild).toBeInTheDocument();
295
-
296
- const resource2 = createMockResource({ id: 'resource-2', name: 'Resource 2' });
297
-
298
- // Should rerender without errors
299
- expect(() => rerender(<JsonLdPanel resource={resource2} />)).not.toThrow();
300
- });
301
-
302
- it('should reinitialize editor when line numbers setting changes', () => {
303
- vi.mocked(useLineNumbers).mockReturnValue({ showLineNumbers: true, toggleLineNumbers: vi.fn() });
304
-
305
- const resource = createMockResource();
306
- const { rerender } = render(<JsonLdPanel resource={resource} />);
307
-
308
- vi.mocked(useLineNumbers).mockReturnValue({ showLineNumbers: false, toggleLineNumbers: vi.fn() });
309
-
310
- // Should rerender without errors
311
- expect(() => rerender(<JsonLdPanel resource={resource} />)).not.toThrow();
312
- });
313
- });
314
-
315
- describe('Edge Cases', () => {
316
- it('should handle resource with minimal data', () => {
317
- const minimalResource: SemiontResource = {
318
- '@context': 'http://www.w3.org/ns/anno.jsonld',
319
- '@id': 'minimal',
320
- id: 'minimal',
321
- name: 'Minimal',
322
- content: 'Test',
323
- format: 'text/plain',
324
- archived: false,
325
- representations: [],
326
- created: '2024-01-01T00:00:00Z',
327
- modified: '2024-01-01T00:00:00Z',
328
- };
329
-
330
- expect(() => {
331
- render(<JsonLdPanel resource={minimalResource} />);
332
- }).not.toThrow();
333
- });
334
-
335
- it('should handle resource with special characters', () => {
336
- const resource = createMockResource({
337
- name: 'Test "Resource" with \'quotes\' & special <chars>',
338
- content: 'Content with\nnewlines\tand\ttabs',
339
- });
340
-
341
- expect(() => {
342
- render(<JsonLdPanel resource={resource} />);
343
- }).not.toThrow();
344
- });
345
-
346
- it('should handle very large resource data', () => {
347
- const largeContent = 'A'.repeat(100000);
348
- const resource = createMockResource({
349
- content: largeContent,
350
- });
351
-
352
- expect(() => {
353
- render(<JsonLdPanel resource={resource} />);
354
- }).not.toThrow();
355
- });
356
-
357
- it('should handle resource with unicode characters', () => {
358
- const resource = createMockResource({
359
- name: '测试资源 テストリソース ресурс тест',
360
- content: '🎉🔥💯 Unicode content',
361
- });
362
-
363
- expect(() => {
364
- render(<JsonLdPanel resource={resource} />);
365
- }).not.toThrow();
366
- });
367
-
368
- it('should handle undefined optional fields', () => {
369
- const resource: SemiontResource = {
370
- '@context': 'http://www.w3.org/ns/anno.jsonld',
371
- '@id': 'test',
372
- id: 'test',
373
- name: 'Test',
374
- content: 'Content',
375
- format: 'text/plain',
376
- archived: false,
377
- representations: [],
378
- created: '2024-01-01T00:00:00Z',
379
- modified: '2024-01-01T00:00:00Z',
380
- // Optional fields omitted
381
- };
382
-
383
- expect(() => {
384
- render(<JsonLdPanel resource={resource} />);
385
- }).not.toThrow();
386
- });
387
- });
388
-
389
- describe('Styling and Appearance', () => {
390
- it('should have proper panel structure', () => {
391
- const resource = createMockResource();
392
- const { container } = render(<JsonLdPanel resource={resource} />);
393
-
394
- const panel = container.firstChild as HTMLElement;
395
- expect(panel).toHaveClass('semiont-jsonld-panel');
396
- });
397
-
398
- it('should have proper header styling', () => {
399
- const resource = createMockResource();
400
- render(<JsonLdPanel resource={resource} />);
401
-
402
- const header = screen.getByText('JSON-LD').parentElement;
403
- expect(header).toHaveClass('semiont-jsonld-panel__header');
404
- });
405
-
406
- it('should have proper button styling', () => {
407
- const resource = createMockResource();
408
- render(<JsonLdPanel resource={resource} />);
409
-
410
- const copyButton = screen.getByText(/Copy/).closest('button');
411
- expect(copyButton).toHaveClass('semiont-button', 'semiont-button--icon');
412
- });
413
-
414
- it('should have proper editor container styling', () => {
415
- const resource = createMockResource();
416
- const { container } = render(<JsonLdPanel resource={resource} />);
417
-
418
- const editorContainer = container.querySelector('.semiont-jsonld-panel__editor');
419
- expect(editorContainer).toBeInTheDocument();
420
- });
421
-
422
- it('should support dark mode styling', () => {
423
- const resource = createMockResource();
424
- const { container } = render(<JsonLdPanel resource={resource} />);
425
-
426
- const panel = container.firstChild as HTMLElement;
427
- expect(panel).toHaveClass('semiont-jsonld-panel');
428
- });
429
- });
430
-
431
- describe('Accessibility', () => {
432
- it('should have title attribute on copy button', () => {
433
- const resource = createMockResource();
434
- render(<JsonLdPanel resource={resource} />);
435
-
164
+ describe('Accessibility / structure', () => {
165
+ it('has the panel structure and a titled copy button', () => {
166
+ const { container } = render(<JsonLdPanel resourceId={RID} />);
167
+ expect(container.firstChild).toHaveClass('semiont-jsonld-panel');
436
168
  const copyButton = screen.getByText(/Copy/).closest('button');
437
169
  expect(copyButton).toHaveAttribute('title', 'Copy to clipboard');
438
- });
439
-
440
- it('should have semantic heading', () => {
441
- const resource = createMockResource();
442
- render(<JsonLdPanel resource={resource} />);
443
-
444
- const heading = screen.getByText('JSON-LD');
445
- expect(heading).toHaveClass('semiont-jsonld-panel__title');
446
- });
447
-
448
- it('should have proper button structure', () => {
449
- const resource = createMockResource();
450
- render(<JsonLdPanel resource={resource} />);
451
-
452
- const copyButton = screen.getByText(/Copy/).closest('button');
453
- expect(copyButton?.tagName).toBe('BUTTON');
454
- });
455
- });
456
-
457
- describe('JSON-LD Format', () => {
458
- it('should produce valid JSON', async () => {
459
- const resource = createMockResource();
460
- render(<JsonLdPanel resource={resource} />);
461
-
462
- const copyButton = screen.getByText(/Copy/);
463
- await userEvent.click(copyButton);
464
-
465
- const copiedText = mockClipboard.writeText.mock.calls[0][0];
466
-
467
- expect(() => {
468
- const parsed = JSON.parse(copiedText);
469
- expect(parsed).toHaveProperty('id');
470
- expect(parsed).toHaveProperty('name');
471
- expect(parsed).toHaveProperty('content');
472
- }).not.toThrow();
473
- });
474
-
475
- it('should include all resource fields', async () => {
476
- const resource = createMockResource({
477
- id: 'full-resource',
478
- name: 'Full Resource',
479
- content: 'Complete content',
480
- format: 'text/markdown',
481
- archived: true,
482
- entityTypes: ['Person', 'Organization'],
483
- locale: 'en-US',
484
- });
485
-
486
- render(<JsonLdPanel resource={resource} />);
487
-
488
- const copyButton = screen.getByText(/Copy/);
489
- await userEvent.click(copyButton);
490
-
491
- const copiedText = mockClipboard.writeText.mock.calls[0][0];
492
- const parsed = JSON.parse(copiedText);
493
-
494
- expect(parsed.id).toBe('full-resource');
495
- expect(parsed.name).toBe('Full Resource');
496
- expect(parsed.format).toBe('text/markdown');
497
- expect(parsed.archived).toBe(true);
498
- expect(parsed.entityTypes).toEqual(['Person', 'Organization']);
499
- expect(parsed.locale).toBe('en-US');
170
+ expect(screen.getByText('JSON-LD')).toHaveClass('semiont-jsonld-panel__title');
500
171
  });
501
172
  });
502
173
  });
@@ -35,7 +35,7 @@ vi.mock('../../../../contexts/TranslationContext', () => ({
35
35
  TranslationProvider: ({ children }: { children: React.ReactNode }) => children,
36
36
  }));
37
37
 
38
- // Mock @semiont/api-client utilities
38
+ // Mock @semiont/http-transport utilities
39
39
  vi.mock('@semiont/core', async () => {
40
40
  const actual = await vi.importActual('@semiont/core');
41
41
  return {
@@ -9,7 +9,7 @@ import type { TagSchema } from '@semiont/core';
9
9
 
10
10
  import type { Annotation, AnnotationId } from '@semiont/core';
11
11
 
12
- // Mock @semiont/api-client
12
+ // Mock @semiont/http-transport
13
13
  vi.mock('@semiont/core', async () => {
14
14
  const actual = await vi.importActual('@semiont/core');
15
15
  return {
@@ -112,7 +112,7 @@ vi.mock('../../../../contexts/TranslationContext', () => ({
112
112
  TranslationProvider: ({ children }: { children: React.ReactNode }) => children,
113
113
  }));
114
114
 
115
- // Mock @semiont/api-client utilities
115
+ // Mock @semiont/http-transport utilities
116
116
  vi.mock('@semiont/core', async () => {
117
117
  const actual = await vi.importActual('@semiont/core');
118
118
  return {
@@ -45,16 +45,12 @@ const createProps = (overrides?: Partial<AdminExchangePageProps>): AdminExchange
45
45
  },
46
46
  progress: {
47
47
  phaseStarted: 'Starting…',
48
- phaseEntityTypes: 'Entity types…',
49
- phaseResources: 'Resources…',
50
- phaseAnnotations: 'Annotations…',
51
48
  phaseComplete: 'Complete',
52
49
  phaseError: 'Failed',
53
- hashChainValid: 'Hash valid',
54
- hashChainInvalid: 'Hash invalid',
55
- streams: 'Streams',
56
- events: 'Events',
57
- blobs: 'Blobs',
50
+ statsEventsReplayed: 'Events replayed',
51
+ statsResourcesCreated: 'Resources created',
52
+ statsAnnotationsCreated: 'Annotations created',
53
+ statsEntityTypesAdded: 'Entity types added',
58
54
  },
59
55
  },
60
56
  ToolbarPanels: () => <div data-testid="toolbar-panels" />,
@@ -100,10 +96,11 @@ describe('AdminExchangePage', () => {
100
96
  it('renders ImportProgress with backup result on completion', () => {
101
97
  render(<AdminExchangePage {...createProps({
102
98
  importPhase: 'complete',
103
- importResult: { stats: { streams: 5, events: 42, blobs: 3 }, hashChainValid: true },
99
+ importResult: { stats: { eventsReplayed: 42, resourcesCreated: 5, annotationsCreated: 12, entityTypesAdded: 3 } },
104
100
  })} />);
105
101
  expect(screen.getByText('Complete')).toBeInTheDocument();
106
- expect(screen.getByText('5')).toBeInTheDocument();
102
+ expect(screen.getByText('42')).toBeInTheDocument();
103
+ expect(screen.getByText('Events replayed')).toBeInTheDocument();
107
104
  });
108
105
 
109
106
  it('applies panel-open class when common panel is active', () => {