@ai-sdk/react 3.0.48 → 3.0.50

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.
@@ -1,165 +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 { screen, waitFor } from '@testing-library/react';
7
- import userEvent from '@testing-library/user-event';
8
- import { UIMessageChunk } from 'ai';
9
- import { setupTestComponent } from './setup-test-component';
10
- import { useCompletion } from './use-completion';
11
- import { describe, it, expect, beforeEach } from 'vitest';
12
-
13
- function formatChunk(part: UIMessageChunk) {
14
- return `data: ${JSON.stringify(part)}\n\n`;
15
- }
16
-
17
- const server = createTestServer({
18
- '/api/completion': {},
19
- });
20
-
21
- describe('stream data stream', () => {
22
- let onFinishResult: { prompt: string; completion: string } | undefined;
23
-
24
- setupTestComponent(() => {
25
- const {
26
- completion,
27
- handleSubmit,
28
- error,
29
- handleInputChange,
30
- input,
31
- isLoading,
32
- } = useCompletion({
33
- onFinish(prompt, completion) {
34
- onFinishResult = { prompt, completion };
35
- },
36
- });
37
-
38
- return (
39
- <div>
40
- <div data-testid="loading">{isLoading.toString()}</div>
41
- <div data-testid="error">{error?.toString()}</div>
42
- <div data-testid="completion">{completion}</div>
43
- <form onSubmit={handleSubmit}>
44
- <input
45
- data-testid="input"
46
- value={input}
47
- placeholder="Say something..."
48
- onChange={handleInputChange}
49
- />
50
- </form>
51
- </div>
52
- );
53
- });
54
-
55
- beforeEach(() => {
56
- onFinishResult = undefined;
57
- });
58
-
59
- describe('render simple stream', () => {
60
- beforeEach(async () => {
61
- server.urls['/api/completion'].response = {
62
- type: 'stream-chunks',
63
- chunks: [
64
- formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }),
65
- formatChunk({ type: 'text-delta', id: '0', delta: ',' }),
66
- formatChunk({ type: 'text-delta', id: '0', delta: ' world' }),
67
- formatChunk({ type: 'text-delta', id: '0', delta: '.' }),
68
- ],
69
- };
70
- await userEvent.type(screen.getByTestId('input'), 'hi{enter}');
71
- });
72
-
73
- it('should render stream', async () => {
74
- await waitFor(() => {
75
- expect(screen.getByTestId('completion')).toHaveTextContent(
76
- 'Hello, world.',
77
- );
78
- });
79
- });
80
-
81
- it("should call 'onFinish' callback", async () => {
82
- await waitFor(() => {
83
- expect(onFinishResult).toEqual({
84
- prompt: 'hi',
85
- completion: 'Hello, world.',
86
- });
87
- });
88
- });
89
- });
90
-
91
- describe('loading state', () => {
92
- it('should show loading state', async () => {
93
- const controller = new TestResponseController();
94
-
95
- server.urls['/api/completion'].response = {
96
- type: 'controlled-stream',
97
- controller,
98
- };
99
-
100
- await userEvent.type(screen.getByTestId('input'), 'hi{enter}');
101
-
102
- controller.write(
103
- formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }),
104
- );
105
-
106
- await waitFor(() => {
107
- expect(screen.getByTestId('loading')).toHaveTextContent('true');
108
- });
109
-
110
- await controller.close();
111
-
112
- await waitFor(() => {
113
- expect(screen.getByTestId('loading')).toHaveTextContent('false');
114
- });
115
- });
116
-
117
- it('should reset loading state on error', async () => {
118
- server.urls['/api/completion'].response = {
119
- type: 'error',
120
- status: 404,
121
- body: 'Not found',
122
- };
123
-
124
- await userEvent.type(screen.getByTestId('input'), 'hi{enter}');
125
-
126
- await screen.findByTestId('loading');
127
- expect(screen.getByTestId('loading')).toHaveTextContent('false');
128
- });
129
- });
130
- });
131
-
132
- describe('text stream', () => {
133
- setupTestComponent(() => {
134
- const { completion, handleSubmit, handleInputChange, input } =
135
- useCompletion({ streamProtocol: 'text' });
136
-
137
- return (
138
- <div>
139
- <div data-testid="completion-text-stream">{completion}</div>
140
- <form onSubmit={handleSubmit}>
141
- <input
142
- data-testid="input-text-stream"
143
- value={input}
144
- placeholder="Say something..."
145
- onChange={handleInputChange}
146
- />
147
- </form>
148
- </div>
149
- );
150
- });
151
-
152
- it('should render stream', async () => {
153
- server.urls['/api/completion'].response = {
154
- type: 'stream-chunks',
155
- chunks: ['Hello', ',', ' world', '.'],
156
- };
157
-
158
- await userEvent.type(screen.getByTestId('input-text-stream'), 'hi{enter}');
159
-
160
- await screen.findByTestId('completion-text-stream');
161
- expect(screen.getByTestId('completion-text-stream')).toHaveTextContent(
162
- 'Hello, world.',
163
- );
164
- });
165
- });
@@ -1,389 +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, render, screen, waitFor } from '@testing-library/react';
7
- import userEvent from '@testing-library/user-event';
8
- import { z } from 'zod/v4';
9
- import { experimental_useObject } from './use-object';
10
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
11
-
12
- const server = createTestServer({
13
- '/api/use-object': {},
14
- });
15
-
16
- describe('text stream', () => {
17
- let onErrorResult: Error | undefined;
18
- let onFinishCalls: Array<{
19
- object: { content: string } | undefined;
20
- error: Error | undefined;
21
- }> = [];
22
-
23
- const TestComponent = ({
24
- headers,
25
- credentials,
26
- }: {
27
- headers?: Record<string, string> | Headers;
28
- credentials?: RequestCredentials;
29
- }) => {
30
- const { object, error, submit, isLoading, stop, clear } =
31
- experimental_useObject({
32
- api: '/api/use-object',
33
- schema: z.object({ content: z.string() }),
34
- onError(error) {
35
- onErrorResult = error;
36
- },
37
- onFinish(event) {
38
- onFinishCalls.push(event);
39
- },
40
- headers,
41
- credentials,
42
- });
43
-
44
- return (
45
- <div>
46
- <div data-testid="loading">{isLoading.toString()}</div>
47
- <div data-testid="object">{JSON.stringify(object)}</div>
48
- <div data-testid="error">{error?.toString()}</div>
49
- <button
50
- data-testid="submit-button"
51
- onClick={() => submit('test-input')}
52
- >
53
- Generate
54
- </button>
55
- <button data-testid="stop-button" onClick={stop}>
56
- Stop
57
- </button>
58
- <button data-testid="clear-button" onClick={clear}>
59
- Clear
60
- </button>
61
- </div>
62
- );
63
- };
64
-
65
- beforeEach(() => {
66
- onErrorResult = undefined;
67
- onFinishCalls = [];
68
- });
69
-
70
- afterEach(() => {
71
- vi.restoreAllMocks();
72
- cleanup();
73
- onErrorResult = undefined;
74
- onFinishCalls = [];
75
- });
76
-
77
- describe('basic component', () => {
78
- beforeEach(() => {
79
- render(<TestComponent />);
80
- });
81
-
82
- describe("when the API returns 'Hello, world!'", () => {
83
- beforeEach(async () => {
84
- server.urls['/api/use-object'].response = {
85
- type: 'stream-chunks',
86
- chunks: ['{ ', '"content": "Hello, ', 'world', '!"'],
87
- };
88
- await userEvent.click(screen.getByTestId('submit-button'));
89
- });
90
-
91
- it('should render stream', async () => {
92
- await screen.findByTestId('object');
93
- expect(screen.getByTestId('object')).toHaveTextContent(
94
- JSON.stringify({ content: 'Hello, world!' }),
95
- );
96
- });
97
-
98
- it("should send 'test' to the API", async () => {
99
- expect(await server.calls[0].requestBodyJson).toBe('test-input');
100
- });
101
-
102
- it('should not have an error', async () => {
103
- await screen.findByTestId('error');
104
- expect(screen.getByTestId('error')).toBeEmptyDOMElement();
105
- expect(onErrorResult).toBeUndefined();
106
- });
107
- });
108
-
109
- describe('isLoading', () => {
110
- it('should be true while loading', async () => {
111
- const controller = new TestResponseController();
112
- server.urls['/api/use-object'].response = {
113
- type: 'controlled-stream',
114
- controller,
115
- };
116
-
117
- controller.write('{"content": ');
118
- await userEvent.click(screen.getByTestId('submit-button'));
119
-
120
- // wait for element "loading" to have text content "true":
121
- await waitFor(() => {
122
- expect(screen.getByTestId('loading')).toHaveTextContent('true');
123
- });
124
-
125
- controller.write('"Hello, world!"}');
126
- controller.close();
127
-
128
- // wait for element "loading" to have text content "false":
129
- await waitFor(() => {
130
- expect(screen.getByTestId('loading')).toHaveTextContent('false');
131
- });
132
- });
133
- });
134
-
135
- it('should abort the stream and not consume any more data', async () => {
136
- const controller = new TestResponseController();
137
- server.urls['/api/use-object'].response = {
138
- type: 'controlled-stream',
139
- controller,
140
- };
141
-
142
- controller.write('{"content": "h');
143
- await userEvent.click(screen.getByTestId('submit-button'));
144
-
145
- // wait for element "loading" and "object" to have text content:
146
- await waitFor(() => {
147
- expect(screen.getByTestId('loading')).toHaveTextContent('true');
148
- });
149
- await waitFor(() => {
150
- expect(screen.getByTestId('object')).toHaveTextContent(
151
- '{"content":"h"}',
152
- );
153
- });
154
-
155
- // click stop button:
156
- await userEvent.click(screen.getByTestId('stop-button'));
157
-
158
- // wait for element "loading" to have text content "false":
159
- await waitFor(() => {
160
- expect(screen.getByTestId('loading')).toHaveTextContent('false');
161
- });
162
-
163
- // this should not be consumed any more:
164
- await expect(controller.write('ello, world!"}')).rejects.toThrow();
165
- await expect(controller.close()).rejects.toThrow();
166
-
167
- // should only show start of object:
168
- await waitFor(() => {
169
- expect(screen.getByTestId('object')).toHaveTextContent(
170
- '{"content":"h"}',
171
- );
172
- });
173
- });
174
-
175
- it('should stop and clear the object state after a call to submit then clear', async () => {
176
- const controller = new TestResponseController();
177
- server.urls['/api/use-object'].response = {
178
- type: 'controlled-stream',
179
- controller,
180
- };
181
-
182
- controller.write('{"content": "h');
183
- await userEvent.click(screen.getByTestId('submit-button'));
184
-
185
- await waitFor(() => {
186
- expect(screen.getByTestId('loading')).toHaveTextContent('true');
187
- });
188
- await waitFor(() => {
189
- expect(screen.getByTestId('object')).toHaveTextContent(
190
- '{"content":"h"}',
191
- );
192
- });
193
-
194
- await userEvent.click(screen.getByTestId('clear-button'));
195
-
196
- await expect(controller.write('ello, world!"}')).rejects.toThrow();
197
- await expect(controller.close()).rejects.toThrow();
198
-
199
- await waitFor(() => {
200
- expect(screen.getByTestId('loading')).toHaveTextContent('false');
201
- expect(screen.getByTestId('error')).toBeEmptyDOMElement();
202
- expect(screen.getByTestId('object')).toBeEmptyDOMElement();
203
- });
204
- });
205
-
206
- describe('when the API returns a 404', () => {
207
- it('should render error', async () => {
208
- server.urls['/api/use-object'].response = {
209
- type: 'error',
210
- status: 404,
211
- body: 'Not found',
212
- };
213
-
214
- await userEvent.click(screen.getByTestId('submit-button'));
215
-
216
- await screen.findByTestId('error');
217
- expect(screen.getByTestId('error')).toHaveTextContent('Not found');
218
- expect(onErrorResult).toBeInstanceOf(Error);
219
- expect(screen.getByTestId('loading')).toHaveTextContent('false');
220
- });
221
- });
222
-
223
- describe('onFinish', () => {
224
- it('should be called with an object when the stream finishes and the object matches the schema', async () => {
225
- server.urls['/api/use-object'].response = {
226
- type: 'stream-chunks',
227
- chunks: ['{ ', '"content": "Hello, ', 'world', '!"', '}'],
228
- };
229
-
230
- await userEvent.click(screen.getByTestId('submit-button'));
231
-
232
- expect(onFinishCalls).toStrictEqual([
233
- { object: { content: 'Hello, world!' }, error: undefined },
234
- ]);
235
- });
236
-
237
- it('should be called with an error when the stream finishes and the object does not match the schema', async () => {
238
- server.urls['/api/use-object'].response = {
239
- type: 'stream-chunks',
240
- chunks: ['{ ', '"content-wrong": "Hello, ', 'world', '!"', '}'],
241
- };
242
-
243
- await userEvent.click(screen.getByTestId('submit-button'));
244
-
245
- expect(onFinishCalls).toStrictEqual([
246
- { object: undefined, error: expect.any(Error) },
247
- ]);
248
- });
249
- });
250
- });
251
-
252
- it('should send custom headers', async () => {
253
- server.urls['/api/use-object'].response = {
254
- type: 'stream-chunks',
255
- chunks: ['{ ', '"content": "Hello, ', 'world', '!"', '}'],
256
- };
257
-
258
- render(
259
- <TestComponent
260
- headers={{
261
- Authorization: 'Bearer TEST_TOKEN',
262
- 'X-Custom-Header': 'CustomValue',
263
- }}
264
- />,
265
- );
266
-
267
- await userEvent.click(screen.getByTestId('submit-button'));
268
-
269
- expect(server.calls[0].requestHeaders).toStrictEqual({
270
- 'content-type': 'application/json',
271
- authorization: 'Bearer TEST_TOKEN',
272
- 'x-custom-header': 'CustomValue',
273
- });
274
- });
275
-
276
- it('should send headers from async function', async () => {
277
- server.urls['/api/use-object'].response = {
278
- type: 'stream-chunks',
279
- chunks: ['{ ', '"content": "Hello, ', 'world', '!"', '}'],
280
- };
281
-
282
- const TestComponentWithAsyncHeaders = () => {
283
- const { submit } = experimental_useObject({
284
- api: '/api/use-object',
285
- schema: z.object({ content: z.string() }),
286
- headers: async () => {
287
- // Simulate async token fetch
288
- await new Promise(resolve => setTimeout(resolve, 10));
289
- return {
290
- Authorization: 'Bearer ASYNC_TOKEN',
291
- 'X-Request-ID': 'async-123',
292
- };
293
- },
294
- });
295
-
296
- return (
297
- <button
298
- data-testid="submit-async-headers"
299
- onClick={() => submit('test-input')}
300
- >
301
- Submit
302
- </button>
303
- );
304
- };
305
-
306
- render(<TestComponentWithAsyncHeaders />);
307
- await userEvent.click(screen.getByTestId('submit-async-headers'));
308
-
309
- await waitFor(() => {
310
- expect(server.calls[0].requestHeaders).toStrictEqual({
311
- 'content-type': 'application/json',
312
- authorization: 'Bearer ASYNC_TOKEN',
313
- 'x-request-id': 'async-123',
314
- });
315
- });
316
- });
317
-
318
- it('should send headers from sync function', async () => {
319
- server.urls['/api/use-object'].response = {
320
- type: 'stream-chunks',
321
- chunks: ['{ ', '"content": "Hello, ', 'world', '!"', '}'],
322
- };
323
-
324
- const TestComponentWithSyncFunctionHeaders = () => {
325
- const { submit } = experimental_useObject({
326
- api: '/api/use-object',
327
- schema: z.object({ content: z.string() }),
328
- headers: () => ({
329
- Authorization: 'Bearer SYNC_TOKEN',
330
- 'X-Request-ID': 'sync-456',
331
- }),
332
- });
333
-
334
- return (
335
- <button
336
- data-testid="submit-sync-headers"
337
- onClick={() => submit('test-input')}
338
- >
339
- Submit
340
- </button>
341
- );
342
- };
343
-
344
- render(<TestComponentWithSyncFunctionHeaders />);
345
- await userEvent.click(screen.getByTestId('submit-sync-headers'));
346
-
347
- await waitFor(() => {
348
- expect(server.calls[0].requestHeaders).toStrictEqual({
349
- 'content-type': 'application/json',
350
- authorization: 'Bearer SYNC_TOKEN',
351
- 'x-request-id': 'sync-456',
352
- });
353
- });
354
- });
355
-
356
- it('should send custom credentials', async () => {
357
- server.urls['/api/use-object'].response = {
358
- type: 'stream-chunks',
359
- chunks: ['{ ', '"content": "Authenticated ', 'content', '!"', '}'],
360
- };
361
-
362
- render(<TestComponent credentials="include" />);
363
- await userEvent.click(screen.getByTestId('submit-button'));
364
- expect(server.calls[0].requestCredentials).toBe('include');
365
- });
366
-
367
- it('should clear the object state after a call to clear', async () => {
368
- server.urls['/api/use-object'].response = {
369
- type: 'stream-chunks',
370
- chunks: ['{ ', '"content": "Hello, ', 'world', '!"', '}'],
371
- };
372
-
373
- render(<TestComponent />);
374
- await userEvent.click(screen.getByTestId('submit-button'));
375
-
376
- await screen.findByTestId('object');
377
- expect(screen.getByTestId('object')).toHaveTextContent(
378
- JSON.stringify({ content: 'Hello, world!' }),
379
- );
380
-
381
- await userEvent.click(screen.getByTestId('clear-button'));
382
-
383
- await waitFor(() => {
384
- expect(screen.getByTestId('object')).toBeEmptyDOMElement();
385
- expect(screen.getByTestId('error')).toBeEmptyDOMElement();
386
- expect(screen.getByTestId('loading')).toHaveTextContent('false');
387
- });
388
- });
389
- });