diff --git a/integration-tests/browser-agent-localhost.dynamic.responses b/integration-tests/browser-agent-localhost.dynamic.responses new file mode 100644 index 0000000000..bade94af88 --- /dev/null +++ b/integration-tests/browser-agent-localhost.dynamic.responses @@ -0,0 +1,6 @@ +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll check the dynamic content page on the localhost server."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to http://127.0.0.1:18923/dynamic.html, wait for the dynamic content to load, then capture the accessibility tree and report what content appeared"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":40,"totalTokenCount":140}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"http://127.0.0.1:18923/dynamic.html"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"totalTokenCount":120}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"wait_for","args":{"selector":"#dynamic-content","state":"visible","timeout":5000}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":25,"totalTokenCount":175}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":180,"candidatesTokenCount":15,"totalTokenCount":195}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"result":{"success":true,"summary":"Dynamic content loaded successfully. Found heading 'Content Loaded', message 'This content was loaded dynamically via JavaScript.', and a list with items: Item Alpha, Item Beta, Item Gamma."}}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":40,"totalTokenCount":240}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The dynamic content page loaded successfully:\n\n- **Heading**: Content Loaded\n- **Message**: This content was loaded dynamically via JavaScript\n- **Items**: Item Alpha, Item Beta, Item Gamma\n\nThe JavaScript-rendered content appeared after the initial page load, replacing the 'Loading...' indicator."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":60,"totalTokenCount":260}}]} diff --git a/integration-tests/browser-agent-localhost.form.responses b/integration-tests/browser-agent-localhost.form.responses new file mode 100644 index 0000000000..119d1ff46f --- /dev/null +++ b/integration-tests/browser-agent-localhost.form.responses @@ -0,0 +1,9 @@ +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll fill out the contact form on the localhost server."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to http://127.0.0.1:18923/form.html, fill in the name field with 'Test User', the email field with 'test@example.com', the message field with 'Hello World', and submit the form"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"http://127.0.0.1:18923/form.html"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"totalTokenCount":120}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"fill","args":{"selector":"#name","value":"Test User"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":25,"totalTokenCount":175}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"fill","args":{"selector":"#email","value":"test@example.com"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":180,"candidatesTokenCount":25,"totalTokenCount":205}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"fill","args":{"selector":"#message","value":"Hello World"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":210,"candidatesTokenCount":25,"totalTokenCount":235}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"click","args":{"selector":"#submit-btn"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":240,"candidatesTokenCount":20,"totalTokenCount":260}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":270,"candidatesTokenCount":15,"totalTokenCount":285}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"result":{"success":true,"summary":"Successfully filled and submitted the contact form. Navigated to form-result.html which shows 'Form Submitted Successfully'."}}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":300,"candidatesTokenCount":40,"totalTokenCount":340}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I've successfully filled out and submitted the contact form:\n\n- **Name**: Test User\n- **Email**: test@example.com\n- **Message**: Hello World\n\nAfter submission, the page navigated to the result page showing 'Form Submitted Successfully'."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":300,"candidatesTokenCount":60,"totalTokenCount":360}}]} diff --git a/integration-tests/browser-agent-localhost.multistep.responses b/integration-tests/browser-agent-localhost.multistep.responses new file mode 100644 index 0000000000..37fc8d438c --- /dev/null +++ b/integration-tests/browser-agent-localhost.multistep.responses @@ -0,0 +1,9 @@ +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll go through the multi-step flow on the localhost server."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to http://127.0.0.1:18923/multi-step/step1.html, fill in 'testuser' as the username, click Next, then on step 2 select 'Option B' and click Finish. Report the final result page content."}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"http://127.0.0.1:18923/multi-step/step1.html"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"totalTokenCount":120}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"fill","args":{"selector":"#username","value":"testuser"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":25,"totalTokenCount":175}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"click","args":{"selector":"#next-btn"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":180,"candidatesTokenCount":20,"totalTokenCount":200}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":210,"candidatesTokenCount":15,"totalTokenCount":225}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"click","args":{"selector":"#finish-btn"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":240,"candidatesTokenCount":20,"totalTokenCount":260}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":270,"candidatesTokenCount":15,"totalTokenCount":285}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"result":{"success":true,"summary":"Completed all steps. Step 1: entered username 'testuser'. Step 2: selected default option. Final result page shows 'Multi-Step Complete' with '✓ Complete' status badge."}}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":300,"candidatesTokenCount":40,"totalTokenCount":340}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I've completed the multi-step flow:\n\n1. **Step 1**: Entered 'testuser' as username and clicked Next\n2. **Step 2**: Confirmed selection and clicked Finish\n3. **Result**: Final page shows 'Multi-Step Complete' with a '✓ Complete' status badge\n\nAll steps were successfully navigated."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":300,"candidatesTokenCount":60,"totalTokenCount":360}}]} diff --git a/integration-tests/browser-agent-localhost.navigate.responses b/integration-tests/browser-agent-localhost.navigate.responses new file mode 100644 index 0000000000..676696bf6b --- /dev/null +++ b/integration-tests/browser-agent-localhost.navigate.responses @@ -0,0 +1,5 @@ +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll navigate to the localhost page and read its content using the browser agent."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to http://127.0.0.1:18923/index.html and tell me the page title and list all links on the page"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":40,"totalTokenCount":140}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"http://127.0.0.1:18923/index.html"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"totalTokenCount":120}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":20,"totalTokenCount":170}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"result":{"success":true,"summary":"Page title is 'Test Fixture - Home'. Found 3 links: Contact Form (/form.html), Multi-Step Flow (/multi-step/step1.html), Dynamic Content (/dynamic.html)."}}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":40,"totalTokenCount":240}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The localhost test fixture page has:\n\n**Title**: Test Fixture - Home\n\n**Links**:\n1. Contact Form (form.html)\n2. Multi-Step Flow (multi-step/step1.html)\n3. Dynamic Content (dynamic.html)\n\nThe page also has a heading 'Test Fixture Home Page' and footer content."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":60,"totalTokenCount":260}}]} diff --git a/integration-tests/browser-agent-localhost.screenshot.responses b/integration-tests/browser-agent-localhost.screenshot.responses new file mode 100644 index 0000000000..762b560697 --- /dev/null +++ b/integration-tests/browser-agent-localhost.screenshot.responses @@ -0,0 +1,5 @@ +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll take a screenshot of the localhost test page."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to http://127.0.0.1:18923/index.html and take a screenshot of the page"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":35,"totalTokenCount":135}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"http://127.0.0.1:18923/index.html"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"totalTokenCount":120}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_screenshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":15,"totalTokenCount":165}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"result":{"success":true,"summary":"Screenshot captured of the localhost test fixture home page showing the heading, navigation links, and footer.","data":{"screenshotTaken":true}}}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":40,"totalTokenCount":240}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I've captured a screenshot of the localhost test fixture page. The screenshot shows the 'Test Fixture Home Page' heading with navigation links to the Contact Form, Multi-Step Flow, and Dynamic Content pages, along with the footer section."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":50,"totalTokenCount":250}}]} diff --git a/integration-tests/browser-agent-localhost.test.ts b/integration-tests/browser-agent-localhost.test.ts new file mode 100644 index 0000000000..2de37ba7a9 --- /dev/null +++ b/integration-tests/browser-agent-localhost.test.ts @@ -0,0 +1,161 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +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'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +describe('browser-agent-localhost', () => { + let rig: TestRig; + + const browserSettings = { + agents: { + overrides: { + browser_agent: { + enabled: true, + }, + }, + browser: { + headless: true, + sessionMode: 'isolated' as const, + }, + }, + }; + + beforeEach(() => { + rig = new TestRig(); + }); + + afterEach(async () => { + await rig.cleanup(); + }); + + it('should navigate to localhost fixture and read page content', async () => { + rig.setup('localhost-navigate', { + fakeResponsesPath: join( + __dirname, + 'browser-agent-localhost.navigate.responses', + ), + settings: browserSettings, + }); + + const result = await rig.run({ + args: 'Navigate to http://127.0.0.1:18923/index.html and tell me the page title and list all links.', + }); + + assertModelHasOutput(result); + + const toolLogs = rig.readToolLogs(); + const browserAgentCall = toolLogs.find( + (t) => t.toolRequest.name === 'browser_agent', + ); + expect( + browserAgentCall, + 'Expected browser_agent to be called', + ).toBeDefined(); + }); + + it('should fill out and submit a form on localhost', async () => { + rig.setup('localhost-form', { + fakeResponsesPath: join( + __dirname, + 'browser-agent-localhost.form.responses', + ), + settings: browserSettings, + }); + + const result = await rig.run({ + args: "Navigate to http://127.0.0.1:18923/form.html, fill in name='Test User', email='test@example.com', message='Hello World', and submit the form.", + }); + + assertModelHasOutput(result); + + const toolLogs = rig.readToolLogs(); + const browserAgentCall = toolLogs.find( + (t) => t.toolRequest.name === 'browser_agent', + ); + expect( + browserAgentCall, + 'Expected browser_agent to be called', + ).toBeDefined(); + }); + + it('should navigate through a multi-step flow', async () => { + rig.setup('localhost-multistep', { + fakeResponsesPath: join( + __dirname, + 'browser-agent-localhost.multistep.responses', + ), + settings: browserSettings, + }); + + const result = await rig.run({ + args: "Go to http://127.0.0.1:18923/multi-step/step1.html, fill in 'testuser' as username, click Next, then click Finish on step 2. Report the result.", + }); + + assertModelHasOutput(result); + + const toolLogs = rig.readToolLogs(); + const browserAgentCall = toolLogs.find( + (t) => t.toolRequest.name === 'browser_agent', + ); + expect( + browserAgentCall, + 'Expected browser_agent to be called', + ).toBeDefined(); + }); + + it('should handle dynamically loaded content', async () => { + rig.setup('localhost-dynamic', { + fakeResponsesPath: join( + __dirname, + 'browser-agent-localhost.dynamic.responses', + ), + settings: browserSettings, + }); + + const result = await rig.run({ + args: 'Navigate to http://127.0.0.1:18923/dynamic.html, wait for content to load, and tell me what items appear.', + }); + + assertModelHasOutput(result); + + const toolLogs = rig.readToolLogs(); + const browserAgentCall = toolLogs.find( + (t) => t.toolRequest.name === 'browser_agent', + ); + expect( + browserAgentCall, + 'Expected browser_agent to be called', + ).toBeDefined(); + }); + + it('should take a screenshot of localhost page', async () => { + rig.setup('localhost-screenshot', { + fakeResponsesPath: join( + __dirname, + 'browser-agent-localhost.screenshot.responses', + ), + settings: browserSettings, + }); + + const result = await rig.run({ + args: 'Navigate to http://127.0.0.1:18923/index.html and take a screenshot.', + }); + + assertModelHasOutput(result); + + const toolLogs = rig.readToolLogs(); + const browserCalls = toolLogs.filter( + (t) => t.toolRequest.name === 'browser_agent', + ); + expect(browserCalls.length).toBeGreaterThan(0); + }); +}); diff --git a/integration-tests/browser-agent.cleanup.responses b/integration-tests/browser-agent.cleanup.responses index 9cf7a7b356..e99c757793 100644 --- a/integration-tests/browser-agent.cleanup.responses +++ b/integration-tests/browser-agent.cleanup.responses @@ -1,4 +1,5 @@ {"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll open https://example.com and check the page title for you."},{"functionCall":{"name":"browser_agent","args":{"task":"Open https://example.com and get the page title"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":35,"totalTokenCount":135}}]} -{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I have opened the page and the title is 'Example Domain'."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":30,"totalTokenCount":230}}]} -{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The task is complete. The page title is 'Example Domain'."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":300,"candidatesTokenCount":20,"totalTokenCount":320}}]} -{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Done."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":400,"candidatesTokenCount":5,"totalTokenCount":405}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"https://example.com"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"totalTokenCount":120}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":20,"totalTokenCount":170}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"result":{"success":true,"summary":"The page title is 'Example Domain'."}}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":30,"totalTokenCount":230}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I have opened the page and the title is 'Example Domain'. The browser session has been cleaned up successfully."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":30,"totalTokenCount":230}}]} diff --git a/integration-tests/browser-agent.interaction.responses b/integration-tests/browser-agent.interaction.responses index 98474d6b59..0b4a1d84f7 100644 --- a/integration-tests/browser-agent.interaction.responses +++ b/integration-tests/browser-agent.interaction.responses @@ -1,2 +1,5 @@ {"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll navigate to https://example.com and analyze the links on the page."},{"functionCall":{"name":"browser_agent","args":{"task":"Go to https://example.com and find all links on the page, then describe them"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":40,"totalTokenCount":140}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"https://example.com"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"totalTokenCount":120}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":20,"totalTokenCount":170}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"result":{"success":true,"summary":"Found one link on https://example.com: 'More information...' linking to the IANA website for details about reserved domains."}}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":40,"totalTokenCount":240}}]} {"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"After analyzing https://example.com, I found the following links:\n\n1. **\"More information...\"** - This is the main link on the page that points to the IANA (Internet Assigned Numbers Authority) website for more details about reserved domains.\n\nThe page is quite minimal with just this single informational link, which is typical for example domains used in documentation."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":70,"totalTokenCount":270}}]} diff --git a/integration-tests/browser-agent.navigate-snapshot.responses b/integration-tests/browser-agent.navigate-snapshot.responses index 481520234d..e9c9490a21 100644 --- a/integration-tests/browser-agent.navigate-snapshot.responses +++ b/integration-tests/browser-agent.navigate-snapshot.responses @@ -1,2 +1,5 @@ {"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll help you open https://example.com and analyze the page. Let me use the browser agent to navigate and capture the page information."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to https://example.com and capture the accessibility tree to get the page title and main content"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"https://example.com"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":30,"totalTokenCount":130}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":20,"totalTokenCount":170}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"result":{"success":true,"summary":"Successfully navigated to https://example.com. The page title is 'Example Domain' and the main content states: 'This domain is for use in illustrative examples in documents.'"}}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":40,"totalTokenCount":240}}]} {"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Based on the browser analysis of https://example.com:\n\n**Page Title**: Example Domain\n\n**Main Content**: The page contains a simple heading \"Example Domain\" and explanatory text stating \"This domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission.\"\n\nThe page has a clean, minimal layout typical of placeholder domains used for documentation and examples."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":80,"totalTokenCount":280}}]} diff --git a/integration-tests/browser-agent.screenshot.responses b/integration-tests/browser-agent.screenshot.responses index c9abaa4268..84467f517a 100644 --- a/integration-tests/browser-agent.screenshot.responses +++ b/integration-tests/browser-agent.screenshot.responses @@ -1,2 +1,5 @@ {"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll navigate to https://example.com and take a screenshot for you."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to https://example.com and take a screenshot of the page"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":40,"totalTokenCount":140}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"https://example.com"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"totalTokenCount":120}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_screenshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":20,"totalTokenCount":170}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"result":{"success":true,"summary":"Successfully navigated to https://example.com and captured a screenshot. The page shows the Example Domain header with explanatory text."}}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":40,"totalTokenCount":240}}]} {"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I've successfully navigated to https://example.com and captured a screenshot. The page shows the familiar \"Example Domain\" header with explanatory text below it. The screenshot captures the clean, minimal layout of this demonstration website."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":50,"totalTokenCount":250}}]} diff --git a/integration-tests/browser-agent.sequential.responses b/integration-tests/browser-agent.sequential.responses index 74732998aa..6dc1e96268 100644 --- a/integration-tests/browser-agent.sequential.responses +++ b/integration-tests/browser-agent.sequential.responses @@ -1,2 +1,6 @@ {"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll help you navigate to https://example.com and perform both operations - capturing the accessibility tree and taking a screenshot."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to https://example.com, take a snapshot of the accessibility tree, then take a screenshot"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":45,"totalTokenCount":145}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"https://example.com"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"totalTokenCount":120}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":20,"totalTokenCount":170}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_screenshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":20,"totalTokenCount":220}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"result":{"success":true,"summary":"Successfully navigated to https://example.com, captured accessibility tree snapshot and took a screenshot. Page title is 'Example Domain'."}}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":250,"candidatesTokenCount":40,"totalTokenCount":290}}]} {"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I've successfully completed both operations on https://example.com:\n\n**Accessibility Tree Snapshot**: The page has a clear structure with the main heading \"Example Domain\" and descriptive text about the domain's purpose for documentation examples.\n\n**Screenshot**: Captured a visual representation of the page showing the clean, minimal layout with the heading and explanatory text.\n\nBoth the accessibility data and visual screenshot confirm this is the standard example domain page used for documentation purposes."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":80,"totalTokenCount":280}}]} diff --git a/integration-tests/browser-agent.test.ts b/integration-tests/browser-agent.test.ts index f9f07d4c9e..6545040e98 100644 --- a/integration-tests/browser-agent.test.ts +++ b/integration-tests/browser-agent.test.ts @@ -77,7 +77,12 @@ describe.skipIf(!chromeAvailable)('browser-agent', () => { ), settings: { agents: { - browser_agent: { + overrides: { + browser_agent: { + enabled: true, + }, + }, + browser: { headless: true, sessionMode: 'isolated', }, @@ -106,7 +111,12 @@ describe.skipIf(!chromeAvailable)('browser-agent', () => { fakeResponsesPath: join(__dirname, 'browser-agent.screenshot.responses'), settings: { agents: { - browser_agent: { + overrides: { + browser_agent: { + enabled: true, + }, + }, + browser: { headless: true, sessionMode: 'isolated', }, @@ -132,7 +142,12 @@ describe.skipIf(!chromeAvailable)('browser-agent', () => { fakeResponsesPath: join(__dirname, 'browser-agent.interaction.responses'), settings: { agents: { - browser_agent: { + overrides: { + browser_agent: { + enabled: true, + }, + }, + browser: { headless: true, sessionMode: 'isolated', }, @@ -161,7 +176,12 @@ describe.skipIf(!chromeAvailable)('browser-agent', () => { fakeResponsesPath: join(__dirname, 'browser-agent.cleanup.responses'), settings: { agents: { - browser_agent: { + overrides: { + browser_agent: { + enabled: true, + }, + }, + browser: { headless: true, sessionMode: 'isolated', }, @@ -182,7 +202,12 @@ describe.skipIf(!chromeAvailable)('browser-agent', () => { fakeResponsesPath: join(__dirname, 'browser-agent.sequential.responses'), settings: { agents: { - browser_agent: { + overrides: { + browser_agent: { + enabled: true, + }, + }, + browser: { headless: true, sessionMode: 'isolated', }, @@ -212,7 +237,12 @@ describe.skipIf(!chromeAvailable)('browser-agent', () => { ), settings: { agents: { - browser_agent: { + overrides: { + browser_agent: { + enabled: true, + }, + }, + browser: { headless: true, sessionMode: 'isolated', }, diff --git a/integration-tests/globalSetup.ts b/integration-tests/globalSetup.ts index 5f963f7459..9dad51f9b3 100644 --- a/integration-tests/globalSetup.ts +++ b/integration-tests/globalSetup.ts @@ -9,16 +9,80 @@ if (process.env['NO_COLOR'] !== undefined) { delete process.env['NO_COLOR']; } -import { mkdir, readdir, rm } from 'node:fs/promises'; -import { join, dirname } from 'node:path'; +import { mkdir, readdir, rm, readFile } from 'node:fs/promises'; +import { join, dirname, extname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { canUseRipgrep } from '../packages/core/src/tools/ripGrep.js'; import { disableMouseTracking } from '@google/gemini-cli-core'; +import { createServer, type Server } from 'node:http'; const __dirname = dirname(fileURLToPath(import.meta.url)); const rootDir = join(__dirname, '..'); const integrationTestsDir = join(rootDir, '.integration-tests'); let runDir = ''; // Make runDir accessible in teardown +let fixtureServer: Server | undefined; + +const FIXTURE_PORT = 18923; +const FIXTURE_DIR = join(__dirname, 'test-fixtures'); + +const MIME_TYPES: Record = { + '.html': 'text/html', + '.css': 'text/css', + '.js': 'application/javascript', + '.json': 'application/json', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.svg': 'image/svg+xml', +}; + +async function startFixtureServer(): Promise { + return new Promise((resolve, reject) => { + const server = createServer(async (req, res) => { + const urlPath = req.url?.split('?')[0] || '/'; + const relativePath = urlPath === '/' ? 'index.html' : urlPath; + const filePath = join(FIXTURE_DIR, relativePath); + + if (!filePath.startsWith(FIXTURE_DIR)) { + res.writeHead(403, { 'Content-Type': 'text/html' }); + res.end('

403 Forbidden

'); + return; + } + + try { + const content = await readFile(filePath); + const ext = extname(filePath); + res.writeHead(200, { + 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream', + }); + res.end(content); + } catch { + res.writeHead(404, { 'Content-Type': 'text/html' }); + res.end('

404 Not Found

'); + } + }); + + server.on('error', (err: NodeJS.ErrnoException) => { + if (err.code === 'EADDRINUSE') { + console.warn( + `Port ${FIXTURE_PORT} in use, trying ${FIXTURE_PORT + 1}...`, + ); + server.listen(FIXTURE_PORT + 1, '127.0.0.1'); + } else { + reject(err); + } + }); + + server.on('listening', () => { + const addr = server.address(); + const port = typeof addr === 'object' && addr ? addr.port : FIXTURE_PORT; + fixtureServer = server; + console.log(`Test fixture server listening on http://127.0.0.1:${port}`); + resolve(port); + }); + + server.listen(FIXTURE_PORT, '127.0.0.1'); + }); +} export async function setup() { runDir = join(integrationTestsDir, `${Date.now()}`); @@ -40,6 +104,10 @@ export async function setup() { throw new Error('Failed to download ripgrep binary'); } + // Start the test fixture server + const port = await startFixtureServer(); + process.env['TEST_FIXTURE_PORT'] = String(port); + // Clean up old test runs, but keep the latest few for debugging try { const testRuns = await readdir(integrationTestsDir); @@ -73,6 +141,14 @@ export async function setup() { } export async function teardown() { + // Stop the fixture server + if (fixtureServer) { + await new Promise((resolve) => { + fixtureServer!.close(() => resolve()); + }); + fixtureServer = undefined; + } + // Disable mouse tracking if (process.stdout.isTTY) { disableMouseTracking(); diff --git a/integration-tests/test-fixtures/dynamic.html b/integration-tests/test-fixtures/dynamic.html new file mode 100644 index 0000000000..73a99b56e4 --- /dev/null +++ b/integration-tests/test-fixtures/dynamic.html @@ -0,0 +1,29 @@ + + + + + Test Fixture - Dynamic Content + + +

Dynamic Content Page

+
Loading...
+ + + + diff --git a/integration-tests/test-fixtures/form-result.html b/integration-tests/test-fixtures/form-result.html new file mode 100644 index 0000000000..182ed70128 --- /dev/null +++ b/integration-tests/test-fixtures/form-result.html @@ -0,0 +1,15 @@ + + + + + Test Fixture - Form Result + + +

Form Submitted Successfully

+

Thank you for your submission.

+
+

Your form data has been received.

+
+ Back to Home + + diff --git a/integration-tests/test-fixtures/form.html b/integration-tests/test-fixtures/form.html new file mode 100644 index 0000000000..848cbe47e8 --- /dev/null +++ b/integration-tests/test-fixtures/form.html @@ -0,0 +1,37 @@ + + + + + Test Fixture - Contact Form + + +

Contact Form

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + diff --git a/integration-tests/test-fixtures/index.html b/integration-tests/test-fixtures/index.html new file mode 100644 index 0000000000..0298ab929d --- /dev/null +++ b/integration-tests/test-fixtures/index.html @@ -0,0 +1,27 @@ + + + + + Test Fixture - Home + + +

Test Fixture Home Page

+

+ This is a test fixture page for browser agent integration tests. +

+ +
+

Footer content for testing.

+
+ + diff --git a/integration-tests/test-fixtures/multi-step/result.html b/integration-tests/test-fixtures/multi-step/result.html new file mode 100644 index 0000000000..f2386215d5 --- /dev/null +++ b/integration-tests/test-fixtures/multi-step/result.html @@ -0,0 +1,15 @@ + + + + + Test Fixture - Result + + +

Multi-Step Complete

+

You have completed all steps successfully.

+
+ ✓ Complete +
+ Back to Home + + diff --git a/integration-tests/test-fixtures/multi-step/step1.html b/integration-tests/test-fixtures/multi-step/step1.html new file mode 100644 index 0000000000..d6d620d4a0 --- /dev/null +++ b/integration-tests/test-fixtures/multi-step/step1.html @@ -0,0 +1,16 @@ + + + + + Test Fixture - Step 1 + + +

Step 1: Enter Your Details

+

Please provide your name to continue.

+
+ + + +
+ + diff --git a/integration-tests/test-fixtures/multi-step/step2.html b/integration-tests/test-fixtures/multi-step/step2.html new file mode 100644 index 0000000000..f0571a7a8e --- /dev/null +++ b/integration-tests/test-fixtures/multi-step/step2.html @@ -0,0 +1,22 @@ + + + + + Test Fixture - Step 2 + + +

Step 2: Confirm Your Selection

+

Choose your preference below.

+
+
+ + +
+ +
+ +