@semiont/react-ui 0.2.36 → 0.2.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/index.d.mts +8 -0
  2. package/dist/index.mjs +252 -166
  3. package/dist/index.mjs.map +1 -1
  4. package/package.json +1 -1
  5. package/src/components/CodeMirrorRenderer.tsx +71 -203
  6. package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +142 -0
  7. package/src/components/__tests__/LiveRegion.hooks.test.tsx +79 -0
  8. package/src/components/__tests__/ResizeHandle.test.tsx +165 -0
  9. package/src/components/__tests__/SessionExpiryBanner.test.tsx +123 -0
  10. package/src/components/__tests__/StatusDisplay.test.tsx +160 -0
  11. package/src/components/__tests__/Toolbar.test.tsx +110 -0
  12. package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +285 -0
  13. package/src/components/annotation-popups/__tests__/SharedPopupElements.test.tsx +273 -0
  14. package/src/components/modals/__tests__/KeyboardShortcutsHelpModal.test.tsx +90 -0
  15. package/src/components/modals/__tests__/ProposeEntitiesModal.test.tsx +129 -0
  16. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +180 -0
  17. package/src/components/navigation/__tests__/ObservableLink.test.tsx +90 -0
  18. package/src/components/navigation/__tests__/SimpleNavigation.test.tsx +169 -0
  19. package/src/components/navigation/__tests__/SortableResourceTab.test.tsx +371 -0
  20. package/src/components/resource/AnnotateView.tsx +27 -153
  21. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +349 -0
  22. package/src/components/resource/__tests__/HistoryEvent.test.tsx +492 -0
  23. package/src/components/resource/__tests__/event-formatting.test.ts +273 -0
  24. package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +226 -0
  25. package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +188 -0
  26. package/src/components/resource/panels/__tests__/PanelHeader.test.tsx +69 -0
  27. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +445 -0
  28. package/src/components/resource/panels/__tests__/StatisticsPanel.test.tsx +271 -0
  29. package/src/components/resource/panels/__tests__/TagEntry.test.tsx +210 -0
  30. package/src/components/settings/__tests__/SettingsPanel.test.tsx +190 -0
  31. package/src/components/viewers/__tests__/ImageViewer.test.tsx +63 -0
  32. package/src/integrations/__tests__/css-modules-helper.test.tsx +225 -0
  33. package/src/integrations/__tests__/styled-components-theme.test.ts +179 -0
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import React from 'react';
3
+ import { screen } from '@testing-library/react';
4
+ import '@testing-library/jest-dom';
5
+ import { renderWithProviders } from '../../../../test-utils';
6
+ import { PanelHeader } from '../PanelHeader';
7
+
8
+ describe('PanelHeader', () => {
9
+ it('should render the title text', () => {
10
+ renderWithProviders(
11
+ <PanelHeader annotationType="highlight" count={5} title="Highlights" />
12
+ );
13
+
14
+ expect(screen.getByText('Highlights')).toBeInTheDocument();
15
+ });
16
+
17
+ it('should render the count in parentheses', () => {
18
+ renderWithProviders(
19
+ <PanelHeader annotationType="comment" count={12} title="Comments" />
20
+ );
21
+
22
+ expect(screen.getByText('(12)')).toBeInTheDocument();
23
+ });
24
+
25
+ it('should render with zero count', () => {
26
+ renderWithProviders(
27
+ <PanelHeader annotationType="tag" count={0} title="Tags" />
28
+ );
29
+
30
+ expect(screen.getByText('(0)')).toBeInTheDocument();
31
+ expect(screen.getByText('Tags')).toBeInTheDocument();
32
+ });
33
+
34
+ it('should render with different annotation types', () => {
35
+ const types = ['highlight', 'reference', 'assessment', 'comment', 'tag'] as const;
36
+
37
+ for (const type of types) {
38
+ const { unmount } = renderWithProviders(
39
+ <PanelHeader annotationType={type} count={3} title={`${type} title`} />
40
+ );
41
+
42
+ expect(screen.getByText(`${type} title`)).toBeInTheDocument();
43
+ expect(screen.getByText('(3)')).toBeInTheDocument();
44
+ unmount();
45
+ }
46
+ });
47
+
48
+ it('should render with correct class names', () => {
49
+ const { container } = renderWithProviders(
50
+ <PanelHeader annotationType="highlight" count={1} title="Highlights" />
51
+ );
52
+
53
+ expect(container.querySelector('.semiont-panel-header')).toBeInTheDocument();
54
+ expect(container.querySelector('.semiont-panel-header__title')).toBeInTheDocument();
55
+ expect(container.querySelector('.semiont-panel-header__text')).toBeInTheDocument();
56
+ expect(container.querySelector('.semiont-panel-header__count')).toBeInTheDocument();
57
+ });
58
+
59
+ it('should render title inside an h2 element', () => {
60
+ renderWithProviders(
61
+ <PanelHeader annotationType="assessment" count={7} title="Assessments" />
62
+ );
63
+
64
+ const heading = screen.getByRole('heading', { level: 2 });
65
+ expect(heading).toBeInTheDocument();
66
+ expect(heading).toHaveTextContent('Assessments');
67
+ expect(heading).toHaveTextContent('(7)');
68
+ });
69
+ });
@@ -0,0 +1,445 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import React from 'react';
3
+ import { screen } from '@testing-library/react';
4
+ import '@testing-library/jest-dom';
5
+ import { renderWithProviders, resetEventBusForTesting } from '../../../../test-utils';
6
+ import userEvent from '@testing-library/user-event';
7
+ import type { components } from '@semiont/core';
8
+ import type { RouteBuilder } from '../../../../contexts/RoutingContext';
9
+
10
+ type Annotation = components['schemas']['Annotation'];
11
+
12
+ // Stable mock functions defined outside vi.mock to avoid re-render loops
13
+ const mockGetAnnotationExactText = vi.fn();
14
+ const mockIsBodyResolved = vi.fn();
15
+ const mockGetBodySource = vi.fn();
16
+ const mockGetFragmentSelector = vi.fn();
17
+ const mockGetSvgSelector = vi.fn();
18
+ const mockGetTargetSelector = vi.fn();
19
+ const mockGetEntityTypes = vi.fn();
20
+ const mockNavigate = vi.fn();
21
+ const mockHoverProps = { onMouseEnter: vi.fn(), onMouseLeave: vi.fn() };
22
+
23
+ vi.mock('@semiont/api-client', async () => {
24
+ const actual = await vi.importActual('@semiont/api-client');
25
+ return {
26
+ ...actual,
27
+ getAnnotationExactText: (...args: unknown[]) => mockGetAnnotationExactText(...args),
28
+ isBodyResolved: (...args: unknown[]) => mockIsBodyResolved(...args),
29
+ getBodySource: (...args: unknown[]) => mockGetBodySource(...args),
30
+ getFragmentSelector: (...args: unknown[]) => mockGetFragmentSelector(...args),
31
+ getSvgSelector: (...args: unknown[]) => mockGetSvgSelector(...args),
32
+ getTargetSelector: (...args: unknown[]) => mockGetTargetSelector(...args),
33
+ };
34
+ });
35
+
36
+ vi.mock('@semiont/ontology', () => ({
37
+ getEntityTypes: (...args: unknown[]) => mockGetEntityTypes(...args),
38
+ }));
39
+
40
+ vi.mock('../../../../lib/resource-utils', () => ({
41
+ getResourceIcon: vi.fn(() => '📄'),
42
+ }));
43
+
44
+ vi.mock('../../../../hooks/useObservableBrowse', () => ({
45
+ useObservableExternalNavigation: () => mockNavigate,
46
+ }));
47
+
48
+ vi.mock('../../../../hooks/useBeckonFlow', () => ({
49
+ useHoverEmitter: () => mockHoverProps,
50
+ }));
51
+
52
+ import { ReferenceEntry } from '../ReferenceEntry';
53
+
54
+ const createMockReference = (overrides?: Partial<Annotation>): Annotation => ({
55
+ '@context': 'http://www.w3.org/ns/anno.jsonld',
56
+ id: 'http://example.com/annotations/ref-1',
57
+ type: 'Annotation',
58
+ motivation: 'linking',
59
+ created: '2024-06-15T12:00:00Z',
60
+ modified: '2024-06-15T12:00:00Z',
61
+ target: {
62
+ source: 'http://example.com/resources/resource-1',
63
+ selector: {
64
+ type: 'TextQuoteSelector',
65
+ exact: 'referenced text',
66
+ },
67
+ },
68
+ body: {
69
+ source: 'http://example.com/resources/linked-doc',
70
+ },
71
+ ...overrides,
72
+ });
73
+
74
+ const mockRoutes: RouteBuilder = {
75
+ resourceDetail: vi.fn((id: string) => `/resources/${id}`),
76
+ resourceList: vi.fn(() => '/resources'),
77
+ };
78
+
79
+ describe('ReferenceEntry', () => {
80
+ const defaultProps = {
81
+ reference: createMockReference(),
82
+ isFocused: false,
83
+ routes: mockRoutes,
84
+ };
85
+
86
+ beforeEach(() => {
87
+ vi.clearAllMocks();
88
+ resetEventBusForTesting();
89
+ mockGetAnnotationExactText.mockReturnValue('referenced text');
90
+ mockIsBodyResolved.mockReturnValue(false);
91
+ mockGetBodySource.mockReturnValue(null);
92
+ mockGetTargetSelector.mockReturnValue(null);
93
+ mockGetFragmentSelector.mockReturnValue(null);
94
+ mockGetSvgSelector.mockReturnValue(null);
95
+ mockGetEntityTypes.mockReturnValue([]);
96
+ });
97
+
98
+ describe('Rendering', () => {
99
+ it('should render the selected text in quotes', () => {
100
+ renderWithProviders(<ReferenceEntry {...defaultProps} />);
101
+
102
+ expect(screen.getByText(/referenced text/)).toBeInTheDocument();
103
+ });
104
+
105
+ it('should truncate text over 100 characters', () => {
106
+ const longText = 'A'.repeat(150);
107
+ mockGetAnnotationExactText.mockReturnValue(longText);
108
+
109
+ renderWithProviders(<ReferenceEntry {...defaultProps} />);
110
+
111
+ expect(screen.getByText(new RegExp(`"${'A'.repeat(100)}`))).toBeInTheDocument();
112
+ expect(screen.getByText(/\.\.\./)).toBeInTheDocument();
113
+ });
114
+
115
+ it('should show stub icon when reference is not resolved', () => {
116
+ mockIsBodyResolved.mockReturnValue(false);
117
+
118
+ const { container } = renderWithProviders(<ReferenceEntry {...defaultProps} />);
119
+
120
+ const icon = container.querySelector('.semiont-reference-icon');
121
+ expect(icon).toBeInTheDocument();
122
+ expect(icon!.textContent).toContain('❓');
123
+ });
124
+
125
+ it('should show link icon when reference is resolved', () => {
126
+ mockIsBodyResolved.mockReturnValue(true);
127
+ mockGetBodySource.mockReturnValue('http://example.com/resources/linked-doc');
128
+
129
+ const { container } = renderWithProviders(<ReferenceEntry {...defaultProps} />);
130
+
131
+ const icon = container.querySelector('.semiont-reference-icon');
132
+ expect(icon).toBeInTheDocument();
133
+ expect(icon!.textContent).toContain('🔗');
134
+ });
135
+
136
+ it('should show annotation type when no selected text', () => {
137
+ mockGetAnnotationExactText.mockReturnValue('');
138
+
139
+ renderWithProviders(<ReferenceEntry {...defaultProps} />);
140
+
141
+ expect(screen.getByText('Annotation')).toBeInTheDocument();
142
+ });
143
+
144
+ it('should show Fragment annotation for fragment selectors', () => {
145
+ mockGetAnnotationExactText.mockReturnValue('');
146
+ mockGetFragmentSelector.mockReturnValue({ type: 'FragmentSelector', value: 'xywh=0,0,100,100' });
147
+
148
+ renderWithProviders(<ReferenceEntry {...defaultProps} />);
149
+
150
+ expect(screen.getByText('Fragment annotation')).toBeInTheDocument();
151
+ });
152
+
153
+ it('should show Image annotation for SVG selectors', () => {
154
+ mockGetAnnotationExactText.mockReturnValue('');
155
+ mockGetSvgSelector.mockReturnValue({ type: 'SvgSelector', value: '<svg/>' });
156
+
157
+ renderWithProviders(<ReferenceEntry {...defaultProps} />);
158
+
159
+ expect(screen.getByText('Image annotation')).toBeInTheDocument();
160
+ });
161
+
162
+ it('should render resolved document name when enriched', () => {
163
+ mockIsBodyResolved.mockReturnValue(true);
164
+ mockGetBodySource.mockReturnValue('http://example.com/resources/linked-doc');
165
+
166
+ const enrichedRef = {
167
+ ...createMockReference(),
168
+ _resolvedDocumentName: 'My Linked Document',
169
+ _resolvedDocumentMediaType: 'text/plain',
170
+ };
171
+
172
+ renderWithProviders(<ReferenceEntry {...defaultProps} reference={enrichedRef as Annotation} />);
173
+
174
+ expect(screen.getByText(/My Linked Document/)).toBeInTheDocument();
175
+ });
176
+ });
177
+
178
+ describe('Entity types', () => {
179
+ it('should render entity type badges', () => {
180
+ mockGetEntityTypes.mockReturnValue(['Person', 'Organization']);
181
+
182
+ renderWithProviders(<ReferenceEntry {...defaultProps} />);
183
+
184
+ expect(screen.getByText('Person')).toBeInTheDocument();
185
+ expect(screen.getByText('Organization')).toBeInTheDocument();
186
+ });
187
+
188
+ it('should not render entity type section when empty', () => {
189
+ mockGetEntityTypes.mockReturnValue([]);
190
+
191
+ const { container } = renderWithProviders(<ReferenceEntry {...defaultProps} />);
192
+
193
+ expect(container.querySelector('.semiont-annotation-entry__tags')).not.toBeInTheDocument();
194
+ });
195
+ });
196
+
197
+ describe('Focus and hover state', () => {
198
+ it('should set data-focused to true when focused', () => {
199
+ const { container } = renderWithProviders(
200
+ <ReferenceEntry {...defaultProps} isFocused={true} />
201
+ );
202
+
203
+ const entry = container.firstChild as HTMLElement;
204
+ expect(entry).toHaveAttribute('data-focused', 'true');
205
+ });
206
+
207
+ it('should set data-focused to false when not focused', () => {
208
+ const { container } = renderWithProviders(
209
+ <ReferenceEntry {...defaultProps} isFocused={false} />
210
+ );
211
+
212
+ const entry = container.firstChild as HTMLElement;
213
+ expect(entry).toHaveAttribute('data-focused', 'false');
214
+ });
215
+
216
+ it('should apply pulse class when isHovered is true', () => {
217
+ const { container } = renderWithProviders(
218
+ <ReferenceEntry {...defaultProps} isHovered={true} />
219
+ );
220
+
221
+ const entry = container.firstChild as HTMLElement;
222
+ expect(entry).toHaveClass('semiont-annotation-pulse');
223
+ });
224
+
225
+ it('should not apply pulse class when isHovered is false', () => {
226
+ const { container } = renderWithProviders(
227
+ <ReferenceEntry {...defaultProps} isHovered={false} />
228
+ );
229
+
230
+ const entry = container.firstChild as HTMLElement;
231
+ expect(entry).not.toHaveClass('semiont-annotation-pulse');
232
+ });
233
+ });
234
+
235
+ describe('Click events', () => {
236
+ it('should emit browse:click on click', async () => {
237
+ const clickHandler = vi.fn();
238
+
239
+ const { container, eventBus } = renderWithProviders(
240
+ <ReferenceEntry {...defaultProps} />,
241
+ { returnEventBus: true }
242
+ );
243
+
244
+ const subscription = eventBus!.get('browse:click').subscribe(clickHandler);
245
+
246
+ const entry = container.firstChild as HTMLElement;
247
+ await userEvent.click(entry);
248
+
249
+ expect(clickHandler).toHaveBeenCalledWith({
250
+ annotationId: 'http://example.com/annotations/ref-1',
251
+ motivation: 'linking',
252
+ });
253
+
254
+ subscription.unsubscribe();
255
+ });
256
+ });
257
+
258
+ describe('Resolved reference actions', () => {
259
+ it('should show open button when resolved', () => {
260
+ mockIsBodyResolved.mockReturnValue(true);
261
+ mockGetBodySource.mockReturnValue('http://example.com/resources/linked-doc');
262
+
263
+ const { container } = renderWithProviders(<ReferenceEntry {...defaultProps} />);
264
+
265
+ const openButton = container.querySelector('button[title="ReferencesPanel.open"]');
266
+ expect(openButton).toBeInTheDocument();
267
+ });
268
+
269
+ it('should show unlink button when resolved and in annotate mode', () => {
270
+ mockIsBodyResolved.mockReturnValue(true);
271
+ mockGetBodySource.mockReturnValue('http://example.com/resources/linked-doc');
272
+
273
+ const { container } = renderWithProviders(
274
+ <ReferenceEntry {...defaultProps} annotateMode={true} />
275
+ );
276
+
277
+ const unlinkButton = container.querySelector('button[title="ReferencesPanel.unlink"]');
278
+ expect(unlinkButton).toBeInTheDocument();
279
+ });
280
+
281
+ it('should not show unlink button when not in annotate mode', () => {
282
+ mockIsBodyResolved.mockReturnValue(true);
283
+ mockGetBodySource.mockReturnValue('http://example.com/resources/linked-doc');
284
+
285
+ const { container } = renderWithProviders(
286
+ <ReferenceEntry {...defaultProps} annotateMode={false} />
287
+ );
288
+
289
+ const unlinkButton = container.querySelector('button[title="ReferencesPanel.unlink"]');
290
+ expect(unlinkButton).not.toBeInTheDocument();
291
+ });
292
+
293
+ it('should navigate on open click', async () => {
294
+ mockIsBodyResolved.mockReturnValue(true);
295
+ mockGetBodySource.mockReturnValue('http://example.com/resources/linked-doc');
296
+
297
+ const { container } = renderWithProviders(<ReferenceEntry {...defaultProps} />);
298
+
299
+ const openButton = container.querySelector('button[title="ReferencesPanel.open"]')!;
300
+ await userEvent.click(openButton);
301
+
302
+ expect(mockRoutes.resourceDetail).toHaveBeenCalledWith('linked-doc');
303
+ expect(mockNavigate).toHaveBeenCalledWith('/resources/linked-doc', { resourceId: 'linked-doc' });
304
+ });
305
+
306
+ it('should emit bind:update-body on unlink click', async () => {
307
+ mockIsBodyResolved.mockReturnValue(true);
308
+ mockGetBodySource.mockReturnValue('http://example.com/resources/linked-doc');
309
+ const unlinkHandler = vi.fn();
310
+
311
+ const { container, eventBus } = renderWithProviders(
312
+ <ReferenceEntry {...defaultProps} annotateMode={true} />,
313
+ { returnEventBus: true }
314
+ );
315
+
316
+ const subscription = eventBus!.get('bind:update-body').subscribe(unlinkHandler);
317
+
318
+ const unlinkButton = container.querySelector('button[title="ReferencesPanel.unlink"]')!;
319
+ await userEvent.click(unlinkButton);
320
+
321
+ expect(unlinkHandler).toHaveBeenCalledWith({
322
+ annotationUri: 'http://example.com/annotations/ref-1',
323
+ resourceId: 'resource-1',
324
+ operations: [{ op: 'remove' }],
325
+ });
326
+
327
+ subscription.unsubscribe();
328
+ });
329
+ });
330
+
331
+ describe('Stub reference actions', () => {
332
+ it('should show generate, search, and create buttons in annotate mode', () => {
333
+ mockIsBodyResolved.mockReturnValue(false);
334
+
335
+ const { container } = renderWithProviders(
336
+ <ReferenceEntry {...defaultProps} annotateMode={true} />
337
+ );
338
+
339
+ expect(container.querySelector('button[title="ReferencesPanel.generate"]')).toBeInTheDocument();
340
+ expect(container.querySelector('button[title="ReferencesPanel.find"]')).toBeInTheDocument();
341
+ expect(container.querySelector('button[title="ReferencesPanel.create"]')).toBeInTheDocument();
342
+ });
343
+
344
+ it('should not show stub actions when not in annotate mode', () => {
345
+ mockIsBodyResolved.mockReturnValue(false);
346
+
347
+ const { container } = renderWithProviders(
348
+ <ReferenceEntry {...defaultProps} annotateMode={false} />
349
+ );
350
+
351
+ expect(container.querySelector('button[title="ReferencesPanel.generate"]')).not.toBeInTheDocument();
352
+ expect(container.querySelector('button[title="ReferencesPanel.find"]')).not.toBeInTheDocument();
353
+ expect(container.querySelector('button[title="ReferencesPanel.create"]')).not.toBeInTheDocument();
354
+ });
355
+
356
+ it('should emit yield:modal-open on generate click', async () => {
357
+ mockIsBodyResolved.mockReturnValue(false);
358
+ const generateHandler = vi.fn();
359
+
360
+ const { container, eventBus } = renderWithProviders(
361
+ <ReferenceEntry {...defaultProps} annotateMode={true} />,
362
+ { returnEventBus: true }
363
+ );
364
+
365
+ const subscription = eventBus!.get('yield:modal-open').subscribe(generateHandler);
366
+
367
+ const generateButton = container.querySelector('button[title="ReferencesPanel.generate"]')!;
368
+ await userEvent.click(generateButton);
369
+
370
+ expect(generateHandler).toHaveBeenCalledWith({
371
+ annotationUri: 'http://example.com/annotations/ref-1',
372
+ resourceUri: 'http://example.com/resources/resource-1',
373
+ defaultTitle: 'referenced text',
374
+ });
375
+
376
+ subscription.unsubscribe();
377
+ });
378
+
379
+ it('should emit bind:link on search click', async () => {
380
+ mockIsBodyResolved.mockReturnValue(false);
381
+ const searchHandler = vi.fn();
382
+
383
+ const { container, eventBus } = renderWithProviders(
384
+ <ReferenceEntry {...defaultProps} annotateMode={true} />,
385
+ { returnEventBus: true }
386
+ );
387
+
388
+ const subscription = eventBus!.get('bind:link').subscribe(searchHandler);
389
+
390
+ const searchButton = container.querySelector('button[title="ReferencesPanel.find"]')!;
391
+ await userEvent.click(searchButton);
392
+
393
+ expect(searchHandler).toHaveBeenCalledWith({
394
+ annotationUri: 'http://example.com/annotations/ref-1',
395
+ searchTerm: 'referenced text',
396
+ });
397
+
398
+ subscription.unsubscribe();
399
+ });
400
+
401
+ it('should emit bind:create-manual on create click', async () => {
402
+ mockIsBodyResolved.mockReturnValue(false);
403
+ mockGetEntityTypes.mockReturnValue(['Person']);
404
+ const createHandler = vi.fn();
405
+
406
+ const { container, eventBus } = renderWithProviders(
407
+ <ReferenceEntry {...defaultProps} annotateMode={true} />,
408
+ { returnEventBus: true }
409
+ );
410
+
411
+ const subscription = eventBus!.get('bind:create-manual').subscribe(createHandler);
412
+
413
+ const createButton = container.querySelector('button[title="ReferencesPanel.create"]')!;
414
+ await userEvent.click(createButton);
415
+
416
+ expect(createHandler).toHaveBeenCalledWith({
417
+ annotationUri: 'http://example.com/annotations/ref-1',
418
+ title: 'referenced text',
419
+ entityTypes: ['Person'],
420
+ });
421
+
422
+ subscription.unsubscribe();
423
+ });
424
+
425
+ it('should set data-generating on generate button', () => {
426
+ mockIsBodyResolved.mockReturnValue(false);
427
+
428
+ const { container } = renderWithProviders(
429
+ <ReferenceEntry {...defaultProps} annotateMode={true} isGenerating={true} />
430
+ );
431
+
432
+ const generateButton = container.querySelector('button[title="ReferencesPanel.generate"]');
433
+ expect(generateButton).toHaveAttribute('data-generating', 'true');
434
+ });
435
+ });
436
+
437
+ describe('data-type attribute', () => {
438
+ it('should have data-type="reference"', () => {
439
+ const { container } = renderWithProviders(<ReferenceEntry {...defaultProps} />);
440
+
441
+ const entry = container.firstChild as HTMLElement;
442
+ expect(entry).toHaveAttribute('data-type', 'reference');
443
+ });
444
+ });
445
+ });