2026-03-05 18:59:35 +05:30
/ * *
* @license
* Copyright 2026 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
/ * *
* Integration tests for the browser agent .
*
* These tests verify the complete end - to - end flow from CLI prompt through
* browser_agent delegation to MCP / Chrome DevTools and back . Unlike the unit
* tests in packages / core / src / agents / browser / which mock all MCP components ,
* these tests launch real Chrome instances in headless mode .
*
* Tests are skipped on systems without Chrome / Chromium installed .
* /
import { describe , it , expect , beforeEach , afterEach } from 'vitest' ;
import { TestRig , assertModelHasOutput } from './test-helper.js' ;
import { dirname , join } from 'node:path' ;
import { fileURLToPath } from 'node:url' ;
import { execSync } from 'node:child_process' ;
import { existsSync } from 'node:fs' ;
const __filename = fileURLToPath ( import . meta . url ) ;
const __dirname = dirname ( __filename ) ;
const chromeAvailable = ( ( ) = > {
try {
if ( process . platform === 'darwin' ) {
execSync (
'test -d "/Applications/Google Chrome.app" || test -d "/Applications/Chromium.app"' ,
{
stdio : 'ignore' ,
} ,
) ;
} else if ( process . platform === 'linux' ) {
execSync (
'which google-chrome || which chromium-browser || which chromium' ,
{ stdio : 'ignore' } ,
) ;
} else if ( process . platform === 'win32' ) {
// Check standard Windows installation paths using Node.js fs
const chromePaths = [
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' ,
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe' ,
` ${ process . env [ 'LOCALAPPDATA' ] ? ? '' } \\ Google \\ Chrome \\ Application \\ chrome.exe ` ,
] ;
const found = chromePaths . some ( ( p ) = > existsSync ( p ) ) ;
if ( ! found ) {
// Fall back to PATH check
execSync ( 'where chrome || where chromium' , { stdio : 'ignore' } ) ;
}
} else {
return false ;
}
return true ;
} catch {
return false ;
}
} ) ( ) ;
describe . skipIf ( ! chromeAvailable ) ( 'browser-agent' , ( ) = > {
let rig : TestRig ;
beforeEach ( ( ) = > {
rig = new TestRig ( ) ;
} ) ;
afterEach ( async ( ) = > await rig . cleanup ( ) ) ;
it ( 'should navigate to a page and capture accessibility tree' , async ( ) = > {
rig . setup ( 'browser-navigate-and-snapshot' , {
fakeResponsesPath : join (
__dirname ,
'browser-agent.navigate-snapshot.responses' ,
) ,
settings : {
agents : {
2026-04-02 22:54:38 +08:00
overrides : {
browser_agent : {
enabled : true ,
} ,
} ,
browser : {
2026-03-05 18:59:35 +05:30
headless : true ,
sessionMode : 'isolated' ,
} ,
} ,
} ,
} ) ;
const result = await rig . run ( {
args : 'Open https://example.com in the browser and tell me the page title and main content.' ,
} ) ;
assertModelHasOutput ( result ) ;
const toolLogs = rig . readToolLogs ( ) ;
const browserAgentCall = toolLogs . find (
2026-04-09 12:48:24 -04:00
( t ) = >
t . toolRequest . name === 'invoke_agent' &&
JSON . parse ( t . toolRequest . args ) . agent_name === 'browser_agent' ,
2026-03-05 18:59:35 +05:30
) ;
expect (
browserAgentCall ,
'Expected browser_agent to be called' ,
) . toBeDefined ( ) ;
} ) ;
it ( 'should take screenshots of web pages' , async ( ) = > {
rig . setup ( 'browser-screenshot' , {
fakeResponsesPath : join ( __dirname , 'browser-agent.screenshot.responses' ) ,
settings : {
agents : {
2026-04-02 22:54:38 +08:00
overrides : {
browser_agent : {
enabled : true ,
} ,
} ,
browser : {
2026-03-05 18:59:35 +05:30
headless : true ,
sessionMode : 'isolated' ,
} ,
} ,
} ,
} ) ;
const result = await rig . run ( {
args : 'Navigate to https://example.com and take a screenshot.' ,
} ) ;
const toolLogs = rig . readToolLogs ( ) ;
const browserCalls = toolLogs . filter (
2026-04-09 12:48:24 -04:00
( t ) = >
t . toolRequest . name === 'invoke_agent' &&
JSON . parse ( t . toolRequest . args ) . agent_name === 'browser_agent' ,
2026-03-05 18:59:35 +05:30
) ;
expect ( browserCalls . length ) . toBeGreaterThan ( 0 ) ;
assertModelHasOutput ( result ) ;
} ) ;
it ( 'should interact with page elements' , async ( ) = > {
rig . setup ( 'browser-interaction' , {
fakeResponsesPath : join ( __dirname , 'browser-agent.interaction.responses' ) ,
settings : {
agents : {
2026-04-02 22:54:38 +08:00
overrides : {
browser_agent : {
enabled : true ,
} ,
} ,
browser : {
2026-03-05 18:59:35 +05:30
headless : true ,
sessionMode : 'isolated' ,
} ,
} ,
} ,
} ) ;
const result = await rig . run ( {
args : 'Go to https://example.com, find any links on the page, and describe them.' ,
} ) ;
const toolLogs = rig . readToolLogs ( ) ;
const browserAgentCall = toolLogs . find (
2026-04-09 12:48:24 -04:00
( t ) = >
t . toolRequest . name === 'invoke_agent' &&
JSON . parse ( t . toolRequest . args ) . agent_name === 'browser_agent' ,
2026-03-05 18:59:35 +05:30
) ;
expect (
browserAgentCall ,
'Expected browser_agent to be called' ,
) . toBeDefined ( ) ;
assertModelHasOutput ( result ) ;
} ) ;
it ( 'should clean up browser processes after completion' , async ( ) = > {
rig . setup ( 'browser-cleanup' , {
fakeResponsesPath : join ( __dirname , 'browser-agent.cleanup.responses' ) ,
settings : {
agents : {
2026-04-02 22:54:38 +08:00
overrides : {
browser_agent : {
enabled : true ,
} ,
} ,
browser : {
2026-03-05 18:59:35 +05:30
headless : true ,
sessionMode : 'isolated' ,
} ,
} ,
} ,
} ) ;
await rig . run ( {
args : 'Open https://example.com in the browser and check the page title.' ,
} ) ;
// Test passes if we reach here, relying on Vitest's timeout mechanism
// to detect hanging browser processes.
} ) ;
it ( 'should handle multiple browser operations in sequence' , async ( ) = > {
rig . setup ( 'browser-sequential' , {
fakeResponsesPath : join ( __dirname , 'browser-agent.sequential.responses' ) ,
settings : {
agents : {
2026-04-02 22:54:38 +08:00
overrides : {
browser_agent : {
enabled : true ,
} ,
} ,
browser : {
2026-03-05 18:59:35 +05:30
headless : true ,
sessionMode : 'isolated' ,
} ,
} ,
} ,
} ) ;
const result = await rig . run ( {
args : 'Navigate to https://example.com, take a snapshot of the accessibility tree, then take a screenshot.' ,
} ) ;
const toolLogs = rig . readToolLogs ( ) ;
const browserCalls = toolLogs . filter (
2026-04-09 12:48:24 -04:00
( t ) = >
t . toolRequest . name === 'invoke_agent' &&
JSON . parse ( t . toolRequest . args ) . agent_name === 'browser_agent' ,
2026-03-05 18:59:35 +05:30
) ;
expect ( browserCalls . length ) . toBeGreaterThan ( 0 ) ;
// Should successfully complete all operations
assertModelHasOutput ( result ) ;
} ) ;
2026-03-13 10:49:33 -07:00
2026-04-07 05:17:31 +08:00
it ( 'should keep browser open across multiple browser_agent invocations' , async ( ) = > {
rig . setup ( 'browser-persistent-session' , {
fakeResponsesPath : join (
__dirname ,
'browser-agent.persistent-session.responses' ,
) ,
settings : {
agents : {
overrides : {
browser_agent : {
enabled : true ,
} ,
} ,
browser : {
headless : true ,
sessionMode : 'isolated' ,
2026-04-09 12:48:24 -04:00
allowedDomains : [ 'example.com' ] ,
2026-04-07 05:17:31 +08:00
} ,
} ,
} ,
} ) ;
const result = await rig . run ( {
2026-04-09 12:48:24 -04:00
args : 'First, ask the browser agent to get the page title of example.com. After you receive that response, you MUST invoke the browser agent a second time to check for links on the page.' ,
2026-04-07 05:17:31 +08:00
} ) ;
const toolLogs = rig . readToolLogs ( ) ;
const browserCalls = toolLogs . filter (
2026-04-09 12:48:24 -04:00
( t ) = >
t . toolRequest . name === 'invoke_agent' &&
JSON . parse ( t . toolRequest . args ) . agent_name === 'browser_agent' ,
2026-04-07 05:17:31 +08:00
) ;
// Both browser_agent invocations must succeed — if the browser was
// incorrectly closed after the first call (regression #24210),
// the second call would fail.
expect (
browserCalls . length ,
'Expected browser_agent to be called twice' ,
) . toBe ( 2 ) ;
expect (
browserCalls . every ( ( c ) = > c . toolRequest . success ) ,
'Both browser_agent calls should succeed' ,
) . toBe ( true ) ;
assertModelHasOutput ( result ) ;
} ) ;
2026-03-13 10:49:33 -07:00
it ( 'should handle tool confirmation for write_file without crashing' , async ( ) = > {
rig . setup ( 'tool-confirmation' , {
fakeResponsesPath : join (
__dirname ,
'browser-agent.confirmation.responses' ,
) ,
settings : {
agents : {
2026-04-02 22:54:38 +08:00
overrides : {
browser_agent : {
enabled : true ,
} ,
} ,
browser : {
2026-03-13 10:49:33 -07:00
headless : true ,
sessionMode : 'isolated' ,
} ,
} ,
} ,
} ) ;
const run = await rig . runInteractive ( { approvalMode : 'default' } ) ;
await run . type ( 'Write hello to test.txt' ) ;
await run . type ( '\r' ) ;
await run . expectText ( 'Allow' , 15000 ) ;
await run . type ( 'y' ) ;
await run . type ( '\r' ) ;
await run . expectText ( 'successfully written' , 15000 ) ;
} ) ;
2026-04-08 22:31:10 +08:00
it ( 'should handle concurrent browser agents with isolated session mode' , async ( ) = > {
rig . setup ( 'browser-concurrent' , {
fakeResponsesPath : join ( __dirname , 'browser-agent.concurrent.responses' ) ,
settings : {
agents : {
overrides : {
browser_agent : {
enabled : true ,
} ,
} ,
browser : {
headless : true ,
// Isolated mode supports concurrent browser agents.
// Persistent/existing modes reject concurrent calls to prevent
// Chrome profile lock conflicts.
sessionMode : 'isolated' ,
} ,
} ,
} ,
} ) ;
const result = await rig . run ( {
args : 'Launch two browser agents concurrently to check example.com' ,
} ) ;
assertModelHasOutput ( result ) ;
const toolLogs = rig . readToolLogs ( ) ;
const browserCalls = toolLogs . filter (
2026-04-09 12:48:24 -04:00
( t ) = >
t . toolRequest . name === 'invoke_agent' &&
JSON . parse ( t . toolRequest . args ) . agent_name === 'browser_agent' ,
2026-04-08 22:31:10 +08:00
) ;
// Both browser_agent invocations should have been called
expect ( browserCalls . length ) . toBe ( 2 ) ;
// Both should complete successfully (no errors)
for ( const call of browserCalls ) {
expect (
call . toolRequest . success ,
` browser_agent call failed: ${ JSON . stringify ( call . toolRequest ) } ` ,
) . toBe ( true ) ;
}
} ) ;
2026-03-05 18:59:35 +05:30
} ) ;