ai-functions 0.2.19 → 0.4.0

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 (227) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/.turbo/turbo-test.log +105 -0
  3. package/README.md +232 -37
  4. package/TODO.md +138 -0
  5. package/dist/ai-promise.d.ts +219 -0
  6. package/dist/ai-promise.d.ts.map +1 -0
  7. package/dist/ai-promise.js +610 -0
  8. package/dist/ai-promise.js.map +1 -0
  9. package/dist/ai.d.ts +285 -0
  10. package/dist/ai.d.ts.map +1 -0
  11. package/dist/ai.js +842 -0
  12. package/dist/ai.js.map +1 -0
  13. package/dist/batch/anthropic.d.ts +23 -0
  14. package/dist/batch/anthropic.d.ts.map +1 -0
  15. package/dist/batch/anthropic.js +257 -0
  16. package/dist/batch/anthropic.js.map +1 -0
  17. package/dist/batch/bedrock.d.ts +64 -0
  18. package/dist/batch/bedrock.d.ts.map +1 -0
  19. package/dist/batch/bedrock.js +586 -0
  20. package/dist/batch/bedrock.js.map +1 -0
  21. package/dist/batch/cloudflare.d.ts +37 -0
  22. package/dist/batch/cloudflare.d.ts.map +1 -0
  23. package/dist/batch/cloudflare.js +289 -0
  24. package/dist/batch/cloudflare.js.map +1 -0
  25. package/dist/batch/google.d.ts +41 -0
  26. package/dist/batch/google.d.ts.map +1 -0
  27. package/dist/batch/google.js +360 -0
  28. package/dist/batch/google.js.map +1 -0
  29. package/dist/batch/index.d.ts +31 -0
  30. package/dist/batch/index.d.ts.map +1 -0
  31. package/dist/batch/index.js +31 -0
  32. package/dist/batch/index.js.map +1 -0
  33. package/dist/batch/memory.d.ts +44 -0
  34. package/dist/batch/memory.d.ts.map +1 -0
  35. package/dist/batch/memory.js +188 -0
  36. package/dist/batch/memory.js.map +1 -0
  37. package/dist/batch/openai.d.ts +37 -0
  38. package/dist/batch/openai.d.ts.map +1 -0
  39. package/dist/batch/openai.js +403 -0
  40. package/dist/batch/openai.js.map +1 -0
  41. package/dist/batch-map.d.ts +125 -0
  42. package/dist/batch-map.d.ts.map +1 -0
  43. package/dist/batch-map.js +406 -0
  44. package/dist/batch-map.js.map +1 -0
  45. package/dist/batch-queue.d.ts +273 -0
  46. package/dist/batch-queue.d.ts.map +1 -0
  47. package/dist/batch-queue.js +271 -0
  48. package/dist/batch-queue.js.map +1 -0
  49. package/dist/context.d.ts +133 -0
  50. package/dist/context.d.ts.map +1 -0
  51. package/dist/context.js +267 -0
  52. package/dist/context.js.map +1 -0
  53. package/dist/embeddings.d.ts +123 -0
  54. package/dist/embeddings.d.ts.map +1 -0
  55. package/dist/embeddings.js +170 -0
  56. package/dist/embeddings.js.map +1 -0
  57. package/dist/eval/index.d.ts +8 -0
  58. package/dist/eval/index.d.ts.map +1 -0
  59. package/dist/eval/index.js +8 -0
  60. package/dist/eval/index.js.map +1 -0
  61. package/dist/eval/models.d.ts +66 -0
  62. package/dist/eval/models.d.ts.map +1 -0
  63. package/dist/eval/models.js +120 -0
  64. package/dist/eval/models.js.map +1 -0
  65. package/dist/eval/runner.d.ts +64 -0
  66. package/dist/eval/runner.d.ts.map +1 -0
  67. package/dist/eval/runner.js +148 -0
  68. package/dist/eval/runner.js.map +1 -0
  69. package/dist/generate.d.ts +168 -0
  70. package/dist/generate.d.ts.map +1 -0
  71. package/dist/generate.js +174 -0
  72. package/dist/generate.js.map +1 -0
  73. package/dist/index.d.ts +30 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/index.js +54 -0
  76. package/dist/index.js.map +1 -0
  77. package/dist/primitives.d.ts +292 -0
  78. package/dist/primitives.d.ts.map +1 -0
  79. package/dist/primitives.js +471 -0
  80. package/dist/primitives.js.map +1 -0
  81. package/dist/providers/cloudflare.d.ts +9 -0
  82. package/dist/providers/cloudflare.d.ts.map +1 -0
  83. package/dist/providers/cloudflare.js +9 -0
  84. package/dist/providers/cloudflare.js.map +1 -0
  85. package/dist/providers/index.d.ts +9 -0
  86. package/dist/providers/index.d.ts.map +1 -0
  87. package/dist/providers/index.js +9 -0
  88. package/dist/providers/index.js.map +1 -0
  89. package/dist/schema.d.ts +54 -0
  90. package/dist/schema.d.ts.map +1 -0
  91. package/dist/schema.js +109 -0
  92. package/dist/schema.js.map +1 -0
  93. package/dist/template.d.ts +73 -0
  94. package/dist/template.d.ts.map +1 -0
  95. package/dist/template.js +129 -0
  96. package/dist/template.js.map +1 -0
  97. package/dist/types.d.ts +481 -0
  98. package/dist/types.d.ts.map +1 -0
  99. package/dist/types.js +5 -0
  100. package/dist/types.js.map +1 -0
  101. package/evalite.config.ts +19 -0
  102. package/evals/README.md +212 -0
  103. package/evals/classification.eval.ts +108 -0
  104. package/evals/marketing.eval.ts +370 -0
  105. package/evals/math.eval.ts +94 -0
  106. package/evals/run-evals.ts +166 -0
  107. package/evals/structured-output.eval.ts +143 -0
  108. package/evals/writing.eval.ts +117 -0
  109. package/examples/batch-blog-posts.ts +160 -0
  110. package/package.json +59 -43
  111. package/src/ai-promise.ts +784 -0
  112. package/src/ai.ts +1183 -0
  113. package/src/batch/anthropic.ts +375 -0
  114. package/src/batch/bedrock.ts +801 -0
  115. package/src/batch/cloudflare.ts +421 -0
  116. package/src/batch/google.ts +491 -0
  117. package/src/batch/index.ts +31 -0
  118. package/src/batch/memory.ts +253 -0
  119. package/src/batch/openai.ts +557 -0
  120. package/src/batch-map.ts +534 -0
  121. package/src/batch-queue.ts +493 -0
  122. package/src/context.ts +332 -0
  123. package/src/embeddings.ts +244 -0
  124. package/src/eval/index.ts +8 -0
  125. package/src/eval/models.ts +158 -0
  126. package/src/eval/runner.ts +217 -0
  127. package/src/generate.ts +245 -0
  128. package/src/index.ts +154 -0
  129. package/src/primitives.ts +612 -0
  130. package/src/providers/cloudflare.ts +15 -0
  131. package/src/providers/index.ts +14 -0
  132. package/src/schema.ts +147 -0
  133. package/src/template.ts +209 -0
  134. package/src/types.ts +540 -0
  135. package/test/README.md +105 -0
  136. package/test/ai-proxy.test.ts +192 -0
  137. package/test/async-iterators.test.ts +327 -0
  138. package/test/batch-background.test.ts +482 -0
  139. package/test/batch-blog-posts.test.ts +387 -0
  140. package/test/blog-generation.test.ts +510 -0
  141. package/test/browse-read.test.ts +611 -0
  142. package/test/core-functions.test.ts +694 -0
  143. package/test/decide.test.ts +393 -0
  144. package/test/define.test.ts +274 -0
  145. package/test/e2e-bedrock-manual.ts +163 -0
  146. package/test/e2e-bedrock.test.ts +191 -0
  147. package/test/e2e-flex-gateway.ts +157 -0
  148. package/test/e2e-flex-manual.ts +183 -0
  149. package/test/e2e-flex.test.ts +209 -0
  150. package/test/e2e-google-manual.ts +178 -0
  151. package/test/e2e-google.test.ts +216 -0
  152. package/test/embeddings.test.ts +284 -0
  153. package/test/evals/define-function.eval.test.ts +379 -0
  154. package/test/evals/primitives.eval.test.ts +384 -0
  155. package/test/function-types.test.ts +492 -0
  156. package/test/generate-core.test.ts +319 -0
  157. package/test/generate.test.ts +163 -0
  158. package/test/implicit-batch.test.ts +422 -0
  159. package/test/schema.test.ts +109 -0
  160. package/test/tagged-templates.test.ts +302 -0
  161. package/tsconfig.json +8 -6
  162. package/vitest.config.ts +42 -0
  163. package/LICENSE +0 -21
  164. package/db/cache.ts +0 -6
  165. package/db/mongo.ts +0 -75
  166. package/dist/mjs/db/cache.d.ts +0 -1
  167. package/dist/mjs/db/cache.js +0 -5
  168. package/dist/mjs/db/mongo.d.ts +0 -31
  169. package/dist/mjs/db/mongo.js +0 -48
  170. package/dist/mjs/examples/data.d.ts +0 -1105
  171. package/dist/mjs/examples/data.js +0 -1105
  172. package/dist/mjs/functions/ai.d.ts +0 -20
  173. package/dist/mjs/functions/ai.js +0 -83
  174. package/dist/mjs/functions/ai.test.d.ts +0 -1
  175. package/dist/mjs/functions/ai.test.js +0 -29
  176. package/dist/mjs/functions/gpt.d.ts +0 -4
  177. package/dist/mjs/functions/gpt.js +0 -10
  178. package/dist/mjs/functions/list.d.ts +0 -7
  179. package/dist/mjs/functions/list.js +0 -72
  180. package/dist/mjs/index.d.ts +0 -3
  181. package/dist/mjs/index.js +0 -3
  182. package/dist/mjs/queue/kafka.d.ts +0 -0
  183. package/dist/mjs/queue/kafka.js +0 -1
  184. package/dist/mjs/queue/memory.d.ts +0 -0
  185. package/dist/mjs/queue/memory.js +0 -1
  186. package/dist/mjs/queue/mongo.d.ts +0 -30
  187. package/dist/mjs/queue/mongo.js +0 -42
  188. package/dist/mjs/streams/kafka.d.ts +0 -0
  189. package/dist/mjs/streams/kafka.js +0 -1
  190. package/dist/mjs/streams/memory.d.ts +0 -0
  191. package/dist/mjs/streams/memory.js +0 -1
  192. package/dist/mjs/streams/mongo.d.ts +0 -0
  193. package/dist/mjs/streams/mongo.js +0 -1
  194. package/dist/mjs/streams/types.d.ts +0 -0
  195. package/dist/mjs/streams/types.js +0 -1
  196. package/dist/mjs/types.d.ts +0 -11
  197. package/dist/mjs/types.js +0 -1
  198. package/dist/mjs/utils/completion.d.ts +0 -9
  199. package/dist/mjs/utils/completion.js +0 -20
  200. package/dist/mjs/utils/schema.d.ts +0 -10
  201. package/dist/mjs/utils/schema.js +0 -72
  202. package/dist/mjs/utils/schema.test.d.ts +0 -1
  203. package/dist/mjs/utils/schema.test.js +0 -60
  204. package/dist/mjs/utils/state.d.ts +0 -1
  205. package/dist/mjs/utils/state.js +0 -19
  206. package/examples/data.ts +0 -1105
  207. package/fixup +0 -11
  208. package/functions/ai.test.ts +0 -41
  209. package/functions/ai.ts +0 -115
  210. package/functions/gpt.ts +0 -12
  211. package/functions/list.ts +0 -84
  212. package/index.ts +0 -3
  213. package/queue/kafka.ts +0 -0
  214. package/queue/memory.ts +0 -0
  215. package/queue/mongo.ts +0 -88
  216. package/streams/kafka.ts +0 -0
  217. package/streams/memory.ts +0 -0
  218. package/streams/mongo.ts +0 -0
  219. package/streams/types.ts +0 -0
  220. package/tsconfig-backup.json +0 -105
  221. package/tsconfig-base.json +0 -26
  222. package/tsconfig-cjs.json +0 -8
  223. package/types.ts +0 -12
  224. package/utils/completion.ts +0 -28
  225. package/utils/schema.test.ts +0 -69
  226. package/utils/schema.ts +0 -74
  227. package/utils/state.ts +0 -23
@@ -0,0 +1,611 @@
1
+ /**
2
+ * Tests for web functions: read() and browse()
3
+ *
4
+ * read(url) - Convert URL to markdown (Firecrawl-style)
5
+ * browse(url) - Browser automation (Stagehand-style)
6
+ */
7
+
8
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
9
+
10
+ // ============================================================================
11
+ // Mock implementations
12
+ // ============================================================================
13
+
14
+ const mockFetchUrl = vi.fn()
15
+ const mockBrowserSession = vi.fn()
16
+
17
+ /**
18
+ * Mock read function - converts URL to markdown
19
+ */
20
+ async function read(
21
+ urlOrStrings: string | TemplateStringsArray,
22
+ ...args: unknown[]
23
+ ): Promise<string> {
24
+ let url: string
25
+
26
+ if (Array.isArray(urlOrStrings) && 'raw' in urlOrStrings) {
27
+ url = (urlOrStrings as TemplateStringsArray).reduce((acc, str, i) => {
28
+ return acc + str + (args[i] ?? '')
29
+ }, '')
30
+ } else {
31
+ url = urlOrStrings as string
32
+ }
33
+
34
+ return mockFetchUrl(url)
35
+ }
36
+
37
+ /**
38
+ * Mock browse function - returns a page context for browser automation
39
+ */
40
+ async function browse(
41
+ urlOrStrings: string | TemplateStringsArray,
42
+ ...args: unknown[]
43
+ ): Promise<{
44
+ do: (action: string) => Promise<void>
45
+ extract: (query: string) => Promise<unknown>
46
+ screenshot: () => Promise<Buffer>
47
+ close: () => Promise<void>
48
+ }> {
49
+ let url: string
50
+
51
+ if (Array.isArray(urlOrStrings) && 'raw' in urlOrStrings) {
52
+ url = (urlOrStrings as TemplateStringsArray).reduce((acc, str, i) => {
53
+ return acc + str + (args[i] ?? '')
54
+ }, '')
55
+ } else {
56
+ url = urlOrStrings as string
57
+ }
58
+
59
+ return mockBrowserSession(url)
60
+ }
61
+
62
+ // ============================================================================
63
+ // read() tests
64
+ // ============================================================================
65
+
66
+ describe('read() - URL to Markdown', () => {
67
+ beforeEach(() => {
68
+ mockFetchUrl.mockReset()
69
+ })
70
+
71
+ describe('basic usage', () => {
72
+ it('converts URL to markdown', async () => {
73
+ mockFetchUrl.mockResolvedValue(`
74
+ # Page Title
75
+
76
+ Some content from the page.
77
+
78
+ ## Section 1
79
+
80
+ More content here.
81
+ `.trim())
82
+
83
+ const content = await read('https://example.com')
84
+
85
+ expect(mockFetchUrl).toHaveBeenCalledWith('https://example.com')
86
+ expect(content).toContain('# Page Title')
87
+ expect(content).toContain('Some content')
88
+ })
89
+
90
+ it('supports tagged template syntax', async () => {
91
+ mockFetchUrl.mockResolvedValue('# Content')
92
+
93
+ const domain = 'example.com'
94
+ const path = '/docs'
95
+ const content = await read`https://${domain}${path}`
96
+
97
+ expect(mockFetchUrl).toHaveBeenCalledWith('https://example.com/docs')
98
+ })
99
+
100
+ it('extracts main content, strips navigation/ads', async () => {
101
+ mockFetchUrl.mockResolvedValue(`
102
+ # Article Title
103
+
104
+ This is the main article content, clean and focused.
105
+ `.trim())
106
+
107
+ const content = await read('https://blog.example.com/article')
108
+
109
+ // Extracted content should be clean without HTML tags
110
+ expect(content).not.toContain('<nav>')
111
+ expect(content).not.toContain('<!-- advertisement -->')
112
+ expect(content).toContain('Article Title')
113
+ })
114
+ })
115
+
116
+ describe('content extraction', () => {
117
+ it('preserves headers and structure', async () => {
118
+ mockFetchUrl.mockResolvedValue(`
119
+ # Main Title
120
+
121
+ ## Introduction
122
+
123
+ Intro paragraph.
124
+
125
+ ## Details
126
+
127
+ Detail paragraph.
128
+
129
+ ### Subsection
130
+
131
+ More details.
132
+ `.trim())
133
+
134
+ const content = await read('https://docs.example.com')
135
+
136
+ expect(content).toContain('# Main Title')
137
+ expect(content).toContain('## Introduction')
138
+ expect(content).toContain('### Subsection')
139
+ })
140
+
141
+ it('converts links to markdown format', async () => {
142
+ mockFetchUrl.mockResolvedValue(
143
+ 'Check out [our documentation](https://docs.example.com) for more info.'
144
+ )
145
+
146
+ const content = await read('https://example.com')
147
+
148
+ expect(content).toContain('[our documentation](https://docs.example.com)')
149
+ })
150
+
151
+ it('handles code blocks', async () => {
152
+ mockFetchUrl.mockResolvedValue(`
153
+ Here's an example:
154
+
155
+ \`\`\`typescript
156
+ const x = 1;
157
+ \`\`\`
158
+ `.trim())
159
+
160
+ const content = await read('https://tutorial.example.com')
161
+
162
+ expect(content).toContain('```typescript')
163
+ expect(content).toContain('const x = 1;')
164
+ })
165
+
166
+ it('handles tables', async () => {
167
+ mockFetchUrl.mockResolvedValue(`
168
+ | Header 1 | Header 2 |
169
+ |----------|----------|
170
+ | Cell 1 | Cell 2 |
171
+ `.trim())
172
+
173
+ const content = await read('https://data.example.com')
174
+
175
+ expect(content).toContain('| Header 1 |')
176
+ expect(content).toContain('| Cell 1 |')
177
+ })
178
+ })
179
+
180
+ describe('use cases', () => {
181
+ it('reads documentation for research', async () => {
182
+ mockFetchUrl.mockResolvedValue(`
183
+ # API Reference
184
+
185
+ ## Authentication
186
+
187
+ Use Bearer tokens for authentication.
188
+
189
+ ## Endpoints
190
+
191
+ ### GET /users
192
+
193
+ Returns list of users.
194
+ `.trim())
195
+
196
+ const docs = await read('https://api.example.com/docs')
197
+
198
+ expect(docs).toContain('API Reference')
199
+ expect(docs).toContain('Authentication')
200
+ expect(docs).toContain('GET /users')
201
+ })
202
+
203
+ it('reads articles for summarization', async () => {
204
+ mockFetchUrl.mockResolvedValue(`
205
+ # The Future of AI
206
+
207
+ Artificial intelligence is transforming industries...
208
+
209
+ ## Impact on Healthcare
210
+
211
+ AI is revolutionizing medical diagnosis...
212
+
213
+ ## Impact on Finance
214
+
215
+ Automated trading systems...
216
+ `.trim())
217
+
218
+ const article = await read('https://news.example.com/ai-future')
219
+
220
+ expect(article).toContain('Future of AI')
221
+ expect(article).toContain('Impact on Healthcare')
222
+ })
223
+ })
224
+ })
225
+
226
+ // ============================================================================
227
+ // browse() tests
228
+ // ============================================================================
229
+
230
+ describe('browse() - Browser Automation', () => {
231
+ beforeEach(() => {
232
+ mockBrowserSession.mockReset()
233
+ })
234
+
235
+ describe('page context', () => {
236
+ it('returns page object with do/extract methods', async () => {
237
+ const mockPage = {
238
+ do: vi.fn(),
239
+ extract: vi.fn(),
240
+ screenshot: vi.fn(),
241
+ close: vi.fn(),
242
+ }
243
+ mockBrowserSession.mockResolvedValue(mockPage)
244
+
245
+ const page = await browse('https://example.com')
246
+
247
+ expect(page).toHaveProperty('do')
248
+ expect(page).toHaveProperty('extract')
249
+ expect(page).toHaveProperty('screenshot')
250
+ expect(page).toHaveProperty('close')
251
+ })
252
+
253
+ it('supports tagged template syntax', async () => {
254
+ mockBrowserSession.mockResolvedValue({
255
+ do: vi.fn(),
256
+ extract: vi.fn(),
257
+ screenshot: vi.fn(),
258
+ close: vi.fn(),
259
+ })
260
+
261
+ const domain = 'example.com'
262
+ await browse`https://${domain}`
263
+
264
+ expect(mockBrowserSession).toHaveBeenCalledWith('https://example.com')
265
+ })
266
+ })
267
+
268
+ describe('page.do() - actions', () => {
269
+ it('performs click actions', async () => {
270
+ const mockDo = vi.fn()
271
+ mockBrowserSession.mockResolvedValue({
272
+ do: mockDo,
273
+ extract: vi.fn(),
274
+ screenshot: vi.fn(),
275
+ close: vi.fn(),
276
+ })
277
+
278
+ const page = await browse('https://app.example.com')
279
+ await page.do('click the login button')
280
+
281
+ expect(mockDo).toHaveBeenCalledWith('click the login button')
282
+ })
283
+
284
+ it('performs form filling', async () => {
285
+ const mockDo = vi.fn()
286
+ mockBrowserSession.mockResolvedValue({
287
+ do: mockDo,
288
+ extract: vi.fn(),
289
+ screenshot: vi.fn(),
290
+ close: vi.fn(),
291
+ })
292
+
293
+ const page = await browse('https://app.example.com/login')
294
+ await page.do('fill in the email field with test@example.com')
295
+ await page.do('fill in the password field with password123')
296
+ await page.do('click submit')
297
+
298
+ expect(mockDo).toHaveBeenCalledTimes(3)
299
+ })
300
+
301
+ it('performs navigation actions', async () => {
302
+ const mockDo = vi.fn()
303
+ mockBrowserSession.mockResolvedValue({
304
+ do: mockDo,
305
+ extract: vi.fn(),
306
+ screenshot: vi.fn(),
307
+ close: vi.fn(),
308
+ })
309
+
310
+ const page = await browse('https://app.example.com')
311
+ await page.do('click on Settings in the navigation menu')
312
+ await page.do('scroll to the bottom of the page')
313
+
314
+ expect(mockDo).toHaveBeenNthCalledWith(1, 'click on Settings in the navigation menu')
315
+ expect(mockDo).toHaveBeenNthCalledWith(2, 'scroll to the bottom of the page')
316
+ })
317
+ })
318
+
319
+ describe('page.extract() - data extraction', () => {
320
+ it('extracts text content', async () => {
321
+ const mockExtract = vi.fn().mockResolvedValue('Welcome, John Doe')
322
+ mockBrowserSession.mockResolvedValue({
323
+ do: vi.fn(),
324
+ extract: mockExtract,
325
+ screenshot: vi.fn(),
326
+ close: vi.fn(),
327
+ })
328
+
329
+ const page = await browse('https://app.example.com/dashboard')
330
+ const username = await page.extract('the username in the header')
331
+
332
+ expect(mockExtract).toHaveBeenCalledWith('the username in the header')
333
+ expect(username).toBe('Welcome, John Doe')
334
+ })
335
+
336
+ it('extracts structured data', async () => {
337
+ const mockExtract = vi.fn().mockResolvedValue([
338
+ { name: 'Product A', price: 29.99 },
339
+ { name: 'Product B', price: 49.99 },
340
+ ])
341
+ mockBrowserSession.mockResolvedValue({
342
+ do: vi.fn(),
343
+ extract: mockExtract,
344
+ screenshot: vi.fn(),
345
+ close: vi.fn(),
346
+ })
347
+
348
+ const page = await browse('https://shop.example.com/products')
349
+ const products = await page.extract('all products with their names and prices')
350
+
351
+ expect(products).toHaveLength(2)
352
+ expect(products[0]).toHaveProperty('name', 'Product A')
353
+ expect(products[0]).toHaveProperty('price', 29.99)
354
+ })
355
+
356
+ it('extracts table data', async () => {
357
+ const mockExtract = vi.fn().mockResolvedValue([
358
+ { date: '2024-01-01', amount: 100, status: 'completed' },
359
+ { date: '2024-01-02', amount: 200, status: 'pending' },
360
+ ])
361
+ mockBrowserSession.mockResolvedValue({
362
+ do: vi.fn(),
363
+ extract: mockExtract,
364
+ screenshot: vi.fn(),
365
+ close: vi.fn(),
366
+ })
367
+
368
+ const page = await browse('https://app.example.com/transactions')
369
+ const transactions = await page.extract('the transaction table data')
370
+
371
+ expect(transactions).toHaveLength(2)
372
+ expect(transactions[0]).toHaveProperty('date')
373
+ expect(transactions[0]).toHaveProperty('amount')
374
+ })
375
+ })
376
+
377
+ describe('page.screenshot()', () => {
378
+ it('captures page screenshot', async () => {
379
+ const mockScreenshot = vi.fn().mockResolvedValue(Buffer.from('fake-image'))
380
+ mockBrowserSession.mockResolvedValue({
381
+ do: vi.fn(),
382
+ extract: vi.fn(),
383
+ screenshot: mockScreenshot,
384
+ close: vi.fn(),
385
+ })
386
+
387
+ const page = await browse('https://example.com')
388
+ const screenshot = await page.screenshot()
389
+
390
+ expect(mockScreenshot).toHaveBeenCalled()
391
+ expect(Buffer.isBuffer(screenshot)).toBe(true)
392
+ })
393
+ })
394
+
395
+ describe('page.close()', () => {
396
+ it('closes browser session', async () => {
397
+ const mockClose = vi.fn()
398
+ mockBrowserSession.mockResolvedValue({
399
+ do: vi.fn(),
400
+ extract: vi.fn(),
401
+ screenshot: vi.fn(),
402
+ close: mockClose,
403
+ })
404
+
405
+ const page = await browse('https://example.com')
406
+ await page.close()
407
+
408
+ expect(mockClose).toHaveBeenCalled()
409
+ })
410
+ })
411
+ })
412
+
413
+ // ============================================================================
414
+ // Combined read + browse workflows
415
+ // ============================================================================
416
+
417
+ describe('combined workflows', () => {
418
+ beforeEach(() => {
419
+ mockFetchUrl.mockReset()
420
+ mockBrowserSession.mockReset()
421
+ })
422
+
423
+ it('read for static content, browse for dynamic', async () => {
424
+ // Use read for documentation
425
+ mockFetchUrl.mockResolvedValue('# API Docs\n\nAuthentication required.')
426
+ const docs = await read('https://api.example.com/docs')
427
+ expect(docs).toContain('API Docs')
428
+
429
+ // Use browse for interactive testing
430
+ const mockPage = {
431
+ do: vi.fn(),
432
+ extract: vi.fn().mockResolvedValue({ status: 'success' }),
433
+ screenshot: vi.fn(),
434
+ close: vi.fn(),
435
+ }
436
+ mockBrowserSession.mockResolvedValue(mockPage)
437
+
438
+ const page = await browse('https://api.example.com/playground')
439
+ await page.do('enter API key in the auth field')
440
+ await page.do('click send request')
441
+ const result = await page.extract('the response status')
442
+
443
+ expect(result).toEqual({ status: 'success' })
444
+ })
445
+
446
+ it('research workflow: read docs, browse to verify', async () => {
447
+ // Step 1: Read documentation
448
+ mockFetchUrl.mockResolvedValue(`
449
+ # Getting Started
450
+
451
+ 1. Sign up at example.com/signup
452
+ 2. Get your API key from settings
453
+ 3. Make your first request
454
+ `.trim())
455
+
456
+ const docs = await read('https://example.com/docs')
457
+ expect(docs).toContain('Sign up')
458
+
459
+ // Step 2: Browse to verify signup flow
460
+ const mockPage = {
461
+ do: vi.fn(),
462
+ extract: vi.fn(),
463
+ screenshot: vi.fn(),
464
+ close: vi.fn(),
465
+ }
466
+ mockBrowserSession.mockResolvedValue(mockPage)
467
+
468
+ const page = await browse('https://example.com/signup')
469
+ await page.do('fill in email with test@test.com')
470
+ await page.do('fill in password with testpass123')
471
+ await page.do('click sign up button')
472
+
473
+ expect(mockPage.do).toHaveBeenCalledTimes(3)
474
+ })
475
+
476
+ it('competitive analysis: read multiple sources', async () => {
477
+ const competitors = ['competitor1.com', 'competitor2.com', 'competitor3.com']
478
+
479
+ mockFetchUrl
480
+ .mockResolvedValueOnce('# Competitor 1\n\nPricing: $10/mo')
481
+ .mockResolvedValueOnce('# Competitor 2\n\nPricing: $15/mo')
482
+ .mockResolvedValueOnce('# Competitor 3\n\nPricing: $20/mo')
483
+
484
+ const analyses = await Promise.all(
485
+ competitors.map(c => read(`https://${c}/pricing`))
486
+ )
487
+
488
+ expect(analyses).toHaveLength(3)
489
+ expect(analyses[0]).toContain('$10/mo')
490
+ expect(analyses[1]).toContain('$15/mo')
491
+ expect(analyses[2]).toContain('$20/mo')
492
+ })
493
+ })
494
+
495
+ // ============================================================================
496
+ // Error handling
497
+ // ============================================================================
498
+
499
+ describe('error handling', () => {
500
+ beforeEach(() => {
501
+ mockFetchUrl.mockReset()
502
+ mockBrowserSession.mockReset()
503
+ })
504
+
505
+ describe('read() errors', () => {
506
+ it('handles 404 errors', async () => {
507
+ mockFetchUrl.mockRejectedValue(new Error('404 Not Found'))
508
+
509
+ await expect(read('https://example.com/nonexistent')).rejects.toThrow('404')
510
+ })
511
+
512
+ it('handles network errors', async () => {
513
+ mockFetchUrl.mockRejectedValue(new Error('Network error'))
514
+
515
+ await expect(read('https://unreachable.example.com')).rejects.toThrow('Network')
516
+ })
517
+
518
+ it('handles timeout errors', async () => {
519
+ mockFetchUrl.mockRejectedValue(new Error('Timeout'))
520
+
521
+ await expect(read('https://slow.example.com')).rejects.toThrow('Timeout')
522
+ })
523
+ })
524
+
525
+ describe('browse() errors', () => {
526
+ it('handles page load failures', async () => {
527
+ mockBrowserSession.mockRejectedValue(new Error('Page failed to load'))
528
+
529
+ await expect(browse('https://broken.example.com')).rejects.toThrow('failed to load')
530
+ })
531
+
532
+ it('handles action failures', async () => {
533
+ const mockDo = vi.fn().mockRejectedValue(new Error('Element not found'))
534
+ mockBrowserSession.mockResolvedValue({
535
+ do: mockDo,
536
+ extract: vi.fn(),
537
+ screenshot: vi.fn(),
538
+ close: vi.fn(),
539
+ })
540
+
541
+ const page = await browse('https://app.example.com')
542
+ await expect(page.do('click nonexistent button')).rejects.toThrow('not found')
543
+ })
544
+
545
+ it('handles extraction failures', async () => {
546
+ const mockExtract = vi.fn().mockRejectedValue(new Error('Cannot find element'))
547
+ mockBrowserSession.mockResolvedValue({
548
+ do: vi.fn(),
549
+ extract: mockExtract,
550
+ screenshot: vi.fn(),
551
+ close: vi.fn(),
552
+ })
553
+
554
+ const page = await browse('https://app.example.com')
555
+ await expect(page.extract('nonexistent element')).rejects.toThrow('Cannot find')
556
+ })
557
+ })
558
+ })
559
+
560
+ // ============================================================================
561
+ // Options
562
+ // ============================================================================
563
+
564
+ describe('options', () => {
565
+ it('read supports timeout option', async () => {
566
+ const mockReadWithOptions = vi.fn().mockResolvedValue('content')
567
+
568
+ await mockReadWithOptions('https://example.com', { timeout: 5000 })
569
+
570
+ expect(mockReadWithOptions).toHaveBeenCalledWith(
571
+ 'https://example.com',
572
+ expect.objectContaining({ timeout: 5000 })
573
+ )
574
+ })
575
+
576
+ it('browse supports headless option', async () => {
577
+ const mockBrowseWithOptions = vi.fn().mockResolvedValue({
578
+ do: vi.fn(),
579
+ extract: vi.fn(),
580
+ screenshot: vi.fn(),
581
+ close: vi.fn(),
582
+ })
583
+
584
+ await mockBrowseWithOptions('https://example.com', { headless: false })
585
+
586
+ expect(mockBrowseWithOptions).toHaveBeenCalledWith(
587
+ 'https://example.com',
588
+ expect.objectContaining({ headless: false })
589
+ )
590
+ })
591
+
592
+ it('browse supports viewport option', async () => {
593
+ const mockBrowseWithOptions = vi.fn().mockResolvedValue({
594
+ do: vi.fn(),
595
+ extract: vi.fn(),
596
+ screenshot: vi.fn(),
597
+ close: vi.fn(),
598
+ })
599
+
600
+ await mockBrowseWithOptions('https://example.com', {
601
+ viewport: { width: 1920, height: 1080 },
602
+ })
603
+
604
+ expect(mockBrowseWithOptions).toHaveBeenCalledWith(
605
+ 'https://example.com',
606
+ expect.objectContaining({
607
+ viewport: { width: 1920, height: 1080 },
608
+ })
609
+ )
610
+ })
611
+ })