๐Ÿ”ฅ 0
โญ 0
Lesson 9 of 10 25 min +200 XP

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

CapabilityE-Commerce Use Case
Network interceptionMock API responses, test error states
Performance metricsMeasure page load times
Console log captureCatch JavaScript errors
Device emulationTest mobile layouts
GeolocationTest location-based features
Network throttlingTest 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.

Checkout & Payment Testing