@ai-sdk/react 0.0.21 → 0.0.23
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/.turbo/turbo-build.log +8 -8
- package/.turbo/turbo-clean.log +1 -1
- package/CHANGELOG.md +15 -0
- package/dist/index.js +41 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +41 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/use-chat.ts +48 -1
- package/src/use-chat.ui.test.tsx +771 -225
package/src/use-chat.ui.test.tsx
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
mockFetchError,
|
|
5
|
-
} from '@ai-sdk/ui-utils/test';
|
|
1
|
+
/* eslint-disable @next/next/no-img-element */
|
|
2
|
+
import { withTestServer } from '@ai-sdk/provider-utils/test';
|
|
3
|
+
import { formatStreamPart, getTextFromDataUrl } from '@ai-sdk/ui-utils';
|
|
6
4
|
import '@testing-library/jest-dom/vitest';
|
|
7
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
RenderResult,
|
|
7
|
+
cleanup,
|
|
8
|
+
findByText,
|
|
9
|
+
render,
|
|
10
|
+
screen,
|
|
11
|
+
waitFor,
|
|
12
|
+
} from '@testing-library/react';
|
|
8
13
|
import userEvent from '@testing-library/user-event';
|
|
9
|
-
import React from 'react';
|
|
14
|
+
import React, { useRef, useState } from 'react';
|
|
10
15
|
import { useChat } from './use-chat';
|
|
11
|
-
import { formatStreamPart } from '@ai-sdk/ui-utils';
|
|
12
16
|
|
|
13
17
|
describe('stream data stream', () => {
|
|
14
18
|
const TestComponent = () => {
|
|
@@ -52,110 +56,127 @@ describe('stream data stream', () => {
|
|
|
52
56
|
cleanup();
|
|
53
57
|
});
|
|
54
58
|
|
|
55
|
-
it(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
await screen.findByTestId('message-1');
|
|
67
|
-
expect(screen.getByTestId('message-1')).toHaveTextContent(
|
|
68
|
-
'AI: Hello, world.',
|
|
69
|
-
);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('should show streamed response with data', async () => {
|
|
73
|
-
mockFetchDataStream({
|
|
74
|
-
url: 'https://example.com/api/chat',
|
|
75
|
-
chunks: ['2:[{"t1":"v1"}]\n', '0:"Hello"\n'],
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
await userEvent.click(screen.getByTestId('do-append'));
|
|
79
|
-
|
|
80
|
-
await screen.findByTestId('data');
|
|
81
|
-
expect(screen.getByTestId('data')).toHaveTextContent('[{"t1":"v1"}]');
|
|
59
|
+
it(
|
|
60
|
+
'should show streamed response',
|
|
61
|
+
withTestServer(
|
|
62
|
+
{
|
|
63
|
+
type: 'stream-values',
|
|
64
|
+
url: '/api/chat',
|
|
65
|
+
content: ['0:"Hello"\n', '0:","\n', '0:" world"\n', '0:"."\n'],
|
|
66
|
+
},
|
|
67
|
+
async () => {
|
|
68
|
+
await userEvent.click(screen.getByTestId('do-append'));
|
|
82
69
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
});
|
|
70
|
+
await screen.findByTestId('message-0');
|
|
71
|
+
expect(screen.getByTestId('message-0')).toHaveTextContent('User: hi');
|
|
86
72
|
|
|
87
|
-
|
|
88
|
-
|
|
73
|
+
await screen.findByTestId('message-1');
|
|
74
|
+
expect(screen.getByTestId('message-1')).toHaveTextContent(
|
|
75
|
+
'AI: Hello, world.',
|
|
76
|
+
);
|
|
77
|
+
},
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
it(
|
|
82
|
+
'should show streamed response with data',
|
|
83
|
+
withTestServer(
|
|
84
|
+
{
|
|
85
|
+
type: 'stream-values',
|
|
86
|
+
url: '/api/chat',
|
|
87
|
+
content: ['2:[{"t1":"v1"}]\n', '0:"Hello"\n'],
|
|
88
|
+
},
|
|
89
|
+
async () => {
|
|
90
|
+
await userEvent.click(screen.getByTestId('do-append'));
|
|
89
91
|
|
|
90
|
-
|
|
92
|
+
await screen.findByTestId('data');
|
|
93
|
+
expect(screen.getByTestId('data')).toHaveTextContent('[{"t1":"v1"}]');
|
|
91
94
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
await screen.findByTestId('message-1');
|
|
96
|
+
expect(screen.getByTestId('message-1')).toHaveTextContent('AI: Hello');
|
|
97
|
+
},
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
it(
|
|
102
|
+
'should show error response',
|
|
103
|
+
withTestServer(
|
|
104
|
+
{ type: 'error', url: '/api/chat', status: 404, content: 'Not found' },
|
|
105
|
+
async () => {
|
|
106
|
+
await userEvent.click(screen.getByTestId('do-append'));
|
|
107
|
+
|
|
108
|
+
await screen.findByTestId('error');
|
|
109
|
+
expect(screen.getByTestId('error')).toHaveTextContent(
|
|
110
|
+
'Error: Not found',
|
|
111
|
+
);
|
|
112
|
+
},
|
|
113
|
+
),
|
|
114
|
+
);
|
|
95
115
|
|
|
96
116
|
describe('loading state', () => {
|
|
97
|
-
it(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
117
|
+
it(
|
|
118
|
+
'should show loading state',
|
|
119
|
+
withTestServer(
|
|
120
|
+
{ url: '/api/chat', type: 'controlled-stream' },
|
|
121
|
+
async ({ streamController }) => {
|
|
122
|
+
streamController.enqueue('0:"Hello"\n');
|
|
102
123
|
|
|
103
|
-
|
|
104
|
-
url: 'https://example.com/api/chat',
|
|
105
|
-
chunkGenerator: (async function* generate() {
|
|
106
|
-
const encoder = new TextEncoder();
|
|
107
|
-
yield encoder.encode('0:"Hello"\n');
|
|
108
|
-
await finishGenerationPromise;
|
|
109
|
-
})(),
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
await userEvent.click(screen.getByTestId('do-append'));
|
|
124
|
+
await userEvent.click(screen.getByTestId('do-append'));
|
|
113
125
|
|
|
114
|
-
|
|
115
|
-
|
|
126
|
+
await screen.findByTestId('loading');
|
|
127
|
+
expect(screen.getByTestId('loading')).toHaveTextContent('true');
|
|
116
128
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
await findByText(await screen.findByTestId('loading'), 'false');
|
|
120
|
-
expect(screen.getByTestId('loading')).toHaveTextContent('false');
|
|
121
|
-
});
|
|
129
|
+
streamController.close();
|
|
122
130
|
|
|
123
|
-
|
|
124
|
-
|
|
131
|
+
await findByText(await screen.findByTestId('loading'), 'false');
|
|
132
|
+
expect(screen.getByTestId('loading')).toHaveTextContent('false');
|
|
133
|
+
},
|
|
134
|
+
),
|
|
135
|
+
);
|
|
125
136
|
|
|
126
|
-
|
|
137
|
+
it(
|
|
138
|
+
'should reset loading state on error',
|
|
139
|
+
withTestServer(
|
|
140
|
+
{ type: 'error', url: '/api/chat', status: 404, content: 'Not found' },
|
|
141
|
+
async () => {
|
|
142
|
+
await userEvent.click(screen.getByTestId('do-append'));
|
|
127
143
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
144
|
+
await screen.findByTestId('loading');
|
|
145
|
+
expect(screen.getByTestId('loading')).toHaveTextContent('false');
|
|
146
|
+
},
|
|
147
|
+
),
|
|
148
|
+
);
|
|
131
149
|
});
|
|
132
150
|
|
|
133
151
|
describe('id', () => {
|
|
134
|
-
it(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
152
|
+
it(
|
|
153
|
+
'should clear out messages when the id changes',
|
|
154
|
+
withTestServer(
|
|
155
|
+
{
|
|
156
|
+
url: '/api/chat',
|
|
157
|
+
type: 'stream-values',
|
|
158
|
+
content: ['0:"Hello"\n', '0:","\n', '0:" world"\n', '0:"."\n'],
|
|
159
|
+
},
|
|
160
|
+
async () => {
|
|
161
|
+
await userEvent.click(screen.getByTestId('do-append'));
|
|
139
162
|
|
|
140
|
-
|
|
163
|
+
await screen.findByTestId('message-1');
|
|
164
|
+
expect(screen.getByTestId('message-1')).toHaveTextContent(
|
|
165
|
+
'AI: Hello, world.',
|
|
166
|
+
);
|
|
141
167
|
|
|
142
|
-
|
|
143
|
-
expect(screen.getByTestId('message-1')).toHaveTextContent(
|
|
144
|
-
'AI: Hello, world.',
|
|
145
|
-
);
|
|
168
|
+
await userEvent.click(screen.getByTestId('do-change-id'));
|
|
146
169
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
170
|
+
expect(screen.queryByTestId('message-0')).not.toBeInTheDocument();
|
|
171
|
+
},
|
|
172
|
+
),
|
|
173
|
+
);
|
|
151
174
|
});
|
|
152
175
|
});
|
|
153
176
|
|
|
154
177
|
describe('text stream', () => {
|
|
155
178
|
const TestComponent = () => {
|
|
156
|
-
const { messages, append } = useChat({
|
|
157
|
-
streamMode: 'text',
|
|
158
|
-
});
|
|
179
|
+
const { messages, append } = useChat({ streamMode: 'text' });
|
|
159
180
|
|
|
160
181
|
return (
|
|
161
182
|
<div>
|
|
@@ -185,38 +206,35 @@ describe('text stream', () => {
|
|
|
185
206
|
cleanup();
|
|
186
207
|
});
|
|
187
208
|
|
|
188
|
-
it(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
209
|
+
it(
|
|
210
|
+
'should show streamed response',
|
|
211
|
+
withTestServer(
|
|
212
|
+
{
|
|
213
|
+
url: '/api/chat',
|
|
214
|
+
type: 'stream-values',
|
|
215
|
+
content: ['Hello', ',', ' world', '.'],
|
|
216
|
+
},
|
|
217
|
+
async () => {
|
|
218
|
+
await userEvent.click(screen.getByTestId('do-append-text-stream'));
|
|
219
|
+
|
|
220
|
+
await screen.findByTestId('message-0-text-stream');
|
|
221
|
+
expect(screen.getByTestId('message-0-text-stream')).toHaveTextContent(
|
|
222
|
+
'User: hi',
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
await screen.findByTestId('message-1-text-stream');
|
|
226
|
+
expect(screen.getByTestId('message-1-text-stream')).toHaveTextContent(
|
|
227
|
+
'AI: Hello, world.',
|
|
228
|
+
);
|
|
229
|
+
},
|
|
230
|
+
),
|
|
231
|
+
);
|
|
206
232
|
});
|
|
207
233
|
|
|
208
234
|
describe('form actions', () => {
|
|
209
235
|
const TestComponent = () => {
|
|
210
|
-
const {
|
|
211
|
-
|
|
212
|
-
append,
|
|
213
|
-
handleSubmit,
|
|
214
|
-
handleInputChange,
|
|
215
|
-
isLoading,
|
|
216
|
-
input,
|
|
217
|
-
} = useChat({
|
|
218
|
-
streamMode: 'text',
|
|
219
|
-
});
|
|
236
|
+
const { messages, handleSubmit, handleInputChange, isLoading, input } =
|
|
237
|
+
useChat({ streamMode: 'text' });
|
|
220
238
|
|
|
221
239
|
return (
|
|
222
240
|
<div>
|
|
@@ -227,12 +245,11 @@ describe('form actions', () => {
|
|
|
227
245
|
</div>
|
|
228
246
|
))}
|
|
229
247
|
|
|
230
|
-
<form onSubmit={handleSubmit}
|
|
248
|
+
<form onSubmit={handleSubmit}>
|
|
231
249
|
<input
|
|
232
250
|
value={input}
|
|
233
251
|
placeholder="Send message..."
|
|
234
252
|
onChange={handleInputChange}
|
|
235
|
-
className="w-full p-2 bg-zinc-100"
|
|
236
253
|
disabled={isLoading}
|
|
237
254
|
data-testid="do-input"
|
|
238
255
|
/>
|
|
@@ -250,37 +267,44 @@ describe('form actions', () => {
|
|
|
250
267
|
cleanup();
|
|
251
268
|
});
|
|
252
269
|
|
|
253
|
-
it(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
270
|
+
it(
|
|
271
|
+
'should show streamed response using handleSubmit',
|
|
272
|
+
withTestServer(
|
|
273
|
+
[
|
|
274
|
+
{
|
|
275
|
+
url: '/api/chat',
|
|
276
|
+
type: 'stream-values',
|
|
277
|
+
content: ['Hello', ',', ' world', '.'],
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
url: '/api/chat',
|
|
281
|
+
type: 'stream-values',
|
|
282
|
+
content: ['How', ' can', ' I', ' help', ' you', '?'],
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
async () => {
|
|
286
|
+
const firstInput = screen.getByTestId('do-input');
|
|
287
|
+
await userEvent.type(firstInput, 'hi');
|
|
288
|
+
await userEvent.keyboard('{Enter}');
|
|
289
|
+
|
|
290
|
+
await screen.findByTestId('message-0');
|
|
291
|
+
expect(screen.getByTestId('message-0')).toHaveTextContent('User: hi');
|
|
292
|
+
|
|
293
|
+
await screen.findByTestId('message-1');
|
|
294
|
+
expect(screen.getByTestId('message-1')).toHaveTextContent(
|
|
295
|
+
'AI: Hello, world.',
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const secondInput = screen.getByTestId('do-input');
|
|
299
|
+
await userEvent.type(secondInput, '{Enter}');
|
|
300
|
+
|
|
301
|
+
await screen.findByTestId('message-2');
|
|
302
|
+
expect(screen.getByTestId('message-2')).toHaveTextContent(
|
|
303
|
+
'AI: How can I help you?',
|
|
304
|
+
);
|
|
305
|
+
},
|
|
306
|
+
),
|
|
307
|
+
);
|
|
284
308
|
});
|
|
285
309
|
|
|
286
310
|
describe('prepareRequestBody', () => {
|
|
@@ -330,30 +354,35 @@ describe('prepareRequestBody', () => {
|
|
|
330
354
|
cleanup();
|
|
331
355
|
});
|
|
332
356
|
|
|
333
|
-
it(
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
357
|
+
it(
|
|
358
|
+
'should show streamed response',
|
|
359
|
+
withTestServer(
|
|
360
|
+
{
|
|
361
|
+
url: '/api/chat',
|
|
362
|
+
type: 'stream-values',
|
|
363
|
+
content: ['0:"Hello"\n', '0:","\n', '0:" world"\n', '0:"."\n'],
|
|
364
|
+
},
|
|
365
|
+
async ({ call }) => {
|
|
366
|
+
await userEvent.click(screen.getByTestId('do-append'));
|
|
340
367
|
|
|
341
|
-
|
|
342
|
-
|
|
368
|
+
await screen.findByTestId('message-0');
|
|
369
|
+
expect(screen.getByTestId('message-0')).toHaveTextContent('User: hi');
|
|
343
370
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
371
|
+
expect(bodyOptions).toStrictEqual({
|
|
372
|
+
messages: [{ role: 'user', content: 'hi', id: expect.any(String) }],
|
|
373
|
+
requestData: { 'test-data-key': 'test-data-value' },
|
|
374
|
+
requestBody: { 'request-body-key': 'request-body-value' },
|
|
375
|
+
});
|
|
349
376
|
|
|
350
|
-
|
|
377
|
+
expect(await call(0).getRequestBodyJson()).toBe('test-request-body');
|
|
351
378
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
379
|
+
await screen.findByTestId('message-1');
|
|
380
|
+
expect(screen.getByTestId('message-1')).toHaveTextContent(
|
|
381
|
+
'AI: Hello, world.',
|
|
382
|
+
);
|
|
383
|
+
},
|
|
384
|
+
),
|
|
385
|
+
);
|
|
357
386
|
});
|
|
358
387
|
|
|
359
388
|
describe('onToolCall', () => {
|
|
@@ -399,36 +428,203 @@ describe('onToolCall', () => {
|
|
|
399
428
|
cleanup();
|
|
400
429
|
});
|
|
401
430
|
|
|
402
|
-
it(
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
431
|
+
it(
|
|
432
|
+
"should invoke onToolCall when a tool call is received from the server's response",
|
|
433
|
+
withTestServer(
|
|
434
|
+
{
|
|
435
|
+
url: '/api/chat',
|
|
436
|
+
type: 'stream-values',
|
|
437
|
+
content: [
|
|
438
|
+
formatStreamPart('tool_call', {
|
|
439
|
+
toolCallId: 'tool-call-0',
|
|
440
|
+
toolName: 'test-tool',
|
|
441
|
+
args: { testArg: 'test-value' },
|
|
442
|
+
}),
|
|
443
|
+
],
|
|
444
|
+
},
|
|
445
|
+
async () => {
|
|
446
|
+
await userEvent.click(screen.getByTestId('do-append'));
|
|
413
447
|
|
|
414
|
-
|
|
448
|
+
await screen.findByTestId('message-1');
|
|
449
|
+
expect(screen.getByTestId('message-1')).toHaveTextContent(
|
|
450
|
+
'test-tool-response: test-tool tool-call-0 {"testArg":"test-value"}',
|
|
451
|
+
);
|
|
452
|
+
},
|
|
453
|
+
),
|
|
454
|
+
);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
describe('tool invocations', () => {
|
|
458
|
+
let rerender: RenderResult['rerender'];
|
|
459
|
+
|
|
460
|
+
const TestComponent = () => {
|
|
461
|
+
const { messages, append } = useChat();
|
|
415
462
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
463
|
+
return (
|
|
464
|
+
<div>
|
|
465
|
+
{messages.map((m, idx) => (
|
|
466
|
+
<div data-testid={`message-${idx}`} key={m.id}>
|
|
467
|
+
{m.toolInvocations?.map((toolInvocation, toolIdx) => {
|
|
468
|
+
return (
|
|
469
|
+
<div key={toolIdx} data-testid={`tool-invocation-${toolIdx}`}>
|
|
470
|
+
{JSON.stringify(toolInvocation)}
|
|
471
|
+
</div>
|
|
472
|
+
);
|
|
473
|
+
})}
|
|
474
|
+
</div>
|
|
475
|
+
))}
|
|
476
|
+
|
|
477
|
+
<button
|
|
478
|
+
data-testid="do-append"
|
|
479
|
+
onClick={() => {
|
|
480
|
+
append({ role: 'user', content: 'hi' });
|
|
481
|
+
}}
|
|
482
|
+
/>
|
|
483
|
+
</div>
|
|
419
484
|
);
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
beforeEach(() => {
|
|
488
|
+
const result = render(<TestComponent />);
|
|
489
|
+
rerender = result.rerender;
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
afterEach(() => {
|
|
493
|
+
vi.restoreAllMocks();
|
|
494
|
+
cleanup();
|
|
420
495
|
});
|
|
496
|
+
|
|
497
|
+
it(
|
|
498
|
+
'should display partial tool call, tool call, and tool result',
|
|
499
|
+
withTestServer(
|
|
500
|
+
{ url: '/api/chat', type: 'controlled-stream' },
|
|
501
|
+
async ({ streamController }) => {
|
|
502
|
+
await userEvent.click(screen.getByTestId('do-append'));
|
|
503
|
+
|
|
504
|
+
streamController.enqueue(
|
|
505
|
+
formatStreamPart('tool_call_streaming_start', {
|
|
506
|
+
toolCallId: 'tool-call-0',
|
|
507
|
+
toolName: 'test-tool',
|
|
508
|
+
}),
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
await waitFor(() => {
|
|
512
|
+
expect(screen.getByTestId('message-1')).toHaveTextContent(
|
|
513
|
+
'{"state":"partial-call","toolCallId":"tool-call-0","toolName":"test-tool"}',
|
|
514
|
+
);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
streamController.enqueue(
|
|
518
|
+
formatStreamPart('tool_call_delta', {
|
|
519
|
+
toolCallId: 'tool-call-0',
|
|
520
|
+
argsTextDelta: '{"testArg":"t',
|
|
521
|
+
}),
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
await waitFor(() => {
|
|
525
|
+
rerender(<TestComponent />);
|
|
526
|
+
expect(screen.getByTestId('message-1')).toHaveTextContent(
|
|
527
|
+
'{"state":"partial-call","toolCallId":"tool-call-0","toolName":"test-tool","args":{"testArg":"t"}}',
|
|
528
|
+
);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
streamController.enqueue(
|
|
532
|
+
formatStreamPart('tool_call_delta', {
|
|
533
|
+
toolCallId: 'tool-call-0',
|
|
534
|
+
argsTextDelta: 'est-value"}}',
|
|
535
|
+
}),
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
await waitFor(() => {
|
|
539
|
+
rerender(<TestComponent />);
|
|
540
|
+
expect(screen.getByTestId('message-1')).toHaveTextContent(
|
|
541
|
+
'{"state":"partial-call","toolCallId":"tool-call-0","toolName":"test-tool","args":{"testArg":"test-value"}}',
|
|
542
|
+
);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
streamController.enqueue(
|
|
546
|
+
formatStreamPart('tool_call', {
|
|
547
|
+
toolCallId: 'tool-call-0',
|
|
548
|
+
toolName: 'test-tool',
|
|
549
|
+
args: { testArg: 'test-value' },
|
|
550
|
+
}),
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
await waitFor(() => {
|
|
554
|
+
rerender(<TestComponent />);
|
|
555
|
+
expect(screen.getByTestId('message-1')).toHaveTextContent(
|
|
556
|
+
'{"state":"call","toolCallId":"tool-call-0","toolName":"test-tool","args":{"testArg":"test-value"}}',
|
|
557
|
+
);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
streamController.enqueue(
|
|
561
|
+
formatStreamPart('tool_result', {
|
|
562
|
+
toolCallId: 'tool-call-0',
|
|
563
|
+
toolName: 'test-tool',
|
|
564
|
+
args: { testArg: 'test-value' },
|
|
565
|
+
result: 'test-result',
|
|
566
|
+
}),
|
|
567
|
+
);
|
|
568
|
+
streamController.close();
|
|
569
|
+
|
|
570
|
+
await waitFor(() => {
|
|
571
|
+
expect(screen.getByTestId('message-1')).toHaveTextContent(
|
|
572
|
+
'{"state":"result","toolCallId":"tool-call-0","toolName":"test-tool","args":{"testArg":"test-value"},"result":"test-result"}',
|
|
573
|
+
);
|
|
574
|
+
});
|
|
575
|
+
},
|
|
576
|
+
),
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
it(
|
|
580
|
+
'should display partial tool call and tool result (when there is no tool call streaming)',
|
|
581
|
+
withTestServer(
|
|
582
|
+
{ url: '/api/chat', type: 'controlled-stream' },
|
|
583
|
+
async ({ streamController }) => {
|
|
584
|
+
await userEvent.click(screen.getByTestId('do-append'));
|
|
585
|
+
|
|
586
|
+
streamController.enqueue(
|
|
587
|
+
formatStreamPart('tool_call', {
|
|
588
|
+
toolCallId: 'tool-call-0',
|
|
589
|
+
toolName: 'test-tool',
|
|
590
|
+
args: { testArg: 'test-value' },
|
|
591
|
+
}),
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
await waitFor(() => {
|
|
595
|
+
expect(screen.getByTestId('message-1')).toHaveTextContent(
|
|
596
|
+
'{"state":"call","toolCallId":"tool-call-0","toolName":"test-tool","args":{"testArg":"test-value"}}',
|
|
597
|
+
);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
streamController.enqueue(
|
|
601
|
+
formatStreamPart('tool_result', {
|
|
602
|
+
toolCallId: 'tool-call-0',
|
|
603
|
+
toolName: 'test-tool',
|
|
604
|
+
args: { testArg: 'test-value' },
|
|
605
|
+
result: 'test-result',
|
|
606
|
+
}),
|
|
607
|
+
);
|
|
608
|
+
streamController.close();
|
|
609
|
+
|
|
610
|
+
await waitFor(() => {
|
|
611
|
+
expect(screen.getByTestId('message-1')).toHaveTextContent(
|
|
612
|
+
'{"state":"result","toolCallId":"tool-call-0","toolName":"test-tool","args":{"testArg":"test-value"},"result":"test-result"}',
|
|
613
|
+
);
|
|
614
|
+
});
|
|
615
|
+
},
|
|
616
|
+
),
|
|
617
|
+
);
|
|
421
618
|
});
|
|
422
619
|
|
|
423
620
|
describe('maxToolRoundtrips', () => {
|
|
424
621
|
describe('single automatic tool roundtrip', () => {
|
|
622
|
+
let onToolCallInvoked = false;
|
|
623
|
+
|
|
425
624
|
const TestComponent = () => {
|
|
426
625
|
const { messages, append } = useChat({
|
|
427
626
|
async onToolCall({ toolCall }) {
|
|
428
|
-
|
|
429
|
-
url: 'https://example.com/api/chat',
|
|
430
|
-
chunks: [formatStreamPart('text', 'final result')],
|
|
431
|
-
});
|
|
627
|
+
onToolCallInvoked = true;
|
|
432
628
|
|
|
433
629
|
return `test-tool-response: ${toolCall.toolName} ${
|
|
434
630
|
toolCall.toolCallId
|
|
@@ -457,6 +653,7 @@ describe('maxToolRoundtrips', () => {
|
|
|
457
653
|
|
|
458
654
|
beforeEach(() => {
|
|
459
655
|
render(<TestComponent />);
|
|
656
|
+
onToolCallInvoked = false;
|
|
460
657
|
});
|
|
461
658
|
|
|
462
659
|
afterEach(() => {
|
|
@@ -464,35 +661,48 @@ describe('maxToolRoundtrips', () => {
|
|
|
464
661
|
cleanup();
|
|
465
662
|
});
|
|
466
663
|
|
|
467
|
-
it(
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
664
|
+
it(
|
|
665
|
+
'should automatically call api when tool call gets executed via onToolCall',
|
|
666
|
+
withTestServer(
|
|
667
|
+
[
|
|
668
|
+
{
|
|
669
|
+
url: '/api/chat',
|
|
670
|
+
type: 'stream-values',
|
|
671
|
+
content: [
|
|
672
|
+
formatStreamPart('tool_call', {
|
|
673
|
+
toolCallId: 'tool-call-0',
|
|
674
|
+
toolName: 'test-tool',
|
|
675
|
+
args: { testArg: 'test-value' },
|
|
676
|
+
}),
|
|
677
|
+
],
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
url: '/api/chat',
|
|
681
|
+
type: 'stream-values',
|
|
682
|
+
content: [formatStreamPart('text', 'final result')],
|
|
683
|
+
},
|
|
476
684
|
],
|
|
477
|
-
|
|
685
|
+
async () => {
|
|
686
|
+
await userEvent.click(screen.getByTestId('do-append'));
|
|
478
687
|
|
|
479
|
-
|
|
688
|
+
expect(onToolCallInvoked).toBe(true);
|
|
480
689
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
690
|
+
await screen.findByTestId('message-2');
|
|
691
|
+
expect(screen.getByTestId('message-2')).toHaveTextContent(
|
|
692
|
+
'final result',
|
|
693
|
+
);
|
|
694
|
+
},
|
|
695
|
+
),
|
|
696
|
+
);
|
|
484
697
|
});
|
|
485
698
|
|
|
486
699
|
describe('single roundtrip with error response', () => {
|
|
700
|
+
let onToolCallCounter = 0;
|
|
701
|
+
|
|
487
702
|
const TestComponent = () => {
|
|
488
703
|
const { messages, append, error } = useChat({
|
|
489
704
|
async onToolCall({ toolCall }) {
|
|
490
|
-
|
|
491
|
-
url: 'https://example.com/api/chat',
|
|
492
|
-
chunks: [formatStreamPart('error', 'some failure')],
|
|
493
|
-
maxCalls: 1,
|
|
494
|
-
});
|
|
495
|
-
|
|
705
|
+
onToolCallCounter++;
|
|
496
706
|
return `test-tool-response: ${toolCall.toolName} ${
|
|
497
707
|
toolCall.toolCallId
|
|
498
708
|
} ${JSON.stringify(toolCall.args)}`;
|
|
@@ -528,6 +738,7 @@ describe('maxToolRoundtrips', () => {
|
|
|
528
738
|
|
|
529
739
|
beforeEach(() => {
|
|
530
740
|
render(<TestComponent />);
|
|
741
|
+
onToolCallCounter = 0;
|
|
531
742
|
});
|
|
532
743
|
|
|
533
744
|
afterEach(() => {
|
|
@@ -535,24 +746,359 @@ describe('maxToolRoundtrips', () => {
|
|
|
535
746
|
cleanup();
|
|
536
747
|
});
|
|
537
748
|
|
|
538
|
-
it(
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
749
|
+
it(
|
|
750
|
+
'should automatically call api when tool call gets executed via onToolCall',
|
|
751
|
+
withTestServer(
|
|
752
|
+
[
|
|
753
|
+
{
|
|
754
|
+
url: '/api/chat',
|
|
755
|
+
type: 'stream-values',
|
|
756
|
+
content: [
|
|
757
|
+
formatStreamPart('tool_call', {
|
|
758
|
+
toolCallId: 'tool-call-0',
|
|
759
|
+
toolName: 'test-tool',
|
|
760
|
+
args: { testArg: 'test-value' },
|
|
761
|
+
}),
|
|
762
|
+
],
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
url: '/api/chat',
|
|
766
|
+
type: 'error',
|
|
767
|
+
status: 400,
|
|
768
|
+
content: 'call failure',
|
|
769
|
+
},
|
|
547
770
|
],
|
|
548
|
-
|
|
771
|
+
async () => {
|
|
772
|
+
await userEvent.click(screen.getByTestId('do-append'));
|
|
549
773
|
|
|
550
|
-
|
|
774
|
+
await screen.findByTestId('error');
|
|
775
|
+
expect(screen.getByTestId('error')).toHaveTextContent(
|
|
776
|
+
'Error: call failure',
|
|
777
|
+
);
|
|
551
778
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
});
|
|
779
|
+
expect(onToolCallCounter).toBe(1);
|
|
780
|
+
},
|
|
781
|
+
),
|
|
782
|
+
);
|
|
557
783
|
});
|
|
558
784
|
});
|
|
785
|
+
|
|
786
|
+
describe('file attachments with data url', () => {
|
|
787
|
+
const TestComponent = () => {
|
|
788
|
+
const { messages, handleSubmit, handleInputChange, isLoading, input } =
|
|
789
|
+
useChat();
|
|
790
|
+
|
|
791
|
+
const [attachments, setAttachments] = useState<FileList | undefined>(
|
|
792
|
+
undefined,
|
|
793
|
+
);
|
|
794
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
795
|
+
|
|
796
|
+
return (
|
|
797
|
+
<div>
|
|
798
|
+
{messages.map((m, idx) => (
|
|
799
|
+
<div data-testid={`message-${idx}`} key={m.id}>
|
|
800
|
+
{m.role === 'user' ? 'User: ' : 'AI: '}
|
|
801
|
+
{m.content}
|
|
802
|
+
{m.experimental_attachments?.map(attachment => {
|
|
803
|
+
if (attachment.contentType?.startsWith('image/')) {
|
|
804
|
+
return (
|
|
805
|
+
<img
|
|
806
|
+
key={attachment.name}
|
|
807
|
+
src={attachment.url}
|
|
808
|
+
alt={attachment.name}
|
|
809
|
+
data-testid={`attachment-${idx}`}
|
|
810
|
+
/>
|
|
811
|
+
);
|
|
812
|
+
} else if (attachment.contentType?.startsWith('text/')) {
|
|
813
|
+
return (
|
|
814
|
+
<div key={attachment.name} data-testid={`attachment-${idx}`}>
|
|
815
|
+
{getTextFromDataUrl(attachment.url)}
|
|
816
|
+
</div>
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
})}
|
|
820
|
+
</div>
|
|
821
|
+
))}
|
|
822
|
+
|
|
823
|
+
<form
|
|
824
|
+
onSubmit={event => {
|
|
825
|
+
handleSubmit(event, {
|
|
826
|
+
experimental_attachments: attachments,
|
|
827
|
+
});
|
|
828
|
+
setAttachments(undefined);
|
|
829
|
+
if (fileInputRef.current) {
|
|
830
|
+
fileInputRef.current.value = '';
|
|
831
|
+
}
|
|
832
|
+
}}
|
|
833
|
+
data-testid="chat-form"
|
|
834
|
+
>
|
|
835
|
+
<input
|
|
836
|
+
type="file"
|
|
837
|
+
onChange={event => {
|
|
838
|
+
if (event.target.files) {
|
|
839
|
+
setAttachments(event.target.files);
|
|
840
|
+
}
|
|
841
|
+
}}
|
|
842
|
+
multiple
|
|
843
|
+
ref={fileInputRef}
|
|
844
|
+
data-testid="file-input"
|
|
845
|
+
/>
|
|
846
|
+
<input
|
|
847
|
+
value={input}
|
|
848
|
+
onChange={handleInputChange}
|
|
849
|
+
disabled={isLoading}
|
|
850
|
+
data-testid="message-input"
|
|
851
|
+
/>
|
|
852
|
+
<button type="submit" data-testid="submit-button">
|
|
853
|
+
Send
|
|
854
|
+
</button>
|
|
855
|
+
</form>
|
|
856
|
+
</div>
|
|
857
|
+
);
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
beforeEach(() => {
|
|
861
|
+
render(<TestComponent />);
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
afterEach(() => {
|
|
865
|
+
vi.restoreAllMocks();
|
|
866
|
+
cleanup();
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
it(
|
|
870
|
+
'should handle text file attachment and submission',
|
|
871
|
+
withTestServer(
|
|
872
|
+
{
|
|
873
|
+
url: '/api/chat',
|
|
874
|
+
type: 'stream-values',
|
|
875
|
+
content: ['0:"Response to message with text attachment"\n'],
|
|
876
|
+
},
|
|
877
|
+
async ({ call }) => {
|
|
878
|
+
const file = new File(['test file content'], 'test.txt', {
|
|
879
|
+
type: 'text/plain',
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
const fileInput = screen.getByTestId('file-input');
|
|
883
|
+
await userEvent.upload(fileInput, file);
|
|
884
|
+
|
|
885
|
+
const messageInput = screen.getByTestId('message-input');
|
|
886
|
+
await userEvent.type(messageInput, 'Message with text attachment');
|
|
887
|
+
|
|
888
|
+
const submitButton = screen.getByTestId('submit-button');
|
|
889
|
+
await userEvent.click(submitButton);
|
|
890
|
+
|
|
891
|
+
expect(await call(0).getRequestBodyJson()).toStrictEqual({
|
|
892
|
+
messages: [
|
|
893
|
+
{
|
|
894
|
+
role: 'user',
|
|
895
|
+
content: 'Message with text attachment',
|
|
896
|
+
experimental_attachments: [
|
|
897
|
+
{
|
|
898
|
+
name: 'test.txt',
|
|
899
|
+
contentType: 'text/plain',
|
|
900
|
+
url: 'data:text/plain;base64,dGVzdCBmaWxlIGNvbnRlbnQ=',
|
|
901
|
+
},
|
|
902
|
+
],
|
|
903
|
+
},
|
|
904
|
+
],
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
await screen.findByTestId('message-0');
|
|
908
|
+
expect(screen.getByTestId('message-0')).toHaveTextContent(
|
|
909
|
+
'User: Message with text attachment',
|
|
910
|
+
);
|
|
911
|
+
|
|
912
|
+
await screen.findByTestId('attachment-0');
|
|
913
|
+
expect(screen.getByTestId('attachment-0')).toHaveTextContent(
|
|
914
|
+
'test file content',
|
|
915
|
+
);
|
|
916
|
+
|
|
917
|
+
await screen.findByTestId('message-1');
|
|
918
|
+
expect(screen.getByTestId('message-1')).toHaveTextContent(
|
|
919
|
+
'AI: Response to message with text attachment',
|
|
920
|
+
);
|
|
921
|
+
},
|
|
922
|
+
),
|
|
923
|
+
);
|
|
924
|
+
|
|
925
|
+
it(
|
|
926
|
+
'should handle image file attachment and submission',
|
|
927
|
+
withTestServer(
|
|
928
|
+
{
|
|
929
|
+
url: '/api/chat',
|
|
930
|
+
type: 'stream-values',
|
|
931
|
+
content: ['0:"Response to message with image attachment"\n'],
|
|
932
|
+
},
|
|
933
|
+
async ({ call }) => {
|
|
934
|
+
const file = new File(['test image content'], 'test.png', {
|
|
935
|
+
type: 'image/png',
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
const fileInput = screen.getByTestId('file-input');
|
|
939
|
+
await userEvent.upload(fileInput, file);
|
|
940
|
+
|
|
941
|
+
const messageInput = screen.getByTestId('message-input');
|
|
942
|
+
await userEvent.type(messageInput, 'Message with image attachment');
|
|
943
|
+
|
|
944
|
+
const submitButton = screen.getByTestId('submit-button');
|
|
945
|
+
await userEvent.click(submitButton);
|
|
946
|
+
|
|
947
|
+
expect(await call(0).getRequestBodyJson()).toStrictEqual({
|
|
948
|
+
messages: [
|
|
949
|
+
{
|
|
950
|
+
role: 'user',
|
|
951
|
+
content: 'Message with image attachment',
|
|
952
|
+
experimental_attachments: [
|
|
953
|
+
{
|
|
954
|
+
name: 'test.png',
|
|
955
|
+
contentType: 'image/png',
|
|
956
|
+
url: 'data:image/png;base64,dGVzdCBpbWFnZSBjb250ZW50',
|
|
957
|
+
},
|
|
958
|
+
],
|
|
959
|
+
},
|
|
960
|
+
],
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
await screen.findByTestId('message-0');
|
|
964
|
+
expect(screen.getByTestId('message-0')).toHaveTextContent(
|
|
965
|
+
'User: Message with image attachment',
|
|
966
|
+
);
|
|
967
|
+
|
|
968
|
+
await screen.findByTestId('attachment-0');
|
|
969
|
+
expect(screen.getByTestId('attachment-0')).toHaveAttribute(
|
|
970
|
+
'src',
|
|
971
|
+
expect.stringContaining('data:image/png;base64'),
|
|
972
|
+
);
|
|
973
|
+
|
|
974
|
+
await screen.findByTestId('message-1');
|
|
975
|
+
expect(screen.getByTestId('message-1')).toHaveTextContent(
|
|
976
|
+
'AI: Response to message with image attachment',
|
|
977
|
+
);
|
|
978
|
+
},
|
|
979
|
+
),
|
|
980
|
+
);
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
describe('file attachments with url', () => {
|
|
984
|
+
const TestComponent = () => {
|
|
985
|
+
const { messages, handleSubmit, handleInputChange, isLoading, input } =
|
|
986
|
+
useChat();
|
|
987
|
+
|
|
988
|
+
return (
|
|
989
|
+
<div>
|
|
990
|
+
{messages.map((m, idx) => (
|
|
991
|
+
<div data-testid={`message-${idx}`} key={m.id}>
|
|
992
|
+
{m.role === 'user' ? 'User: ' : 'AI: '}
|
|
993
|
+
{m.content}
|
|
994
|
+
{m.experimental_attachments?.map(attachment => {
|
|
995
|
+
if (attachment.contentType?.startsWith('image/')) {
|
|
996
|
+
return (
|
|
997
|
+
<img
|
|
998
|
+
key={attachment.name}
|
|
999
|
+
src={attachment.url}
|
|
1000
|
+
alt={attachment.name}
|
|
1001
|
+
data-testid={`attachment-${idx}`}
|
|
1002
|
+
/>
|
|
1003
|
+
);
|
|
1004
|
+
} else if (attachment.contentType?.startsWith('text/')) {
|
|
1005
|
+
return (
|
|
1006
|
+
<div key={attachment.name} data-testid={`attachment-${idx}`}>
|
|
1007
|
+
{Buffer.from(
|
|
1008
|
+
attachment.url.split(',')[1],
|
|
1009
|
+
'base64',
|
|
1010
|
+
).toString('utf-8')}
|
|
1011
|
+
</div>
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
})}
|
|
1015
|
+
</div>
|
|
1016
|
+
))}
|
|
1017
|
+
|
|
1018
|
+
<form
|
|
1019
|
+
onSubmit={event => {
|
|
1020
|
+
handleSubmit(event, {
|
|
1021
|
+
experimental_attachments: [
|
|
1022
|
+
{
|
|
1023
|
+
name: 'test.png',
|
|
1024
|
+
contentType: 'image/png',
|
|
1025
|
+
url: 'https://example.com/image.png',
|
|
1026
|
+
},
|
|
1027
|
+
],
|
|
1028
|
+
});
|
|
1029
|
+
}}
|
|
1030
|
+
data-testid="chat-form"
|
|
1031
|
+
>
|
|
1032
|
+
<input
|
|
1033
|
+
value={input}
|
|
1034
|
+
onChange={handleInputChange}
|
|
1035
|
+
disabled={isLoading}
|
|
1036
|
+
data-testid="message-input"
|
|
1037
|
+
/>
|
|
1038
|
+
<button type="submit" data-testid="submit-button">
|
|
1039
|
+
Send
|
|
1040
|
+
</button>
|
|
1041
|
+
</form>
|
|
1042
|
+
</div>
|
|
1043
|
+
);
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
beforeEach(() => {
|
|
1047
|
+
render(<TestComponent />);
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
afterEach(() => {
|
|
1051
|
+
vi.restoreAllMocks();
|
|
1052
|
+
cleanup();
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
it(
|
|
1056
|
+
'should handle image file attachment and submission',
|
|
1057
|
+
withTestServer(
|
|
1058
|
+
{
|
|
1059
|
+
url: '/api/chat',
|
|
1060
|
+
type: 'stream-values',
|
|
1061
|
+
content: ['0:"Response to message with image attachment"\n'],
|
|
1062
|
+
},
|
|
1063
|
+
async ({ call }) => {
|
|
1064
|
+
const messageInput = screen.getByTestId('message-input');
|
|
1065
|
+
await userEvent.type(messageInput, 'Message with image attachment');
|
|
1066
|
+
|
|
1067
|
+
const submitButton = screen.getByTestId('submit-button');
|
|
1068
|
+
await userEvent.click(submitButton);
|
|
1069
|
+
|
|
1070
|
+
expect(await call(0).getRequestBodyJson()).toStrictEqual({
|
|
1071
|
+
messages: [
|
|
1072
|
+
{
|
|
1073
|
+
role: 'user',
|
|
1074
|
+
content: 'Message with image attachment',
|
|
1075
|
+
experimental_attachments: [
|
|
1076
|
+
{
|
|
1077
|
+
name: 'test.png',
|
|
1078
|
+
contentType: 'image/png',
|
|
1079
|
+
url: 'https://example.com/image.png',
|
|
1080
|
+
},
|
|
1081
|
+
],
|
|
1082
|
+
},
|
|
1083
|
+
],
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
await screen.findByTestId('message-0');
|
|
1087
|
+
expect(screen.getByTestId('message-0')).toHaveTextContent(
|
|
1088
|
+
'User: Message with image attachment',
|
|
1089
|
+
);
|
|
1090
|
+
|
|
1091
|
+
await screen.findByTestId('attachment-0');
|
|
1092
|
+
expect(screen.getByTestId('attachment-0')).toHaveAttribute(
|
|
1093
|
+
'src',
|
|
1094
|
+
expect.stringContaining('https://example.com/image.png'),
|
|
1095
|
+
);
|
|
1096
|
+
|
|
1097
|
+
await screen.findByTestId('message-1');
|
|
1098
|
+
expect(screen.getByTestId('message-1')).toHaveTextContent(
|
|
1099
|
+
'AI: Response to message with image attachment',
|
|
1100
|
+
);
|
|
1101
|
+
},
|
|
1102
|
+
),
|
|
1103
|
+
);
|
|
1104
|
+
});
|