@ai-sdk/vue 3.0.47 → 3.0.49

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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # @ai-sdk/vue
2
2
 
3
+ ## 3.0.49
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [ded661b]
8
+ - ai@6.0.49
9
+
10
+ ## 3.0.48
11
+
12
+ ### Patch Changes
13
+
14
+ - 4de5a1d: chore: excluded tests from src folder in npm package
15
+ - Updated dependencies [4de5a1d]
16
+ - ai@6.0.48
17
+ - @ai-sdk/provider-utils@4.0.9
18
+
3
19
  ## 3.0.47
4
20
 
5
21
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/vue",
3
- "version": "3.0.47",
3
+ "version": "3.0.49",
4
4
  "license": "Apache-2.0",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",
@@ -17,13 +17,18 @@
17
17
  "files": [
18
18
  "dist/**/*",
19
19
  "src",
20
+ "!src/**/*.test.ts",
21
+ "!src/**/*.test.tsx",
22
+ "!src/**/*.test-d.ts",
23
+ "!src/**/__snapshots__",
24
+ "!src/**/__fixtures__",
20
25
  "CHANGELOG.md",
21
26
  "README.md"
22
27
  ],
23
28
  "dependencies": {
24
29
  "swrv": "^1.0.4",
25
- "@ai-sdk/provider-utils": "4.0.8",
26
- "ai": "6.0.47"
30
+ "@ai-sdk/provider-utils": "4.0.9",
31
+ "ai": "6.0.49"
27
32
  },
28
33
  "devDependencies": {
29
34
  "@testing-library/jest-dom": "^6.6.3",
@@ -38,7 +43,7 @@
38
43
  "typescript": "5.8.3",
39
44
  "vitest": "2.1.4",
40
45
  "zod": "3.25.76",
41
- "@ai-sdk/test-server": "1.0.2",
46
+ "@ai-sdk/test-server": "1.0.3",
42
47
  "@vercel/ai-tsconfig": "0.0.0",
43
48
  "eslint-config-vercel-ai": "0.0.0"
44
49
  },
@@ -1,954 +0,0 @@
1
- import {
2
- createTestServer,
3
- TestResponseController,
4
- } from '@ai-sdk/test-server/with-vitest';
5
- import '@testing-library/jest-dom/vitest';
6
- import userEvent from '@testing-library/user-event';
7
- import { screen, waitFor } from '@testing-library/vue';
8
- import { UIMessageChunk } from 'ai';
9
- import { setupTestComponent } from './setup-test-component';
10
- import TestChatAppendAttachmentsComponent from './TestChatAppendAttachmentsComponent.vue';
11
- import TestChatAttachmentsComponent from './TestChatAttachmentsComponent.vue';
12
- import TestChatComponent from './TestChatComponent.vue';
13
- import TestChatInitMessages from './TestChatInitMessages.vue';
14
- import TestChatPrepareRequestBodyComponent from './TestChatPrepareRequestBodyComponent.vue';
15
- import TestChatReloadComponent from './TestChatReloadComponent.vue';
16
- import TestChatTextStreamComponent from './TestChatTextStreamComponent.vue';
17
- import TestChatToolInvocationsComponent from './TestChatToolInvocationsComponent.vue';
18
- import TestChatUrlAttachmentsComponent from './TestChatUrlAttachmentsComponent.vue';
19
- import { describe, it, expect } from 'vitest';
20
-
21
- function formatChunk(part: UIMessageChunk) {
22
- return `data: ${JSON.stringify(part)}\n\n`;
23
- }
24
-
25
- const server = createTestServer({
26
- '/api/chat': {},
27
- });
28
-
29
- describe('prepareSubmitMessagesRequest', () => {
30
- setupTestComponent(TestChatPrepareRequestBodyComponent);
31
-
32
- it('should show streamed response', async () => {
33
- server.urls['/api/chat'].response = {
34
- type: 'stream-chunks',
35
- chunks: [
36
- formatChunk({ type: 'text-start', id: '0' }),
37
- formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }),
38
- formatChunk({ type: 'text-delta', id: '0', delta: ',' }),
39
- formatChunk({ type: 'text-delta', id: '0', delta: ' world' }),
40
- formatChunk({ type: 'text-delta', id: '0', delta: '.' }),
41
- formatChunk({ type: 'text-end', id: '0' }),
42
- ],
43
- };
44
-
45
- await userEvent.click(screen.getByTestId('do-append'));
46
-
47
- await waitFor(() => {
48
- const element = screen.getByTestId('on-options');
49
- expect(element.textContent?.trim() ?? '').not.toBe('');
50
- });
51
-
52
- const value = JSON.parse(
53
- screen.getByTestId('on-options').textContent ?? '',
54
- );
55
-
56
- expect(screen.getByTestId('message-0')).toHaveTextContent('User: hi');
57
-
58
- expect(value).toStrictEqual({
59
- id: expect.any(String),
60
- api: '/api/chat',
61
- trigger: 'submit-message',
62
- body: { 'request-body-key': 'request-body-value' },
63
- headers: { 'request-header-key': 'request-header-value' },
64
- requestMetadata: { 'request-metadata-key': 'request-metadata-value' },
65
- messages: [
66
- {
67
- role: 'user',
68
- id: expect.any(String),
69
- parts: [{ type: 'text', text: 'hi' }],
70
- },
71
- ],
72
- });
73
-
74
- expect(await server.calls[0].requestBodyJson).toStrictEqual({
75
- 'body-key': 'body-value',
76
- });
77
- expect(server.calls[0].requestHeaders).toStrictEqual({
78
- 'content-type': 'application/json',
79
- 'header-key': 'header-value',
80
- });
81
-
82
- await screen.findByTestId('message-1');
83
- expect(screen.getByTestId('message-1')).toHaveTextContent(
84
- 'AI: Hello, world.',
85
- );
86
- });
87
- });
88
-
89
- describe('data protocol stream', () => {
90
- setupTestComponent(TestChatComponent);
91
-
92
- it('should show streamed response', async () => {
93
- server.urls['/api/chat'].response = {
94
- type: 'stream-chunks',
95
- chunks: [
96
- formatChunk({ type: 'text-start', id: '0' }),
97
- formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }),
98
- formatChunk({ type: 'text-delta', id: '0', delta: ',' }),
99
- formatChunk({ type: 'text-delta', id: '0', delta: ' world' }),
100
- formatChunk({ type: 'text-delta', id: '0', delta: '.' }),
101
- formatChunk({ type: 'text-end', id: '0' }),
102
- ],
103
- };
104
-
105
- await userEvent.click(screen.getByTestId('do-append'));
106
-
107
- await screen.findByTestId('message-0');
108
- expect(screen.getByTestId('message-0')).toHaveTextContent('User: hi');
109
-
110
- await screen.findByTestId('message-1');
111
- expect(screen.getByTestId('message-1')).toHaveTextContent(
112
- 'AI: Hello, world.',
113
- );
114
- });
115
-
116
- it('should show error response', async () => {
117
- server.urls['/api/chat'].response = {
118
- type: 'error',
119
- status: 404,
120
- body: 'Not found',
121
- };
122
-
123
- await userEvent.click(screen.getByTestId('do-append'));
124
-
125
- // TODO bug? the user message does not show up
126
- // await screen.findByTestId('message-0');
127
- // expect(screen.getByTestId('message-0')).toHaveTextContent('User: hi');
128
-
129
- await screen.findByTestId('error');
130
- expect(screen.getByTestId('error')).toHaveTextContent('Error: Not found');
131
- });
132
-
133
- describe('status', () => {
134
- it('should show status', async () => {
135
- const controller = new TestResponseController();
136
- server.urls['/api/chat'].response = {
137
- type: 'controlled-stream',
138
- controller,
139
- };
140
-
141
- await userEvent.click(screen.getByTestId('do-append'));
142
-
143
- await waitFor(() => {
144
- expect(screen.getByTestId('status')).toHaveTextContent('submitted');
145
- });
146
-
147
- controller.write(formatChunk({ type: 'text-start', id: '0' }));
148
- controller.write(
149
- formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }),
150
- );
151
- controller.write(formatChunk({ type: 'text-end', id: '0' }));
152
-
153
- await waitFor(() => {
154
- expect(screen.getByTestId('status')).toHaveTextContent('streaming');
155
- });
156
-
157
- controller.close();
158
-
159
- await waitFor(() => {
160
- expect(screen.getByTestId('status')).toHaveTextContent('ready');
161
- });
162
- });
163
-
164
- it('should update status when the tab is hidden', async () => {
165
- const controller = new TestResponseController();
166
- server.urls['/api/chat'].response = {
167
- type: 'controlled-stream',
168
- controller,
169
- };
170
-
171
- const originalVisibilityState = document.visibilityState;
172
-
173
- try {
174
- await userEvent.click(screen.getByTestId('do-append'));
175
- await waitFor(() =>
176
- expect(screen.getByTestId('status')).toHaveTextContent('submitted'),
177
- );
178
-
179
- controller.write(formatChunk({ type: 'text-start', id: '0' }));
180
- controller.write(
181
- formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }),
182
- );
183
-
184
- await waitFor(() =>
185
- expect(screen.getByTestId('status')).toHaveTextContent('streaming'),
186
- );
187
-
188
- Object.defineProperty(document, 'visibilityState', {
189
- configurable: true,
190
- get: () => 'hidden',
191
- });
192
- document.dispatchEvent(new Event('visibilitychange'));
193
-
194
- controller.write(
195
- formatChunk({ type: 'text-delta', id: '0', delta: ' world.' }),
196
- );
197
- controller.write(formatChunk({ type: 'text-end', id: '0' }));
198
- controller.close();
199
-
200
- await waitFor(() =>
201
- expect(screen.getByTestId('status')).toHaveTextContent('ready'),
202
- );
203
- } finally {
204
- Object.defineProperty(document, 'visibilityState', {
205
- configurable: true,
206
- get: () => originalVisibilityState,
207
- });
208
- document.dispatchEvent(new Event('visibilitychange'));
209
- }
210
- });
211
-
212
- it('should set status to error when there is a server error', async () => {
213
- server.urls['/api/chat'].response = {
214
- type: 'error',
215
- status: 404,
216
- body: 'Not found',
217
- };
218
-
219
- await userEvent.click(screen.getByTestId('do-append'));
220
-
221
- await waitFor(() => {
222
- expect(screen.getByTestId('status')).toHaveTextContent('error');
223
- });
224
- });
225
- });
226
-
227
- it('should invoke onFinish when the stream finishes', async () => {
228
- server.urls['/api/chat'].response = {
229
- type: 'stream-chunks',
230
- chunks: [
231
- formatChunk({ type: 'text-start', id: '0' }),
232
- formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }),
233
- formatChunk({ type: 'text-delta', id: '0', delta: ',' }),
234
- formatChunk({ type: 'text-delta', id: '0', delta: ' world' }),
235
- formatChunk({ type: 'text-delta', id: '0', delta: '.' }),
236
- formatChunk({ type: 'text-end', id: '0' }),
237
- formatChunk({ type: 'finish' }),
238
- ],
239
- };
240
-
241
- await userEvent.click(screen.getByTestId('do-append'));
242
-
243
- await waitFor(() => {
244
- const element = screen.getByTestId('on-finish-calls');
245
- expect(element.textContent?.trim() ?? '').not.toBe('');
246
- });
247
-
248
- const value = JSON.parse(
249
- screen.getByTestId('on-finish-calls').textContent ?? '',
250
- );
251
-
252
- expect(value).toStrictEqual([
253
- {
254
- isAbort: false,
255
- isDisconnect: false,
256
- isError: false,
257
- message: {
258
- id: expect.any(String),
259
- role: 'assistant',
260
- parts: [{ text: 'Hello, world.', type: 'text', state: 'done' }],
261
- },
262
- messages: [
263
- {
264
- id: expect.any(String),
265
- role: 'user',
266
- parts: [{ text: 'hi', type: 'text' }],
267
- },
268
- {
269
- id: expect.any(String),
270
- role: 'assistant',
271
- parts: [{ text: 'Hello, world.', type: 'text', state: 'done' }],
272
- },
273
- ],
274
- },
275
- ]);
276
- });
277
- });
278
-
279
- describe('text stream', () => {
280
- setupTestComponent(TestChatTextStreamComponent);
281
-
282
- it('should show streamed response', async () => {
283
- server.urls['/api/chat'].response = {
284
- type: 'stream-chunks',
285
- chunks: ['Hello', ',', ' world', '.'],
286
- };
287
-
288
- await userEvent.click(screen.getByTestId('do-append'));
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
-
299
- it('should invoke onFinish when the stream finishes', async () => {
300
- server.urls['/api/chat'].response = {
301
- type: 'stream-chunks',
302
- chunks: ['Hello', ',', ' world', '.'],
303
- };
304
-
305
- await userEvent.click(screen.getByTestId('do-append'));
306
-
307
- await waitFor(() => {
308
- const element = screen.getByTestId('on-finish-calls');
309
- expect(element.textContent?.trim() ?? '').not.toBe('');
310
- });
311
-
312
- const value = JSON.parse(
313
- screen.getByTestId('on-finish-calls').textContent ?? '',
314
- );
315
-
316
- expect(value).toStrictEqual([
317
- {
318
- isAbort: false,
319
- isDisconnect: false,
320
- isError: false,
321
- message: {
322
- id: expect.any(String),
323
- role: 'assistant',
324
- parts: [
325
- { type: 'step-start' },
326
- { text: 'Hello, world.', type: 'text', state: 'done' },
327
- ],
328
- },
329
- messages: [
330
- {
331
- id: expect.any(String),
332
- role: 'user',
333
- parts: [{ text: 'hi', type: 'text' }],
334
- },
335
- {
336
- id: expect.any(String),
337
- role: 'assistant',
338
- parts: [
339
- { type: 'step-start' },
340
- { text: 'Hello, world.', type: 'text', state: 'done' },
341
- ],
342
- },
343
- ],
344
- },
345
- ]);
346
- });
347
- });
348
-
349
- describe('regenerate', () => {
350
- setupTestComponent(TestChatReloadComponent);
351
-
352
- it('should show streamed response', async () => {
353
- server.urls['/api/chat'].response = [
354
- {
355
- type: 'stream-chunks',
356
- chunks: [
357
- formatChunk({ type: 'text-start', id: '0' }),
358
- formatChunk({
359
- type: 'text-delta',
360
- id: '0',
361
- delta: 'first response',
362
- }),
363
- formatChunk({ type: 'text-end', id: '0' }),
364
- ],
365
- },
366
- {
367
- type: 'stream-chunks',
368
- chunks: [
369
- formatChunk({ type: 'text-start', id: '0' }),
370
- formatChunk({
371
- type: 'text-delta',
372
- id: '0',
373
- delta: 'second response',
374
- }),
375
- formatChunk({ type: 'text-end', id: '0' }),
376
- ],
377
- },
378
- ];
379
-
380
- await userEvent.click(screen.getByTestId('do-append'));
381
-
382
- await screen.findByTestId('message-0');
383
- expect(screen.getByTestId('message-0')).toHaveTextContent('User: hi');
384
-
385
- await screen.findByTestId('message-1');
386
-
387
- // setup done, click reload:
388
- await userEvent.click(screen.getByTestId('do-regenerate'));
389
-
390
- expect(await server.calls[1].requestBodyJson).toStrictEqual({
391
- id: expect.any(String),
392
- messages: [
393
- {
394
- id: 'id-0',
395
- parts: [
396
- {
397
- text: 'hi',
398
- type: 'text',
399
- },
400
- ],
401
- role: 'user',
402
- },
403
- ],
404
- 'request-body-key': 'request-body-value',
405
- trigger: 'regenerate-message',
406
- });
407
-
408
- expect(server.calls[1].requestHeaders).toStrictEqual({
409
- 'content-type': 'application/json',
410
- 'header-key': 'header-value',
411
- });
412
-
413
- await screen.findByTestId('message-1');
414
- expect(screen.getByTestId('message-1')).toHaveTextContent(
415
- 'AI: second response',
416
- );
417
- });
418
- });
419
-
420
- describe('tool invocations', () => {
421
- setupTestComponent(TestChatToolInvocationsComponent);
422
-
423
- it('should display partial tool call, tool call, and tool result', async () => {
424
- const controller = new TestResponseController();
425
- server.urls['/api/chat'].response = [
426
- { type: 'controlled-stream', controller },
427
- ];
428
-
429
- await userEvent.click(screen.getByTestId('do-append'));
430
-
431
- controller.write(
432
- formatChunk({
433
- type: 'tool-input-start',
434
- toolCallId: 'tool-call-0',
435
- toolName: 'test-tool',
436
- }),
437
- );
438
-
439
- await waitFor(() => {
440
- expect(screen.getByTestId('message-1')).toHaveTextContent(
441
- '{"type":"tool-test-tool","toolCallId":"tool-call-0","state":"input-streaming"}',
442
- );
443
- });
444
-
445
- controller.write(
446
- formatChunk({
447
- type: 'tool-input-delta',
448
- toolCallId: 'tool-call-0',
449
- inputTextDelta: '{"testArg":"t',
450
- }),
451
- );
452
-
453
- await waitFor(() => {
454
- expect(screen.getByTestId('message-1')).toHaveTextContent(
455
- '{"type":"tool-test-tool","toolCallId":"tool-call-0","state":"input-streaming","input":{"testArg":"t"}}',
456
- );
457
- });
458
-
459
- controller.write(
460
- formatChunk({
461
- type: 'tool-input-delta',
462
- toolCallId: 'tool-call-0',
463
- inputTextDelta: 'est-value"}}',
464
- }),
465
- );
466
-
467
- await waitFor(() => {
468
- expect(screen.getByTestId('message-1')).toHaveTextContent(
469
- '{"type":"tool-test-tool","toolCallId":"tool-call-0","state":"input-streaming","input":{"testArg":"test-value"}}',
470
- );
471
- });
472
-
473
- controller.write(
474
- formatChunk({
475
- type: 'tool-input-available',
476
- toolCallId: 'tool-call-0',
477
- toolName: 'test-tool',
478
- input: { testArg: 'test-value' },
479
- }),
480
- );
481
-
482
- await waitFor(() => {
483
- expect(
484
- JSON.parse(screen.getByTestId('message-1').textContent ?? ''),
485
- ).toStrictEqual({
486
- state: 'input-available',
487
- input: { testArg: 'test-value' },
488
- toolCallId: 'tool-call-0',
489
- type: 'tool-test-tool',
490
- });
491
- });
492
-
493
- controller.write(
494
- formatChunk({
495
- type: 'tool-output-available',
496
- toolCallId: 'tool-call-0',
497
- output: 'test-result',
498
- }),
499
- );
500
- controller.close();
501
-
502
- await waitFor(() => {
503
- expect(
504
- JSON.parse(screen.getByTestId('message-1').textContent ?? ''),
505
- ).toStrictEqual({
506
- state: 'output-available',
507
- input: { testArg: 'test-value' },
508
- toolCallId: 'tool-call-0',
509
- type: 'tool-test-tool',
510
- output: 'test-result',
511
- });
512
- });
513
- });
514
-
515
- it('should display tool call and tool result (when there is no tool call streaming)', async () => {
516
- const controller = new TestResponseController();
517
- server.urls['/api/chat'].response = [
518
- {
519
- type: 'controlled-stream',
520
- controller,
521
- },
522
- ];
523
-
524
- await userEvent.click(screen.getByTestId('do-append'));
525
-
526
- controller.write(
527
- formatChunk({
528
- type: 'tool-input-available',
529
- toolCallId: 'tool-call-0',
530
- toolName: 'test-tool',
531
- input: { testArg: 'test-value' },
532
- }),
533
- );
534
-
535
- await waitFor(() => {
536
- expect(
537
- JSON.parse(screen.getByTestId('message-1').textContent ?? ''),
538
- ).toStrictEqual({
539
- state: 'input-available',
540
- input: { testArg: 'test-value' },
541
- toolCallId: 'tool-call-0',
542
- type: 'tool-test-tool',
543
- });
544
- });
545
-
546
- controller.write(
547
- formatChunk({
548
- type: 'tool-output-available',
549
- toolCallId: 'tool-call-0',
550
- output: 'test-result',
551
- }),
552
- );
553
- controller.close();
554
-
555
- await waitFor(() => {
556
- expect(screen.getByTestId('message-1')).toHaveTextContent('test-result');
557
- });
558
- });
559
-
560
- it('should update tool call to result when addToolOutput is called', async () => {
561
- const controller = new TestResponseController();
562
- server.urls['/api/chat'].response = [
563
- {
564
- type: 'controlled-stream',
565
- controller,
566
- },
567
- ];
568
-
569
- await userEvent.click(screen.getByTestId('do-append'));
570
-
571
- controller.write(formatChunk({ type: 'start' }));
572
- controller.write(formatChunk({ type: 'start-step' }));
573
- controller.write(
574
- formatChunk({
575
- type: 'tool-input-available',
576
- toolCallId: 'tool-call-0',
577
- toolName: 'test-tool',
578
- input: { testArg: 'test-value' },
579
- }),
580
- );
581
-
582
- await waitFor(() => {
583
- expect(screen.getByTestId('message-1')).toHaveTextContent(
584
- '{"type":"tool-test-tool","toolCallId":"tool-call-0","state":"input-available","input":{"testArg":"test-value"}}',
585
- );
586
- });
587
-
588
- await userEvent.click(screen.getByTestId('add-result-0'));
589
-
590
- await waitFor(() => {
591
- expect(screen.getByTestId('message-1')).toHaveTextContent(
592
- '{"type":"tool-test-tool","toolCallId":"tool-call-0","state":"output-available","input":{"testArg":"test-value"},"output":"test-result"}',
593
- );
594
- });
595
-
596
- controller.write(formatChunk({ type: 'text-start', id: '0' }));
597
- controller.write(
598
- formatChunk({
599
- type: 'text-delta',
600
- id: '0',
601
- delta: 'more text',
602
- }),
603
- );
604
- controller.write(formatChunk({ type: 'text-end', id: '0' }));
605
- controller.close();
606
- });
607
- });
608
-
609
- describe('file attachments with data url', () => {
610
- setupTestComponent(TestChatAttachmentsComponent);
611
-
612
- it('should handle text file attachment and submission', async () => {
613
- server.urls['/api/chat'].response = {
614
- type: 'stream-chunks',
615
- chunks: [
616
- formatChunk({
617
- type: 'text-start',
618
- id: '0',
619
- }),
620
- formatChunk({
621
- type: 'text-delta',
622
- id: '0',
623
- delta: 'Response to message with text attachment',
624
- }),
625
- formatChunk({ type: 'text-end', id: '0' }),
626
- ],
627
- };
628
-
629
- const file = new File(['test file content'], 'test.txt', {
630
- type: 'text/plain',
631
- });
632
-
633
- const fileInput = screen.getByTestId('file-input');
634
- await userEvent.upload(fileInput, file);
635
-
636
- const messageInput = screen.getByTestId('message-input');
637
- await userEvent.type(messageInput, 'Message with text attachment');
638
-
639
- const submitButton = screen.getByTestId('submit-button');
640
- await userEvent.click(submitButton);
641
-
642
- await waitFor(() => {
643
- expect(
644
- JSON.parse(screen.getByTestId('messages').textContent ?? ''),
645
- ).toStrictEqual([
646
- {
647
- id: 'id-0',
648
- role: 'user',
649
- parts: [
650
- {
651
- type: 'file',
652
- mediaType: 'text/plain',
653
- filename: 'test.txt',
654
- url: 'data:text/plain;base64,dGVzdCBmaWxlIGNvbnRlbnQ=',
655
- },
656
- {
657
- type: 'text',
658
- text: 'Message with text attachment',
659
- },
660
- ],
661
- },
662
- {
663
- id: 'id-1',
664
- parts: [
665
- {
666
- text: 'Response to message with text attachment',
667
- type: 'text',
668
- state: 'done',
669
- },
670
- ],
671
- role: 'assistant',
672
- },
673
- ]);
674
- });
675
- });
676
-
677
- it('should handle image file attachment and submission', async () => {
678
- server.urls['/api/chat'].response = {
679
- type: 'stream-chunks',
680
- chunks: [
681
- formatChunk({
682
- type: 'text-start',
683
- id: '0',
684
- }),
685
- formatChunk({
686
- type: 'text-delta',
687
- id: '0',
688
- delta: 'Response to message with image attachment',
689
- }),
690
- formatChunk({ type: 'text-end', id: '0' }),
691
- ],
692
- };
693
-
694
- const file = new File(['test image content'], 'test.png', {
695
- type: 'image/png',
696
- });
697
-
698
- const fileInput = screen.getByTestId('file-input');
699
- await userEvent.upload(fileInput, file);
700
-
701
- const messageInput = screen.getByTestId('message-input');
702
- await userEvent.type(messageInput, 'Message with image attachment');
703
-
704
- const submitButton = screen.getByTestId('submit-button');
705
- await userEvent.click(submitButton);
706
-
707
- await waitFor(() => {
708
- expect(
709
- JSON.parse(screen.getByTestId('messages').textContent ?? ''),
710
- ).toStrictEqual([
711
- {
712
- role: 'user',
713
- id: 'id-0',
714
- parts: [
715
- {
716
- type: 'file',
717
- mediaType: 'image/png',
718
- filename: 'test.png',
719
- url: 'data:image/png;base64,dGVzdCBpbWFnZSBjb250ZW50',
720
- },
721
- {
722
- type: 'text',
723
- text: 'Message with image attachment',
724
- },
725
- ],
726
- },
727
- {
728
- role: 'assistant',
729
- id: 'id-1',
730
- parts: [
731
- {
732
- type: 'text',
733
- text: 'Response to message with image attachment',
734
- state: 'done',
735
- },
736
- ],
737
- },
738
- ]);
739
- });
740
- });
741
- });
742
-
743
- describe('file attachments with url', () => {
744
- setupTestComponent(TestChatUrlAttachmentsComponent);
745
-
746
- it('should handle image file attachment and submission', async () => {
747
- server.urls['/api/chat'].response = {
748
- type: 'stream-chunks',
749
- chunks: [
750
- formatChunk({
751
- type: 'text-start',
752
- id: '0',
753
- }),
754
- formatChunk({
755
- type: 'text-delta',
756
- id: '0',
757
- delta: 'Response to message with image attachment',
758
- }),
759
- formatChunk({ type: 'text-end', id: '0' }),
760
- ],
761
- };
762
-
763
- const messageInput = screen.getByTestId('message-input');
764
- await userEvent.type(messageInput, 'Message with image attachment');
765
-
766
- const submitButton = screen.getByTestId('submit-button');
767
- await userEvent.click(submitButton);
768
-
769
- await waitFor(() => {
770
- expect(
771
- JSON.parse(screen.getByTestId('messages').textContent ?? ''),
772
- ).toStrictEqual([
773
- {
774
- role: 'user',
775
- id: 'id-0',
776
- parts: [
777
- {
778
- type: 'file',
779
- mediaType: 'image/png',
780
- url: 'https://example.com/image.png',
781
- },
782
- {
783
- type: 'text',
784
- text: 'Message with image attachment',
785
- },
786
- ],
787
- },
788
- {
789
- role: 'assistant',
790
- id: 'id-1',
791
- parts: [
792
- {
793
- type: 'text',
794
- text: 'Response to message with image attachment',
795
- state: 'done',
796
- },
797
- ],
798
- },
799
- ]);
800
- });
801
- });
802
- });
803
-
804
- describe('attachments with empty submit', () => {
805
- setupTestComponent(TestChatAttachmentsComponent);
806
-
807
- it('should handle image file attachment and empty submission', async () => {
808
- server.urls['/api/chat'].response = {
809
- type: 'stream-chunks',
810
- chunks: [
811
- formatChunk({
812
- type: 'text-start',
813
- id: '0',
814
- }),
815
- formatChunk({
816
- type: 'text-delta',
817
- id: '0',
818
- delta: 'Response to empty message with attachment',
819
- }),
820
- formatChunk({ type: 'text-end', id: '0' }),
821
- ],
822
- };
823
-
824
- const file = new File(['test image content'], 'test.png', {
825
- type: 'image/png',
826
- });
827
-
828
- const fileInput = screen.getByTestId('file-input');
829
- await userEvent.upload(fileInput, file);
830
-
831
- const submitButton = screen.getByTestId('submit-button');
832
- await userEvent.click(submitButton);
833
-
834
- await waitFor(() => {
835
- expect(
836
- JSON.parse(screen.getByTestId('messages').textContent ?? ''),
837
- ).toStrictEqual([
838
- {
839
- id: 'id-0',
840
- role: 'user',
841
- parts: [
842
- {
843
- type: 'file',
844
- mediaType: 'image/png',
845
- filename: 'test.png',
846
- url: 'data:image/png;base64,dGVzdCBpbWFnZSBjb250ZW50',
847
- },
848
- {
849
- type: 'text',
850
- text: '',
851
- },
852
- ],
853
- },
854
- {
855
- id: 'id-1',
856
- role: 'assistant',
857
- parts: [
858
- {
859
- type: 'text',
860
- text: 'Response to empty message with attachment',
861
- state: 'done',
862
- },
863
- ],
864
- },
865
- ]);
866
- });
867
- });
868
- });
869
-
870
- describe('should append message with attachments', () => {
871
- setupTestComponent(TestChatAppendAttachmentsComponent);
872
-
873
- it('should handle image file attachment with append', async () => {
874
- server.urls['/api/chat'].response = {
875
- type: 'stream-chunks',
876
- chunks: [
877
- formatChunk({
878
- type: 'text-start',
879
- id: '0',
880
- }),
881
- formatChunk({
882
- type: 'text-delta',
883
- id: '0',
884
- delta: 'Response to message with image attachment',
885
- }),
886
- formatChunk({ type: 'text-end', id: '0' }),
887
- ],
888
- };
889
-
890
- const appendButton = screen.getByTestId('do-append');
891
- await userEvent.click(appendButton);
892
-
893
- await waitFor(() => {
894
- expect(
895
- JSON.parse(screen.getByTestId('messages').textContent ?? ''),
896
- ).toStrictEqual([
897
- {
898
- id: 'id-0',
899
- parts: [
900
- {
901
- mediaType: 'image/png',
902
- type: 'file',
903
- url: 'https://example.com/image.png',
904
- },
905
- {
906
- text: 'Message with image attachment',
907
- type: 'text',
908
- },
909
- ],
910
- role: 'user',
911
- },
912
- {
913
- id: 'id-1',
914
- parts: [
915
- {
916
- text: 'Response to message with image attachment',
917
- type: 'text',
918
- state: 'done',
919
- },
920
- ],
921
- role: 'assistant',
922
- },
923
- ]);
924
- });
925
- });
926
- });
927
-
928
- describe('init messages', () => {
929
- setupTestComponent(TestChatInitMessages);
930
-
931
- it('should show streamed response', async () => {
932
- server.urls['/api/chat'].response = {
933
- type: 'stream-chunks',
934
- chunks: [
935
- formatChunk({ type: 'text-start', id: '0' }),
936
- formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }),
937
- formatChunk({ type: 'text-delta', id: '0', delta: ',' }),
938
- formatChunk({ type: 'text-delta', id: '0', delta: ' world' }),
939
- formatChunk({ type: 'text-delta', id: '0', delta: '.' }),
940
- formatChunk({ type: 'text-end', id: '0' }),
941
- ],
942
- };
943
-
944
- await userEvent.click(screen.getByTestId('do-append'));
945
-
946
- await screen.findByTestId('message-2');
947
- expect(screen.getByTestId('message-2')).toHaveTextContent('User: Hi.');
948
-
949
- await screen.findByTestId('message-3');
950
- expect(screen.getByTestId('message-3')).toHaveTextContent(
951
- 'AI: Hello, world.',
952
- );
953
- });
954
- });
@@ -1,97 +0,0 @@
1
- import {
2
- createTestServer,
3
- TestResponseController,
4
- } from '@ai-sdk/test-server/with-vitest';
5
- import '@testing-library/jest-dom/vitest';
6
- import userEvent from '@testing-library/user-event';
7
- import { findByText, screen } from '@testing-library/vue';
8
- import { UIMessageChunk } from 'ai';
9
- import TestCompletionComponent from './TestCompletionComponent.vue';
10
- import TestCompletionTextStreamComponent from './TestCompletionTextStreamComponent.vue';
11
- import { setupTestComponent } from './setup-test-component';
12
- import { describe, it, expect } from 'vitest';
13
-
14
- function formatChunk(part: UIMessageChunk) {
15
- return `data: ${JSON.stringify(part)}\n\n`;
16
- }
17
-
18
- const server = createTestServer({
19
- '/api/completion': {},
20
- });
21
-
22
- describe('stream data stream', () => {
23
- setupTestComponent(TestCompletionComponent);
24
-
25
- it('should show streamed response', async () => {
26
- server.urls['/api/completion'].response = {
27
- type: 'stream-chunks',
28
- chunks: [
29
- formatChunk({ type: 'text-start', id: '0' }),
30
- formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }),
31
- formatChunk({ type: 'text-delta', id: '0', delta: ',' }),
32
- formatChunk({ type: 'text-delta', id: '0', delta: ' world' }),
33
- formatChunk({ type: 'text-delta', id: '0', delta: '.' }),
34
- formatChunk({ type: 'text-end', id: '0' }),
35
- ],
36
- };
37
-
38
- await userEvent.type(screen.getByTestId('input'), 'hi{enter}');
39
-
40
- await screen.findByTestId('completion');
41
- expect(screen.getByTestId('completion')).toHaveTextContent('Hello, world.');
42
- });
43
-
44
- describe('loading state', () => {
45
- it('should show loading state', async () => {
46
- const controller = new TestResponseController();
47
- server.urls['/api/completion'].response = {
48
- type: 'controlled-stream',
49
- controller,
50
- };
51
-
52
- await userEvent.type(screen.getByTestId('input'), 'hi{enter}');
53
-
54
- await screen.findByTestId('loading');
55
- expect(screen.getByTestId('loading')).toHaveTextContent('true');
56
-
57
- controller.write(formatChunk({ type: 'text-start', id: '0' }));
58
- controller.write(
59
- formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }),
60
- );
61
- controller.write(formatChunk({ type: 'text-end', id: '0' }));
62
- controller.close();
63
-
64
- await findByText(await screen.findByTestId('loading'), 'false');
65
- expect(screen.getByTestId('loading')).toHaveTextContent('false');
66
- });
67
-
68
- it('should reset loading state on error', async () => {
69
- server.urls['/api/completion'].response = {
70
- type: 'error',
71
- status: 404,
72
- body: 'Not found',
73
- };
74
-
75
- await userEvent.type(screen.getByTestId('input'), 'hi{enter}');
76
-
77
- await screen.findByTestId('loading');
78
- expect(screen.getByTestId('loading')).toHaveTextContent('false');
79
- });
80
- });
81
- });
82
-
83
- describe('stream data stream', () => {
84
- setupTestComponent(TestCompletionTextStreamComponent);
85
-
86
- it('should show streamed response', async () => {
87
- server.urls['/api/completion'].response = {
88
- type: 'stream-chunks',
89
- chunks: ['Hello', ',', ' world', '.'],
90
- };
91
-
92
- await userEvent.type(screen.getByTestId('input'), 'hi{enter}');
93
-
94
- await screen.findByTestId('completion');
95
- expect(screen.getByTestId('completion')).toHaveTextContent('Hello, world.');
96
- });
97
- });
@@ -1,250 +0,0 @@
1
- import {
2
- createTestServer,
3
- TestResponseController,
4
- } from '@ai-sdk/test-server/with-vitest';
5
- import '@testing-library/jest-dom/vitest';
6
- import { cleanup, screen, waitFor } from '@testing-library/vue';
7
- import userEvent from '@testing-library/user-event';
8
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
9
- import { setupTestComponent } from './setup-test-component';
10
- import TestUseObjectComponent from './TestUseObjectComponent.vue';
11
- import TestUseObjectCustomTransportComponent from './TestUseObjectCustomTransportComponent.vue';
12
-
13
- const server = createTestServer({
14
- '/api/use-object': {},
15
- });
16
-
17
- describe('text stream', () => {
18
- afterEach(() => {
19
- vi.restoreAllMocks();
20
- cleanup();
21
- });
22
-
23
- describe('basic component', () => {
24
- setupTestComponent(TestUseObjectComponent);
25
-
26
- describe("when the API returns 'Hello, world!'", () => {
27
- beforeEach(async () => {
28
- server.urls['/api/use-object'].response = {
29
- type: 'stream-chunks',
30
- chunks: ['{ ', '"content": "Hello, ', 'world', '!"'],
31
- };
32
- await userEvent.click(screen.getByTestId('submit-button'));
33
- });
34
-
35
- it('should render stream', async () => {
36
- await screen.findByTestId('object');
37
- expect(screen.getByTestId('object')).toHaveTextContent(
38
- JSON.stringify({ content: 'Hello, world!' }),
39
- );
40
- });
41
-
42
- it("should send 'test' to the API", async () => {
43
- expect(await server.calls[0].requestBodyJson).toBe('test-input');
44
- });
45
-
46
- it('should not have an error', async () => {
47
- await screen.findByTestId('error');
48
- expect(screen.getByTestId('error')).toBeEmptyDOMElement();
49
- expect(screen.getByTestId('on-error-result')).toBeEmptyDOMElement();
50
- });
51
- });
52
-
53
- describe('isLoading', () => {
54
- it('should be true while loading', async () => {
55
- const controller = new TestResponseController();
56
- server.urls['/api/use-object'].response = {
57
- type: 'controlled-stream',
58
- controller,
59
- };
60
-
61
- controller.write('{"content": ');
62
- await userEvent.click(screen.getByTestId('submit-button'));
63
-
64
- // wait for element "loading" to have text content "true":
65
- await waitFor(() => {
66
- expect(screen.getByTestId('loading')).toHaveTextContent('true');
67
- });
68
-
69
- controller.write('"Hello, world!"}');
70
- controller.close();
71
-
72
- // wait for element "loading" to have text content "false":
73
- await waitFor(() => {
74
- expect(screen.getByTestId('loading')).toHaveTextContent('false');
75
- });
76
- });
77
- });
78
-
79
- it('should abort the stream and not consume any more data', async () => {
80
- const controller = new TestResponseController();
81
- server.urls['/api/use-object'].response = {
82
- type: 'controlled-stream',
83
- controller,
84
- };
85
-
86
- controller.write('{"content": "h');
87
- await userEvent.click(screen.getByTestId('submit-button'));
88
-
89
- // wait for element "loading" and "object" to have text content:
90
- await waitFor(() => {
91
- expect(screen.getByTestId('loading')).toHaveTextContent('true');
92
- });
93
- await waitFor(() => {
94
- expect(screen.getByTestId('object')).toHaveTextContent(
95
- '{"content":"h"}',
96
- );
97
- });
98
-
99
- // click stop button:
100
- await userEvent.click(screen.getByTestId('stop-button'));
101
-
102
- // wait for element "loading" to have text content "false":
103
- await waitFor(() => {
104
- expect(screen.getByTestId('loading')).toHaveTextContent('false');
105
- });
106
-
107
- // this should not be consumed any more:
108
- await expect(controller.write('ello, world!"}')).rejects.toThrow();
109
- await expect(controller.close()).rejects.toThrow();
110
-
111
- // should only show start of object:
112
- await waitFor(() => {
113
- expect(screen.getByTestId('object')).toHaveTextContent(
114
- '{"content":"h"}',
115
- );
116
- });
117
- });
118
-
119
- it('should stop and clear the object state after a call to submit then clear', async () => {
120
- const controller = new TestResponseController();
121
- server.urls['/api/use-object'].response = {
122
- type: 'controlled-stream',
123
- controller,
124
- };
125
-
126
- controller.write('{"content": "h');
127
- await userEvent.click(screen.getByTestId('submit-button'));
128
-
129
- await waitFor(() => {
130
- expect(screen.getByTestId('loading')).toHaveTextContent('true');
131
- });
132
- await waitFor(() => {
133
- expect(screen.getByTestId('object')).toHaveTextContent(
134
- '{"content":"h"}',
135
- );
136
- });
137
-
138
- await userEvent.click(screen.getByTestId('clear-button'));
139
-
140
- await expect(controller.write('ello, world!"}')).rejects.toThrow();
141
- await expect(controller.close()).rejects.toThrow();
142
-
143
- await waitFor(() => {
144
- expect(screen.getByTestId('loading')).toHaveTextContent('false');
145
- expect(screen.getByTestId('error')).toBeEmptyDOMElement();
146
- expect(screen.getByTestId('object')).toBeEmptyDOMElement();
147
- });
148
- });
149
-
150
- describe('when the API returns a 404', () => {
151
- it('should render error', async () => {
152
- server.urls['/api/use-object'].response = {
153
- type: 'error',
154
- status: 404,
155
- body: 'Not found',
156
- };
157
-
158
- await userEvent.click(screen.getByTestId('submit-button'));
159
-
160
- await screen.findByTestId('error');
161
- expect(screen.getByTestId('error')).toHaveTextContent('Not found');
162
- expect(screen.getByTestId('on-error-result')).toHaveTextContent(
163
- 'Not found',
164
- );
165
- expect(screen.getByTestId('loading')).toHaveTextContent('false');
166
- });
167
- });
168
-
169
- describe('onFinish', () => {
170
- it('should be called with an object when the stream finishes and the object matches the schema', async () => {
171
- server.urls['/api/use-object'].response = {
172
- type: 'stream-chunks',
173
- chunks: ['{ ', '"content": "Hello, ', 'world', '!"', '}'],
174
- };
175
-
176
- await userEvent.click(screen.getByTestId('submit-button'));
177
-
178
- expect(screen.getByTestId('on-finish-calls')).toHaveTextContent(
179
- JSON.stringify([
180
- { object: { content: 'Hello, world!' }, error: undefined },
181
- ]),
182
- );
183
- });
184
-
185
- it('should be called with an error when the stream finishes and the object does not match the schema', async () => {
186
- server.urls['/api/use-object'].response = {
187
- type: 'stream-chunks',
188
- chunks: ['{ ', '"content-wrong": "Hello, ', 'world', '!"', '}'],
189
- };
190
-
191
- await userEvent.click(screen.getByTestId('submit-button'));
192
-
193
- expect(screen.getByTestId('on-finish-calls')).toHaveTextContent(
194
- 'ZodError',
195
- );
196
- });
197
- });
198
-
199
- it('should clear the object state after a call to clear', async () => {
200
- server.urls['/api/use-object'].response = {
201
- type: 'stream-chunks',
202
- chunks: ['{ ', '"content": "Hello, ', 'world', '!"', '}'],
203
- };
204
-
205
- await userEvent.click(screen.getByTestId('submit-button'));
206
-
207
- await screen.findByTestId('object');
208
- expect(screen.getByTestId('object')).toHaveTextContent(
209
- JSON.stringify({ content: 'Hello, world!' }),
210
- );
211
-
212
- await userEvent.click(screen.getByTestId('clear-button'));
213
-
214
- await waitFor(() => {
215
- expect(screen.getByTestId('object')).toBeEmptyDOMElement();
216
- expect(screen.getByTestId('error')).toBeEmptyDOMElement();
217
- expect(screen.getByTestId('loading')).toHaveTextContent('false');
218
- });
219
- });
220
- });
221
-
222
- describe('custom transport', () => {
223
- setupTestComponent(TestUseObjectCustomTransportComponent);
224
-
225
- it('should send custom headers', async () => {
226
- server.urls['/api/use-object'].response = {
227
- type: 'stream-chunks',
228
- chunks: ['{ ', '"content": "Hello, ', 'world', '!"', '}'],
229
- };
230
-
231
- await userEvent.click(screen.getByTestId('submit-button'));
232
-
233
- expect(server.calls[0].requestHeaders).toStrictEqual({
234
- 'content-type': 'application/json',
235
- authorization: 'Bearer TEST_TOKEN',
236
- 'x-custom-header': 'CustomValue',
237
- });
238
- });
239
-
240
- it('should send custom credentials', async () => {
241
- server.urls['/api/use-object'].response = {
242
- type: 'stream-chunks',
243
- chunks: ['{ ', '"content": "Authenticated ', 'content', '!"', '}'],
244
- };
245
-
246
- await userEvent.click(screen.getByTestId('submit-button'));
247
- expect(server.calls[0].requestCredentials).toBe('include');
248
- });
249
- });
250
- });