Selenium technical reference

Self-Healing Selenium WebDriver with LLM Selector Fallback

LumaBrowser is a drop-in ChromeDriver replacement for Selenium. Its W3C WebDriver Level 2 server runs on port 9515 inside the browser process, so Selenium, WebdriverIO, Capybara, and Nightwatch clients connect unchanged — same capabilities, same locator strategies, same session lifecycle. Opt in to lumabyte:llmFallback and CSS selectors self-heal via an LLM when a redesign moves the DOM underneath your tests.

Looking for the marketing intro? Start at Self-Healing Selectors — the hub page covers Selenium, Playwright, and Puppeteer side-by-side. This page is the Selenium-specific deep dive.

LumaBrowser vs Selenium at a glance

Selenium is a browser-automation framework that speaks the W3C WebDriver protocol to a separate driver binary (typically ChromeDriver). LumaBrowser is the browser itself, with a built-in W3C WebDriver server on port 9515 and the ai-description locator strategy for LLM-resolved selectors. The migration is a URL change — your Page Object suite does not need to move.

CapabilitySelenium + ChromeDriverLumaBrowser
ProtocolW3C WebDriver Level 2W3C WebDriver Level 2 on port 9515
Driver binarySeparate ChromeDriver binary, must match Chrome versionNone — server is inside the browser
Locator strategiesCSS, XPath, link text, partial link text, tag nameAll of the above, plus ai-description
LLM selector fallbackNot availablelumabyte:llmFallback capability + lumabyte:description hint
Chrome DevTools Protocol accessgoog:cdp/executelumabyte:cdp/execute — aliased, same payload
goog:chromeOptionsNativeAccepted as-is; pass-through with partial enforcement
Selenium GridNative, /wd/hub prefixSupported — set URL prefix to /wd/hub in extension settings
Frame switching, async scriptFullv1 limitation — not yet implemented
Also works with Puppeteer / PlaywrightNoYes — separate CDP server on port 9222
MCP server for AI agentsNot providedBuilt-in local MCP server
SetupInstall ChromeDriver + browser + clientnpx lumabrowser start
The drop-in promise

LumaBrowser’s selenium-driver extension is a standards-compliant WebDriver HTTP server on port 9515 — the same port, path structure, and session lifecycle as ChromeDriver. What you don’t have to change:

  • No new library. Keep your existing Selenium, WebdriverIO, Capybara, Nightwatch, or Playwright-over-WebDriver client.
  • ChromeOptions() is accepted as-is. goog:chromeOptions is passed through untouched (not every key is enforced yet — see the matrix below).
  • W3C locator strategies behave identically. CSS, XPath, link text, partial link text, and tag name resolve against the active tab exactly as they do under ChromeDriver.
  • driver.execute_cdp_cmd(...) still works. ChromeDriver’s goog:cdp/execute endpoint is aliased to LumaBrowser’s lumabyte:cdp/execute passthrough.
  • Selenium Grid compatible. Set the URL prefix to /wd/hub in the extension settings and point your hub at LumaBrowser.

Just change the URL:

# Before
driver = webdriver.Remote("http://127.0.0.1:9515", options=ChromeOptions())

# After
driver = webdriver.Remote("http://127.0.0.1:9515", options=ChromeOptions())
# (same URL — ChromeDriver is no longer running; LumaBrowser is.)

That’s the entire migration for W3C-compatible suites. Everything below is what you gain by opting in.

What’s actually different

Three concrete wins. Each one is opt-in: pay the cost only where you want the benefit.

Win 1: Resilient selectors that survive a redesign

Tests break when a marketing team renames a CSS class. LumaBrowser retries the find via an LLM using a natural-language description of what the element is — not how it’s currently styled.

Two opt-in modes:

  1. Keep your CSS, add an AI hint. If the selector misses, the LLM re-resolves using the description and returns a fresh selector.
  2. Skip CSS entirely. Use the ai-description locator strategy and let the LLM own the resolution end-to-end.
Selenium (classic)
# Breaks the moment the button’s class
# name changes during a UI refresh.
from selenium import webdriver
from selenium.webdriver.common.by import By

driver = webdriver.Remote(
    "http://127.0.0.1:9515",
    options=webdriver.ChromeOptions()
)

button = driver.find_element(
    By.CSS_SELECTOR,
    "button.btn-primary.signup"
)
button.click()
LumaBrowser
# LLM fallback re-resolves when CSS misses.
from selenium import webdriver
from selenium.webdriver.common.by import By

opts = webdriver.ChromeOptions()
opts.set_capability(
    "lumabyte:llmFallback",
    {"enabled": True}
)
driver = webdriver.Remote(
    "http://127.0.0.1:9515", options=opts
)

# Option A: keep CSS, add an AI hint
button = driver.execute(
    "find element",
    {"using": "css selector",
     "value": "button.btn-primary.signup",
     "lumabyte:description": "the sign-up button"}
)["value"]

# Option B: skip CSS entirely
button = driver.find_element(
    "ai-description", "the sign-up button"
)
button.click()
Selenium (classic)
// Breaks the moment the button’s class
// name changes during a UI refresh.
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Remote;

var driver = new RemoteWebDriver(
    new Uri("http://127.0.0.1:9515"),
    new ChromeOptions()
);

var button = driver.FindElement(
    By.CssSelector("button.btn-primary.signup")
);
button.Click();
LumaBrowser
// LLM fallback re-resolves when CSS misses.
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Remote;

var opts = new ChromeOptions();
opts.AddAdditionalOption(
    "lumabyte:llmFallback",
    new Dictionary<string, object> { { "enabled", true } }
);

var driver = new RemoteWebDriver(
    new Uri("http://127.0.0.1:9515"), opts
);

// Option A: keep CSS, add an AI hint (vendor param
// rides along on the find-element command).
var found = driver.ExecuteCustomDriverCommand(
    DriverCommand.FindElement,
    new Dictionary<string, object> {
        { "using", "css selector" },
        { "value", "button.btn-primary.signup" },
        { "lumabyte:description", "the sign-up button" }
    }
);

// Option B: skip CSS entirely.
var button = driver.FindElement(
    By.Custom("ai-description", "the sign-up button")
);
button.Click();

Note: By.Custom requires registering the ai-description strategy on the driver once at startup (see the .NET docs for CustomFinderFactory).

Selenium (classic)
// Breaks the moment the button's class
// name changes during a UI refresh.
const { Builder, By } = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');

const driver = await new Builder()
  .usingServer('http://127.0.0.1:9515')
  .forBrowser('chrome')
  .setChromeOptions(new chrome.Options())
  .build();

const button = await driver.findElement(
  By.css('button.btn-primary.signup')
);
await button.click();
LumaBrowser
// LLM fallback re-resolves when CSS misses.
const { Builder, By } = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');

const opts = new chrome.Options();
opts.set('lumabyte:llmFallback', { enabled: true });

const driver = await new Builder()
  .usingServer('http://127.0.0.1:9515')
  .forBrowser('chrome')
  .setChromeOptions(opts)
  .build();

const { Command } = require('selenium-webdriver/lib/command');

// Option A: keep CSS, add an AI hint.
const foundA = await driver.execute(
  new Command('findElement')
    .setParameter('using', 'css selector')
    .setParameter('value', 'button.btn-primary.signup')
    .setParameter('lumabyte:description', 'the sign-up button')
);

// Option B: skip CSS, dispatch the ai-description strategy directly.
const foundB = await driver.execute(
  new Command('findElement')
    .setParameter('using', 'ai-description')
    .setParameter('value', 'the sign-up button')
);
// foundB is a raw element reference; wrap or click via the executor.

Note: selenium-webdriver for Node doesn’t expose a first-class hook for custom locator strategies, so the raw findElement command is the honest path here. Python and C# bindings wrap this more ergonomically.

Win 2: One round-trip to read the whole page

Classic Selenium makes N HTTP calls to read N fields — each one is a round-trip through the WebDriver wire protocol, a DOM query, and a response. LumaBrowser’s vendor endpoint /session/:sessionId/lumabyte/dom/snapshot returns the page URL, title, source, and an optional base64 screenshot in a single call.

Selenium (classic)
# One round-trip per field.
title  = driver.find_element(By.CSS_SELECTOR, "h1").text
price  = driver.find_element(By.CSS_SELECTOR, ".price").text
rating = driver.find_element(By.CSS_SELECTOR, ".rating").text
stock  = driver.find_element(By.CSS_SELECTOR, ".stock").text
# 4 requests, 4 DOM walks, 4 responses.
LumaBrowser
# One call returns URL, title, source, and
# (optionally) a full-page screenshot.
from selenium.webdriver.remote.command import Command

# Register the vendor command once per driver.
driver.command_executor._commands["lumabyteSnapshot"] = (
    "POST", "/session/$sessionId/lumabyte/dom/snapshot"
)

snap = driver.execute(
    "lumabyteSnapshot",
    {"includeScreenshot": True}
)["value"]
# 1 request. Parse locally.
Selenium (classic)
// One round-trip per field.
var title  = driver.FindElement(By.CssSelector("h1")).Text;
var price  = driver.FindElement(By.CssSelector(".price")).Text;
var rating = driver.FindElement(By.CssSelector(".rating")).Text;
var stock  = driver.FindElement(By.CssSelector(".stock")).Text;
// 4 requests, 4 DOM walks, 4 responses.
LumaBrowser
// One call returns URL, title, source, and
// (optionally) a full-page screenshot.
using OpenQA.Selenium.Remote;

// ExecuteCustomDriverCommand dispatches a vendor verb
// without polluting CommandInfoRepository at the caller.
var snap = driver.ExecuteCustomDriverCommand(
    "lumabyte:dom/snapshot",
    new Dictionary<string, object> {
        { "includeScreenshot", true }
    }
);
// 1 request. Parse locally.

Note: ExecuteCustomDriverCommand needs the vendor command registered on CommandInfoRepository first (one-time TryAddCommand at driver construction) — a two-line helper wraps that setup in most test bases.

Selenium (classic)
// One round-trip per field.
const title  = await driver.findElement(By.css('h1')).getText();
const price  = await driver.findElement(By.css('.price')).getText();
const rating = await driver.findElement(By.css('.rating')).getText();
const stock  = await driver.findElement(By.css('.stock')).getText();
// 4 requests, 4 DOM walks, 4 responses.
LumaBrowser
// One call returns URL, title, source, and
// (optionally) a full-page screenshot.
const { Command } = require('selenium-webdriver/lib/command');

// Teach the executor about the vendor verb once.
driver.getExecutor().defineCommand(
  'lumabyteSnapshot',
  'POST',
  '/session/:sessionId/lumabyte/dom/snapshot'
);

const snap = await driver.execute(
  new Command('lumabyteSnapshot')
    .setParameter('includeScreenshot', true)
);
// 1 request. Parse locally.
Win 3: No ChromeDriver binary to manage

Classic Selenium needs a version-matched ChromeDriver binary on every CI agent and every developer laptop. Chrome auto-updates, ChromeDriver doesn’t, and your pipeline breaks at 3am until someone bumps webdriver-manager. LumaBrowser’s WebDriver server runs inside the browser process itself — there is no binary to download, no version drift, no child process to reap.

Selenium (classic)
# Selenium Manager has to download the
# right ChromeDriver for whatever Chrome
# version is installed today.
from selenium import webdriver

driver = webdriver.Chrome()  # spawns chromedriver

# Plus: ChromeDriver lifecycle, process
# cleanup, version-match CI matrix, and
# the occasional "session not created:
# This version of ChromeDriver only
# supports Chrome version N" at 3am.
LumaBrowser
# LumaBrowser is already running.
# Connect directly.
from selenium import webdriver

driver = webdriver.Remote(
    "http://127.0.0.1:9515",
    options=webdriver.ChromeOptions()
)

# No binary download.
# No version pinning.
# No child process.
Selenium (classic)
// Selenium Manager has to download the
// right ChromeDriver for whatever Chrome
// version is installed today.
using OpenQA.Selenium.Chrome;

var driver = new ChromeDriver(); // spawns chromedriver

// Plus: ChromeDriver lifecycle, process
// cleanup, version-match CI matrix, and
// the occasional "session not created:
// This version of ChromeDriver only
// supports Chrome version N" at 3am.
LumaBrowser
// LumaBrowser is already running.
// Connect directly.
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Remote;

var driver = new RemoteWebDriver(
    new Uri("http://127.0.0.1:9515"),
    new ChromeOptions()
);

// No binary download.
// No version pinning.
// No child process.
Selenium (classic)
// selenium-webdriver + Selenium Manager has
// to download the right ChromeDriver for
// whatever Chrome version is installed today.
const { Builder } = require('selenium-webdriver');

const driver = await new Builder()
  .forBrowser('chrome')
  .build();

// Plus: ChromeDriver lifecycle, process
// cleanup, version-match CI matrix, and
// the occasional "session not created:
// This version of ChromeDriver only
// supports Chrome version N" at 3am.
LumaBrowser
// LumaBrowser is already running.
// Connect directly.
const { Builder } = require('selenium-webdriver');

const driver = await new Builder()
  .usingServer('http://127.0.0.1:9515')
  .forBrowser('chrome')
  .build();

// No binary download.
// No version pinning.
// No child process.
Full example: a semantic end-to-end test

What a realistic test looks like when you lean on ai-description, per-find hints, and the DOM snapshot together. Notice how close the code reads to the test spec.

import requests
from selenium import webdriver

# 1. Opt in to LLM fallback for this session.
opts = webdriver.ChromeOptions()
opts.set_capability("lumabyte:llmFallback", {
    "enabled": True,
    "onFindFail": True,           # retry failed finds via LLM
    "onClickIntercepted": True,   # retry intercepted clicks via LLM
})

driver = webdriver.Remote("http://127.0.0.1:9515", options=opts)
session_id = driver.session_id

try:
    driver.get("https://example.shop/products/coffee-grinder")

    # Describe elements, don't select them.
    driver.find_element("ai-description", "the add-to-cart button").click()
    driver.find_element("ai-description", "the cart icon in the header").click()

    # Pull the whole checkout summary in one call.
    snap = requests.post(
        f"http://127.0.0.1:9515/session/{session_id}/lumabyte/dom/snapshot",
        json={"includeScreenshot": True},
    ).json()["value"]

    assert "Coffee Grinder" in snap["source"]
    assert snap["url"].endswith("/cart")
    # snap["screenshot"] is base64 PNG — feed straight into your visual diff.
finally:
    driver.quit()

Twenty-five lines, zero brittle CSS, one round-trip to assert the cart state. Try writing the same suite in classic Selenium and count the selectors you’d need to maintain next quarter.

Compatibility matrix

The full row-by-row matrix — W3C session lifecycle, navigation, locator strategies, element interactions, screenshot, goog:chromeOptions pass-through, the ai-description locator, lumabyte:cdp/execute, and the v1 limitations (frame switching, shadow DOM subqueries, async script execution) — lives in the API reference: /apis → Selenium Driver section. Anything currently labelled v1 limitation is covered today by the CDP passthrough (lumabyte:cdp/execute).

How to enable it

Two ways, takes under a minute either way:

1. From the UI

  1. Open Settings → Selenium.
  2. (Optional) Check “Start automatically when LumaBrowser launches.”
  3. Click Save & Start.
  4. Point your Selenium client at http://127.0.0.1:9515.

Or start it programmatically: POST /api/selenium/start. Full endpoint reference, capability payload shape, and MCP tool surface are in the Selenium Driver section of the API docs.

FAQ

Does my existing test suite just work?

Yes for the W3C-compatible subset — sessions, navigation, the four locator strategies, click/clear/send-keys, cookies, timeouts, sync script execution, full-page screenshots, and goog:chromeOptions pass-through all behave as they do against ChromeDriver. The compatibility matrix above calls out exactly what doesn’t yet.

Which LLM runs the fallback?

Whichever slot you configure in the selenium-driver extension settings (selenium.fallback.slot). That slot routes through LumaBrowser’s LLM Service, so you can point it at Anthropic, any OpenAI-compatible endpoint, or a local model via LM Studio or Ollama — same LLM configuration as the rest of the browser.

What about Playwright or Puppeteer?

LumaBrowser ships a dedicated CDP server alongside this WebDriver one. Connect Puppeteer via puppeteer.connect({ browserURL: 'http://127.0.0.1:9222' }) or Playwright via chromium.connectOverCDP('http://127.0.0.1:9222') — both pick up the same Lumabyte.* CDP domain. See Puppeteer, Playwright, or the cross-driver hub.

Ready to try it

Install LumaBrowser, enable the selenium-driver extension, and change one URL in your test suite. The W3C parts just work. The LLM fallback is there when you want it.