From 5a94182cfeba07df1e47ca8c60321ed3972ef7f9 Mon Sep 17 00:00:00 2001 From: rcholic Date: Wed, 31 Dec 2025 16:49:35 -0800 Subject: [PATCH 1/3] add viewport to browser init --- package-lock.json | 6 +-- package.json | 2 +- src/browser.ts | 87 +++++++++++++++++++++++++++++- tests/browser.test.ts | 119 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 207 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9ad70238..b74c9ccd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,13 @@ { "name": "sentienceapi", - "version": "0.90.2", + "version": "0.90.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sentienceapi", - "version": "0.90.2", - "license": "MIT", + "version": "0.90.15", + "license": "(MIT OR Apache-2.0)", "dependencies": { "playwright": "^1.40.0", "turndown": "^7.2.2", diff --git a/package.json b/package.json index ab029d69..8920bb8b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sentienceapi", - "version": "0.90.15", + "version": "0.90.16", "description": "TypeScript SDK for Sentience AI Agent Browser Automation", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/browser.ts b/src/browser.ts index 160b74a0..a186f9cc 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -23,6 +23,7 @@ export class SentienceBrowser { private _storageState?: string | StorageState | object; private _recordVideoDir?: string; private _recordVideoSize?: { width: number; height: number }; + private _viewport?: { width: number; height: number }; constructor( apiKey?: string, @@ -32,7 +33,8 @@ export class SentienceBrowser { userDataDir?: string, storageState?: string | StorageState | object, recordVideoDir?: string, - recordVideoSize?: { width: number; height: number } + recordVideoSize?: { width: number; height: number }, + viewport?: { width: number; height: number } ) { this._apiKey = apiKey; @@ -62,6 +64,9 @@ export class SentienceBrowser { // Video recording support this._recordVideoDir = recordVideoDir; this._recordVideoSize = recordVideoSize || { width: 1280, height: 800 }; + + // Viewport configuration + this._viewport = viewport || { width: 1280, height: 800 }; } async start(): Promise { @@ -150,7 +155,7 @@ export class SentienceBrowser { const launchOptions: any = { headless: false, // Must be false here, handled via args above args: args, - viewport: { width: 1920, height: 1080 }, + viewport: this._viewport, // Clean User-Agent to avoid "HeadlessChrome" detection userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', proxy: proxyConfig, // Pass proxy configuration @@ -649,6 +654,84 @@ export class SentienceBrowser { return this.context; } + /** + * Create SentienceBrowser from an existing Playwright BrowserContext. + * + * This allows you to use Sentience SDK with a browser context you've already created, + * giving you more control over browser initialization. + * + * @param context - Existing Playwright BrowserContext + * @param apiKey - Optional API key for server-side processing + * @param apiUrl - Optional API URL (defaults to https://api.sentienceapi.com if apiKey provided) + * @returns SentienceBrowser instance configured to use the existing context + * + * @example + * ```typescript + * import { chromium } from 'playwright'; + * import { SentienceBrowser } from '@sentience/sdk'; + * + * const context = await chromium.launchPersistentContext(...); + * const browser = SentienceBrowser.fromExisting(context); + * await browser.getPage().goto('https://example.com'); + * ``` + */ + static async fromExisting( + context: BrowserContext, + apiKey?: string, + apiUrl?: string + ): Promise { + const instance = new SentienceBrowser(apiKey, apiUrl); + instance.context = context; + const pages = context.pages(); + instance.page = pages.length > 0 ? pages[0] : await context.newPage(); + + // Wait for extension to be ready (if extension is loaded) + // Note: In TypeScript, we can't easily apply stealth here without the page + // The user should ensure stealth is applied to their context if needed + + return instance; + } + + /** + * Create SentienceBrowser from an existing Playwright Page. + * + * This allows you to use Sentience SDK with a page you've already created, + * giving you more control over browser initialization. + * + * @param page - Existing Playwright Page + * @param apiKey - Optional API key for server-side processing + * @param apiUrl - Optional API URL (defaults to https://api.sentienceapi.com if apiKey provided) + * @returns SentienceBrowser instance configured to use the existing page + * + * @example + * ```typescript + * import { chromium } from 'playwright'; + * import { SentienceBrowser } from '@sentience/sdk'; + * + * const browserInstance = await chromium.launch(); + * const context = await browserInstance.newContext(); + * const page = await context.newPage(); + * await page.goto('https://example.com'); + * + * const browser = SentienceBrowser.fromPage(page); + * ``` + */ + static fromPage( + page: Page, + apiKey?: string, + apiUrl?: string + ): SentienceBrowser { + const instance = new SentienceBrowser(apiKey, apiUrl); + instance.page = page; + instance.context = page.context(); + + // Wait for extension to be ready (if extension is loaded) + // Note: In TypeScript, we can't easily apply stealth here without the page + // The user should ensure stealth is applied to their context if needed + + return instance; + } + async close(outputPath?: string): Promise { let tempVideoPath: string | null = null; diff --git a/tests/browser.test.ts b/tests/browser.test.ts index a12677c8..3da03179 100644 --- a/tests/browser.test.ts +++ b/tests/browser.test.ts @@ -1,8 +1,9 @@ /** - * Test browser proxy support + * Test browser proxy support and Phase 2 features (viewport, from_existing, from_page) */ import { SentienceBrowser } from '../src/browser'; +import { chromium, BrowserContext, Page } from 'playwright'; describe('Browser Proxy Support', () => { describe('Proxy Parsing', () => { @@ -138,5 +139,121 @@ describe('Browser Proxy Support', () => { expect((browser as any)._proxy).toBeUndefined(); }); }); + + describe('Viewport Configuration', () => { + it('should use default viewport 1280x800', () => { + const browser = new SentienceBrowser(); + expect((browser as any)._viewport).toEqual({ width: 1280, height: 800 }); + }); + + it('should accept custom viewport', () => { + const customViewport = { width: 1920, height: 1080 }; + const browser = new SentienceBrowser(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, customViewport); + expect((browser as any)._viewport).toEqual(customViewport); + }); + + it('should accept mobile viewport', () => { + const mobileViewport = { width: 375, height: 667 }; + const browser = new SentienceBrowser(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, mobileViewport); + expect((browser as any)._viewport).toEqual(mobileViewport); + }); + }); + + describe('fromExisting', () => { + it('should create SentienceBrowser from existing context', async () => { + const context = await chromium.launchPersistentContext('', { + headless: false, + viewport: { width: 1600, height: 900 }, + }); + + try { + const browser = await SentienceBrowser.fromExisting(context); + + expect(browser.getContext()).toBe(context); + expect(browser.getPage()).toBeDefined(); + + // Verify viewport is preserved + const page = browser.getPage(); + await page.goto('https://example.com'); + await page.waitForLoadState('networkidle'); + + const viewportSize = await page.evaluate(() => ({ + width: window.innerWidth, + height: window.innerHeight, + })); + + expect(viewportSize.width).toBe(1600); + expect(viewportSize.height).toBe(900); + } finally { + await context.close(); + } + }, 30000); + + it('should accept API key configuration', async () => { + const context = await chromium.launchPersistentContext('', { + headless: false, + }); + + try { + const browser = await SentienceBrowser.fromExisting(context, 'test_key', 'https://test.api.com'); + + expect(browser.getApiKey()).toBe('test_key'); + expect(browser.getApiUrl()).toBe('https://test.api.com'); + expect(browser.getContext()).toBe(context); + } finally { + await context.close(); + } + }, 30000); + }); + + describe('fromPage', () => { + it('should create SentienceBrowser from existing page', async () => { + const browserInstance = await chromium.launch({ headless: false }); + const context = await browserInstance.newContext({ + viewport: { width: 1440, height: 900 }, + }); + const page = await context.newPage(); + + try { + const sentienceBrowser = SentienceBrowser.fromPage(page); + + expect(sentienceBrowser.getPage()).toBe(page); + expect(sentienceBrowser.getContext()).toBe(context); + + // Test that we can use it + await page.goto('https://example.com'); + await page.waitForLoadState('networkidle'); + + // Verify viewport is preserved + const viewportSize = await page.evaluate(() => ({ + width: window.innerWidth, + height: window.innerHeight, + })); + + expect(viewportSize.width).toBe(1440); + expect(viewportSize.height).toBe(900); + } finally { + await context.close(); + await browserInstance.close(); + } + }, 30000); + + it('should accept API key configuration', async () => { + const browserInstance = await chromium.launch({ headless: false }); + const context = await browserInstance.newContext(); + const page = await context.newPage(); + + try { + const sentienceBrowser = SentienceBrowser.fromPage(page, 'test_key', 'https://test.api.com'); + + expect(sentienceBrowser.getApiKey()).toBe('test_key'); + expect(sentienceBrowser.getApiUrl()).toBe('https://test.api.com'); + expect(sentienceBrowser.getPage()).toBe(page); + } finally { + await context.close(); + await browserInstance.close(); + } + }, 30000); + }); }); From ac8be0a43a6dc192bbbc607c6cd3a48d8f8db20a Mon Sep 17 00:00:00 2001 From: rcholic Date: Wed, 31 Dec 2025 17:21:57 -0800 Subject: [PATCH 2/3] fix tests --- tests/stealth.test.ts | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/tests/stealth.test.ts b/tests/stealth.test.ts index 5b358fd4..746552b9 100644 --- a/tests/stealth.test.ts +++ b/tests/stealth.test.ts @@ -44,13 +44,31 @@ describe('Stealth Mode / Bot Evasion', () => { }); test('viewport should be realistic (1920x1080 or larger)', async () => { - const page = browser.getPage(); - const viewport = await page.evaluate(() => ({ - width: window.innerWidth, - height: window.innerHeight, - })); - expect(viewport.width).toBeGreaterThanOrEqual(1920); - expect(viewport.height).toBeGreaterThanOrEqual(1080); + // Create a browser with a realistic viewport for this test + const testBrowser = new SentienceBrowser( + undefined, // apiKey + undefined, // apiUrl + undefined, // headless + undefined, // proxy + undefined, // userDataDir + undefined, // storageState + undefined, // recordVideoDir + undefined, // recordVideoSize + { width: 1920, height: 1080 } // viewport + ); + await testBrowser.start(); + + try { + const page = testBrowser.getPage(); + const viewport = await page.evaluate(() => ({ + width: window.innerWidth, + height: window.innerHeight, + })); + expect(viewport.width).toBeGreaterThanOrEqual(1920); + expect(viewport.height).toBeGreaterThanOrEqual(1080); + } finally { + await testBrowser.close(); + } }); test('navigator.plugins should exist', async () => { From 58f884c4d70b7bcd7b1d331f12acf6ab7d927f6b Mon Sep 17 00:00:00 2001 From: rcholic Date: Wed, 31 Dec 2025 17:39:23 -0800 Subject: [PATCH 3/3] fix tests --- tests/browser.test.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/browser.test.ts b/tests/browser.test.ts index 3da03179..c52de0f2 100644 --- a/tests/browser.test.ts +++ b/tests/browser.test.ts @@ -161,8 +161,10 @@ describe('Browser Proxy Support', () => { describe('fromExisting', () => { it('should create SentienceBrowser from existing context', async () => { + // Auto-detect headless mode (headless in CI, headed locally) + const isCI = process.env.CI === 'true' || process.env.CI === '1'; const context = await chromium.launchPersistentContext('', { - headless: false, + headless: isCI, viewport: { width: 1600, height: 900 }, }); @@ -190,8 +192,10 @@ describe('Browser Proxy Support', () => { }, 30000); it('should accept API key configuration', async () => { + // Auto-detect headless mode (headless in CI, headed locally) + const isCI = process.env.CI === 'true' || process.env.CI === '1'; const context = await chromium.launchPersistentContext('', { - headless: false, + headless: isCI, }); try { @@ -208,7 +212,9 @@ describe('Browser Proxy Support', () => { describe('fromPage', () => { it('should create SentienceBrowser from existing page', async () => { - const browserInstance = await chromium.launch({ headless: false }); + // Auto-detect headless mode (headless in CI, headed locally) + const isCI = process.env.CI === 'true' || process.env.CI === '1'; + const browserInstance = await chromium.launch({ headless: isCI }); const context = await browserInstance.newContext({ viewport: { width: 1440, height: 900 }, }); @@ -239,7 +245,9 @@ describe('Browser Proxy Support', () => { }, 30000); it('should accept API key configuration', async () => { - const browserInstance = await chromium.launch({ headless: false }); + // Auto-detect headless mode (headless in CI, headed locally) + const isCI = process.env.CI === 'true' || process.env.CI === '1'; + const browserInstance = await chromium.launch({ headless: isCI }); const context = await browserInstance.newContext(); const page = await context.newPage();