@semiont/react-ui 0.4.14 → 0.4.15
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/README.md +18 -12
- package/dist/KnowledgeBaseSessionContext-CpYaCbnC.d.mts +174 -0
- package/dist/{PdfAnnotationCanvas.client-CW6SKH2U.mjs → PdfAnnotationCanvas.client-CHDCGQBR.mjs} +3 -3
- package/dist/{chunk-HNZOXH4L.mjs → chunk-OZICDVH7.mjs} +5 -3
- package/dist/chunk-OZICDVH7.mjs.map +1 -0
- package/dist/chunk-R2U7P4TK.mjs +865 -0
- package/dist/chunk-R2U7P4TK.mjs.map +1 -0
- package/dist/{chunk-BQJWOK4C.mjs → chunk-VN5NY4SN.mjs} +9 -8
- package/dist/chunk-VN5NY4SN.mjs.map +1 -0
- package/dist/index.d.mts +139 -169
- package/dist/index.mjs +2197 -1947
- package/dist/index.mjs.map +1 -1
- package/dist/test-utils.d.mts +13 -62
- package/dist/test-utils.mjs +40 -21
- package/dist/test-utils.mjs.map +1 -1
- package/package.json +5 -3
- package/src/components/ProtectedErrorBoundary.tsx +95 -0
- package/src/components/__tests__/ProtectedErrorBoundary.test.tsx +197 -0
- package/src/components/modals/PermissionDeniedModal.tsx +140 -0
- package/src/components/modals/ReferenceWizardModal.tsx +3 -2
- package/src/components/modals/SessionExpiredModal.tsx +101 -0
- package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +150 -0
- package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +115 -0
- package/src/components/resource/AnnotationHistory.tsx +5 -6
- package/src/components/resource/HistoryEvent.tsx +7 -7
- package/src/components/resource/__tests__/AnnotationHistory.test.tsx +33 -34
- package/src/components/resource/__tests__/HistoryEvent.test.tsx +17 -19
- package/src/components/resource/__tests__/event-formatting.test.ts +70 -94
- package/src/components/resource/event-formatting.ts +56 -56
- package/src/components/resource/panels/ReferenceEntry.tsx +7 -5
- package/src/components/resource/panels/ResourceInfoPanel.tsx +8 -6
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +12 -12
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +1 -0
- package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +1 -1
- package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +4 -4
- package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +5 -10
- package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +23 -54
- package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +6 -6
- package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +7 -19
- package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +1 -1
- package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +18 -44
- package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +6 -6
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +24 -26
- package/dist/TranslationManager-CudgH3gw.d.mts +0 -107
- package/dist/chunk-BQJWOK4C.mjs.map +0 -1
- package/dist/chunk-HNZOXH4L.mjs.map +0 -1
- package/dist/chunk-OL5UST25.mjs +0 -413
- package/dist/chunk-OL5UST25.mjs.map +0 -1
- /package/dist/{PdfAnnotationCanvas.client-CW6SKH2U.mjs.map → PdfAnnotationCanvas.client-CHDCGQBR.mjs.map} +0 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PermissionDeniedModal Tests
|
|
3
|
+
*
|
|
4
|
+
* The modal renders content when `permissionDeniedAt` is non-null on the
|
|
5
|
+
* KnowledgeBaseSession context, and is hidden otherwise. Button clicks call
|
|
6
|
+
* `acknowledgePermissionDenied()` and navigate the window or history.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { screen, fireEvent } from '@testing-library/react';
|
|
12
|
+
import '@testing-library/jest-dom';
|
|
13
|
+
import {
|
|
14
|
+
renderWithProviders,
|
|
15
|
+
createMockKnowledgeBaseSession,
|
|
16
|
+
} from '../../../test-utils';
|
|
17
|
+
import { PermissionDeniedModal } from '../PermissionDeniedModal';
|
|
18
|
+
|
|
19
|
+
vi.mock('@headlessui/react', () => ({
|
|
20
|
+
Dialog: ({ children, ...props }: any) => <div role="dialog" {...props}>{typeof children === 'function' ? children({ open: true }) : children}</div>,
|
|
21
|
+
DialogPanel: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
|
22
|
+
DialogTitle: ({ children, ...props }: any) => <h2 {...props}>{children}</h2>,
|
|
23
|
+
Transition: ({ show, children }: any) => show ? <>{children}</> : null,
|
|
24
|
+
TransitionChild: ({ children }: any) => <>{children}</>,
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
const originalLocation = window.location;
|
|
28
|
+
const originalHistoryBack = window.history.back;
|
|
29
|
+
let mockLocation: { href: string; pathname: string };
|
|
30
|
+
let mockHistoryBack: ReturnType<typeof vi.fn>;
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
mockLocation = { href: '', pathname: '/admin/users' };
|
|
34
|
+
Object.defineProperty(window, 'location', {
|
|
35
|
+
value: mockLocation,
|
|
36
|
+
writable: true,
|
|
37
|
+
configurable: true,
|
|
38
|
+
});
|
|
39
|
+
mockHistoryBack = vi.fn();
|
|
40
|
+
window.history.back = mockHistoryBack;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
Object.defineProperty(window, 'location', {
|
|
45
|
+
value: originalLocation,
|
|
46
|
+
writable: true,
|
|
47
|
+
configurable: true,
|
|
48
|
+
});
|
|
49
|
+
window.history.back = originalHistoryBack;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('PermissionDeniedModal', () => {
|
|
53
|
+
describe('initial render', () => {
|
|
54
|
+
it('does not render modal content when permissionDeniedAt is null', () => {
|
|
55
|
+
renderWithProviders(<PermissionDeniedModal />, {
|
|
56
|
+
knowledgeBaseSession: createMockKnowledgeBaseSession({
|
|
57
|
+
permissionDeniedAt: null,
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
expect(screen.queryByText('Access Denied')).not.toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('when permissionDeniedAt is set', () => {
|
|
65
|
+
it('shows modal with default message when no message provided', () => {
|
|
66
|
+
renderWithProviders(<PermissionDeniedModal />, {
|
|
67
|
+
knowledgeBaseSession: createMockKnowledgeBaseSession({
|
|
68
|
+
permissionDeniedAt: Date.now(),
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(screen.getByText('Access Denied')).toBeInTheDocument();
|
|
73
|
+
expect(screen.getByText('You do not have permission to perform this action.')).toBeInTheDocument();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('shows custom message from permissionDeniedMessage', () => {
|
|
77
|
+
renderWithProviders(<PermissionDeniedModal />, {
|
|
78
|
+
knowledgeBaseSession: createMockKnowledgeBaseSession({
|
|
79
|
+
permissionDeniedAt: Date.now(),
|
|
80
|
+
permissionDeniedMessage: 'Admin access required for this resource',
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(screen.getByText('Admin access required for this resource')).toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('renders all three action buttons', () => {
|
|
88
|
+
renderWithProviders(<PermissionDeniedModal />, {
|
|
89
|
+
knowledgeBaseSession: createMockKnowledgeBaseSession({
|
|
90
|
+
permissionDeniedAt: Date.now(),
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(screen.getByRole('button', { name: /go back/i })).toBeInTheDocument();
|
|
95
|
+
expect(screen.getByRole('button', { name: /go to home/i })).toBeInTheDocument();
|
|
96
|
+
expect(screen.getByRole('button', { name: /switch account/i })).toBeInTheDocument();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('button actions', () => {
|
|
101
|
+
it('acknowledges and calls window.history.back on Go Back', () => {
|
|
102
|
+
const ack = vi.fn();
|
|
103
|
+
renderWithProviders(<PermissionDeniedModal />, {
|
|
104
|
+
knowledgeBaseSession: createMockKnowledgeBaseSession({
|
|
105
|
+
permissionDeniedAt: Date.now(),
|
|
106
|
+
permissionDeniedMessage: 'denied',
|
|
107
|
+
acknowledgePermissionDenied: ack,
|
|
108
|
+
}),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
fireEvent.click(screen.getByRole('button', { name: /go back/i }));
|
|
112
|
+
|
|
113
|
+
expect(ack).toHaveBeenCalled();
|
|
114
|
+
expect(mockHistoryBack).toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('acknowledges and navigates to / on Go to Home', () => {
|
|
118
|
+
const ack = vi.fn();
|
|
119
|
+
renderWithProviders(<PermissionDeniedModal />, {
|
|
120
|
+
knowledgeBaseSession: createMockKnowledgeBaseSession({
|
|
121
|
+
permissionDeniedAt: Date.now(),
|
|
122
|
+
permissionDeniedMessage: 'denied',
|
|
123
|
+
acknowledgePermissionDenied: ack,
|
|
124
|
+
}),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
fireEvent.click(screen.getByRole('button', { name: /go to home/i }));
|
|
128
|
+
|
|
129
|
+
expect(ack).toHaveBeenCalled();
|
|
130
|
+
expect(mockLocation.href).toBe('/');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('acknowledges and navigates to /auth/connect with current path on Switch Account', () => {
|
|
134
|
+
const ack = vi.fn();
|
|
135
|
+
mockLocation.pathname = '/admin/users';
|
|
136
|
+
renderWithProviders(<PermissionDeniedModal />, {
|
|
137
|
+
knowledgeBaseSession: createMockKnowledgeBaseSession({
|
|
138
|
+
permissionDeniedAt: Date.now(),
|
|
139
|
+
permissionDeniedMessage: 'denied',
|
|
140
|
+
acknowledgePermissionDenied: ack,
|
|
141
|
+
}),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
fireEvent.click(screen.getByRole('button', { name: /switch account/i }));
|
|
145
|
+
|
|
146
|
+
expect(ack).toHaveBeenCalled();
|
|
147
|
+
expect(mockLocation.href).toBe('/auth/connect?callbackUrl=%2Fadmin%2Fusers');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionExpiredModal Tests
|
|
3
|
+
*
|
|
4
|
+
* The modal renders content when `sessionExpiredAt` is non-null on the
|
|
5
|
+
* KnowledgeBaseSession context, and is hidden otherwise. Button clicks
|
|
6
|
+
* call `acknowledgeSessionExpired()` and navigate the window.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { screen, fireEvent } from '@testing-library/react';
|
|
12
|
+
import '@testing-library/jest-dom';
|
|
13
|
+
import {
|
|
14
|
+
renderWithProviders,
|
|
15
|
+
createMockKnowledgeBaseSession,
|
|
16
|
+
} from '../../../test-utils';
|
|
17
|
+
import { SessionExpiredModal } from '../SessionExpiredModal';
|
|
18
|
+
|
|
19
|
+
vi.mock('@headlessui/react', () => ({
|
|
20
|
+
Dialog: ({ children, ...props }: any) => <div role="dialog" {...props}>{typeof children === 'function' ? children({ open: true }) : children}</div>,
|
|
21
|
+
DialogPanel: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
|
22
|
+
DialogTitle: ({ children, ...props }: any) => <h2 {...props}>{children}</h2>,
|
|
23
|
+
Transition: ({ show, children }: any) => show ? <>{children}</> : null,
|
|
24
|
+
TransitionChild: ({ children }: any) => <>{children}</>,
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
const originalLocation = window.location;
|
|
28
|
+
let mockLocation: { href: string; pathname: string };
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
mockLocation = { href: '', pathname: '/know/discover' };
|
|
32
|
+
Object.defineProperty(window, 'location', {
|
|
33
|
+
value: mockLocation,
|
|
34
|
+
writable: true,
|
|
35
|
+
configurable: true,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
Object.defineProperty(window, 'location', {
|
|
41
|
+
value: originalLocation,
|
|
42
|
+
writable: true,
|
|
43
|
+
configurable: true,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('SessionExpiredModal', () => {
|
|
48
|
+
describe('initial render', () => {
|
|
49
|
+
it('does not render modal content when sessionExpiredAt is null', () => {
|
|
50
|
+
renderWithProviders(<SessionExpiredModal />, {
|
|
51
|
+
knowledgeBaseSession: createMockKnowledgeBaseSession({
|
|
52
|
+
sessionExpiredAt: null,
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
expect(screen.queryByText('Session Expired')).not.toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('when sessionExpiredAt is set', () => {
|
|
60
|
+
it('renders the modal with default message', () => {
|
|
61
|
+
renderWithProviders(<SessionExpiredModal />, {
|
|
62
|
+
knowledgeBaseSession: createMockKnowledgeBaseSession({
|
|
63
|
+
sessionExpiredAt: Date.now(),
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(screen.getByText('Session Expired')).toBeInTheDocument();
|
|
68
|
+
expect(screen.getByRole('button', { name: /sign in again/i })).toBeInTheDocument();
|
|
69
|
+
expect(screen.getByRole('button', { name: /go to home/i })).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('renders the custom message from sessionExpiredMessage', () => {
|
|
73
|
+
renderWithProviders(<SessionExpiredModal />, {
|
|
74
|
+
knowledgeBaseSession: createMockKnowledgeBaseSession({
|
|
75
|
+
sessionExpiredAt: Date.now(),
|
|
76
|
+
sessionExpiredMessage: 'Your token expired at 5pm',
|
|
77
|
+
}),
|
|
78
|
+
});
|
|
79
|
+
expect(screen.getByText(/your token expired at 5pm/i)).toBeInTheDocument();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('button actions', () => {
|
|
84
|
+
it('calls acknowledgeSessionExpired and navigates to /auth/connect on Sign In Again', () => {
|
|
85
|
+
const ack = vi.fn();
|
|
86
|
+
mockLocation.pathname = '/know/discover';
|
|
87
|
+
renderWithProviders(<SessionExpiredModal />, {
|
|
88
|
+
knowledgeBaseSession: createMockKnowledgeBaseSession({
|
|
89
|
+
sessionExpiredAt: Date.now(),
|
|
90
|
+
acknowledgeSessionExpired: ack,
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
fireEvent.click(screen.getByRole('button', { name: /sign in again/i }));
|
|
95
|
+
|
|
96
|
+
expect(ack).toHaveBeenCalled();
|
|
97
|
+
expect(mockLocation.href).toBe('/auth/connect?callbackUrl=%2Fknow%2Fdiscover');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('calls acknowledgeSessionExpired and navigates to / on Go to Home', () => {
|
|
101
|
+
const ack = vi.fn();
|
|
102
|
+
renderWithProviders(<SessionExpiredModal />, {
|
|
103
|
+
knowledgeBaseSession: createMockKnowledgeBaseSession({
|
|
104
|
+
sessionExpiredAt: Date.now(),
|
|
105
|
+
acknowledgeSessionExpired: ack,
|
|
106
|
+
}),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
fireEvent.click(screen.getByRole('button', { name: /go to home/i }));
|
|
110
|
+
|
|
111
|
+
expect(ack).toHaveBeenCalled();
|
|
112
|
+
expect(mockLocation.href).toBe('/');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -5,7 +5,7 @@ import { useTranslations } from '../../contexts/TranslationContext';
|
|
|
5
5
|
import type { RouteBuilder, LinkComponentProps } from '../../contexts/RoutingContext';
|
|
6
6
|
import { useResources } from '../../lib/api-hooks';
|
|
7
7
|
import type { ResourceId } from '@semiont/core';
|
|
8
|
-
import { getAnnotationUriFromEvent } from '@semiont/core';
|
|
8
|
+
import { getAnnotationUriFromEvent, type StoredEventLike } from '@semiont/core';
|
|
9
9
|
import { HistoryEvent } from './HistoryEvent';
|
|
10
10
|
|
|
11
11
|
interface Props {
|
|
@@ -36,11 +36,10 @@ export function AnnotationHistory({ rUri, hoveredAnnotationId, onEventHover, onE
|
|
|
36
36
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
37
37
|
|
|
38
38
|
// Sort events by oldest first (most recent at bottom)
|
|
39
|
-
// Filter out
|
|
40
|
-
const events = !eventsData?.events ? [] :
|
|
39
|
+
// Filter out job events - they're represented by mark:body-updated events instead
|
|
40
|
+
const events: StoredEventLike[] = !eventsData?.events ? [] : (eventsData.events as StoredEventLike[])
|
|
41
41
|
.filter((e) => {
|
|
42
|
-
|
|
43
|
-
return eventType !== 'job.started' && eventType !== 'job.progress' && eventType !== 'job.completed';
|
|
42
|
+
return e.type !== 'job:started' && e.type !== 'job:progress' && e.type !== 'job:completed';
|
|
44
43
|
})
|
|
45
44
|
.sort((a, b) => a.metadata.sequenceNumber - b.metadata.sequenceNumber);
|
|
46
45
|
|
|
@@ -110,7 +109,7 @@ export function AnnotationHistory({ rUri, hoveredAnnotationId, onEventHover, onE
|
|
|
110
109
|
|
|
111
110
|
return (
|
|
112
111
|
<HistoryEvent
|
|
113
|
-
key={stored.
|
|
112
|
+
key={stored.id}
|
|
114
113
|
event={stored}
|
|
115
114
|
annotations={annotations}
|
|
116
115
|
allEvents={events}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useRef, useCallback, useEffect } from 'react';
|
|
4
4
|
import type { RouteBuilder, LinkComponentProps } from '../../contexts/RoutingContext';
|
|
5
|
-
import type { StoredEventLike,
|
|
5
|
+
import type { StoredEventLike, PersistedEventType } from '@semiont/core';
|
|
6
6
|
import { getAnnotationUriFromEvent } from '@semiont/core';
|
|
7
7
|
import {
|
|
8
8
|
formatEventType,
|
|
@@ -85,7 +85,7 @@ export function HistoryEvent({
|
|
|
85
85
|
const eventWrapperProps = annotationUri ? {
|
|
86
86
|
type: 'button' as const,
|
|
87
87
|
onClick: () => onEventClick?.(annotationUri),
|
|
88
|
-
'aria-label': t('viewAnnotation', { content: displayContent?.exact || formatEventType(event.
|
|
88
|
+
'aria-label': t('viewAnnotation', { content: displayContent?.exact || formatEventType(event.type as PersistedEventType, t) }),
|
|
89
89
|
className: 'semiont-history-event',
|
|
90
90
|
'data-related': isRelated ? 'true' : 'false',
|
|
91
91
|
'data-interactive': 'true'
|
|
@@ -109,7 +109,7 @@ export function HistoryEvent({
|
|
|
109
109
|
onMouseEnter={handleEmojiMouseEnter}
|
|
110
110
|
onMouseLeave={handleEmojiMouseLeave}
|
|
111
111
|
>
|
|
112
|
-
{getEventEmoji(event.
|
|
112
|
+
{getEventEmoji(event.type as PersistedEventType, event.payload)}
|
|
113
113
|
</span>
|
|
114
114
|
{displayContent ? (
|
|
115
115
|
displayContent.isTag ? (
|
|
@@ -127,16 +127,16 @@ export function HistoryEvent({
|
|
|
127
127
|
)
|
|
128
128
|
) : (
|
|
129
129
|
<span className="semiont-history-event__text">
|
|
130
|
-
{formatEventType(event.
|
|
130
|
+
{formatEventType(event.type as PersistedEventType, t, event.payload)}
|
|
131
131
|
</span>
|
|
132
132
|
)}
|
|
133
|
-
{event.
|
|
133
|
+
{event.userId && (
|
|
134
134
|
<span className="semiont-history-event__user">
|
|
135
|
-
{event.
|
|
135
|
+
{event.userId}
|
|
136
136
|
</span>
|
|
137
137
|
)}
|
|
138
138
|
<span className="semiont-history-event__timestamp">
|
|
139
|
-
{formatRelativeTime(event.
|
|
139
|
+
{formatRelativeTime(event.timestamp, t)}
|
|
140
140
|
</span>
|
|
141
141
|
</div>
|
|
142
142
|
{entityTypes.length > 0 && (
|
|
@@ -40,8 +40,8 @@ vi.mock('../../../lib/api-hooks', () => ({
|
|
|
40
40
|
|
|
41
41
|
// Mock HistoryEvent to avoid deep rendering and mocking all its dependencies
|
|
42
42
|
const MockHistoryEvent = vi.fn(({ event }: any) => (
|
|
43
|
-
<div data-testid={`history-event-${event.
|
|
44
|
-
{event.
|
|
43
|
+
<div data-testid={`history-event-${event.id}`}>
|
|
44
|
+
{event.type}
|
|
45
45
|
</div>
|
|
46
46
|
));
|
|
47
47
|
|
|
@@ -54,23 +54,22 @@ const mockGetAnnotationUri = getAnnotationUriFromEvent as ReturnType<typeof vi.f
|
|
|
54
54
|
|
|
55
55
|
const testRId = 'res-1' as ResourceId;
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
/** Returns flat StoredEventResponse shape (matches API response) */
|
|
58
|
+
function makeStoredEvent(id: string, type: string, seq: number, overrides: Record<string, any> = {}): any {
|
|
58
59
|
return {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
...overrides,
|
|
68
|
-
},
|
|
60
|
+
id,
|
|
61
|
+
type,
|
|
62
|
+
timestamp: '2026-03-06T12:00:00Z',
|
|
63
|
+
resourceId: 'res-1',
|
|
64
|
+
userId: 'user-1',
|
|
65
|
+
version: 1,
|
|
66
|
+
payload: {},
|
|
67
|
+
...overrides,
|
|
69
68
|
metadata: {
|
|
70
69
|
sequenceNumber: seq,
|
|
71
|
-
|
|
70
|
+
streamPosition: 0,
|
|
72
71
|
},
|
|
73
|
-
}
|
|
72
|
+
};
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
const MockLink = ({ href, children, ...props }: any) => <a href={href} {...props}>{children}</a>;
|
|
@@ -130,9 +129,9 @@ describe('AnnotationHistory', () => {
|
|
|
130
129
|
|
|
131
130
|
it('renders events sorted by sequence number', () => {
|
|
132
131
|
const events = [
|
|
133
|
-
makeStoredEvent('evt-3', '
|
|
134
|
-
makeStoredEvent('evt-1', '
|
|
135
|
-
makeStoredEvent('evt-2', '
|
|
132
|
+
makeStoredEvent('evt-3', 'mark:added', 3),
|
|
133
|
+
makeStoredEvent('evt-1', 'yield:created', 1),
|
|
134
|
+
makeStoredEvent('evt-2', 'mark:added', 2),
|
|
136
135
|
];
|
|
137
136
|
mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
|
|
138
137
|
|
|
@@ -152,18 +151,18 @@ describe('AnnotationHistory', () => {
|
|
|
152
151
|
|
|
153
152
|
// Verify HistoryEvent was called with events in sequence order
|
|
154
153
|
const calls = MockHistoryEvent.mock.calls;
|
|
155
|
-
expect(calls[0][0].event.
|
|
156
|
-
expect(calls[1][0].event.
|
|
157
|
-
expect(calls[2][0].event.
|
|
154
|
+
expect(calls[0][0].event.id).toBe('evt-1'); // .event is the React prop name
|
|
155
|
+
expect(calls[1][0].event.id).toBe('evt-2');
|
|
156
|
+
expect(calls[2][0].event.id).toBe('evt-3');
|
|
158
157
|
});
|
|
159
158
|
|
|
160
159
|
it('filters out job events', () => {
|
|
161
160
|
const events = [
|
|
162
|
-
makeStoredEvent('evt-1', '
|
|
163
|
-
makeStoredEvent('evt-2', 'job
|
|
164
|
-
makeStoredEvent('evt-3', 'job
|
|
165
|
-
makeStoredEvent('evt-4', 'job
|
|
166
|
-
makeStoredEvent('evt-5', '
|
|
161
|
+
makeStoredEvent('evt-1', 'yield:created', 1),
|
|
162
|
+
makeStoredEvent('evt-2', 'job:started', 2),
|
|
163
|
+
makeStoredEvent('evt-3', 'job:progress', 3),
|
|
164
|
+
makeStoredEvent('evt-4', 'job:completed', 4),
|
|
165
|
+
makeStoredEvent('evt-5', 'mark:added', 5),
|
|
167
166
|
];
|
|
168
167
|
mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
|
|
169
168
|
|
|
@@ -188,7 +187,7 @@ describe('AnnotationHistory', () => {
|
|
|
188
187
|
mockGetAnnotationUri.mockReturnValue(annotationUri);
|
|
189
188
|
|
|
190
189
|
const events = [
|
|
191
|
-
makeStoredEvent('evt-1', '
|
|
190
|
+
makeStoredEvent('evt-1', 'mark:added', 1),
|
|
192
191
|
];
|
|
193
192
|
mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
|
|
194
193
|
|
|
@@ -209,7 +208,7 @@ describe('AnnotationHistory', () => {
|
|
|
209
208
|
mockGetAnnotationUri.mockReturnValue('http://localhost/annotations/ann-other');
|
|
210
209
|
|
|
211
210
|
const events = [
|
|
212
|
-
makeStoredEvent('evt-1', '
|
|
211
|
+
makeStoredEvent('evt-1', 'mark:added', 1),
|
|
213
212
|
];
|
|
214
213
|
mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
|
|
215
214
|
|
|
@@ -228,7 +227,7 @@ describe('AnnotationHistory', () => {
|
|
|
228
227
|
|
|
229
228
|
it('passes isRelated=false when no hoveredAnnotationId', () => {
|
|
230
229
|
const events = [
|
|
231
|
-
makeStoredEvent('evt-1', '
|
|
230
|
+
makeStoredEvent('evt-1', 'mark:added', 1),
|
|
232
231
|
];
|
|
233
232
|
mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
|
|
234
233
|
|
|
@@ -249,7 +248,7 @@ describe('AnnotationHistory', () => {
|
|
|
249
248
|
const onEventHover = vi.fn();
|
|
250
249
|
|
|
251
250
|
const events = [
|
|
252
|
-
makeStoredEvent('evt-1', '
|
|
251
|
+
makeStoredEvent('evt-1', 'yield:created', 1),
|
|
253
252
|
];
|
|
254
253
|
mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
|
|
255
254
|
|
|
@@ -270,7 +269,7 @@ describe('AnnotationHistory', () => {
|
|
|
270
269
|
|
|
271
270
|
it('does not pass onEventClick/onEventHover when not provided', () => {
|
|
272
271
|
const events = [
|
|
273
|
-
makeStoredEvent('evt-1', '
|
|
272
|
+
makeStoredEvent('evt-1', 'yield:created', 1),
|
|
274
273
|
];
|
|
275
274
|
mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
|
|
276
275
|
|
|
@@ -292,7 +291,7 @@ describe('AnnotationHistory', () => {
|
|
|
292
291
|
mockAnnotationsUseQuery.mockReturnValue({ data: { annotations: mockAnnotations } });
|
|
293
292
|
|
|
294
293
|
const events = [
|
|
295
|
-
makeStoredEvent('evt-1', '
|
|
294
|
+
makeStoredEvent('evt-1', 'yield:created', 1),
|
|
296
295
|
];
|
|
297
296
|
mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
|
|
298
297
|
|
|
@@ -312,7 +311,7 @@ describe('AnnotationHistory', () => {
|
|
|
312
311
|
mockAnnotationsUseQuery.mockReturnValue({ data: undefined });
|
|
313
312
|
|
|
314
313
|
const events = [
|
|
315
|
-
makeStoredEvent('evt-1', '
|
|
314
|
+
makeStoredEvent('evt-1', 'yield:created', 1),
|
|
316
315
|
];
|
|
317
316
|
mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
|
|
318
317
|
|
|
@@ -330,7 +329,7 @@ describe('AnnotationHistory', () => {
|
|
|
330
329
|
|
|
331
330
|
it('renders history panel structure with title and list', () => {
|
|
332
331
|
const events = [
|
|
333
|
-
makeStoredEvent('evt-1', '
|
|
332
|
+
makeStoredEvent('evt-1', 'yield:created', 1),
|
|
334
333
|
];
|
|
335
334
|
mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
|
|
336
335
|
|
|
@@ -49,23 +49,21 @@ const mockGetEventEntityTypes = getEventEntityTypes as ReturnType<typeof vi.fn>;
|
|
|
49
49
|
const mockGetResourceCreationDetails = getResourceCreationDetails as ReturnType<typeof vi.fn>;
|
|
50
50
|
const mockFormatEventType = formatEventType as ReturnType<typeof vi.fn>;
|
|
51
51
|
|
|
52
|
-
function makeStoredEvent(overrides:
|
|
52
|
+
function makeStoredEvent(overrides: Record<string, any> = {}): any {
|
|
53
53
|
return {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
...overrides,
|
|
63
|
-
},
|
|
54
|
+
id: 'evt-1',
|
|
55
|
+
type: 'yield:created',
|
|
56
|
+
timestamp: '2026-03-06T12:00:00Z',
|
|
57
|
+
resourceId: 'res-1',
|
|
58
|
+
userId: 'user-1',
|
|
59
|
+
version: 1,
|
|
60
|
+
payload: { name: 'Test', format: 'text/plain', contentChecksum: 'abc', creationMethod: 'upload' },
|
|
61
|
+
...overrides,
|
|
64
62
|
metadata: {
|
|
65
63
|
sequenceNumber: 1,
|
|
66
|
-
|
|
64
|
+
streamPosition: 0,
|
|
67
65
|
},
|
|
68
|
-
}
|
|
66
|
+
};
|
|
69
67
|
}
|
|
70
68
|
|
|
71
69
|
const mockT = (key: string) => key;
|
|
@@ -139,7 +137,7 @@ describe('HistoryEvent', () => {
|
|
|
139
137
|
|
|
140
138
|
it('renders as button when annotationUri exists', () => {
|
|
141
139
|
mockGetAnnotationUri.mockReturnValue('http://localhost/annotations/ann-1');
|
|
142
|
-
const event = makeStoredEvent({ type: '
|
|
140
|
+
const event = makeStoredEvent({ type: 'mark:added' } as any);
|
|
143
141
|
const { container } = renderWithProviders(
|
|
144
142
|
<HistoryEvent
|
|
145
143
|
event={event}
|
|
@@ -160,7 +158,7 @@ describe('HistoryEvent', () => {
|
|
|
160
158
|
const annotationUri = 'http://localhost/annotations/ann-1';
|
|
161
159
|
mockGetAnnotationUri.mockReturnValue(annotationUri);
|
|
162
160
|
const onEventClick = vi.fn();
|
|
163
|
-
const event = makeStoredEvent({ type: '
|
|
161
|
+
const event = makeStoredEvent({ type: 'mark:added' } as any);
|
|
164
162
|
|
|
165
163
|
renderWithProviders(
|
|
166
164
|
<HistoryEvent
|
|
@@ -364,7 +362,7 @@ describe('HistoryEvent', () => {
|
|
|
364
362
|
const annotationUri = 'http://localhost/annotations/ann-1';
|
|
365
363
|
mockGetAnnotationUri.mockReturnValue(annotationUri);
|
|
366
364
|
const onEventRef = vi.fn();
|
|
367
|
-
const event = makeStoredEvent({ type: '
|
|
365
|
+
const event = makeStoredEvent({ type: 'mark:added' } as any);
|
|
368
366
|
|
|
369
367
|
renderWithProviders(
|
|
370
368
|
<HistoryEvent
|
|
@@ -387,7 +385,7 @@ describe('HistoryEvent', () => {
|
|
|
387
385
|
const annotationUri = 'http://localhost/annotations/ann-1';
|
|
388
386
|
mockGetAnnotationUri.mockReturnValue(annotationUri);
|
|
389
387
|
const onEventHover = vi.fn();
|
|
390
|
-
const event = makeStoredEvent({ type: '
|
|
388
|
+
const event = makeStoredEvent({ type: 'mark:added' } as any);
|
|
391
389
|
|
|
392
390
|
const { container } = renderWithProviders(
|
|
393
391
|
<HistoryEvent
|
|
@@ -420,7 +418,7 @@ describe('HistoryEvent', () => {
|
|
|
420
418
|
const annotationUri = 'http://localhost/annotations/ann-1';
|
|
421
419
|
mockGetAnnotationUri.mockReturnValue(annotationUri);
|
|
422
420
|
const onEventHover = vi.fn();
|
|
423
|
-
const event = makeStoredEvent({ type: '
|
|
421
|
+
const event = makeStoredEvent({ type: 'mark:added' } as any);
|
|
424
422
|
|
|
425
423
|
const { container } = renderWithProviders(
|
|
426
424
|
<HistoryEvent
|
|
@@ -454,7 +452,7 @@ describe('HistoryEvent', () => {
|
|
|
454
452
|
|
|
455
453
|
it('sets data-interactive on button wrapper', () => {
|
|
456
454
|
mockGetAnnotationUri.mockReturnValue('http://localhost/annotations/ann-1');
|
|
457
|
-
const event = makeStoredEvent({ type: '
|
|
455
|
+
const event = makeStoredEvent({ type: 'mark:added' } as any);
|
|
458
456
|
const { container } = renderWithProviders(
|
|
459
457
|
<HistoryEvent
|
|
460
458
|
event={event}
|