ai-functions
Version:
Core AI primitives for building intelligent applications
481 lines (458 loc) • 19.1 kB
JavaScript
/**
* Tests for web functions: read() and browse()
*
* read(url) - Convert URL to markdown (Firecrawl-style)
* browse(url) - Browser automation (Stagehand-style)
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
// ============================================================================
// Mock implementations
// ============================================================================
const mockFetchUrl = vi.fn();
const mockBrowserSession = vi.fn();
/**
* Mock read function - converts URL to markdown
*/
async function read(urlOrStrings, ...args) {
let url;
if (Array.isArray(urlOrStrings) && 'raw' in urlOrStrings) {
url = urlOrStrings.reduce((acc, str, i) => {
return acc + str + (args[i] ?? '');
}, '');
}
else {
url = urlOrStrings;
}
return mockFetchUrl(url);
}
/**
* Mock browse function - returns a page context for browser automation
*/
async function browse(urlOrStrings, ...args) {
let url;
if (Array.isArray(urlOrStrings) && 'raw' in urlOrStrings) {
url = urlOrStrings.reduce((acc, str, i) => {
return acc + str + (args[i] ?? '');
}, '');
}
else {
url = urlOrStrings;
}
return mockBrowserSession(url);
}
// ============================================================================
// read() tests
// ============================================================================
describe('read() - URL to Markdown', () => {
beforeEach(() => {
mockFetchUrl.mockReset();
});
describe('basic usage', () => {
it('converts URL to markdown', async () => {
mockFetchUrl.mockResolvedValue(`
# Page Title
Some content from the page.
## Section 1
More content here.
`.trim());
const content = await read('https://example.com');
expect(mockFetchUrl).toHaveBeenCalledWith('https://example.com');
expect(content).toContain('# Page Title');
expect(content).toContain('Some content');
});
it('supports tagged template syntax', async () => {
mockFetchUrl.mockResolvedValue('# Content');
const domain = 'example.com';
const path = '/docs';
const content = await read `https://${domain}${path}`;
expect(mockFetchUrl).toHaveBeenCalledWith('https://example.com/docs');
});
it('extracts main content, strips navigation/ads', async () => {
mockFetchUrl.mockResolvedValue(`
# Article Title
This is the main article content, clean and focused.
`.trim());
const content = await read('https://blog.example.com/article');
// Extracted content should be clean without HTML tags
expect(content).not.toContain('<nav>');
expect(content).not.toContain('<!-- advertisement -->');
expect(content).toContain('Article Title');
});
});
describe('content extraction', () => {
it('preserves headers and structure', async () => {
mockFetchUrl.mockResolvedValue(`
# Main Title
## Introduction
Intro paragraph.
## Details
Detail paragraph.
### Subsection
More details.
`.trim());
const content = await read('https://docs.example.com');
expect(content).toContain('# Main Title');
expect(content).toContain('## Introduction');
expect(content).toContain('### Subsection');
});
it('converts links to markdown format', async () => {
mockFetchUrl.mockResolvedValue('Check out [our documentation](https://docs.example.com) for more info.');
const content = await read('https://example.com');
expect(content).toContain('[our documentation](https://docs.example.com)');
});
it('handles code blocks', async () => {
mockFetchUrl.mockResolvedValue(`
Here's an example:
\`\`\`typescript
const x = 1;
\`\`\`
`.trim());
const content = await read('https://tutorial.example.com');
expect(content).toContain('```typescript');
expect(content).toContain('const x = 1;');
});
it('handles tables', async () => {
mockFetchUrl.mockResolvedValue(`
| Header 1 | Header 2 |
|----------|----------|
| Cell 1 | Cell 2 |
`.trim());
const content = await read('https://data.example.com');
expect(content).toContain('| Header 1 |');
expect(content).toContain('| Cell 1 |');
});
});
describe('use cases', () => {
it('reads documentation for research', async () => {
mockFetchUrl.mockResolvedValue(`
# API Reference
## Authentication
Use Bearer tokens for authentication.
## Endpoints
### GET /users
Returns list of users.
`.trim());
const docs = await read('https://api.example.com/docs');
expect(docs).toContain('API Reference');
expect(docs).toContain('Authentication');
expect(docs).toContain('GET /users');
});
it('reads articles for summarization', async () => {
mockFetchUrl.mockResolvedValue(`
# The Future of AI
Artificial intelligence is transforming industries...
## Impact on Healthcare
AI is revolutionizing medical diagnosis...
## Impact on Finance
Automated trading systems...
`.trim());
const article = await read('https://news.example.com/ai-future');
expect(article).toContain('Future of AI');
expect(article).toContain('Impact on Healthcare');
});
});
});
// ============================================================================
// browse() tests
// ============================================================================
describe('browse() - Browser Automation', () => {
beforeEach(() => {
mockBrowserSession.mockReset();
});
describe('page context', () => {
it('returns page object with do/extract methods', async () => {
const mockPage = {
do: vi.fn(),
extract: vi.fn(),
screenshot: vi.fn(),
close: vi.fn(),
};
mockBrowserSession.mockResolvedValue(mockPage);
const page = await browse('https://example.com');
expect(page).toHaveProperty('do');
expect(page).toHaveProperty('extract');
expect(page).toHaveProperty('screenshot');
expect(page).toHaveProperty('close');
});
it('supports tagged template syntax', async () => {
mockBrowserSession.mockResolvedValue({
do: vi.fn(),
extract: vi.fn(),
screenshot: vi.fn(),
close: vi.fn(),
});
const domain = 'example.com';
await browse `https://${domain}`;
expect(mockBrowserSession).toHaveBeenCalledWith('https://example.com');
});
});
describe('page.do() - actions', () => {
it('performs click actions', async () => {
const mockDo = vi.fn();
mockBrowserSession.mockResolvedValue({
do: mockDo,
extract: vi.fn(),
screenshot: vi.fn(),
close: vi.fn(),
});
const page = await browse('https://app.example.com');
await page.do('click the login button');
expect(mockDo).toHaveBeenCalledWith('click the login button');
});
it('performs form filling', async () => {
const mockDo = vi.fn();
mockBrowserSession.mockResolvedValue({
do: mockDo,
extract: vi.fn(),
screenshot: vi.fn(),
close: vi.fn(),
});
const page = await browse('https://app.example.com/login');
await page.do('fill in the email field with test@example.com');
await page.do('fill in the password field with password123');
await page.do('click submit');
expect(mockDo).toHaveBeenCalledTimes(3);
});
it('performs navigation actions', async () => {
const mockDo = vi.fn();
mockBrowserSession.mockResolvedValue({
do: mockDo,
extract: vi.fn(),
screenshot: vi.fn(),
close: vi.fn(),
});
const page = await browse('https://app.example.com');
await page.do('click on Settings in the navigation menu');
await page.do('scroll to the bottom of the page');
expect(mockDo).toHaveBeenNthCalledWith(1, 'click on Settings in the navigation menu');
expect(mockDo).toHaveBeenNthCalledWith(2, 'scroll to the bottom of the page');
});
});
describe('page.extract() - data extraction', () => {
it('extracts text content', async () => {
const mockExtract = vi.fn().mockResolvedValue('Welcome, John Doe');
mockBrowserSession.mockResolvedValue({
do: vi.fn(),
extract: mockExtract,
screenshot: vi.fn(),
close: vi.fn(),
});
const page = await browse('https://app.example.com/dashboard');
const username = await page.extract('the username in the header');
expect(mockExtract).toHaveBeenCalledWith('the username in the header');
expect(username).toBe('Welcome, John Doe');
});
it('extracts structured data', async () => {
const mockExtract = vi.fn().mockResolvedValue([
{ name: 'Product A', price: 29.99 },
{ name: 'Product B', price: 49.99 },
]);
mockBrowserSession.mockResolvedValue({
do: vi.fn(),
extract: mockExtract,
screenshot: vi.fn(),
close: vi.fn(),
});
const page = await browse('https://shop.example.com/products');
const products = await page.extract('all products with their names and prices');
expect(products).toHaveLength(2);
expect(products[0]).toHaveProperty('name', 'Product A');
expect(products[0]).toHaveProperty('price', 29.99);
});
it('extracts table data', async () => {
const mockExtract = vi.fn().mockResolvedValue([
{ date: '2024-01-01', amount: 100, status: 'completed' },
{ date: '2024-01-02', amount: 200, status: 'pending' },
]);
mockBrowserSession.mockResolvedValue({
do: vi.fn(),
extract: mockExtract,
screenshot: vi.fn(),
close: vi.fn(),
});
const page = await browse('https://app.example.com/transactions');
const transactions = await page.extract('the transaction table data');
expect(transactions).toHaveLength(2);
expect(transactions[0]).toHaveProperty('date');
expect(transactions[0]).toHaveProperty('amount');
});
});
describe('page.screenshot()', () => {
it('captures page screenshot', async () => {
const mockScreenshot = vi.fn().mockResolvedValue(Buffer.from('fake-image'));
mockBrowserSession.mockResolvedValue({
do: vi.fn(),
extract: vi.fn(),
screenshot: mockScreenshot,
close: vi.fn(),
});
const page = await browse('https://example.com');
const screenshot = await page.screenshot();
expect(mockScreenshot).toHaveBeenCalled();
expect(Buffer.isBuffer(screenshot)).toBe(true);
});
});
describe('page.close()', () => {
it('closes browser session', async () => {
const mockClose = vi.fn();
mockBrowserSession.mockResolvedValue({
do: vi.fn(),
extract: vi.fn(),
screenshot: vi.fn(),
close: mockClose,
});
const page = await browse('https://example.com');
await page.close();
expect(mockClose).toHaveBeenCalled();
});
});
});
// ============================================================================
// Combined read + browse workflows
// ============================================================================
describe('combined workflows', () => {
beforeEach(() => {
mockFetchUrl.mockReset();
mockBrowserSession.mockReset();
});
it('read for static content, browse for dynamic', async () => {
// Use read for documentation
mockFetchUrl.mockResolvedValue('# API Docs\n\nAuthentication required.');
const docs = await read('https://api.example.com/docs');
expect(docs).toContain('API Docs');
// Use browse for interactive testing
const mockPage = {
do: vi.fn(),
extract: vi.fn().mockResolvedValue({ status: 'success' }),
screenshot: vi.fn(),
close: vi.fn(),
};
mockBrowserSession.mockResolvedValue(mockPage);
const page = await browse('https://api.example.com/playground');
await page.do('enter API key in the auth field');
await page.do('click send request');
const result = await page.extract('the response status');
expect(result).toEqual({ status: 'success' });
});
it('research workflow: read docs, browse to verify', async () => {
// Step 1: Read documentation
mockFetchUrl.mockResolvedValue(`
# Getting Started
1. Sign up at example.com/signup
2. Get your API key from settings
3. Make your first request
`.trim());
const docs = await read('https://example.com/docs');
expect(docs).toContain('Sign up');
// Step 2: Browse to verify signup flow
const mockPage = {
do: vi.fn(),
extract: vi.fn(),
screenshot: vi.fn(),
close: vi.fn(),
};
mockBrowserSession.mockResolvedValue(mockPage);
const page = await browse('https://example.com/signup');
await page.do('fill in email with test@test.com');
await page.do('fill in password with testpass123');
await page.do('click sign up button');
expect(mockPage.do).toHaveBeenCalledTimes(3);
});
it('competitive analysis: read multiple sources', async () => {
const competitors = ['competitor1.com', 'competitor2.com', 'competitor3.com'];
mockFetchUrl
.mockResolvedValueOnce('# Competitor 1\n\nPricing: $10/mo')
.mockResolvedValueOnce('# Competitor 2\n\nPricing: $15/mo')
.mockResolvedValueOnce('# Competitor 3\n\nPricing: $20/mo');
const analyses = await Promise.all(competitors.map(c => read(`https://${c}/pricing`)));
expect(analyses).toHaveLength(3);
expect(analyses[0]).toContain('$10/mo');
expect(analyses[1]).toContain('$15/mo');
expect(analyses[2]).toContain('$20/mo');
});
});
// ============================================================================
// Error handling
// ============================================================================
describe('error handling', () => {
beforeEach(() => {
mockFetchUrl.mockReset();
mockBrowserSession.mockReset();
});
describe('read() errors', () => {
it('handles 404 errors', async () => {
mockFetchUrl.mockRejectedValue(new Error('404 Not Found'));
await expect(read('https://example.com/nonexistent')).rejects.toThrow('404');
});
it('handles network errors', async () => {
mockFetchUrl.mockRejectedValue(new Error('Network error'));
await expect(read('https://unreachable.example.com')).rejects.toThrow('Network');
});
it('handles timeout errors', async () => {
mockFetchUrl.mockRejectedValue(new Error('Timeout'));
await expect(read('https://slow.example.com')).rejects.toThrow('Timeout');
});
});
describe('browse() errors', () => {
it('handles page load failures', async () => {
mockBrowserSession.mockRejectedValue(new Error('Page failed to load'));
await expect(browse('https://broken.example.com')).rejects.toThrow('failed to load');
});
it('handles action failures', async () => {
const mockDo = vi.fn().mockRejectedValue(new Error('Element not found'));
mockBrowserSession.mockResolvedValue({
do: mockDo,
extract: vi.fn(),
screenshot: vi.fn(),
close: vi.fn(),
});
const page = await browse('https://app.example.com');
await expect(page.do('click nonexistent button')).rejects.toThrow('not found');
});
it('handles extraction failures', async () => {
const mockExtract = vi.fn().mockRejectedValue(new Error('Cannot find element'));
mockBrowserSession.mockResolvedValue({
do: vi.fn(),
extract: mockExtract,
screenshot: vi.fn(),
close: vi.fn(),
});
const page = await browse('https://app.example.com');
await expect(page.extract('nonexistent element')).rejects.toThrow('Cannot find');
});
});
});
// ============================================================================
// Options
// ============================================================================
describe('options', () => {
it('read supports timeout option', async () => {
const mockReadWithOptions = vi.fn().mockResolvedValue('content');
await mockReadWithOptions('https://example.com', { timeout: 5000 });
expect(mockReadWithOptions).toHaveBeenCalledWith('https://example.com', expect.objectContaining({ timeout: 5000 }));
});
it('browse supports headless option', async () => {
const mockBrowseWithOptions = vi.fn().mockResolvedValue({
do: vi.fn(),
extract: vi.fn(),
screenshot: vi.fn(),
close: vi.fn(),
});
await mockBrowseWithOptions('https://example.com', { headless: false });
expect(mockBrowseWithOptions).toHaveBeenCalledWith('https://example.com', expect.objectContaining({ headless: false }));
});
it('browse supports viewport option', async () => {
const mockBrowseWithOptions = vi.fn().mockResolvedValue({
do: vi.fn(),
extract: vi.fn(),
screenshot: vi.fn(),
close: vi.fn(),
});
await mockBrowseWithOptions('https://example.com', {
viewport: { width: 1920, height: 1080 },
});
expect(mockBrowseWithOptions).toHaveBeenCalledWith('https://example.com', expect.objectContaining({
viewport: { width: 1920, height: 1080 },
}));
});
});