Selenium 4: CDP & BiDi Protocol
Beyond Basic WebDriver
Selenium 4 introduced direct access to browser internals via:
- CDP (Chrome DevTools Protocol) - For Chromium browsers
- BiDi (WebDriver Bidirectional) - Cross-browser future standard
These enable testing scenarios that were impossible before.
What CDP Enables
| Capability | E-Commerce Use Case |
|---|---|
| Network interception | Mock API responses, test error states |
| Performance metrics | Measure page load times |
| Console log capture | Catch JavaScript errors |
| Device emulation | Test mobile layouts |
| Geolocation | Test location-based features |
| Network throttling | Test slow connection handling |
Setting Up CDP Access
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
# CDP requires Chrome/Chromium
options = Options()
options.add_argument("--remote-allow-origins=*")
driver = webdriver.Chrome(options=options)
# Access DevTools
dev_tools = driver.execute_cdp_cmd
Network Interception
Capture Network Requests
# Enable network domain
driver.execute_cdp_cmd("Network.enable", {})
# Set up request interception
driver.execute_cdp_cmd("Fetch.enable", {
"patterns": [{"urlPattern": "*"}]
})
# Navigate and capture
driver.get("https://shop.example.com")
# Get all requests
requests = driver.execute_cdp_cmd("Network.getAllCookies", {})
Mock API Responses
Perfect for testing error states without backend changes:
import json
from selenium.webdriver.common.devtools.v120 import network, fetch
def intercept_and_mock_api(driver):
"""Mock the cart API to return empty cart"""
# Use Selenium 4's BiDi for cleaner API
driver.execute_cdp_cmd("Fetch.enable", {
"patterns": [
{"urlPattern": "*/api/cart*", "requestStage": "Response"}
]
})
def mock_cart_response(request):
if "/api/cart" in request.get("request", {}).get("url", ""):
# Return mocked empty cart
return {
"responseCode": 200,
"body": json.dumps({"items": [], "total": 0})
}
return None
# Note: Full implementation requires event handling
# This is a simplified example
# Alternative: Use selenium-wire for easier interception
# pip install selenium-wire
from seleniumwire import webdriver
def test_cart_api_error():
driver = webdriver.Chrome()
def interceptor(request):
if "/api/cart" in request.url:
request.create_response(
status_code=500,
headers={"Content-Type": "application/json"},
body=json.dumps({"error": "Server error"})
)
driver.request_interceptor = interceptor
driver.get("https://shop.example.com/cart")
# Test how UI handles API error
assert driver.find_element(By.CSS_SELECTOR, ".error-message").is_displayed()
Performance Metrics
Measure Page Load Times
def get_performance_metrics(driver, url):
"""Get detailed page load metrics"""
# Enable performance domain
driver.execute_cdp_cmd("Performance.enable", {})
# Navigate
driver.get(url)
# Get metrics
metrics = driver.execute_cdp_cmd("Performance.getMetrics", {})
# Parse relevant metrics
result = {}
for metric in metrics.get("metrics", []):
name = metric["name"]
value = metric["value"]
if name in ["DomContentLoaded", "FirstMeaningfulPaint", "NavigationStart"]:
result[name] = value
return result
# E-commerce specific: Measure checkout load time
def test_checkout_performance(driver, cart_with_one_item):
"""Checkout page should load within 3 seconds"""
cart = cart_with_one_item
start_time = time.time()
cart.proceed_to_checkout()
load_time = time.time() - start_time
assert load_time < 3.0, f"Checkout took {load_time}s (max 3s)"
Using Navigation Timing API
def get_page_load_time(driver):
"""Get page load time using Navigation Timing API"""
timing = driver.execute_script("""
const timing = performance.timing;
return {
pageLoad: timing.loadEventEnd - timing.navigationStart,
domContentLoaded: timing.domContentLoadedEventEnd - timing.navigationStart,
firstPaint: performance.getEntriesByType('paint')
.find(p => p.name === 'first-contentful-paint')?.startTime || 0
};
""")
return timing
# Test
def test_product_page_load_performance(driver):
"""Product page should load quickly"""
driver.get("https://shop.example.com/product/123")
timing = get_page_load_time(driver)
assert timing["domContentLoaded"] < 2000, "DOM content should load in 2s"
assert timing["pageLoad"] < 4000, "Full page should load in 4s"
assert timing["firstPaint"] < 1500, "First paint should be under 1.5s"
Console Log Capture
Catch JavaScript Errors
def capture_console_logs(driver):
"""Capture and return all console logs"""
# Enable log domain
driver.execute_cdp_cmd("Log.enable", {})
driver.execute_cdp_cmd("Runtime.enable", {})
# Navigate
driver.get("https://shop.example.com")
# Get console logs via browser logs
logs = driver.get_log("browser")
errors = [log for log in logs if log["level"] == "SEVERE"]
warnings = [log for log in logs if log["level"] == "WARNING"]
return {"errors": errors, "warnings": warnings}
def test_no_javascript_errors_on_checkout(driver, cart_with_one_item):
"""Checkout should have no JS errors"""
cart_with_one_item.proceed_to_checkout()
logs = capture_console_logs(driver)
assert len(logs["errors"]) == 0, f"Found JS errors: {logs['errors']}"
Device Emulation
Test Mobile Layouts
def set_mobile_device(driver, device_name="iPhone 12"):
"""Emulate mobile device"""
devices = {
"iPhone 12": {
"width": 390,
"height": 844,
"deviceScaleFactor": 3,
"mobile": True,
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)..."
},
"Pixel 5": {
"width": 393,
"height": 851,
"deviceScaleFactor": 2.75,
"mobile": True,
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5)..."
}
}
device = devices.get(device_name)
# Set viewport
driver.execute_cdp_cmd("Emulation.setDeviceMetricsOverride", {
"width": device["width"],
"height": device["height"],
"deviceScaleFactor": device["deviceScaleFactor"],
"mobile": device["mobile"]
})
# Set user agent
driver.execute_cdp_cmd("Emulation.setUserAgentOverride", {
"userAgent": device["userAgent"]
})
def test_mobile_checkout_flow(driver, cart_with_one_item):
"""Test checkout works on mobile"""
set_mobile_device(driver, "iPhone 12")
checkout = cart_with_one_item.proceed_to_checkout()
# Verify mobile layout
assert checkout.is_visible(checkout.MOBILE_MENU)
assert not checkout.is_visible(checkout.DESKTOP_SIDEBAR)
# Complete checkout
checkout.fill_shipping_info(VALID_SHIPPING)
checkout.continue_to_payment()
# ... rest of checkout
Network Throttling
Test Slow Connection Handling
def set_network_conditions(driver, latency=0, download=0, upload=0, offline=False):
"""Set network conditions"""
driver.execute_cdp_cmd("Network.emulateNetworkConditions", {
"offline": offline,
"latency": latency, # ms
"downloadThroughput": download, # bytes/s, -1 for no throttle
"uploadThroughput": upload # bytes/s, -1 for no throttle
})
# Presets
def set_slow_3g(driver):
set_network_conditions(driver, latency=400, download=400*1024, upload=400*1024)
def set_offline(driver):
set_network_conditions(driver, offline=True)
def test_slow_network_shows_loading(driver, cart_with_one_item):
"""Verify loading states on slow network"""
set_slow_3g(driver)
cart = cart_with_one_item
# Start checkout (will be slow)
cart.click(cart.CHECKOUT_BTN)
# Should show loading indicator
assert cart.is_visible((By.CSS_SELECTOR, ".loading-spinner"))
# Eventually completes
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
WebDriverWait(driver, 30).until(EC.url_contains("/checkout"))
def test_offline_shows_error(driver, product_page):
"""Verify offline handling"""
set_offline(driver)
# Try to add to cart
product_page.click_add_to_cart()
# Should show offline error
assert product_page.is_visible((By.CSS_SELECTOR, ".offline-error"))
Geolocation Testing
def set_geolocation(driver, latitude, longitude, accuracy=100):
"""Set device geolocation"""
driver.execute_cdp_cmd("Emulation.setGeolocationOverride", {
"latitude": latitude,
"longitude": longitude,
"accuracy": accuracy
})
def test_store_locator_uses_location(driver):
"""Test store locator with mocked location"""
# Set location to San Francisco
set_geolocation(driver, 37.7749, -122.4194)
driver.get("https://shop.example.com/store-locator")
# Click "Use my location"
driver.find_element(By.CSS_SELECTOR, "[data-testid='use-location']").click()
# Verify nearest store is SF store
nearest = driver.find_element(By.CSS_SELECTOR, "[data-testid='nearest-store']")
assert "San Francisco" in nearest.text
WebDriver BiDi (The Future)
BiDi is the cross-browser replacement for CDP. It's still evolving but available in Selenium 4.
# BiDi example (Selenium 4.x)
from selenium.webdriver.common.bidi.console import Console
async def capture_console_with_bidi(driver):
"""Use BiDi for console capture"""
async with driver.bidi_connection() as connection:
log = Log(driver, connection)
async with log.add_listener(Console.ALL) as messages:
driver.get("https://shop.example.com")
# Get all console messages
for message in messages:
print(f"{message.level}: {message.text}")
Note: BiDi APIs are still maturing. CDP works today; BiDi is the future.
Key Takeaways
- CDP unlocks advanced testing - Network, performance, emulation
- Test error states easily - Mock API failures without backend changes
- Performance matters - Measure and assert load times
- Mobile testing built-in - Device emulation without separate tools
- BiDi is coming - Learn CDP now, transition to BiDi later
Next Lesson
Finally, let's bring it all together with AI-Augmented Testing & CI/CDโpractical AI integration and production deployment.