Skip to content

Commit b4fca41

Browse files
committed
all tests passed
1 parent b5c9503 commit b4fca41

10 files changed

Lines changed: 228 additions & 35 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ npx playwright install chromium
2121
import { SentienceBrowser, snapshot, find, click } from './src';
2222

2323
async function main() {
24-
const browser = new SentienceBrowser(undefined, false);
24+
const browser = new SentienceBrowser(undefined, undefined, false);
2525

2626
try {
2727
await browser.start();

examples/basic-agent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { SentienceBrowser, snapshot } from '../src/index';
66
import * as fs from 'fs';
77

88
async function main() {
9-
const browser = new SentienceBrowser(undefined, false);
9+
const browser = new SentienceBrowser(undefined, undefined, false);
1010

1111
try {
1212
await browser.start();

examples/hello.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import { SentienceBrowser } from '../src/index';
66

77
async function main() {
8-
const browser = new SentienceBrowser(undefined, false);
8+
const browser = new SentienceBrowser(undefined, undefined, false);
99

1010
try {
1111
await browser.start();

examples/query-demo.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import { SentienceBrowser, snapshot, query, find } from '../src/index';
66

77
async function main() {
8-
const browser = new SentienceBrowser(undefined, false);
8+
const browser = new SentienceBrowser(undefined, undefined, false);
99

1010
try {
1111
await browser.start();

examples/wait-and-click.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import { SentienceBrowser, snapshot, find, waitFor, click, expect } from '../src/index';
66

77
async function main() {
8-
const browser = new SentienceBrowser(undefined, false);
8+
const browser = new SentienceBrowser(undefined, undefined, false);
99

1010
try {
1111
await browser.start();

src/browser.ts

Lines changed: 95 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ export class SentienceBrowser {
2424
) {
2525
this._apiKey = apiKey;
2626
this.headless = headless;
27-
// Set default API URL if API key is provided
28-
if (apiKey && !apiUrl) {
29-
this._apiUrl = 'https://api.sentienceapi.com';
27+
// Only set apiUrl if apiKey is provided, otherwise undefined (free tier)
28+
// Default to https://api.sentienceapi.com if apiKey is provided but apiUrl is not
29+
if (apiKey) {
30+
this._apiUrl = apiUrl || 'https://api.sentienceapi.com';
3031
} else {
31-
this._apiUrl = apiUrl;
32+
this._apiUrl = undefined;
3233
}
3334
}
3435

@@ -111,29 +112,64 @@ export class SentienceBrowser {
111112
const launchTimeout = 30000; // 30 seconds
112113
const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sentience-profile-'));
113114

115+
// Stealth arguments for bot evasion
116+
const stealthArgs = [
117+
`--load-extension=${tempDir}`,
118+
`--disable-extensions-except=${tempDir}`,
119+
'--disable-blink-features=AutomationControlled', // Hide automation indicators
120+
'--no-sandbox', // Required for some environments
121+
'--disable-infobars', // Hide "Chrome is being controlled" message
122+
];
123+
124+
// Realistic viewport and user-agent for better evasion
125+
const viewportConfig = { width: 1920, height: 1080 };
126+
const 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';
127+
128+
// Launch browser with extension
129+
// Note: channel="chrome" (system Chrome) has known issues with extension loading
130+
// We use bundled Chromium for reliable extension loading, but still apply stealth features
131+
const useChromeChannel = false; // Disabled for now due to extension loading issues
132+
114133
try {
115-
this.context = await Promise.race([
116-
chromium.launchPersistentContext(userDataDir, {
117-
headless: this.headless,
118-
args: [
119-
`--load-extension=${tempDir}`,
120-
`--disable-extensions-except=${tempDir}`,
121-
],
122-
timeout: launchTimeout,
123-
}),
124-
new Promise<never>((_, reject) =>
125-
setTimeout(() => reject(new Error(`Browser launch timed out after ${launchTimeout}ms. Make sure Playwright browsers are installed: npx playwright install chromium`)), launchTimeout)
126-
),
127-
]);
128-
} catch (e: any) {
134+
if (useChromeChannel) {
135+
// Try with system Chrome first (better evasion, but may have extension issues)
136+
this.context = await Promise.race([
137+
chromium.launchPersistentContext(userDataDir, {
138+
channel: 'chrome', // Use system Chrome (better evasion)
139+
headless: this.headless,
140+
args: stealthArgs,
141+
viewport: viewportConfig,
142+
userAgent: userAgent,
143+
timeout: launchTimeout,
144+
}),
145+
new Promise<never>((_, reject) =>
146+
setTimeout(() => reject(new Error(`Browser launch timed out after ${launchTimeout}ms. Make sure Playwright browsers are installed: npx playwright install chromium`)), launchTimeout)
147+
),
148+
]);
149+
} else {
150+
// Use bundled Chromium (more reliable for extensions)
151+
this.context = await Promise.race([
152+
chromium.launchPersistentContext(userDataDir, {
153+
headless: this.headless,
154+
args: stealthArgs,
155+
viewport: viewportConfig,
156+
userAgent: userAgent,
157+
timeout: launchTimeout,
158+
}),
159+
new Promise<never>((_, reject) =>
160+
setTimeout(() => reject(new Error(`Browser launch timed out after ${launchTimeout}ms. Make sure Playwright browsers are installed: npx playwright install chromium`)), launchTimeout)
161+
),
162+
]);
163+
}
164+
} catch (launchError: any) {
129165
// Clean up user data dir on failure
130166
try {
131167
fs.rmSync(userDataDir, { recursive: true, force: true });
132168
} catch (cleanupError) {
133169
// Ignore cleanup errors
134170
}
135171
throw new Error(
136-
`Failed to launch browser: ${e.message}\n` +
172+
`Failed to launch browser: ${launchError.message}\n` +
137173
'Make sure Playwright browsers are installed: npx playwright install chromium'
138174
);
139175
}
@@ -149,6 +185,34 @@ export class SentienceBrowser {
149185
// Store user data dir for cleanup
150186
this.userDataDir = userDataDir;
151187

188+
// Apply basic stealth patches for bot evasion
189+
// Note: TypeScript doesn't have playwright-stealth equivalent, so we apply basic patches
190+
await this.page.addInitScript(() => {
191+
// Override navigator.webdriver
192+
Object.defineProperty(navigator, 'webdriver', {
193+
get: () => false,
194+
});
195+
196+
// Override chrome runtime
197+
(window as any).chrome = {
198+
runtime: {},
199+
};
200+
201+
// Override permissions
202+
const originalQuery = (window.navigator as any).permissions?.query;
203+
if (originalQuery) {
204+
(window.navigator as any).permissions.query = (parameters: any) =>
205+
parameters.name === 'notifications'
206+
? Promise.resolve({ state: Notification.permission } as PermissionStatus)
207+
: originalQuery(parameters);
208+
}
209+
210+
// Override plugins
211+
Object.defineProperty(navigator, 'plugins', {
212+
get: () => [1, 2, 3, 4, 5],
213+
});
214+
});
215+
152216
// Navigate to a real page so extension can inject
153217
// Extension content scripts only run on actual pages (not about:blank)
154218
// Use a simple page that loads quickly
@@ -158,10 +222,11 @@ export class SentienceBrowser {
158222
});
159223

160224
// Give extension time to initialize (WASM loading is async)
161-
await this.page.waitForTimeout(1000);
225+
// Content scripts run at document_idle, so we need to wait for that
226+
await this.page.waitForTimeout(3000);
162227

163228
// Wait for extension to load
164-
if (!(await this.waitForExtension())) {
229+
if (!(await this.waitForExtension(25000))) {
165230
// Extension might need more time, try waiting a bit longer
166231
await this.page.waitForTimeout(3000);
167232

@@ -174,6 +239,8 @@ export class SentienceBrowser {
174239
registry_defined: typeof (window as any).sentience_registry !== 'undefined',
175240
snapshot_defined: typeof (window as any).sentience?.snapshot === 'function',
176241
wasm_loaded: !!(window as any).sentience?._wasmModule,
242+
extension_id: document.documentElement.dataset.sentienceExtensionId || 'not set',
243+
url: window.location.href,
177244
};
178245
// Check console errors if possible
179246
if ((window as any).sentience) {
@@ -185,12 +252,12 @@ export class SentienceBrowser {
185252
diagnosticInfo = `Could not get diagnostic info: ${e}`;
186253
}
187254

188-
if (!(await this.waitForExtension(10000))) {
255+
if (!(await this.waitForExtension(15000))) {
189256
throw new Error(
190257
'Extension failed to load after navigation. Make sure:\n' +
191258
'1. Extension is built (cd sentience-chrome && ./build.sh)\n' +
192259
'2. All files are present (manifest.json, content.js, injected_api.js, pkg/)\n' +
193-
'3. Check browser console for errors\n' +
260+
'3. Check browser console for errors (run with headless=false to see console)\n' +
194261
`4. Extension path: ${tempDir}\n` +
195262
`5. Diagnostic info: ${diagnosticInfo}`
196263
);
@@ -238,11 +305,13 @@ export class SentienceBrowser {
238305
if ((window as any).sentience_registry === undefined) {
239306
return { ready: false, reason: 'registry not initialized' };
240307
}
241-
// Check if WASM module itself is loaded
308+
// Check if WASM module itself is loaded (check internal _wasmModule if available)
242309
const sentience = (window as any).sentience;
243-
if (!sentience._wasmModule || !sentience._wasmModule.analyze_page) {
244-
return { ready: false, reason: 'WASM module not loaded' };
310+
if (sentience._wasmModule && !sentience._wasmModule.analyze_page) {
311+
return { ready: false, reason: 'WASM module not fully loaded' };
245312
}
313+
// If _wasmModule is not exposed, that's okay - it might be internal
314+
// Just verify the API structure is correct
246315
return { ready: true };
247316
});
248317

src/generator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export class ScriptGenerator {
4242
"import { SentienceBrowser, snapshot, find, click, typeText, press } from './src';",
4343
'',
4444
'async function main() {',
45-
' const browser = new SentienceBrowser(undefined, false);',
45+
' const browser = new SentienceBrowser(undefined, undefined, false);',
4646
'',
4747
' try {',
4848
' await browser.start();',

tests/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ import { SentienceBrowser, inspect } from '../src';
8686

8787
describe('Inspector', () => {
8888
it('should start and stop', async () => {
89-
const browser = new SentienceBrowser(undefined, false);
89+
const browser = new SentienceBrowser(undefined, undefined, false);
9090
await browser.start();
9191

9292
try {
@@ -121,7 +121,7 @@ import { SentienceBrowser, record } from '../src';
121121

122122
describe('Recorder', () => {
123123
it('should record click events', async () => {
124-
const browser = new SentienceBrowser(undefined, false);
124+
const browser = new SentienceBrowser(undefined, undefined, false);
125125
await browser.start();
126126

127127
try {

tests/stealth.test.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/**
2+
* Test bot evasion and stealth mode features.
3+
*
4+
* This test verifies that stealth features are working:
5+
* - navigator.webdriver is false
6+
* - window.chrome exists
7+
* - User-agent is realistic
8+
* - Viewport is realistic
9+
* - Stealth arguments are applied
10+
*/
11+
12+
import { SentienceBrowser } from '../src/browser';
13+
14+
describe('Stealth Mode / Bot Evasion', () => {
15+
let browser: SentienceBrowser;
16+
17+
beforeAll(async () => {
18+
browser = new SentienceBrowser(undefined, undefined, false);
19+
await browser.start();
20+
});
21+
22+
afterAll(async () => {
23+
await browser.close();
24+
});
25+
26+
test('navigator.webdriver should be false', async () => {
27+
const page = browser.getPage();
28+
const webdriver = await page.evaluate(() => (navigator as any).webdriver);
29+
expect(webdriver).toBeFalsy();
30+
});
31+
32+
test('window.chrome should exist', async () => {
33+
const page = browser.getPage();
34+
const chromeExists = await page.evaluate(() => typeof (window as any).chrome !== 'undefined');
35+
expect(chromeExists).toBe(true);
36+
});
37+
38+
test('user-agent should not contain HeadlessChrome', async () => {
39+
const page = browser.getPage();
40+
const userAgent = await page.evaluate(() => navigator.userAgent);
41+
expect(userAgent).not.toContain('HeadlessChrome');
42+
expect(userAgent).toContain('Chrome');
43+
});
44+
45+
test('viewport should be realistic (1920x1080 or larger)', async () => {
46+
const page = browser.getPage();
47+
const viewport = await page.evaluate(() => ({
48+
width: window.innerWidth,
49+
height: window.innerHeight,
50+
}));
51+
expect(viewport.width).toBeGreaterThanOrEqual(1920);
52+
expect(viewport.height).toBeGreaterThanOrEqual(1080);
53+
});
54+
55+
test('navigator.plugins should exist', async () => {
56+
const page = browser.getPage();
57+
const pluginsCount = await page.evaluate(() => navigator.plugins.length);
58+
expect(pluginsCount).toBeGreaterThan(0);
59+
});
60+
61+
test('permissions API should be patched', async () => {
62+
const page = browser.getPage();
63+
const hasPermissions = await page.evaluate(() => {
64+
return !!(navigator.permissions && navigator.permissions.query);
65+
});
66+
expect(hasPermissions).toBe(true);
67+
});
68+
69+
test('should pass basic bot detection checks', async () => {
70+
const page = browser.getPage();
71+
72+
const detectionResults = await page.evaluate(() => {
73+
return {
74+
webdriver: (navigator as any).webdriver,
75+
chrome: typeof (window as any).chrome !== 'undefined',
76+
plugins: navigator.plugins.length,
77+
languages: navigator.languages.length,
78+
userAgent: navigator.userAgent,
79+
};
80+
});
81+
82+
// Count stealth features working
83+
let stealthScore = 0;
84+
if (detectionResults.webdriver === false) stealthScore++;
85+
if (detectionResults.chrome === true) stealthScore++;
86+
if (detectionResults.plugins > 0) stealthScore++;
87+
88+
expect(stealthScore).toBeGreaterThanOrEqual(2);
89+
});
90+
91+
test('should be able to navigate to bot detection test site', async () => {
92+
const page = browser.getPage();
93+
94+
try {
95+
await page.goto('https://bot.sannysoft.com/', {
96+
waitUntil: 'domcontentloaded',
97+
timeout: 10000,
98+
});
99+
100+
await page.waitForTimeout(2000); // Wait for page to load
101+
102+
// Check detection results
103+
const results = await page.evaluate(() => {
104+
return {
105+
webdriver: (navigator as any).webdriver,
106+
chrome: typeof (window as any).chrome !== 'undefined',
107+
plugins: navigator.plugins.length,
108+
};
109+
});
110+
111+
// At least 2 out of 3 should pass
112+
let passCount = 0;
113+
if (results.webdriver === false) passCount++;
114+
if (results.chrome === true) passCount++;
115+
if (results.plugins > 0) passCount++;
116+
117+
expect(passCount).toBeGreaterThanOrEqual(2);
118+
} catch (e: any) {
119+
// Site may be down or blocked - that's okay
120+
console.warn(`Could not test against bot detection site: ${e.message}`);
121+
}
122+
});
123+
});
124+

tests/test-utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { SentienceBrowser } from '../src';
88
* Creates a browser instance and starts it with better error handling
99
*/
1010
export async function createTestBrowser(headless: boolean = false): Promise<SentienceBrowser> {
11-
const browser = new SentienceBrowser(undefined, headless);
11+
const browser = new SentienceBrowser(undefined, undefined, headless);
1212
try {
1313
await browser.start();
1414
return browser;

0 commit comments

Comments
 (0)