Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
87 changes: 85 additions & 2 deletions src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;

Expand Down Expand Up @@ -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<void> {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<SentienceBrowser> {
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<string | null> {
let tempVideoPath: string | null = null;

Expand Down
127 changes: 126 additions & 1 deletion tests/browser.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -138,5 +139,129 @@ 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 () => {
// 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: isCI,
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 () => {
// 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: isCI,
});

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 () => {
// 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 },
});
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 () => {
// 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();

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);
});
});

32 changes: 25 additions & 7 deletions tests/stealth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down