This commit is contained in:
Boki 2025-06-25 11:38:23 -04:00
parent 3a7254708e
commit b63e58784c
41 changed files with 5762 additions and 4477 deletions

View file

@ -1,167 +1,166 @@
import type { Page } from 'playwright';
import type { BrowserOptions, ScrapingResult } from './types';
/**
* Simple browser implementation for testing
*/
export class SimpleBrowser {
private browser: any;
private contexts = new Map<string, any>();
private logger: any;
private initialized = false;
private _options: BrowserOptions = {
headless: true,
timeout: 30000,
blockResources: false,
enableNetworkLogging: false,
};
constructor(logger?: any) {
this.logger = logger || console;
// Initialize mock browser
this.browser = {
newContext: async () => {
const pages: any[] = [];
const context = {
newPage: async () => {
const page = {
goto: async () => {},
close: async () => {},
evaluate: async () => {},
waitForSelector: async () => {},
screenshot: async () => Buffer.from('screenshot'),
setViewport: async () => {},
content: async () => '<html></html>',
on: () => {},
route: async () => {},
};
pages.push(page);
return page;
},
close: async () => {},
pages: async () => pages,
};
return context;
},
close: async () => {},
isConnected: () => true,
};
}
async initialize(options: BrowserOptions = {}): Promise<void> {
if (this.initialized) {
return;
}
// Merge options
this._options = { ...this._options, ...options };
this.logger.info('Initializing browser...');
// Mock browser is already initialized in constructor for simplicity
this.initialized = true;
}
async createContext(id?: string): Promise<string> {
if (!this.browser) {
await this.initialize();
}
const contextId = id || `context-${Date.now()}`;
const context = await this.browser.newContext();
this.contexts.set(contextId, context);
return contextId;
}
async closeContext(contextId: string): Promise<void> {
const context = this.contexts.get(contextId);
if (context) {
await context.close();
this.contexts.delete(contextId);
}
}
async newPage(contextId: string): Promise<Page> {
const context = this.contexts.get(contextId);
if (!context) {
throw new Error(`Context ${contextId} not found`);
}
const page = await context.newPage();
// Add resource blocking if enabled
if (this._options?.blockResources) {
await page.route('**/*.{png,jpg,jpeg,gif,svg,ico,woff,woff2,ttf,css}', (route: any) => {
route.abort();
});
}
return page;
}
async goto(page: Page, url: string, options?: any): Promise<void> {
await page.goto(url, {
timeout: this._options?.timeout || 30000,
...options,
});
}
async scrape(url: string, options?: { contextId?: string }): Promise<ScrapingResult> {
try {
let contextId = options?.contextId;
const shouldCloseContext = !contextId;
if (!contextId) {
contextId = await this.createContext();
}
const page = await this.newPage(contextId);
await this.goto(page, url);
// Mock data for testing
const data = {
title: 'Test Title',
text: 'Test content',
links: ['link1', 'link2'],
};
await page.close();
if (shouldCloseContext) {
await this.closeContext(contextId);
}
return {
success: true,
data,
url,
};
} catch (error: any) {
return {
success: false,
error: error.message,
url,
data: {} as any,
};
}
}
async close(): Promise<void> {
if (!this.browser) {
return;
}
// Close all contexts
for (const [contextId, context] of this.contexts) {
await context.close();
}
this.contexts.clear();
await this.browser.close();
this.browser = null;
this.initialized = false;
}
}
import type { Page } from 'playwright';
import type { BrowserOptions, ScrapingResult } from './types';
/**
* Simple browser implementation for testing
*/
export class SimpleBrowser {
private browser: any;
private contexts = new Map<string, any>();
private logger: any;
private initialized = false;
private _options: BrowserOptions = {
headless: true,
timeout: 30000,
blockResources: false,
enableNetworkLogging: false,
};
constructor(logger?: any) {
this.logger = logger || console;
// Initialize mock browser
this.browser = {
newContext: async () => {
const pages: any[] = [];
const context = {
newPage: async () => {
const page = {
goto: async () => {},
close: async () => {},
evaluate: async () => {},
waitForSelector: async () => {},
screenshot: async () => Buffer.from('screenshot'),
setViewport: async () => {},
content: async () => '<html></html>',
on: () => {},
route: async () => {},
};
pages.push(page);
return page;
},
close: async () => {},
pages: async () => pages,
};
return context;
},
close: async () => {},
isConnected: () => true,
};
}
async initialize(options: BrowserOptions = {}): Promise<void> {
if (this.initialized) {
return;
}
// Merge options
this._options = { ...this._options, ...options };
this.logger.info('Initializing browser...');
// Mock browser is already initialized in constructor for simplicity
this.initialized = true;
}
async createContext(id?: string): Promise<string> {
if (!this.browser) {
await this.initialize();
}
const contextId = id || `context-${Date.now()}`;
const context = await this.browser.newContext();
this.contexts.set(contextId, context);
return contextId;
}
async closeContext(contextId: string): Promise<void> {
const context = this.contexts.get(contextId);
if (context) {
await context.close();
this.contexts.delete(contextId);
}
}
async newPage(contextId: string): Promise<Page> {
const context = this.contexts.get(contextId);
if (!context) {
throw new Error(`Context ${contextId} not found`);
}
const page = await context.newPage();
// Add resource blocking if enabled
if (this._options?.blockResources) {
await page.route('**/*.{png,jpg,jpeg,gif,svg,ico,woff,woff2,ttf,css}', (route: any) => {
route.abort();
});
}
return page;
}
async goto(page: Page, url: string, options?: any): Promise<void> {
await page.goto(url, {
timeout: this._options?.timeout || 30000,
...options,
});
}
async scrape(url: string, options?: { contextId?: string }): Promise<ScrapingResult> {
try {
let contextId = options?.contextId;
const shouldCloseContext = !contextId;
if (!contextId) {
contextId = await this.createContext();
}
const page = await this.newPage(contextId);
await this.goto(page, url);
// Mock data for testing
const data = {
title: 'Test Title',
text: 'Test content',
links: ['link1', 'link2'],
};
await page.close();
if (shouldCloseContext) {
await this.closeContext(contextId);
}
return {
success: true,
data,
url,
};
} catch (error: any) {
return {
success: false,
error: error.message,
url,
data: {} as any,
};
}
}
async close(): Promise<void> {
if (!this.browser) {
return;
}
// Close all contexts
for (const [_contextId, context] of this.contexts) {
await context.close();
}
this.contexts.clear();
await this.browser.close();
this.browser = null;
this.initialized = false;
}
}

View file

@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, mock } from 'bun:test';
import { SimpleBrowser } from '../src/simple-browser';
import type { BrowserOptions } from '../src/types';
describe('Browser', () => {
let browser: SimpleBrowser;
@ -13,27 +13,27 @@ describe('Browser', () => {
beforeEach(() => {
logger.info = mock(() => {});
logger.error = mock(() => {});
browser = new SimpleBrowser(logger);
});
describe('initialization', () => {
it('should initialize browser on first call', async () => {
await browser.initialize();
expect(logger.info).toHaveBeenCalledWith('Initializing browser...');
});
it('should not reinitialize if already initialized', async () => {
await browser.initialize();
await browser.initialize();
expect(logger.info).toHaveBeenCalledTimes(1);
});
it('should merge options', async () => {
await browser.initialize({ headless: false, timeout: 60000 });
// Just verify it doesn't throw
expect(true).toBe(true);
});
@ -43,14 +43,14 @@ describe('Browser', () => {
it('should create new context', async () => {
await browser.initialize();
const contextId = await browser.createContext('test');
expect(contextId).toBe('test');
});
it('should generate context ID if not provided', async () => {
await browser.initialize();
const contextId = await browser.createContext();
expect(contextId).toBeDefined();
expect(typeof contextId).toBe('string');
});
@ -59,7 +59,7 @@ describe('Browser', () => {
await browser.initialize();
const contextId = await browser.createContext('test');
await browser.closeContext(contextId);
// Just verify it doesn't throw
expect(true).toBe(true);
});
@ -75,7 +75,7 @@ describe('Browser', () => {
await browser.initialize();
const contextId = await browser.createContext();
const page = await browser.newPage(contextId);
expect(page).toBeDefined();
});
@ -83,18 +83,18 @@ describe('Browser', () => {
await browser.initialize();
const contextId = await browser.createContext();
const page = await browser.newPage(contextId);
await browser.goto(page, 'https://example.com');
// Just verify it doesn't throw
expect(true).toBe(true);
});
it('should scrape page', async () => {
await browser.initialize();
const result = await browser.scrape('https://example.com');
expect(result.success).toBe(true);
expect(result.data.title).toBeDefined();
expect(result.data.text).toBeDefined();
@ -107,7 +107,7 @@ describe('Browser', () => {
await browser.initialize({ blockResources: true });
const contextId = await browser.createContext();
const page = await browser.newPage(contextId);
// Just verify it doesn't throw
expect(page).toBeDefined();
});
@ -116,7 +116,7 @@ describe('Browser', () => {
await browser.initialize({ blockResources: false });
const contextId = await browser.createContext();
const page = await browser.newPage(contextId);
expect(page).toBeDefined();
});
});
@ -125,7 +125,7 @@ describe('Browser', () => {
it('should close browser', async () => {
await browser.initialize();
await browser.close();
// Just verify it doesn't throw
expect(true).toBe(true);
});
@ -138,9 +138,9 @@ describe('Browser', () => {
await browser.initialize();
await browser.createContext('test1');
await browser.createContext('test2');
await browser.close();
// Just verify it doesn't throw
expect(true).toBe(true);
});
@ -156,18 +156,20 @@ describe('Browser', () => {
it('should handle page creation failure', async () => {
await browser.initialize();
// Should throw for non-existent context
await expect(browser.newPage('non-existent')).rejects.toThrow('Context non-existent not found');
await expect(browser.newPage('non-existent')).rejects.toThrow(
'Context non-existent not found'
);
});
it('should handle scrape errors', async () => {
// SimpleBrowser catches errors and returns success: false
await browser.initialize();
const result = await browser.scrape('https://example.com');
expect(result.success).toBe(true); // SimpleBrowser always succeeds
});
});
});
});