@eeacms/volto-eea-chatbot 1.0.9 → 1.0.11
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/CHANGELOG.md +15 -752
- package/package.json +1 -1
- package/razzle.extend.js +8 -4
- package/src/ChatBlock/ChatBlockView.jsx +26 -2
- package/src/ChatBlock/chat/AIMessage.tsx +5 -1
- package/src/ChatBlock/chat/ChatWindow.tsx +12 -3
- package/src/ChatBlock/components/AutoResizeTextarea.jsx +2 -1
- package/src/ChatBlock/components/QualityCheckToggle.jsx +1 -1
- package/src/ChatBlock/hooks/useChatController.ts +10 -2
- package/src/ChatBlock/index.js +1 -1
- package/src/ChatBlock/packets/renderers/MessageTextRenderer.tsx +8 -0
- package/src/ChatBlock/services/streamingService.ts +30 -26
- package/src/ChatBlock/style.less +3 -1
- package/src/ChatBlock/tests/ChatMessage.test.jsx +75 -0
- package/src/ChatBlock/tests/ClaimModal.test.jsx +136 -0
- package/src/ChatBlock/tests/ClaimSegments.test.jsx +206 -0
- package/src/ChatBlock/tests/CustomToolRenderer.test.jsx +241 -0
- package/src/ChatBlock/tests/FetchToolRenderer.test.jsx +161 -0
- package/src/ChatBlock/tests/ImageToolRenderer.test.jsx +178 -0
- package/src/ChatBlock/tests/MessageTextRenderer.test.jsx +227 -0
- package/src/ChatBlock/tests/MultiToolRenderer.test.jsx +134 -0
- package/src/ChatBlock/tests/ReasoningRenderer.test.jsx +163 -0
- package/src/ChatBlock/tests/RenderClaimView.test.jsx +191 -0
- package/src/ChatBlock/tests/RendererComponent.test.jsx +139 -0
- package/src/ChatBlock/tests/SearchToolRenderer.test.jsx +295 -0
- package/src/ChatBlock/tests/SourceChip.test.jsx +108 -0
- package/src/ChatBlock/tests/UserActionsToolbar.test.jsx +135 -0
- package/src/ChatBlock/tests/UserMessage.test.jsx +83 -0
- package/src/ChatBlock/tests/WebResultIcon.test.jsx +61 -0
- package/src/ChatBlock/tests/citations.test.js +114 -0
- package/src/ChatBlock/tests/messageProcessor.test.jsx +285 -1
- package/src/ChatBlock/tests/packetUtils.test.js +158 -0
- package/src/ChatBlock/tests/streamingService.test.js +467 -0
- package/src/ChatBlock/tests/useChatController.test.jsx +268 -0
- package/src/ChatBlock/tests/useChatStreaming.test.jsx +163 -0
- package/src/ChatBlock/tests/useMarked.test.jsx +107 -0
- package/src/ChatBlock/tests/useQualityMarkers.test.jsx +150 -0
- package/src/ChatBlock/tests/useScrollonStream.test.jsx +121 -0
- package/src/ChatBlock/tests/utils.test.jsx +241 -0
- package/src/ChatBlock/tests/withOnyxData.test.jsx +81 -0
- package/src/ChatBlock/utils/citations.ts +1 -1
- package/src/halloumi/generative.js +1 -0
- package/src/halloumi/generative.test.js +278 -0
- package/src/halloumi/middleware.test.js +69 -0
- package/src/index.js +1 -0
- package/src/middleware.js +21 -13
- package/src/middleware.test.js +221 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { renderHook, act } from '@testing-library/react-hooks';
|
|
2
|
+
import { useScrollonStream } from '../hooks/useScrollonStream';
|
|
3
|
+
|
|
4
|
+
describe('useScrollonStream', () => {
|
|
5
|
+
let addEventListenerSpy;
|
|
6
|
+
let removeEventListenerSpy;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
jest.useFakeTimers();
|
|
10
|
+
addEventListenerSpy = jest.spyOn(window, 'addEventListener');
|
|
11
|
+
removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
jest.useRealTimers();
|
|
16
|
+
addEventListenerSpy.mockRestore();
|
|
17
|
+
removeEventListenerSpy.mockRestore();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('sets up event listeners when enabled and streaming', () => {
|
|
21
|
+
const bottomRef = { current: document.createElement('div') };
|
|
22
|
+
renderHook(() =>
|
|
23
|
+
useScrollonStream({
|
|
24
|
+
bottomRef,
|
|
25
|
+
isStreaming: true,
|
|
26
|
+
enabled: true,
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const eventTypes = addEventListenerSpy.mock.calls.map((c) => c[0]);
|
|
31
|
+
expect(eventTypes).toContain('wheel');
|
|
32
|
+
expect(eventTypes).toContain('touchstart');
|
|
33
|
+
expect(eventTypes).toContain('keydown');
|
|
34
|
+
expect(eventTypes).toContain('mousedown');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('disables scrolling on user wheel event', () => {
|
|
38
|
+
const bottomRef = { current: document.createElement('div') };
|
|
39
|
+
renderHook(() =>
|
|
40
|
+
useScrollonStream({
|
|
41
|
+
bottomRef,
|
|
42
|
+
isStreaming: true,
|
|
43
|
+
enabled: true,
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
act(() => {
|
|
48
|
+
window.dispatchEvent(new Event('wheel'));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// After wheel event, the scroll listener should be removed
|
|
52
|
+
expect(removeEventListenerSpy).toHaveBeenCalled();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('sets isActive to false after streaming stops with grace period', () => {
|
|
56
|
+
const bottomRef = { current: document.createElement('div') };
|
|
57
|
+
const { rerender } = renderHook(
|
|
58
|
+
({ isStreaming }) =>
|
|
59
|
+
useScrollonStream({
|
|
60
|
+
bottomRef,
|
|
61
|
+
isStreaming,
|
|
62
|
+
enabled: true,
|
|
63
|
+
}),
|
|
64
|
+
{ initialProps: { isStreaming: true } },
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Stop streaming
|
|
68
|
+
rerender({ isStreaming: false });
|
|
69
|
+
|
|
70
|
+
// Should still be active before timeout
|
|
71
|
+
act(() => {
|
|
72
|
+
jest.advanceTimersByTime(500);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('does not set up event listeners when disabled', () => {
|
|
77
|
+
const bottomRef = { current: document.createElement('div') };
|
|
78
|
+
const callsBefore = addEventListenerSpy.mock.calls.length;
|
|
79
|
+
|
|
80
|
+
renderHook(() =>
|
|
81
|
+
useScrollonStream({
|
|
82
|
+
bottomRef,
|
|
83
|
+
isStreaming: true,
|
|
84
|
+
enabled: false,
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Only the streaming state listener calls, not user input listeners
|
|
89
|
+
const newCalls = addEventListenerSpy.mock.calls
|
|
90
|
+
.slice(callsBefore)
|
|
91
|
+
.map((c) => c[0]);
|
|
92
|
+
expect(newCalls).not.toContain('wheel');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('handles missing bottomRef gracefully', () => {
|
|
96
|
+
const bottomRef = { current: null };
|
|
97
|
+
expect(() => {
|
|
98
|
+
renderHook(() =>
|
|
99
|
+
useScrollonStream({
|
|
100
|
+
bottomRef,
|
|
101
|
+
isStreaming: true,
|
|
102
|
+
enabled: true,
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
}).not.toThrow();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('cleans up on unmount', () => {
|
|
109
|
+
const bottomRef = { current: document.createElement('div') };
|
|
110
|
+
const { unmount } = renderHook(() =>
|
|
111
|
+
useScrollonStream({
|
|
112
|
+
bottomRef,
|
|
113
|
+
isStreaming: true,
|
|
114
|
+
enabled: true,
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
unmount();
|
|
119
|
+
expect(removeEventListenerSpy).toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { act } from '@testing-library/react';
|
|
2
|
+
import { renderHook } from '@testing-library/react-hooks';
|
|
3
|
+
import '@testing-library/jest-dom/extend-expect';
|
|
4
|
+
import {
|
|
5
|
+
EMAIL_REGEX,
|
|
6
|
+
transformEmailsToLinks,
|
|
7
|
+
debounce,
|
|
8
|
+
useCopyToClipboard,
|
|
9
|
+
convertToPercentage,
|
|
10
|
+
createChatMessageFeedback,
|
|
11
|
+
} from '../utils';
|
|
12
|
+
|
|
13
|
+
describe('utils', () => {
|
|
14
|
+
describe('EMAIL_REGEX', () => {
|
|
15
|
+
it('matches valid email addresses', () => {
|
|
16
|
+
expect('test@example.com').toMatch(EMAIL_REGEX);
|
|
17
|
+
expect('user.name@domain.org').toMatch(EMAIL_REGEX);
|
|
18
|
+
expect('user+tag@example.co.uk').toMatch(EMAIL_REGEX);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('does not match invalid email addresses', () => {
|
|
22
|
+
expect('not-an-email').not.toMatch(EMAIL_REGEX);
|
|
23
|
+
expect('@missing-local.com').not.toMatch(EMAIL_REGEX);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('transformEmailsToLinks', () => {
|
|
28
|
+
it('transforms email addresses to mailto links', () => {
|
|
29
|
+
const text = 'Contact us at test@example.com';
|
|
30
|
+
const result = transformEmailsToLinks(text);
|
|
31
|
+
// Result includes: ['Contact us at ', <a>email</a>, '']
|
|
32
|
+
expect(result.length).toBeGreaterThanOrEqual(2);
|
|
33
|
+
expect(result[0]).toBe('Contact us at ');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('handles text without email addresses', () => {
|
|
37
|
+
const text = 'No email here';
|
|
38
|
+
const result = transformEmailsToLinks(text);
|
|
39
|
+
expect(result).toHaveLength(1);
|
|
40
|
+
expect(result[0]).toBe('No email here');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('handles multiple email addresses', () => {
|
|
44
|
+
const text = 'Email one@test.com and two@test.com';
|
|
45
|
+
const result = transformEmailsToLinks(text);
|
|
46
|
+
expect(result.length).toBeGreaterThan(1);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('debounce', () => {
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
jest.useFakeTimers();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
jest.useRealTimers();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('calls the function immediately on first call', () => {
|
|
60
|
+
const callable = jest.fn();
|
|
61
|
+
const clickSignal = { current: null };
|
|
62
|
+
|
|
63
|
+
debounce(callable, clickSignal);
|
|
64
|
+
|
|
65
|
+
expect(callable).toHaveBeenCalledTimes(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('prevents multiple calls within debounce period', () => {
|
|
69
|
+
const callable = jest.fn();
|
|
70
|
+
const clickSignal = { current: null };
|
|
71
|
+
|
|
72
|
+
debounce(callable, clickSignal);
|
|
73
|
+
debounce(callable, clickSignal);
|
|
74
|
+
debounce(callable, clickSignal);
|
|
75
|
+
|
|
76
|
+
expect(callable).toHaveBeenCalledTimes(1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('allows call after debounce period', () => {
|
|
80
|
+
const callable = jest.fn();
|
|
81
|
+
const clickSignal = { current: null };
|
|
82
|
+
|
|
83
|
+
debounce(callable, clickSignal);
|
|
84
|
+
expect(callable).toHaveBeenCalledTimes(1);
|
|
85
|
+
|
|
86
|
+
jest.advanceTimersByTime(1000);
|
|
87
|
+
|
|
88
|
+
debounce(callable, clickSignal);
|
|
89
|
+
expect(callable).toHaveBeenCalledTimes(2);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('useCopyToClipboard', () => {
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
jest.useFakeTimers();
|
|
96
|
+
Object.assign(navigator, {
|
|
97
|
+
clipboard: {
|
|
98
|
+
writeText: jest.fn().mockResolvedValue(undefined),
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
jest.useRealTimers();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('returns initial state as not copied', () => {
|
|
108
|
+
const { result } = renderHook(() => useCopyToClipboard('test'));
|
|
109
|
+
expect(result.current[0]).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('copies text to clipboard when copy is called', async () => {
|
|
113
|
+
const { result } = renderHook(() => useCopyToClipboard('test text'));
|
|
114
|
+
|
|
115
|
+
await act(async () => {
|
|
116
|
+
result.current[1]();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('test text');
|
|
120
|
+
expect(result.current[0]).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('resets copied state after timeout', async () => {
|
|
124
|
+
const { result } = renderHook(() => useCopyToClipboard('test'));
|
|
125
|
+
|
|
126
|
+
await act(async () => {
|
|
127
|
+
result.current[1]();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(result.current[0]).toBe(true);
|
|
131
|
+
|
|
132
|
+
act(() => {
|
|
133
|
+
jest.advanceTimersByTime(2000);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(result.current[0]).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('handles clipboard write failure', async () => {
|
|
140
|
+
navigator.clipboard.writeText = jest.fn().mockRejectedValue(new Error());
|
|
141
|
+
|
|
142
|
+
const { result } = renderHook(() => useCopyToClipboard('test'));
|
|
143
|
+
|
|
144
|
+
await act(async () => {
|
|
145
|
+
result.current[1]();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(result.current[0]).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('convertToPercentage', () => {
|
|
153
|
+
it('converts float to percentage string', () => {
|
|
154
|
+
expect(convertToPercentage(0.5)).toBe('50.00%');
|
|
155
|
+
expect(convertToPercentage(0.123)).toBe('12.30%');
|
|
156
|
+
expect(convertToPercentage(1)).toBe('100.00%');
|
|
157
|
+
expect(convertToPercentage(0)).toBe('0.00%');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('handles custom digit precision', () => {
|
|
161
|
+
expect(convertToPercentage(0.5, 0)).toBe('50%');
|
|
162
|
+
expect(convertToPercentage(0.5, 1)).toBe('50.0%');
|
|
163
|
+
expect(convertToPercentage(0.123, 1)).toBe('12.3%');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('returns 0% for values outside 0-1 range', () => {
|
|
167
|
+
expect(convertToPercentage(-0.5)).toBe('0%');
|
|
168
|
+
expect(convertToPercentage(1.5)).toBe('0%');
|
|
169
|
+
expect(convertToPercentage(-1)).toBe('0%');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('createChatMessageFeedback', () => {
|
|
174
|
+
beforeEach(() => {
|
|
175
|
+
global.fetch = jest.fn();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
afterEach(() => {
|
|
179
|
+
jest.resetAllMocks();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('sends positive feedback correctly', async () => {
|
|
183
|
+
const mockResponse = { success: true };
|
|
184
|
+
global.fetch.mockResolvedValue({
|
|
185
|
+
ok: true,
|
|
186
|
+
json: () => Promise.resolve(mockResponse),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await createChatMessageFeedback({
|
|
190
|
+
chat_message_id: '123',
|
|
191
|
+
is_positive: true,
|
|
192
|
+
feedback_text: 'Great response!',
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
196
|
+
'/_da/chat/create-chat-message-feedback',
|
|
197
|
+
expect.objectContaining({
|
|
198
|
+
method: 'POST',
|
|
199
|
+
headers: { 'Content-Type': 'application/json' },
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const callBody = JSON.parse(fetch.mock.calls[0][1].body);
|
|
204
|
+
expect(callBody.chat_message_id).toBe('123');
|
|
205
|
+
expect(callBody.is_positive).toBe(true);
|
|
206
|
+
expect(callBody.predefined_feedback).toBeUndefined();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('sends negative feedback with predefined reason', async () => {
|
|
210
|
+
const mockResponse = { success: true };
|
|
211
|
+
global.fetch.mockResolvedValue({
|
|
212
|
+
ok: true,
|
|
213
|
+
json: () => Promise.resolve(mockResponse),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await createChatMessageFeedback({
|
|
217
|
+
chat_message_id: '123',
|
|
218
|
+
is_positive: false,
|
|
219
|
+
predefined_feedback: 'Incorrect information',
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const callBody = JSON.parse(fetch.mock.calls[0][1].body);
|
|
223
|
+
expect(callBody.is_positive).toBe(false);
|
|
224
|
+
expect(callBody.predefined_feedback).toBe('Incorrect information');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('throws error on failed request', async () => {
|
|
228
|
+
global.fetch.mockResolvedValue({
|
|
229
|
+
ok: false,
|
|
230
|
+
status: 500,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
await expect(
|
|
234
|
+
createChatMessageFeedback({
|
|
235
|
+
chat_message_id: '123',
|
|
236
|
+
is_positive: true,
|
|
237
|
+
}),
|
|
238
|
+
).rejects.toThrow('Failed to submit feedback.');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import '@testing-library/jest-dom/extend-expect';
|
|
3
|
+
import withOnyxData from '../hocs/withOnyxData';
|
|
4
|
+
|
|
5
|
+
describe('withOnyxData', () => {
|
|
6
|
+
it('shows loader initially', () => {
|
|
7
|
+
const TestComponent = ({ data }) => <div>Data: {data}</div>;
|
|
8
|
+
const callback = () => ['data', null, 'test'];
|
|
9
|
+
const WrappedComponent = withOnyxData(callback)(TestComponent);
|
|
10
|
+
|
|
11
|
+
render(<WrappedComponent />);
|
|
12
|
+
|
|
13
|
+
// The Placeholder component should be rendered
|
|
14
|
+
expect(screen.queryByText('Data:')).not.toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('renders component with fetched data', async () => {
|
|
18
|
+
const TestComponent = ({ testData }) => <div>Data: {testData}</div>;
|
|
19
|
+
const mockFetcher = Promise.resolve({ body: 'fetched value' });
|
|
20
|
+
const callback = () => ['testData', mockFetcher, 'test-key'];
|
|
21
|
+
const WrappedComponent = withOnyxData(callback)(TestComponent);
|
|
22
|
+
|
|
23
|
+
render(<WrappedComponent />);
|
|
24
|
+
|
|
25
|
+
await waitFor(() => {
|
|
26
|
+
expect(screen.getByText('Data: fetched value')).toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('passes original props to wrapped component', async () => {
|
|
31
|
+
const TestComponent = ({ testData, originalProp }) => (
|
|
32
|
+
<div>
|
|
33
|
+
Data: {testData}, Original: {originalProp}
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
const mockFetcher = Promise.resolve({ body: 'fetched value' });
|
|
37
|
+
const callback = () => ['testData', mockFetcher, 'test-key'];
|
|
38
|
+
const WrappedComponent = withOnyxData(callback)(TestComponent);
|
|
39
|
+
|
|
40
|
+
render(<WrappedComponent originalProp="original value" />);
|
|
41
|
+
|
|
42
|
+
await waitFor(() => {
|
|
43
|
+
expect(
|
|
44
|
+
screen.getByText('Data: fetched value, Original: original value'),
|
|
45
|
+
).toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('handles null fetcher', async () => {
|
|
50
|
+
const TestComponent = ({ data }) => <div>Data: {data || 'none'}</div>;
|
|
51
|
+
const callback = () => ['data', null, 'test'];
|
|
52
|
+
const WrappedComponent = withOnyxData(callback)(TestComponent);
|
|
53
|
+
|
|
54
|
+
render(<WrappedComponent />);
|
|
55
|
+
|
|
56
|
+
// Should stay in loading state
|
|
57
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
58
|
+
expect(screen.queryByText('Data: none')).not.toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('refetches when depKey changes', async () => {
|
|
62
|
+
const TestComponent = ({ testData }) => <div>Data: {testData}</div>;
|
|
63
|
+
const callback = (props) => {
|
|
64
|
+
const mockFetcher = Promise.resolve({ body: `value-${props.depKey}` });
|
|
65
|
+
return ['testData', mockFetcher, props.depKey];
|
|
66
|
+
};
|
|
67
|
+
const WrappedComponent = withOnyxData(callback)(TestComponent);
|
|
68
|
+
|
|
69
|
+
const { rerender } = render(<WrappedComponent depKey="key1" />);
|
|
70
|
+
|
|
71
|
+
await waitFor(() => {
|
|
72
|
+
expect(screen.getByText('Data: value-key1')).toBeInTheDocument();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
rerender(<WrappedComponent depKey="key2" />);
|
|
76
|
+
|
|
77
|
+
await waitFor(() => {
|
|
78
|
+
expect(screen.getByText('Data: value-key2')).toBeInTheDocument();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -18,7 +18,7 @@ export function addCitations(text: string, message: Message): string {
|
|
|
18
18
|
return text.replaceAll(CITATION_MATCH, (match) => {
|
|
19
19
|
const number = match.match(/\d+/)?.[0];
|
|
20
20
|
if (!number || !message.citations) {
|
|
21
|
-
return
|
|
21
|
+
return match;
|
|
22
22
|
}
|
|
23
23
|
return `[${match}](${message.citations[number]})`;
|
|
24
24
|
});
|
|
@@ -79,6 +79,7 @@ async function getLLMResponse(model, prompt) {
|
|
|
79
79
|
const filePath = process.env.MOCK_HALLOUMI_FILE_PATH;
|
|
80
80
|
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
|
81
81
|
jsonData = JSON.parse(fileContent);
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
82
83
|
return jsonData;
|
|
83
84
|
}
|
|
84
85
|
|