@semiont/react-ui 0.2.36 → 0.2.38
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 +8 -0
- package/dist/index.mjs +252 -166
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/CodeMirrorRenderer.tsx +71 -203
- package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +142 -0
- package/src/components/__tests__/LiveRegion.hooks.test.tsx +79 -0
- package/src/components/__tests__/ResizeHandle.test.tsx +165 -0
- package/src/components/__tests__/SessionExpiryBanner.test.tsx +123 -0
- package/src/components/__tests__/StatusDisplay.test.tsx +160 -0
- package/src/components/__tests__/Toolbar.test.tsx +110 -0
- package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +285 -0
- package/src/components/annotation-popups/__tests__/SharedPopupElements.test.tsx +273 -0
- package/src/components/modals/__tests__/KeyboardShortcutsHelpModal.test.tsx +90 -0
- package/src/components/modals/__tests__/ProposeEntitiesModal.test.tsx +129 -0
- package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +180 -0
- package/src/components/navigation/__tests__/ObservableLink.test.tsx +90 -0
- package/src/components/navigation/__tests__/SimpleNavigation.test.tsx +169 -0
- package/src/components/navigation/__tests__/SortableResourceTab.test.tsx +371 -0
- package/src/components/resource/AnnotateView.tsx +27 -153
- package/src/components/resource/__tests__/AnnotationHistory.test.tsx +349 -0
- package/src/components/resource/__tests__/HistoryEvent.test.tsx +492 -0
- package/src/components/resource/__tests__/event-formatting.test.ts +273 -0
- package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +226 -0
- package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +188 -0
- package/src/components/resource/panels/__tests__/PanelHeader.test.tsx +69 -0
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +445 -0
- package/src/components/resource/panels/__tests__/StatisticsPanel.test.tsx +271 -0
- package/src/components/resource/panels/__tests__/TagEntry.test.tsx +210 -0
- package/src/components/settings/__tests__/SettingsPanel.test.tsx +190 -0
- package/src/components/viewers/__tests__/ImageViewer.test.tsx +63 -0
- package/src/integrations/__tests__/css-modules-helper.test.tsx +225 -0
- package/src/integrations/__tests__/styled-components-theme.test.ts +179 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { screen, fireEvent, act } from '@testing-library/react';
|
|
4
|
+
import '@testing-library/jest-dom';
|
|
5
|
+
import { HistoryEvent } from '../HistoryEvent';
|
|
6
|
+
import { renderWithProviders } from '../../../test-utils';
|
|
7
|
+
import type { StoredEvent } from '@semiont/core';
|
|
8
|
+
|
|
9
|
+
// Mock @semiont/core - must use importOriginal to preserve EventBus etc.
|
|
10
|
+
vi.mock('@semiont/core', async (importOriginal) => {
|
|
11
|
+
const actual = await importOriginal<typeof import('@semiont/core')>();
|
|
12
|
+
return {
|
|
13
|
+
...actual,
|
|
14
|
+
getAnnotationUriFromEvent: vi.fn(() => null),
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Stable mock return values (defined outside vi.mock to avoid re-render loops)
|
|
19
|
+
const mockDisplayContent = { exact: 'Test content', isTag: false, isQuoted: false };
|
|
20
|
+
const mockEmptyDisplayContent = null;
|
|
21
|
+
const mockTagContent = { exact: 'Person', isTag: true, isQuoted: false };
|
|
22
|
+
const mockQuotedContent = { exact: 'quoted text', isTag: false, isQuoted: true };
|
|
23
|
+
const mockEntityTypes: string[] = [];
|
|
24
|
+
const mockCreationDetails = null;
|
|
25
|
+
|
|
26
|
+
// Mock event-formatting utilities
|
|
27
|
+
vi.mock('../event-formatting', () => ({
|
|
28
|
+
formatEventType: vi.fn((_type: string, t: (key: string) => string) => t('resourceCreated')),
|
|
29
|
+
getEventEmoji: vi.fn(() => '\u{1F4C4}'),
|
|
30
|
+
formatRelativeTime: vi.fn(() => '2 minutes ago'),
|
|
31
|
+
getEventDisplayContent: vi.fn(() => mockDisplayContent),
|
|
32
|
+
getEventEntityTypes: vi.fn(() => mockEntityTypes),
|
|
33
|
+
getResourceCreationDetails: vi.fn(() => mockCreationDetails),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
import { getAnnotationUriFromEvent } from '@semiont/core';
|
|
37
|
+
import {
|
|
38
|
+
formatEventType,
|
|
39
|
+
getEventEmoji,
|
|
40
|
+
formatRelativeTime,
|
|
41
|
+
getEventDisplayContent,
|
|
42
|
+
getEventEntityTypes,
|
|
43
|
+
getResourceCreationDetails,
|
|
44
|
+
} from '../event-formatting';
|
|
45
|
+
|
|
46
|
+
const mockGetAnnotationUri = getAnnotationUriFromEvent as ReturnType<typeof vi.fn>;
|
|
47
|
+
const mockGetEventDisplayContent = getEventDisplayContent as ReturnType<typeof vi.fn>;
|
|
48
|
+
const mockGetEventEntityTypes = getEventEntityTypes as ReturnType<typeof vi.fn>;
|
|
49
|
+
const mockGetResourceCreationDetails = getResourceCreationDetails as ReturnType<typeof vi.fn>;
|
|
50
|
+
const mockFormatEventType = formatEventType as ReturnType<typeof vi.fn>;
|
|
51
|
+
|
|
52
|
+
function makeStoredEvent(overrides: Partial<StoredEvent['event']> = {}): StoredEvent {
|
|
53
|
+
return {
|
|
54
|
+
event: {
|
|
55
|
+
id: 'evt-1',
|
|
56
|
+
type: 'resource.created',
|
|
57
|
+
timestamp: '2026-03-06T12:00:00Z',
|
|
58
|
+
resourceId: 'http://localhost/resources/res-1',
|
|
59
|
+
userId: 'user-1',
|
|
60
|
+
version: 1,
|
|
61
|
+
payload: { name: 'Test', format: 'text/plain', contentChecksum: 'abc', creationMethod: 'upload' },
|
|
62
|
+
...overrides,
|
|
63
|
+
},
|
|
64
|
+
metadata: {
|
|
65
|
+
sequenceNumber: 1,
|
|
66
|
+
storedAt: '2026-03-06T12:00:00Z',
|
|
67
|
+
},
|
|
68
|
+
} as StoredEvent;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const mockT = (key: string) => key;
|
|
72
|
+
const MockLink = ({ href, children, ...props }: any) => <a href={href} {...props}>{children}</a>;
|
|
73
|
+
const mockRoutes = {
|
|
74
|
+
resourceDetail: (id: string) => `/resources/${id}`,
|
|
75
|
+
} as any;
|
|
76
|
+
|
|
77
|
+
describe('HistoryEvent', () => {
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
vi.clearAllMocks();
|
|
80
|
+
mockGetAnnotationUri.mockReturnValue(null);
|
|
81
|
+
mockGetEventDisplayContent.mockReturnValue(mockDisplayContent);
|
|
82
|
+
mockGetEventEntityTypes.mockReturnValue(mockEntityTypes);
|
|
83
|
+
mockGetResourceCreationDetails.mockReturnValue(mockCreationDetails);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('renders basic event with display content', () => {
|
|
87
|
+
const event = makeStoredEvent();
|
|
88
|
+
renderWithProviders(
|
|
89
|
+
<HistoryEvent
|
|
90
|
+
event={event}
|
|
91
|
+
annotations={[]}
|
|
92
|
+
allEvents={[event]}
|
|
93
|
+
isRelated={false}
|
|
94
|
+
t={mockT}
|
|
95
|
+
Link={MockLink}
|
|
96
|
+
routes={mockRoutes}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expect(screen.getByText('Test content')).toBeInTheDocument();
|
|
101
|
+
expect(screen.getByText('2 minutes ago')).toBeInTheDocument();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('renders emoji from getEventEmoji', () => {
|
|
105
|
+
const event = makeStoredEvent();
|
|
106
|
+
renderWithProviders(
|
|
107
|
+
<HistoryEvent
|
|
108
|
+
event={event}
|
|
109
|
+
annotations={[]}
|
|
110
|
+
allEvents={[event]}
|
|
111
|
+
isRelated={false}
|
|
112
|
+
t={mockT}
|
|
113
|
+
Link={MockLink}
|
|
114
|
+
routes={mockRoutes}
|
|
115
|
+
/>
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
expect(screen.getByText('\u{1F4C4}')).toBeInTheDocument();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('renders as div when no annotationUri', () => {
|
|
122
|
+
mockGetAnnotationUri.mockReturnValue(null);
|
|
123
|
+
const event = makeStoredEvent();
|
|
124
|
+
const { container } = renderWithProviders(
|
|
125
|
+
<HistoryEvent
|
|
126
|
+
event={event}
|
|
127
|
+
annotations={[]}
|
|
128
|
+
allEvents={[event]}
|
|
129
|
+
isRelated={false}
|
|
130
|
+
t={mockT}
|
|
131
|
+
Link={MockLink}
|
|
132
|
+
routes={mockRoutes}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const wrapper = container.querySelector('.semiont-history-event');
|
|
137
|
+
expect(wrapper?.tagName).toBe('DIV');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('renders as button when annotationUri exists', () => {
|
|
141
|
+
mockGetAnnotationUri.mockReturnValue('http://localhost/annotations/ann-1');
|
|
142
|
+
const event = makeStoredEvent({ type: 'annotation.added' } as any);
|
|
143
|
+
const { container } = renderWithProviders(
|
|
144
|
+
<HistoryEvent
|
|
145
|
+
event={event}
|
|
146
|
+
annotations={[]}
|
|
147
|
+
allEvents={[event]}
|
|
148
|
+
isRelated={false}
|
|
149
|
+
t={mockT}
|
|
150
|
+
Link={MockLink}
|
|
151
|
+
routes={mockRoutes}
|
|
152
|
+
/>
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const wrapper = container.querySelector('.semiont-history-event');
|
|
156
|
+
expect(wrapper?.tagName).toBe('BUTTON');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('calls onEventClick when button is clicked', () => {
|
|
160
|
+
const annotationUri = 'http://localhost/annotations/ann-1';
|
|
161
|
+
mockGetAnnotationUri.mockReturnValue(annotationUri);
|
|
162
|
+
const onEventClick = vi.fn();
|
|
163
|
+
const event = makeStoredEvent({ type: 'annotation.added' } as any);
|
|
164
|
+
|
|
165
|
+
renderWithProviders(
|
|
166
|
+
<HistoryEvent
|
|
167
|
+
event={event}
|
|
168
|
+
annotations={[]}
|
|
169
|
+
allEvents={[event]}
|
|
170
|
+
isRelated={false}
|
|
171
|
+
t={mockT}
|
|
172
|
+
Link={MockLink}
|
|
173
|
+
routes={mockRoutes}
|
|
174
|
+
onEventClick={onEventClick}
|
|
175
|
+
/>
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const button = screen.getByRole('button');
|
|
179
|
+
fireEvent.click(button);
|
|
180
|
+
expect(onEventClick).toHaveBeenCalledWith(annotationUri);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('sets data-related attribute based on isRelated prop', () => {
|
|
184
|
+
const event = makeStoredEvent();
|
|
185
|
+
const { container } = renderWithProviders(
|
|
186
|
+
<HistoryEvent
|
|
187
|
+
event={event}
|
|
188
|
+
annotations={[]}
|
|
189
|
+
allEvents={[event]}
|
|
190
|
+
isRelated={true}
|
|
191
|
+
t={mockT}
|
|
192
|
+
Link={MockLink}
|
|
193
|
+
routes={mockRoutes}
|
|
194
|
+
/>
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const wrapper = container.querySelector('.semiont-history-event');
|
|
198
|
+
expect(wrapper).toHaveAttribute('data-related', 'true');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('sets data-related=false when not related', () => {
|
|
202
|
+
const event = makeStoredEvent();
|
|
203
|
+
const { container } = renderWithProviders(
|
|
204
|
+
<HistoryEvent
|
|
205
|
+
event={event}
|
|
206
|
+
annotations={[]}
|
|
207
|
+
allEvents={[event]}
|
|
208
|
+
isRelated={false}
|
|
209
|
+
t={mockT}
|
|
210
|
+
Link={MockLink}
|
|
211
|
+
routes={mockRoutes}
|
|
212
|
+
/>
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const wrapper = container.querySelector('.semiont-history-event');
|
|
216
|
+
expect(wrapper).toHaveAttribute('data-related', 'false');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('renders userId when present', () => {
|
|
220
|
+
const event = makeStoredEvent({ userId: 'alice' } as any);
|
|
221
|
+
renderWithProviders(
|
|
222
|
+
<HistoryEvent
|
|
223
|
+
event={event}
|
|
224
|
+
annotations={[]}
|
|
225
|
+
allEvents={[event]}
|
|
226
|
+
isRelated={false}
|
|
227
|
+
t={mockT}
|
|
228
|
+
Link={MockLink}
|
|
229
|
+
routes={mockRoutes}
|
|
230
|
+
/>
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
expect(screen.getByText('alice')).toBeInTheDocument();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('renders tag content with tag class', () => {
|
|
237
|
+
mockGetEventDisplayContent.mockReturnValue(mockTagContent);
|
|
238
|
+
const event = makeStoredEvent();
|
|
239
|
+
const { container } = renderWithProviders(
|
|
240
|
+
<HistoryEvent
|
|
241
|
+
event={event}
|
|
242
|
+
annotations={[]}
|
|
243
|
+
allEvents={[event]}
|
|
244
|
+
isRelated={false}
|
|
245
|
+
t={mockT}
|
|
246
|
+
Link={MockLink}
|
|
247
|
+
routes={mockRoutes}
|
|
248
|
+
/>
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const tagEl = container.querySelector('.semiont-history-event__tag');
|
|
252
|
+
expect(tagEl).toBeInTheDocument();
|
|
253
|
+
expect(tagEl).toHaveTextContent('Person');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('renders quoted content with quoted class', () => {
|
|
257
|
+
mockGetEventDisplayContent.mockReturnValue(mockQuotedContent);
|
|
258
|
+
const event = makeStoredEvent();
|
|
259
|
+
const { container } = renderWithProviders(
|
|
260
|
+
<HistoryEvent
|
|
261
|
+
event={event}
|
|
262
|
+
annotations={[]}
|
|
263
|
+
allEvents={[event]}
|
|
264
|
+
isRelated={false}
|
|
265
|
+
t={mockT}
|
|
266
|
+
Link={MockLink}
|
|
267
|
+
routes={mockRoutes}
|
|
268
|
+
/>
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const quotedEl = container.querySelector('.semiont-history-event__text--quoted');
|
|
272
|
+
expect(quotedEl).toBeInTheDocument();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('falls back to formatEventType when displayContent is null', () => {
|
|
276
|
+
mockGetEventDisplayContent.mockReturnValue(mockEmptyDisplayContent);
|
|
277
|
+
mockFormatEventType.mockReturnValue('Resource Created');
|
|
278
|
+
const event = makeStoredEvent();
|
|
279
|
+
renderWithProviders(
|
|
280
|
+
<HistoryEvent
|
|
281
|
+
event={event}
|
|
282
|
+
annotations={[]}
|
|
283
|
+
allEvents={[event]}
|
|
284
|
+
isRelated={false}
|
|
285
|
+
t={mockT}
|
|
286
|
+
Link={MockLink}
|
|
287
|
+
routes={mockRoutes}
|
|
288
|
+
/>
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
expect(screen.getByText('Resource Created')).toBeInTheDocument();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('renders entity type tags when present', () => {
|
|
295
|
+
mockGetEventEntityTypes.mockReturnValue(['Person', 'Organization']);
|
|
296
|
+
const event = makeStoredEvent();
|
|
297
|
+
const { container } = renderWithProviders(
|
|
298
|
+
<HistoryEvent
|
|
299
|
+
event={event}
|
|
300
|
+
annotations={[]}
|
|
301
|
+
allEvents={[event]}
|
|
302
|
+
isRelated={false}
|
|
303
|
+
t={mockT}
|
|
304
|
+
Link={MockLink}
|
|
305
|
+
routes={mockRoutes}
|
|
306
|
+
/>
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
expect(screen.getByText('Person')).toBeInTheDocument();
|
|
310
|
+
expect(screen.getByText('Organization')).toBeInTheDocument();
|
|
311
|
+
const tags = container.querySelectorAll('.semiont-tag--small');
|
|
312
|
+
expect(tags).toHaveLength(2);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('renders creation details when present', () => {
|
|
316
|
+
mockGetResourceCreationDetails.mockReturnValue({
|
|
317
|
+
type: 'created',
|
|
318
|
+
userId: 'alice',
|
|
319
|
+
method: 'upload',
|
|
320
|
+
});
|
|
321
|
+
const event = makeStoredEvent();
|
|
322
|
+
renderWithProviders(
|
|
323
|
+
<HistoryEvent
|
|
324
|
+
event={event}
|
|
325
|
+
annotations={[]}
|
|
326
|
+
allEvents={[event]}
|
|
327
|
+
isRelated={false}
|
|
328
|
+
t={mockT}
|
|
329
|
+
Link={MockLink}
|
|
330
|
+
routes={mockRoutes}
|
|
331
|
+
/>
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
expect(screen.getByText('alice')).toBeInTheDocument();
|
|
335
|
+
expect(screen.getByText('upload')).toBeInTheDocument();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('renders "View Original" link for cloned resources', () => {
|
|
339
|
+
mockGetResourceCreationDetails.mockReturnValue({
|
|
340
|
+
type: 'cloned',
|
|
341
|
+
userId: 'bob',
|
|
342
|
+
method: 'clone',
|
|
343
|
+
sourceDocId: 'doc-source-123',
|
|
344
|
+
});
|
|
345
|
+
const event = makeStoredEvent();
|
|
346
|
+
renderWithProviders(
|
|
347
|
+
<HistoryEvent
|
|
348
|
+
event={event}
|
|
349
|
+
annotations={[]}
|
|
350
|
+
allEvents={[event]}
|
|
351
|
+
isRelated={false}
|
|
352
|
+
t={mockT}
|
|
353
|
+
Link={MockLink}
|
|
354
|
+
routes={mockRoutes}
|
|
355
|
+
/>
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const link = screen.getByText('viewOriginal');
|
|
359
|
+
expect(link).toBeInTheDocument();
|
|
360
|
+
expect(link).toHaveAttribute('href', '/resources/doc-source-123');
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('calls onEventRef with annotationUri and element', () => {
|
|
364
|
+
const annotationUri = 'http://localhost/annotations/ann-1';
|
|
365
|
+
mockGetAnnotationUri.mockReturnValue(annotationUri);
|
|
366
|
+
const onEventRef = vi.fn();
|
|
367
|
+
const event = makeStoredEvent({ type: 'annotation.added' } as any);
|
|
368
|
+
|
|
369
|
+
renderWithProviders(
|
|
370
|
+
<HistoryEvent
|
|
371
|
+
event={event}
|
|
372
|
+
annotations={[]}
|
|
373
|
+
allEvents={[event]}
|
|
374
|
+
isRelated={false}
|
|
375
|
+
t={mockT}
|
|
376
|
+
Link={MockLink}
|
|
377
|
+
routes={mockRoutes}
|
|
378
|
+
onEventRef={onEventRef}
|
|
379
|
+
/>
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
expect(onEventRef).toHaveBeenCalledWith(annotationUri, expect.any(HTMLElement));
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('handles emoji hover with delayed callback', () => {
|
|
386
|
+
vi.useFakeTimers();
|
|
387
|
+
const annotationUri = 'http://localhost/annotations/ann-1';
|
|
388
|
+
mockGetAnnotationUri.mockReturnValue(annotationUri);
|
|
389
|
+
const onEventHover = vi.fn();
|
|
390
|
+
const event = makeStoredEvent({ type: 'annotation.added' } as any);
|
|
391
|
+
|
|
392
|
+
const { container } = renderWithProviders(
|
|
393
|
+
<HistoryEvent
|
|
394
|
+
event={event}
|
|
395
|
+
annotations={[]}
|
|
396
|
+
allEvents={[event]}
|
|
397
|
+
isRelated={false}
|
|
398
|
+
t={mockT}
|
|
399
|
+
Link={MockLink}
|
|
400
|
+
routes={mockRoutes}
|
|
401
|
+
onEventHover={onEventHover}
|
|
402
|
+
/>
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
const emoji = container.querySelector('.semiont-history-event__emoji')!;
|
|
406
|
+
fireEvent.mouseEnter(emoji);
|
|
407
|
+
|
|
408
|
+
// Should not fire immediately
|
|
409
|
+
expect(onEventHover).not.toHaveBeenCalled();
|
|
410
|
+
|
|
411
|
+
// Should fire after 300ms delay
|
|
412
|
+
act(() => { vi.advanceTimersByTime(300); });
|
|
413
|
+
expect(onEventHover).toHaveBeenCalledWith(annotationUri);
|
|
414
|
+
|
|
415
|
+
vi.useRealTimers();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('clears hover on mouse leave and calls with null', () => {
|
|
419
|
+
vi.useFakeTimers();
|
|
420
|
+
const annotationUri = 'http://localhost/annotations/ann-1';
|
|
421
|
+
mockGetAnnotationUri.mockReturnValue(annotationUri);
|
|
422
|
+
const onEventHover = vi.fn();
|
|
423
|
+
const event = makeStoredEvent({ type: 'annotation.added' } as any);
|
|
424
|
+
|
|
425
|
+
const { container } = renderWithProviders(
|
|
426
|
+
<HistoryEvent
|
|
427
|
+
event={event}
|
|
428
|
+
annotations={[]}
|
|
429
|
+
allEvents={[event]}
|
|
430
|
+
isRelated={false}
|
|
431
|
+
t={mockT}
|
|
432
|
+
Link={MockLink}
|
|
433
|
+
routes={mockRoutes}
|
|
434
|
+
onEventHover={onEventHover}
|
|
435
|
+
/>
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
const emoji = container.querySelector('.semiont-history-event__emoji')!;
|
|
439
|
+
fireEvent.mouseEnter(emoji);
|
|
440
|
+
|
|
441
|
+
// Leave before 300ms timeout fires
|
|
442
|
+
act(() => { vi.advanceTimersByTime(100); });
|
|
443
|
+
fireEvent.mouseLeave(emoji);
|
|
444
|
+
|
|
445
|
+
// The timeout should have been cleared, and hover state cleared
|
|
446
|
+
expect(onEventHover).toHaveBeenCalledWith(null);
|
|
447
|
+
|
|
448
|
+
// After full timeout, the delayed hover should NOT have fired
|
|
449
|
+
act(() => { vi.advanceTimersByTime(300); });
|
|
450
|
+
expect(onEventHover).toHaveBeenCalledTimes(1); // Only the null call
|
|
451
|
+
|
|
452
|
+
vi.useRealTimers();
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('sets data-interactive on button wrapper', () => {
|
|
456
|
+
mockGetAnnotationUri.mockReturnValue('http://localhost/annotations/ann-1');
|
|
457
|
+
const event = makeStoredEvent({ type: 'annotation.added' } as any);
|
|
458
|
+
const { container } = renderWithProviders(
|
|
459
|
+
<HistoryEvent
|
|
460
|
+
event={event}
|
|
461
|
+
annotations={[]}
|
|
462
|
+
allEvents={[event]}
|
|
463
|
+
isRelated={false}
|
|
464
|
+
t={mockT}
|
|
465
|
+
Link={MockLink}
|
|
466
|
+
routes={mockRoutes}
|
|
467
|
+
/>
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
const wrapper = container.querySelector('.semiont-history-event');
|
|
471
|
+
expect(wrapper).toHaveAttribute('data-interactive', 'true');
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('does not set data-interactive on div wrapper', () => {
|
|
475
|
+
mockGetAnnotationUri.mockReturnValue(null);
|
|
476
|
+
const event = makeStoredEvent();
|
|
477
|
+
const { container } = renderWithProviders(
|
|
478
|
+
<HistoryEvent
|
|
479
|
+
event={event}
|
|
480
|
+
annotations={[]}
|
|
481
|
+
allEvents={[event]}
|
|
482
|
+
isRelated={false}
|
|
483
|
+
t={mockT}
|
|
484
|
+
Link={MockLink}
|
|
485
|
+
routes={mockRoutes}
|
|
486
|
+
/>
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
const wrapper = container.querySelector('.semiont-history-event');
|
|
490
|
+
expect(wrapper).not.toHaveAttribute('data-interactive');
|
|
491
|
+
});
|
|
492
|
+
});
|