Skip to content

Commit ff8a997

Browse files
author
Sentience Dev
committed
Merge pull request #81 from SentienceAPI/sdk_async
add viewport to browser init
2 parents fedd7fc + a1144b6 commit ff8a997

File tree

5 files changed

+240
-14
lines changed

5 files changed

+240
-14
lines changed

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sentienceapi",
3-
"version": "0.90.15",
3+
"version": "0.90.16",
44
"description": "TypeScript SDK for Sentience AI Agent Browser Automation",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

src/browser.ts

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export class SentienceBrowser {
2323
private _storageState?: string | StorageState | object;
2424
private _recordVideoDir?: string;
2525
private _recordVideoSize?: { width: number; height: number };
26+
private _viewport?: { width: number; height: number };
2627

2728
constructor(
2829
apiKey?: string,
@@ -32,7 +33,8 @@ export class SentienceBrowser {
3233
userDataDir?: string,
3334
storageState?: string | StorageState | object,
3435
recordVideoDir?: string,
35-
recordVideoSize?: { width: number; height: number }
36+
recordVideoSize?: { width: number; height: number },
37+
viewport?: { width: number; height: number }
3638
) {
3739
this._apiKey = apiKey;
3840

@@ -62,6 +64,9 @@ export class SentienceBrowser {
6264
// Video recording support
6365
this._recordVideoDir = recordVideoDir;
6466
this._recordVideoSize = recordVideoSize || { width: 1280, height: 800 };
67+
68+
// Viewport configuration
69+
this._viewport = viewport || { width: 1280, height: 800 };
6570
}
6671

6772
async start(): Promise<void> {
@@ -150,7 +155,7 @@ export class SentienceBrowser {
150155
const launchOptions: any = {
151156
headless: false, // Must be false here, handled via args above
152157
args: args,
153-
viewport: { width: 1920, height: 1080 },
158+
viewport: this._viewport,
154159
// Clean User-Agent to avoid "HeadlessChrome" detection
155160
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',
156161
proxy: proxyConfig, // Pass proxy configuration
@@ -649,6 +654,84 @@ export class SentienceBrowser {
649654
return this.context;
650655
}
651656

657+
/**
658+
* Create SentienceBrowser from an existing Playwright BrowserContext.
659+
*
660+
* This allows you to use Sentience SDK with a browser context you've already created,
661+
* giving you more control over browser initialization.
662+
*
663+
* @param context - Existing Playwright BrowserContext
664+
* @param apiKey - Optional API key for server-side processing
665+
* @param apiUrl - Optional API URL (defaults to https://api.sentienceapi.com if apiKey provided)
666+
* @returns SentienceBrowser instance configured to use the existing context
667+
*
668+
* @example
669+
* ```typescript
670+
* import { chromium } from 'playwright';
671+
* import { SentienceBrowser } from '@sentience/sdk';
672+
*
673+
* const context = await chromium.launchPersistentContext(...);
674+
* const browser = SentienceBrowser.fromExisting(context);
675+
* await browser.getPage().goto('https://example.com');
676+
* ```
677+
*/
678+
static async fromExisting(
679+
context: BrowserContext,
680+
apiKey?: string,
681+
apiUrl?: string
682+
): Promise<SentienceBrowser> {
683+
const instance = new SentienceBrowser(apiKey, apiUrl);
684+
instance.context = context;
685+
const pages = context.pages();
686+
instance.page = pages.length > 0 ? pages[0] : await context.newPage();
687+
688+
// Wait for extension to be ready (if extension is loaded)
689+
// Note: In TypeScript, we can't easily apply stealth here without the page
690+
// The user should ensure stealth is applied to their context if needed
691+
692+
return instance;
693+
}
694+
695+
/**
696+
* Create SentienceBrowser from an existing Playwright Page.
697+
*
698+
* This allows you to use Sentience SDK with a page you've already created,
699+
* giving you more control over browser initialization.
700+
*
701+
* @param page - Existing Playwright Page
702+
* @param apiKey - Optional API key for server-side processing
703+
* @param apiUrl - Optional API URL (defaults to https://api.sentienceapi.com if apiKey provided)
704+
* @returns SentienceBrowser instance configured to use the existing page
705+
*
706+
* @example
707+
* ```typescript
708+
* import { chromium } from 'playwright';
709+
* import { SentienceBrowser } from '@sentience/sdk';
710+
*
711+
* const browserInstance = await chromium.launch();
712+
* const context = await browserInstance.newContext();
713+
* const page = await context.newPage();
714+
* await page.goto('https://example.com');
715+
*
716+
* const browser = SentienceBrowser.fromPage(page);
717+
* ```
718+
*/
719+
static fromPage(
720+
page: Page,
721+
apiKey?: string,
722+
apiUrl?: string
723+
): SentienceBrowser {
724+
const instance = new SentienceBrowser(apiKey, apiUrl);
725+
instance.page = page;
726+
instance.context = page.context();
727+
728+
// Wait for extension to be ready (if extension is loaded)
729+
// Note: In TypeScript, we can't easily apply stealth here without the page
730+
// The user should ensure stealth is applied to their context if needed
731+
732+
return instance;
733+
}
734+
652735
async close(outputPath?: string): Promise<string | null> {
653736
let tempVideoPath: string | null = null;
654737

tests/browser.test.ts

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
/**
2-
* Test browser proxy support
2+
* Test browser proxy support and Phase 2 features (viewport, from_existing, from_page)
33
*/
44

55
import { SentienceBrowser } from '../src/browser';
6+
import { chromium, BrowserContext, Page } from 'playwright';
67

78
describe('Browser Proxy Support', () => {
89
describe('Proxy Parsing', () => {
@@ -138,5 +139,129 @@ describe('Browser Proxy Support', () => {
138139
expect((browser as any)._proxy).toBeUndefined();
139140
});
140141
});
142+
143+
describe('Viewport Configuration', () => {
144+
it('should use default viewport 1280x800', () => {
145+
const browser = new SentienceBrowser();
146+
expect((browser as any)._viewport).toEqual({ width: 1280, height: 800 });
147+
});
148+
149+
it('should accept custom viewport', () => {
150+
const customViewport = { width: 1920, height: 1080 };
151+
const browser = new SentienceBrowser(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, customViewport);
152+
expect((browser as any)._viewport).toEqual(customViewport);
153+
});
154+
155+
it('should accept mobile viewport', () => {
156+
const mobileViewport = { width: 375, height: 667 };
157+
const browser = new SentienceBrowser(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, mobileViewport);
158+
expect((browser as any)._viewport).toEqual(mobileViewport);
159+
});
160+
});
161+
162+
describe('fromExisting', () => {
163+
it('should create SentienceBrowser from existing context', async () => {
164+
// Auto-detect headless mode (headless in CI, headed locally)
165+
const isCI = process.env.CI === 'true' || process.env.CI === '1';
166+
const context = await chromium.launchPersistentContext('', {
167+
headless: isCI,
168+
viewport: { width: 1600, height: 900 },
169+
});
170+
171+
try {
172+
const browser = await SentienceBrowser.fromExisting(context);
173+
174+
expect(browser.getContext()).toBe(context);
175+
expect(browser.getPage()).toBeDefined();
176+
177+
// Verify viewport is preserved
178+
const page = browser.getPage();
179+
await page.goto('https://example.com');
180+
await page.waitForLoadState('networkidle');
181+
182+
const viewportSize = await page.evaluate(() => ({
183+
width: window.innerWidth,
184+
height: window.innerHeight,
185+
}));
186+
187+
expect(viewportSize.width).toBe(1600);
188+
expect(viewportSize.height).toBe(900);
189+
} finally {
190+
await context.close();
191+
}
192+
}, 30000);
193+
194+
it('should accept API key configuration', async () => {
195+
// Auto-detect headless mode (headless in CI, headed locally)
196+
const isCI = process.env.CI === 'true' || process.env.CI === '1';
197+
const context = await chromium.launchPersistentContext('', {
198+
headless: isCI,
199+
});
200+
201+
try {
202+
const browser = await SentienceBrowser.fromExisting(context, 'test_key', 'https://test.api.com');
203+
204+
expect(browser.getApiKey()).toBe('test_key');
205+
expect(browser.getApiUrl()).toBe('https://test.api.com');
206+
expect(browser.getContext()).toBe(context);
207+
} finally {
208+
await context.close();
209+
}
210+
}, 30000);
211+
});
212+
213+
describe('fromPage', () => {
214+
it('should create SentienceBrowser from existing page', async () => {
215+
// Auto-detect headless mode (headless in CI, headed locally)
216+
const isCI = process.env.CI === 'true' || process.env.CI === '1';
217+
const browserInstance = await chromium.launch({ headless: isCI });
218+
const context = await browserInstance.newContext({
219+
viewport: { width: 1440, height: 900 },
220+
});
221+
const page = await context.newPage();
222+
223+
try {
224+
const sentienceBrowser = SentienceBrowser.fromPage(page);
225+
226+
expect(sentienceBrowser.getPage()).toBe(page);
227+
expect(sentienceBrowser.getContext()).toBe(context);
228+
229+
// Test that we can use it
230+
await page.goto('https://example.com');
231+
await page.waitForLoadState('networkidle');
232+
233+
// Verify viewport is preserved
234+
const viewportSize = await page.evaluate(() => ({
235+
width: window.innerWidth,
236+
height: window.innerHeight,
237+
}));
238+
239+
expect(viewportSize.width).toBe(1440);
240+
expect(viewportSize.height).toBe(900);
241+
} finally {
242+
await context.close();
243+
await browserInstance.close();
244+
}
245+
}, 30000);
246+
247+
it('should accept API key configuration', async () => {
248+
// Auto-detect headless mode (headless in CI, headed locally)
249+
const isCI = process.env.CI === 'true' || process.env.CI === '1';
250+
const browserInstance = await chromium.launch({ headless: isCI });
251+
const context = await browserInstance.newContext();
252+
const page = await context.newPage();
253+
254+
try {
255+
const sentienceBrowser = SentienceBrowser.fromPage(page, 'test_key', 'https://test.api.com');
256+
257+
expect(sentienceBrowser.getApiKey()).toBe('test_key');
258+
expect(sentienceBrowser.getApiUrl()).toBe('https://test.api.com');
259+
expect(sentienceBrowser.getPage()).toBe(page);
260+
} finally {
261+
await context.close();
262+
await browserInstance.close();
263+
}
264+
}, 30000);
265+
});
141266
});
142267

tests/stealth.test.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,31 @@ describe('Stealth Mode / Bot Evasion', () => {
4444
});
4545

4646
test('viewport should be realistic (1920x1080 or larger)', async () => {
47-
const page = browser.getPage();
48-
const viewport = await page.evaluate(() => ({
49-
width: window.innerWidth,
50-
height: window.innerHeight,
51-
}));
52-
expect(viewport.width).toBeGreaterThanOrEqual(1920);
53-
expect(viewport.height).toBeGreaterThanOrEqual(1080);
47+
// Create a browser with a realistic viewport for this test
48+
const testBrowser = new SentienceBrowser(
49+
undefined, // apiKey
50+
undefined, // apiUrl
51+
undefined, // headless
52+
undefined, // proxy
53+
undefined, // userDataDir
54+
undefined, // storageState
55+
undefined, // recordVideoDir
56+
undefined, // recordVideoSize
57+
{ width: 1920, height: 1080 } // viewport
58+
);
59+
await testBrowser.start();
60+
61+
try {
62+
const page = testBrowser.getPage();
63+
const viewport = await page.evaluate(() => ({
64+
width: window.innerWidth,
65+
height: window.innerHeight,
66+
}));
67+
expect(viewport.width).toBeGreaterThanOrEqual(1920);
68+
expect(viewport.height).toBeGreaterThanOrEqual(1080);
69+
} finally {
70+
await testBrowser.close();
71+
}
5472
});
5573

5674
test('navigator.plugins should exist', async () => {

0 commit comments

Comments
 (0)