@ai-sdk/provider-utils 4.0.8 → 4.0.10

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.js +29 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/index.mjs +29 -1
  5. package/dist/index.mjs.map +1 -1
  6. package/package.json +6 -2
  7. package/src/handle-fetch-error.ts +33 -0
  8. package/src/__snapshots__/schema.test.ts.snap +0 -346
  9. package/src/add-additional-properties-to-json-schema.test.ts +0 -289
  10. package/src/convert-async-iterator-to-readable-stream.test.ts +0 -78
  11. package/src/convert-image-model-file-to-data-uri.test.ts +0 -85
  12. package/src/convert-to-form-data.test.ts +0 -167
  13. package/src/create-tool-name-mapping.test.ts +0 -163
  14. package/src/delay.test.ts +0 -212
  15. package/src/delayed-promise.test.ts +0 -132
  16. package/src/download-blob.test.ts +0 -145
  17. package/src/generate-id.test.ts +0 -31
  18. package/src/get-from-api.test.ts +0 -199
  19. package/src/get-runtime-environment-user-agent.test.ts +0 -47
  20. package/src/inject-json-instruction.test.ts +0 -404
  21. package/src/is-url-supported.test.ts +0 -282
  22. package/src/media-type-to-extension.test.ts +0 -26
  23. package/src/normalize-headers.test.ts +0 -64
  24. package/src/parse-json.test.ts +0 -191
  25. package/src/remove-undefined-entries.test.ts +0 -57
  26. package/src/resolve.test.ts +0 -125
  27. package/src/response-handler.test.ts +0 -89
  28. package/src/schema.test-d.ts +0 -11
  29. package/src/schema.test.ts +0 -502
  30. package/src/secure-json-parse.test.ts +0 -59
  31. package/src/to-json-schema/zod3-to-json-schema/parse-def.test.ts +0 -224
  32. package/src/to-json-schema/zod3-to-json-schema/parsers/array.test.ts +0 -98
  33. package/src/to-json-schema/zod3-to-json-schema/parsers/bigint.test.ts +0 -51
  34. package/src/to-json-schema/zod3-to-json-schema/parsers/branded.test.ts +0 -16
  35. package/src/to-json-schema/zod3-to-json-schema/parsers/catch.test.ts +0 -15
  36. package/src/to-json-schema/zod3-to-json-schema/parsers/date.test.ts +0 -97
  37. package/src/to-json-schema/zod3-to-json-schema/parsers/default.test.ts +0 -54
  38. package/src/to-json-schema/zod3-to-json-schema/parsers/effects.test.ts +0 -41
  39. package/src/to-json-schema/zod3-to-json-schema/parsers/intersection.test.ts +0 -92
  40. package/src/to-json-schema/zod3-to-json-schema/parsers/map.test.ts +0 -48
  41. package/src/to-json-schema/zod3-to-json-schema/parsers/native-enum.test.ts +0 -102
  42. package/src/to-json-schema/zod3-to-json-schema/parsers/nullable.test.ts +0 -67
  43. package/src/to-json-schema/zod3-to-json-schema/parsers/number.test.ts +0 -65
  44. package/src/to-json-schema/zod3-to-json-schema/parsers/object.test.ts +0 -149
  45. package/src/to-json-schema/zod3-to-json-schema/parsers/optional.test.ts +0 -147
  46. package/src/to-json-schema/zod3-to-json-schema/parsers/pipe.test.ts +0 -35
  47. package/src/to-json-schema/zod3-to-json-schema/parsers/promise.test.ts +0 -15
  48. package/src/to-json-schema/zod3-to-json-schema/parsers/readonly.test.ts +0 -20
  49. package/src/to-json-schema/zod3-to-json-schema/parsers/record.test.ts +0 -108
  50. package/src/to-json-schema/zod3-to-json-schema/parsers/set.test.ts +0 -20
  51. package/src/to-json-schema/zod3-to-json-schema/parsers/string.test.ts +0 -438
  52. package/src/to-json-schema/zod3-to-json-schema/parsers/tuple.test.ts +0 -33
  53. package/src/to-json-schema/zod3-to-json-schema/parsers/union.test.ts +0 -226
  54. package/src/to-json-schema/zod3-to-json-schema/refs.test.ts +0 -919
  55. package/src/to-json-schema/zod3-to-json-schema/zod3-to-json-schema.test.ts +0 -862
  56. package/src/types/tool.test-d.ts +0 -228
  57. package/src/validate-types.test.ts +0 -105
  58. package/src/with-user-agent-suffix.test.ts +0 -84
@@ -1,78 +0,0 @@
1
- import { convertAsyncIteratorToReadableStream } from './convert-async-iterator-to-readable-stream';
2
- import { describe, it, expect } from 'vitest';
3
-
4
- async function* makeGenerator(onFinally: () => void) {
5
- try {
6
- let i = 0;
7
- while (true) {
8
- await new Promise(r => setTimeout(r, 0));
9
- yield i++;
10
- }
11
- } finally {
12
- onFinally();
13
- }
14
- }
15
-
16
- describe('convertAsyncIteratorToReadableStream', () => {
17
- it('calls iterator.return() on cancel and triggers finally', async () => {
18
- let finallyCalled = false;
19
- const it = makeGenerator(() => {
20
- finallyCalled = true;
21
- });
22
- const stream = convertAsyncIteratorToReadableStream(it);
23
- const reader = stream.getReader();
24
-
25
- await reader.read();
26
-
27
- await reader.cancel('stop');
28
-
29
- // give microtasks a tick for finally to run
30
- await new Promise(r => setTimeout(r, 0));
31
-
32
- expect(finallyCalled).toBe(true);
33
- });
34
-
35
- it('does not enqueue further values after cancel', async () => {
36
- const it = makeGenerator(() => {});
37
- const stream = convertAsyncIteratorToReadableStream(it);
38
- const reader = stream.getReader();
39
-
40
- await reader.read();
41
- await reader.cancel('stop');
42
-
43
- const { done, value } = await reader.read();
44
- expect(done).toBe(true);
45
- expect(value).toBeUndefined();
46
- });
47
-
48
- it('works with iterator without return() method', async () => {
49
- const it: AsyncIterator<number> = {
50
- async next() {
51
- return { value: 42, done: false };
52
- },
53
- };
54
- const stream = convertAsyncIteratorToReadableStream(it);
55
- const reader = stream.getReader();
56
-
57
- const { value } = await reader.read();
58
- expect(value).toBe(42);
59
-
60
- await expect(reader.cancel()).resolves.toBeUndefined();
61
- });
62
-
63
- it('ignores errors from iterator.return()', async () => {
64
- const it: AsyncIterator<number> = {
65
- async next() {
66
- return { value: 1, done: false };
67
- },
68
- async return() {
69
- throw new Error('return() failed');
70
- },
71
- };
72
- const stream = convertAsyncIteratorToReadableStream(it);
73
- const reader = stream.getReader();
74
-
75
- await reader.read();
76
- await expect(reader.cancel()).resolves.toBeUndefined();
77
- });
78
- });
@@ -1,85 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { convertImageModelFileToDataUri } from './convert-image-model-file-to-data-uri';
3
-
4
- describe('convertImageModelFileToDataUri()', () => {
5
- describe('URL files', () => {
6
- it('should return the URL as-is for URL type files', () => {
7
- const result = convertImageModelFileToDataUri({
8
- type: 'url',
9
- url: 'https://example.com/image.png',
10
- });
11
-
12
- expect(result).toBe('https://example.com/image.png');
13
- });
14
-
15
- it('should handle URLs with query parameters', () => {
16
- const result = convertImageModelFileToDataUri({
17
- type: 'url',
18
- url: 'https://example.com/image.png?width=100&height=200',
19
- });
20
-
21
- expect(result).toBe('https://example.com/image.png?width=100&height=200');
22
- });
23
- });
24
-
25
- describe('base64 string files', () => {
26
- it('should return a data URI for base64 string data', () => {
27
- const result = convertImageModelFileToDataUri({
28
- type: 'file',
29
- mediaType: 'image/png',
30
- data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
31
- });
32
-
33
- expect(result).toBe(
34
- 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
35
- );
36
- });
37
-
38
- it('should handle different media types', () => {
39
- const result = convertImageModelFileToDataUri({
40
- type: 'file',
41
- mediaType: 'image/jpeg',
42
- data: 'base64data',
43
- });
44
-
45
- expect(result).toBe('data:image/jpeg;base64,base64data');
46
- });
47
- });
48
-
49
- describe('Uint8Array files', () => {
50
- it('should convert Uint8Array to base64 and return a data URI', () => {
51
- // "Hello" in bytes
52
- const data = new Uint8Array([72, 101, 108, 108, 111]);
53
-
54
- const result = convertImageModelFileToDataUri({
55
- type: 'file',
56
- mediaType: 'image/png',
57
- data,
58
- });
59
-
60
- expect(result).toBe('data:image/png;base64,SGVsbG8=');
61
- });
62
-
63
- it('should handle empty Uint8Array', () => {
64
- const result = convertImageModelFileToDataUri({
65
- type: 'file',
66
- mediaType: 'image/png',
67
- data: new Uint8Array([]),
68
- });
69
-
70
- expect(result).toBe('data:image/png;base64,');
71
- });
72
-
73
- it('should handle different media types with Uint8Array', () => {
74
- const data = new Uint8Array([72, 101, 108, 108, 111]);
75
-
76
- const result = convertImageModelFileToDataUri({
77
- type: 'file',
78
- mediaType: 'image/webp',
79
- data,
80
- });
81
-
82
- expect(result).toBe('data:image/webp;base64,SGVsbG8=');
83
- });
84
- });
85
- });
@@ -1,167 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { convertToFormData } from './convert-to-form-data';
3
-
4
- describe('convertToFormData()', () => {
5
- describe('basic values', () => {
6
- it('should add string values to form data', () => {
7
- const formData = convertToFormData({
8
- model: 'gpt-image-1',
9
- prompt: 'A cute cat',
10
- });
11
-
12
- expect(formData.get('model')).toBe('gpt-image-1');
13
- expect(formData.get('prompt')).toBe('A cute cat');
14
- });
15
-
16
- it('should add number values as strings', () => {
17
- const formData = convertToFormData({
18
- n: 2,
19
- seed: 42,
20
- });
21
-
22
- expect(formData.get('n')).toBe('2');
23
- expect(formData.get('seed')).toBe('42');
24
- });
25
-
26
- it('should add Blob values to form data', () => {
27
- const blob = new Blob(['test'], { type: 'image/png' });
28
- const formData = convertToFormData({
29
- image: blob,
30
- });
31
-
32
- expect(formData.get('image')).toBeInstanceOf(Blob);
33
- });
34
- });
35
-
36
- describe('null and undefined values', () => {
37
- it('should skip null values', () => {
38
- const formData = convertToFormData({
39
- model: 'gpt-image-1',
40
- mask: null,
41
- });
42
-
43
- expect(formData.get('model')).toBe('gpt-image-1');
44
- expect(formData.has('mask')).toBe(false);
45
- });
46
-
47
- it('should skip undefined values', () => {
48
- const formData = convertToFormData({
49
- model: 'gpt-image-1',
50
- size: undefined,
51
- });
52
-
53
- expect(formData.get('model')).toBe('gpt-image-1');
54
- expect(formData.has('size')).toBe(false);
55
- });
56
- });
57
-
58
- describe('array values', () => {
59
- it('should add single-element arrays as single value without [] suffix', () => {
60
- const blob = new Blob(['test'], { type: 'image/png' });
61
- const formData = convertToFormData({
62
- image: [blob],
63
- });
64
-
65
- expect(formData.get('image')).toBeInstanceOf(Blob);
66
- expect(formData.has('image[]')).toBe(false);
67
- });
68
-
69
- it('should add multi-element arrays with [] suffix', () => {
70
- const blob1 = new Blob(['test1'], { type: 'image/png' });
71
- const blob2 = new Blob(['test2'], { type: 'image/jpeg' });
72
- const formData = convertToFormData({
73
- image: [blob1, blob2],
74
- });
75
-
76
- expect(formData.has('image')).toBe(false);
77
- const images = formData.getAll('image[]');
78
- expect(images).toHaveLength(2);
79
- expect(images[0]).toBeInstanceOf(Blob);
80
- expect(images[1]).toBeInstanceOf(Blob);
81
- });
82
-
83
- it('should add multi-element arrays without [] suffix when useArrayBrackets is false', () => {
84
- const blob1 = new Blob(['test1'], { type: 'image/png' });
85
- const blob2 = new Blob(['test2'], { type: 'image/jpeg' });
86
- const formData = convertToFormData(
87
- {
88
- image: [blob1, blob2],
89
- },
90
- { useArrayBrackets: false },
91
- );
92
-
93
- expect(formData.has('image[]')).toBe(false);
94
- const images = formData.getAll('image');
95
- expect(images).toHaveLength(2);
96
- expect(images[0]).toBeInstanceOf(Blob);
97
- expect(images[1]).toBeInstanceOf(Blob);
98
- });
99
-
100
- it('should handle empty arrays by not adding any values', () => {
101
- const formData = convertToFormData({
102
- model: 'test',
103
- images: [],
104
- });
105
-
106
- expect(formData.get('model')).toBe('test');
107
- expect(formData.has('images')).toBe(false);
108
- expect(formData.has('images[]')).toBe(false);
109
- });
110
-
111
- it('should add string arrays with [] suffix', () => {
112
- const formData = convertToFormData({
113
- tags: ['cat', 'cute', 'animal'],
114
- });
115
-
116
- const tags = formData.getAll('tags[]');
117
- expect(tags).toHaveLength(3);
118
- expect(tags).toEqual(['cat', 'cute', 'animal']);
119
- });
120
- });
121
-
122
- describe('type validation', () => {
123
- it('should accept typed input objects', () => {
124
- type ImageInput = {
125
- model: string;
126
- prompt: string;
127
- n: number;
128
- size: `${number}x${number}` | undefined;
129
- };
130
-
131
- const formData = convertToFormData<ImageInput>({
132
- model: 'dall-e-3',
133
- prompt: 'A sunset',
134
- n: 1,
135
- size: '1024x1024',
136
- });
137
-
138
- expect(formData.get('model')).toBe('dall-e-3');
139
- expect(formData.get('prompt')).toBe('A sunset');
140
- expect(formData.get('n')).toBe('1');
141
- expect(formData.get('size')).toBe('1024x1024');
142
- });
143
- });
144
-
145
- describe('mixed values', () => {
146
- it('should handle a complex input with various types', () => {
147
- const blob = new Blob(['image data'], { type: 'image/png' });
148
- const formData = convertToFormData({
149
- model: 'gpt-image-1',
150
- prompt: 'Edit this image',
151
- image: [blob],
152
- mask: null,
153
- n: 1,
154
- size: '1024x1024',
155
- quality: 'high',
156
- });
157
-
158
- expect(formData.get('model')).toBe('gpt-image-1');
159
- expect(formData.get('prompt')).toBe('Edit this image');
160
- expect(formData.get('image')).toBeInstanceOf(Blob);
161
- expect(formData.has('mask')).toBe(false);
162
- expect(formData.get('n')).toBe('1');
163
- expect(formData.get('size')).toBe('1024x1024');
164
- expect(formData.get('quality')).toBe('high');
165
- });
166
- });
167
- });
@@ -1,163 +0,0 @@
1
- import {
2
- LanguageModelV3FunctionTool,
3
- LanguageModelV3ProviderTool,
4
- } from '@ai-sdk/provider';
5
- import { describe, expect, it } from 'vitest';
6
- import { createToolNameMapping } from './create-tool-name-mapping';
7
-
8
- describe('createToolNameMapping', () => {
9
- it('should create mappings for provider-defined tools', () => {
10
- const tools: Array<
11
- LanguageModelV3FunctionTool | LanguageModelV3ProviderTool
12
- > = [
13
- {
14
- type: 'provider',
15
- id: 'anthropic.computer-use',
16
- name: 'custom-computer-tool',
17
- args: {},
18
- },
19
- {
20
- type: 'provider',
21
- id: 'openai.code-interpreter',
22
- name: 'custom-code-tool',
23
- args: {},
24
- },
25
- ];
26
-
27
- const providerToolNames: Record<`${string}.${string}`, string> = {
28
- 'anthropic.computer-use': 'computer_use',
29
- 'openai.code-interpreter': 'code_interpreter',
30
- };
31
-
32
- const mapping = createToolNameMapping({ tools, providerToolNames });
33
-
34
- expect(mapping.toProviderToolName('custom-computer-tool')).toBe(
35
- 'computer_use',
36
- );
37
- expect(mapping.toProviderToolName('custom-code-tool')).toBe(
38
- 'code_interpreter',
39
- );
40
- expect(mapping.toCustomToolName('computer_use')).toBe(
41
- 'custom-computer-tool',
42
- );
43
- expect(mapping.toCustomToolName('code_interpreter')).toBe(
44
- 'custom-code-tool',
45
- );
46
- });
47
-
48
- it('should ignore function tools', () => {
49
- const tools: Array<
50
- LanguageModelV3FunctionTool | LanguageModelV3ProviderTool
51
- > = [
52
- {
53
- type: 'function',
54
- name: 'my-function-tool',
55
- description: 'A function tool',
56
- inputSchema: { type: 'object' },
57
- },
58
- ];
59
-
60
- const providerToolNames: Record<`${string}.${string}`, string> = {};
61
-
62
- const mapping = createToolNameMapping({ tools, providerToolNames });
63
-
64
- expect(mapping.toProviderToolName('my-function-tool')).toBe(
65
- 'my-function-tool',
66
- );
67
- expect(mapping.toCustomToolName('my-function-tool')).toBe(
68
- 'my-function-tool',
69
- );
70
- });
71
-
72
- it('should return input name when tool is not in providerToolNames', () => {
73
- const tools: Array<
74
- LanguageModelV3FunctionTool | LanguageModelV3ProviderTool
75
- > = [
76
- {
77
- type: 'provider',
78
- id: 'unknown.tool',
79
- name: 'custom-tool',
80
- args: {},
81
- },
82
- ];
83
-
84
- const providerToolNames: Record<`${string}.${string}`, string> = {};
85
-
86
- const mapping = createToolNameMapping({ tools, providerToolNames });
87
-
88
- expect(mapping.toProviderToolName('custom-tool')).toBe('custom-tool');
89
- expect(mapping.toCustomToolName('unknown-name')).toBe('unknown-name');
90
- });
91
-
92
- it('should return input name when mapping does not exist', () => {
93
- const tools: Array<
94
- LanguageModelV3FunctionTool | LanguageModelV3ProviderTool
95
- > = [
96
- {
97
- type: 'provider',
98
- id: 'anthropic.computer-use',
99
- name: 'custom-computer-tool',
100
- args: {},
101
- },
102
- ];
103
-
104
- const providerToolNames: Record<`${string}.${string}`, string> = {
105
- 'anthropic.computer-use': 'computer_use',
106
- };
107
-
108
- const mapping = createToolNameMapping({ tools, providerToolNames });
109
-
110
- expect(mapping.toProviderToolName('non-existent-tool')).toBe(
111
- 'non-existent-tool',
112
- );
113
- expect(mapping.toCustomToolName('non-existent-provider-tool')).toBe(
114
- 'non-existent-provider-tool',
115
- );
116
- });
117
-
118
- it('should handle empty tools array', () => {
119
- const tools: Array<
120
- LanguageModelV3FunctionTool | LanguageModelV3ProviderTool
121
- > = [];
122
-
123
- const providerToolNames: Record<`${string}.${string}`, string> = {};
124
-
125
- const mapping = createToolNameMapping({ tools, providerToolNames });
126
-
127
- expect(mapping.toProviderToolName('any-tool')).toBe('any-tool');
128
- expect(mapping.toCustomToolName('any-tool')).toBe('any-tool');
129
- });
130
-
131
- it('should handle mixed function and provider-defined tools', () => {
132
- const tools: Array<
133
- LanguageModelV3FunctionTool | LanguageModelV3ProviderTool
134
- > = [
135
- {
136
- type: 'function',
137
- name: 'function-tool',
138
- description: 'A function tool',
139
- inputSchema: { type: 'object' },
140
- },
141
- {
142
- type: 'provider',
143
- id: 'anthropic.computer-use',
144
- name: 'provider-tool',
145
- args: {},
146
- },
147
- ];
148
-
149
- const providerToolNames: Record<`${string}.${string}`, string> = {
150
- 'anthropic.computer-use': 'computer_use',
151
- };
152
-
153
- const mapping = createToolNameMapping({ tools, providerToolNames });
154
-
155
- // Function tool should not be mapped
156
- expect(mapping.toProviderToolName('function-tool')).toBe('function-tool');
157
- expect(mapping.toCustomToolName('function-tool')).toBe('function-tool');
158
-
159
- // Provider-defined tool should be mapped
160
- expect(mapping.toProviderToolName('provider-tool')).toBe('computer_use');
161
- expect(mapping.toCustomToolName('computer_use')).toBe('provider-tool');
162
- });
163
- });
package/src/delay.test.ts DELETED
@@ -1,212 +0,0 @@
1
- import { delay } from './delay';
2
- import { describe, beforeEach, afterEach, expect, it, vi } from 'vitest';
3
-
4
- describe('delay', () => {
5
- beforeEach(() => {
6
- vi.useFakeTimers();
7
- });
8
-
9
- afterEach(() => {
10
- vi.useRealTimers();
11
- });
12
-
13
- describe('basic delay functionality', () => {
14
- it('should resolve after the specified delay', async () => {
15
- const delayPromise = delay(1000);
16
-
17
- // Promise should not be resolved immediately
18
- let resolved = false;
19
- delayPromise.then(() => {
20
- resolved = true;
21
- });
22
-
23
- expect(resolved).toBe(false);
24
-
25
- // Advance timers by less than the delay
26
- await vi.advanceTimersByTimeAsync(500);
27
- expect(resolved).toBe(false);
28
-
29
- // Advance timers to complete the delay
30
- await vi.advanceTimersByTimeAsync(500);
31
- expect(resolved).toBe(true);
32
-
33
- // Verify the promise resolves
34
- await expect(delayPromise).resolves.toBeUndefined();
35
- });
36
-
37
- it('should resolve immediately when delayInMs is null', async () => {
38
- const delayPromise = delay(null);
39
- await expect(delayPromise).resolves.toBeUndefined();
40
- });
41
-
42
- it('should resolve immediately when delayInMs is undefined', async () => {
43
- const delayPromise = delay(undefined);
44
- await expect(delayPromise).resolves.toBeUndefined();
45
- });
46
-
47
- it('should resolve immediately when delayInMs is 0', async () => {
48
- const delayPromise = delay(0);
49
-
50
- // Even with 0 delay, setTimeout is used, so we need to advance timers
51
- await vi.advanceTimersByTimeAsync(0);
52
- await expect(delayPromise).resolves.toBeUndefined();
53
- });
54
- });
55
-
56
- describe('abort signal functionality', () => {
57
- it('should reject immediately if signal is already aborted', async () => {
58
- const controller = new AbortController();
59
- controller.abort();
60
-
61
- const delayPromise = delay(1000, { abortSignal: controller.signal });
62
-
63
- await expect(delayPromise).rejects.toThrow('Delay was aborted');
64
- expect(vi.getTimerCount()).toBe(0); // No timer should be set
65
- });
66
-
67
- it('should reject when signal is aborted during delay', async () => {
68
- const controller = new AbortController();
69
- const delayPromise = delay(1000, { abortSignal: controller.signal });
70
-
71
- // Advance time partially
72
- await vi.advanceTimersByTimeAsync(500);
73
-
74
- // Abort the signal
75
- controller.abort();
76
-
77
- await expect(delayPromise).rejects.toThrow('Delay was aborted');
78
- });
79
-
80
- it('should clean up timeout when aborted', async () => {
81
- const controller = new AbortController();
82
- const delayPromise = delay(1000, { abortSignal: controller.signal });
83
-
84
- expect(vi.getTimerCount()).toBe(1);
85
-
86
- controller.abort();
87
-
88
- try {
89
- await delayPromise;
90
- } catch {
91
- // Expected to throw
92
- }
93
-
94
- expect(vi.getTimerCount()).toBe(0);
95
- });
96
-
97
- it('should clean up event listener when delay completes normally', async () => {
98
- const controller = new AbortController();
99
- const addEventListenerSpy = vi.spyOn(
100
- controller.signal,
101
- 'addEventListener',
102
- );
103
- const removeEventListenerSpy = vi.spyOn(
104
- controller.signal,
105
- 'removeEventListener',
106
- );
107
-
108
- const delayPromise = delay(1000, { abortSignal: controller.signal });
109
-
110
- expect(addEventListenerSpy).toHaveBeenCalledWith(
111
- 'abort',
112
- expect.any(Function),
113
- );
114
-
115
- await vi.advanceTimersByTimeAsync(1000);
116
- await delayPromise;
117
-
118
- expect(removeEventListenerSpy).toHaveBeenCalledWith(
119
- 'abort',
120
- expect.any(Function),
121
- );
122
- });
123
-
124
- it('should work without signal option', async () => {
125
- const delayPromise = delay(1000);
126
-
127
- await vi.advanceTimersByTimeAsync(1000);
128
- await expect(delayPromise).resolves.toBeUndefined();
129
- });
130
- });
131
-
132
- describe('error handling', () => {
133
- it('should create proper DOMException for abort', async () => {
134
- const controller = new AbortController();
135
- controller.abort();
136
-
137
- const delayPromise = delay(1000, { abortSignal: controller.signal });
138
-
139
- try {
140
- await delayPromise;
141
- expect.fail('Should have thrown');
142
- } catch (error) {
143
- expect(error).toBeInstanceOf(DOMException);
144
- expect((error as DOMException).message).toBe('Delay was aborted');
145
- expect((error as DOMException).name).toBe('AbortError');
146
- }
147
- });
148
- });
149
-
150
- describe('edge cases', () => {
151
- it('should handle very large delays', async () => {
152
- const delayPromise = delay(Number.MAX_SAFE_INTEGER);
153
-
154
- await vi.advanceTimersByTimeAsync(1000);
155
- let resolved = false;
156
- delayPromise.then(() => {
157
- resolved = true;
158
- });
159
-
160
- expect(resolved).toBe(false);
161
-
162
- // Fast forward to complete
163
- await vi.advanceTimersByTimeAsync(1000);
164
- await expect(delayPromise).resolves.toBeUndefined();
165
- });
166
-
167
- it('should handle negative delays (treated as 0)', async () => {
168
- const delayPromise = delay(-100);
169
-
170
- vi.advanceTimersByTime(0);
171
- await expect(delayPromise).resolves.toBeUndefined();
172
- });
173
-
174
- it('should handle multiple delays simultaneously', async () => {
175
- const delay1 = delay(100);
176
- const delay2 = delay(200);
177
- const delay3 = delay(300);
178
-
179
- let resolved1 = false;
180
- let resolved2 = false;
181
- let resolved3 = false;
182
-
183
- delay1.then(() => {
184
- resolved1 = true;
185
- });
186
- delay2.then(() => {
187
- resolved2 = true;
188
- });
189
- delay3.then(() => {
190
- resolved3 = true;
191
- });
192
-
193
- // After 100ms, only first should resolve
194
- await vi.advanceTimersByTimeAsync(100);
195
- expect(resolved1).toBe(true);
196
- expect(resolved2).toBe(false);
197
- expect(resolved3).toBe(false);
198
-
199
- // After 200ms, first two should resolve
200
- await vi.advanceTimersByTimeAsync(100);
201
- expect(resolved1).toBe(true);
202
- expect(resolved2).toBe(true);
203
- expect(resolved3).toBe(false);
204
-
205
- // After 300ms, all should resolve
206
- await vi.advanceTimersByTimeAsync(100);
207
- expect(resolved1).toBe(true);
208
- expect(resolved2).toBe(true);
209
- expect(resolved3).toBe(true);
210
- });
211
- });
212
- });