"""
Test multi-tab operations: creation, switching, closing, and background tabs.
Tests verify that:
1. Agent can create multiple tabs (3) and switch between them
2. Agent can close tabs with vision=True
3. Agent can handle buttons that open new tabs in background
4. Agent can continue and call done() after each tab operation
5. Browser state doesn't timeout during background tab operations
All tests use:
- max_steps=5 to allow multiple tab operations
- 120s timeout to fail if test takes too long
- Mock LLM to verify agent can still make decisions after tab operations
Usage:
uv run pytest tests/ci/browser/test_tabs.py -v -s
"""
import asyncio
import time
import pytest
from pytest_httpserver import HTTPServer
from browser_use.agent.service import Agent
from browser_use.browser import BrowserSession
from browser_use.browser.profile import BrowserProfile
from tests.ci.conftest import create_mock_llm
@pytest.fixture(scope='session')
def http_server():
"""Create and provide a test HTTP server for tab tests."""
server = HTTPServer()
server.start()
# Route 1: Home page
server.expect_request('/home').respond_with_data(
'
',
content_type='text/html',
)
# Route 5: Background tab page - has a link that opens a new tab in the background
server.expect_request('/background-tab-test').respond_with_data(
"""
Background Tab Test
Background Tab Test
Click the link below to open a new tab in the background:
""",
content_type='text/html',
)
yield server
server.stop()
@pytest.fixture(scope='session')
def base_url(http_server):
"""Return the base URL for the test HTTP server."""
return f'http://{http_server.host}:{http_server.port}'
@pytest.fixture(scope='function')
async def browser_session():
"""Create a browser session for tab tests."""
session = BrowserSession(
browser_profile=BrowserProfile(
headless=True,
user_data_dir=None,
keep_alive=True,
)
)
await session.start()
yield session
await session.kill()
class TestMultiTabOperations:
"""Test multi-tab creation, switching, and closing."""
async def test_create_and_switch_three_tabs(self, browser_session, base_url):
"""Test that agent can create 3 tabs, switch between them, and call done().
This test verifies that browser state is retrieved between each step.
"""
start_time = time.time()
actions = [
# Action 1: Navigate to home page
f"""
{{
"thinking": "I'll start by navigating to the home page",
"evaluation_previous_goal": "Starting task",
"memory": "Navigating to home page",
"next_goal": "Navigate to home page",
"action": [
{{
"navigate": {{
"url": "{base_url}/home",
"new_tab": false
}}
}}
]
}}
""",
# Action 2: Open page1 in new tab
f"""
{{
"thinking": "Now I'll open page 1 in a new tab",
"evaluation_previous_goal": "Home page loaded",
"memory": "Opening page 1 in new tab",
"next_goal": "Open page 1 in new tab",
"action": [
{{
"navigate": {{
"url": "{base_url}/page1",
"new_tab": true
}}
}}
]
}}
""",
# Action 3: Open page2 in new tab
f"""
{{
"thinking": "Now I'll open page 2 in a new tab",
"evaluation_previous_goal": "Page 1 opened in new tab",
"memory": "Opening page 2 in new tab",
"next_goal": "Open page 2 in new tab",
"action": [
{{
"navigate": {{
"url": "{base_url}/page2",
"new_tab": true
}}
}}
]
}}
""",
# Action 4: Switch to first tab
"""
{
"thinking": "Now I'll switch back to the first tab",
"evaluation_previous_goal": "Page 2 opened in new tab",
"memory": "Switching to first tab",
"next_goal": "Switch to first tab",
"action": [
{
"switch": {
"tab_id": "0000"
}
}
]
}
""",
# Action 5: Done
"""
{
"thinking": "I've successfully created 3 tabs and switched between them",
"evaluation_previous_goal": "Switched to first tab",
"memory": "All tabs created and switched",
"next_goal": "Complete task",
"action": [
{
"done": {
"text": "Successfully created 3 tabs and switched between them",
"success": true
}
}
]
}
""",
]
mock_llm = create_mock_llm(actions=actions)
agent = Agent(
task=f'Navigate to {base_url}/home, then open {base_url}/page1 and {base_url}/page2 in new tabs, then switch back to the first tab',
llm=mock_llm,
browser_session=browser_session,
)
# Run with timeout - should complete within 2 minutes
try:
history = await asyncio.wait_for(agent.run(max_steps=5), timeout=120)
elapsed = time.time() - start_time
print(f'\n⏱️ Test completed in {elapsed:.2f} seconds')
print(f'📊 Completed {len(history)} steps')
# Verify each step has browser state
for i, step in enumerate(history.history):
assert step.state is not None, f'Step {i} should have browser state'
assert step.state.url is not None, f'Step {i} should have URL in browser state'
print(f' Step {i + 1}: URL={step.state.url}, tabs={len(step.state.tabs) if step.state.tabs else 0}')
assert len(history) >= 4, 'Agent should have completed at least 4 steps'
# Verify we have 3 tabs open
tabs = await browser_session.get_tabs()
assert len(tabs) >= 3, f'Should have at least 3 tabs open, got {len(tabs)}'
# Verify agent completed successfully
final_result = history.final_result()
assert final_result is not None, 'Agent should return a final result'
assert 'Successfully' in final_result, 'Agent should report success'
# Note: Test is fast (< 1s) because mock LLM returns instantly and pages are simple,
# but browser state IS being retrieved correctly between steps as verified above
except TimeoutError:
pytest.fail('Test timed out after 2 minutes - agent hung during tab operations')
async def test_close_tab_with_vision(self, browser_session, base_url):
"""Test that agent can close a tab with vision=True and call done()."""
actions = [
# Action 1: Navigate to home page
f"""
{{
"thinking": "I'll start by navigating to the home page",
"evaluation_previous_goal": "Starting task",
"memory": "Navigating to home page",
"next_goal": "Navigate to home page",
"action": [
{{
"navigate": {{
"url": "{base_url}/home",
"new_tab": false
}}
}}
]
}}
""",
# Action 2: Open page1 in new tab
f"""
{{
"thinking": "Now I'll open page 1 in a new tab",
"evaluation_previous_goal": "Home page loaded",
"memory": "Opening page 1 in new tab",
"next_goal": "Open page 1 in new tab",
"action": [
{{
"navigate": {{
"url": "{base_url}/page1",
"new_tab": true
}}
}}
]
}}
""",
# Action 3: Close the current tab
"""
{
"thinking": "Now I'll close the current tab (page1)",
"evaluation_previous_goal": "Page 1 opened in new tab",
"memory": "Closing current tab",
"next_goal": "Close current tab",
"action": [
{
"close": {
"tab_id": "0001"
}
}
]
}
""",
# Action 4: Done
"""
{
"thinking": "I've successfully closed the tab",
"evaluation_previous_goal": "Tab closed",
"memory": "Tab closed successfully",
"next_goal": "Complete task",
"action": [
{
"done": {
"text": "Successfully closed the tab",
"success": true
}
}
]
}
""",
]
mock_llm = create_mock_llm(actions=actions)
agent = Agent(
task=f'Navigate to {base_url}/home, then open {base_url}/page1 in a new tab, then close the page1 tab',
llm=mock_llm,
browser_session=browser_session,
use_vision=True, # Enable vision for this test
)
# Run with timeout - should complete within 2 minutes
try:
history = await asyncio.wait_for(agent.run(max_steps=5), timeout=120)
assert len(history) >= 3, 'Agent should have completed at least 3 steps'
# Verify agent completed successfully
final_result = history.final_result()
assert final_result is not None, 'Agent should return a final result'
assert 'Successfully' in final_result, 'Agent should report success'
except TimeoutError:
pytest.fail('Test timed out after 2 minutes - agent hung during tab closing with vision')
async def test_background_tab_open_no_timeout(self, browser_session, base_url):
"""Test that browser state doesn't timeout when a new tab opens in the background."""
start_time = time.time()
actions = [
# Action 1: Navigate to home page
f"""
{{
"thinking": "I'll navigate to the home page first",
"evaluation_previous_goal": "Starting task",
"memory": "Navigating to home page",
"next_goal": "Navigate to home page",
"action": [
{{
"navigate": {{
"url": "{base_url}/home",
"new_tab": false
}}
}}
]
}}
""",
# Action 2: Open page1 in new background tab (stay on home page)
f"""
{{
"thinking": "I'll open page1 in a new background tab",
"evaluation_previous_goal": "Home page loaded",
"memory": "Opening background tab",
"next_goal": "Open background tab without switching to it",
"action": [
{{
"navigate": {{
"url": "{base_url}/page1",
"new_tab": true
}}
}}
]
}}
""",
# Action 3: Immediately check browser state after background tab opens
"""
{
"thinking": "After opening background tab, browser state should still be accessible",
"evaluation_previous_goal": "Background tab opened",
"memory": "Verifying browser state works",
"next_goal": "Complete task",
"action": [
{
"done": {
"text": "Successfully opened background tab, browser state remains accessible",
"success": true
}
}
]
}
""",
]
mock_llm = create_mock_llm(actions=actions)
agent = Agent(
task=f'Navigate to {base_url}/home and open {base_url}/page1 in a new tab',
llm=mock_llm,
browser_session=browser_session,
)
# Run with timeout - this tests if browser state times out when new tabs open
try:
history = await asyncio.wait_for(agent.run(max_steps=3), timeout=120)
elapsed = time.time() - start_time
print(f'\n⏱️ Test completed in {elapsed:.2f} seconds')
print(f'📊 Completed {len(history)} steps')
# Verify each step has browser state (the key test - no timeouts)
for i, step in enumerate(history.history):
assert step.state is not None, f'Step {i} should have browser state'
assert step.state.url is not None, f'Step {i} should have URL in browser state'
print(f' Step {i + 1}: URL={step.state.url}, tabs={len(step.state.tabs) if step.state.tabs else 0}')
assert len(history) >= 2, 'Agent should have completed at least 2 steps'
# Verify agent completed successfully
final_result = history.final_result()
assert final_result is not None, 'Agent should return a final result'
assert 'Successfully' in final_result, 'Agent should report success'
# Verify we have at least 2 tabs
tabs = await browser_session.get_tabs()
print(f' Final tab count: {len(tabs)}')
assert len(tabs) >= 2, f'Should have at least 2 tabs after opening background tab, got {len(tabs)}'
except TimeoutError:
pytest.fail('Test timed out after 2 minutes - browser state timed out after opening background tab')
async def test_rapid_tab_operations_no_timeout(self, browser_session, base_url):
"""Test that browser state doesn't timeout during rapid tab operations."""
actions = [
# Action 1: Navigate to home page
f"""
{{
"thinking": "I'll navigate to the home page",
"evaluation_previous_goal": "Starting task",
"memory": "Navigating to home page",
"next_goal": "Navigate to home page",
"action": [
{{
"navigate": {{
"url": "{base_url}/home",
"new_tab": false
}}
}}
]
}}
""",
# Action 2: Open page1 in new tab
f"""
{{
"thinking": "Opening page1 in new tab",
"evaluation_previous_goal": "Home page loaded",
"memory": "Opening page1",
"next_goal": "Open page1",
"action": [
{{
"navigate": {{
"url": "{base_url}/page1",
"new_tab": true
}}
}}
]
}}
""",
# Action 3: Open page2 in new tab
f"""
{{
"thinking": "Opening page2 in new tab",
"evaluation_previous_goal": "Page1 opened",
"memory": "Opening page2",
"next_goal": "Open page2",
"action": [
{{
"navigate": {{
"url": "{base_url}/page2",
"new_tab": true
}}
}}
]
}}
""",
# Action 4: Open page3 in new tab
f"""
{{
"thinking": "Opening page3 in new tab",
"evaluation_previous_goal": "Page2 opened",
"memory": "Opening page3",
"next_goal": "Open page3",
"action": [
{{
"navigate": {{
"url": "{base_url}/page3",
"new_tab": true
}}
}}
]
}}
""",
# Action 5: Verify browser state is still accessible
"""
{
"thinking": "All tabs opened rapidly, browser state should still be accessible",
"evaluation_previous_goal": "Page3 opened",
"memory": "All tabs opened",
"next_goal": "Complete task",
"action": [
{
"done": {
"text": "Successfully opened 4 tabs rapidly without timeout",
"success": true
}
}
]
}
""",
]
mock_llm = create_mock_llm(actions=actions)
agent = Agent(
task='Open multiple tabs rapidly and verify browser state remains accessible',
llm=mock_llm,
browser_session=browser_session,
)
# Run with timeout - should complete within 2 minutes
try:
history = await asyncio.wait_for(agent.run(max_steps=5), timeout=120)
assert len(history) >= 4, 'Agent should have completed at least 4 steps'
# Verify we have 4 tabs open
tabs = await browser_session.get_tabs()
assert len(tabs) >= 4, f'Should have at least 4 tabs open, got {len(tabs)}'
# Verify agent completed successfully
final_result = history.final_result()
assert final_result is not None, 'Agent should return a final result'
assert 'Successfully' in final_result, 'Agent should report success'
except TimeoutError:
pytest.fail('Test timed out after 2 minutes - browser state timed out during rapid tab operations')
async def test_multiple_tab_switches_and_close(self, browser_session, base_url):
"""Test that agent can switch between multiple tabs and close one."""
actions = [
# Action 1: Navigate to home page
f"""
{{
"thinking": "I'll start by navigating to the home page",
"evaluation_previous_goal": "Starting task",
"memory": "Navigating to home page",
"next_goal": "Navigate to home page",
"action": [
{{
"navigate": {{
"url": "{base_url}/home",
"new_tab": false
}}
}}
]
}}
""",
# Action 2: Open page1 in new tab
f"""
{{
"thinking": "Opening page 1 in new tab",
"evaluation_previous_goal": "Home page loaded",
"memory": "Opening page 1",
"next_goal": "Open page 1",
"action": [
{{
"navigate": {{
"url": "{base_url}/page1",
"new_tab": true
}}
}}
]
}}
""",
# Action 3: Open page2 in new tab
f"""
{{
"thinking": "Opening page 2 in new tab",
"evaluation_previous_goal": "Page 1 opened",
"memory": "Opening page 2",
"next_goal": "Open page 2",
"action": [
{{
"navigate": {{
"url": "{base_url}/page2",
"new_tab": true
}}
}}
]
}}
""",
# Action 4: Switch to tab 1
"""
{
"thinking": "Switching to tab 1 (page1)",
"evaluation_previous_goal": "Page 2 opened",
"memory": "Switching to page 1",
"next_goal": "Switch to page 1",
"action": [
{
"switch": {
"tab_id": "0001"
}
}
]
}
""",
# Action 5: Close current tab
"""
{
"thinking": "Closing the current tab (page1)",
"evaluation_previous_goal": "Switched to page 1",
"memory": "Closing page 1",
"next_goal": "Close page 1",
"action": [
{
"close": {
"tab_id": "0001"
}
}
]
}
""",
# Action 6: Done
"""
{
"thinking": "Successfully completed all tab operations",
"evaluation_previous_goal": "Tab closed",
"memory": "All operations completed",
"next_goal": "Complete task",
"action": [
{
"done": {
"text": "Successfully created, switched, and closed tabs",
"success": true
}
}
]
}
""",
]
mock_llm = create_mock_llm(actions=actions)
agent = Agent(
task='Create 3 tabs, switch to the second one, then close it',
llm=mock_llm,
browser_session=browser_session,
)
# Run with timeout - should complete within 2 minutes
try:
history = await asyncio.wait_for(agent.run(max_steps=6), timeout=120)
assert len(history) >= 5, 'Agent should have completed at least 5 steps'
# Verify agent completed successfully
final_result = history.final_result()
assert final_result is not None, 'Agent should return a final result'
assert 'Successfully' in final_result, 'Agent should report success'
except TimeoutError:
pytest.fail('Test timed out after 2 minutes - agent hung during multiple tab operations')