Selenium WebDriver skill for Codex
Explicit waits, stable locators, a pinned Grid. Codex maintains Selenium suites the way they stay green: explicit waits on specific conditions with implicit waits off, locators on test ids instead of generated classes or absolute XPath, one browser session per test with screenshots and page source on failure, and page objects that expose user actions. It also stands up Selenium Grid 4 in Docker for parallel cross-browser CI.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- selenium-webdriver Installs to ~/.agents/skills/selenium-webdriver/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install selenium-webdriver The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project selenium-webdriver Installs to .agents/skills/selenium-webdriver/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/selenium-webdriver.zipunzip -o selenium-webdriver.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) selenium-webdriver PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $selenium-webdriver, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Writes and maintains Selenium 4 WebDriver suites in Python, Java and JavaScript - Selenium Manager driver setup, stable locators, explicit waits with expected conditions instead of sleeps or implicit waits, page objects, per-test driver lifecycle, failure screenshots, parallel runs, and Selenium Grid 4 in Docker (standalone or hub and nodes) for cross-browser CI. Use when working on an existing Selenium suite, fixing NoSuchElement, StaleElementReference or timeout flakiness, setting up Selenium Grid, or running WebDriver tests headless in CI.
Use it when
- Working on an existing Selenium WebDriver suite
- Selenium tests are flaky with element or timeout exceptions
- Setting up Selenium Grid or running WebDriver headless in CI
- Cross-browser tests must run on a device cloud via WebDriver
Use something else when
- Starting a new browser test suite in a JavaScript or TypeScript project: use playwright-e2e
- You want to automate a browser for scraping or chores, not testing: use browser-automation
How to check the result
Changed tests pass five consecutive headless runs at CI parallelism, no new sleeps or implicit waits appear, and a deliberately broken locator fails fast with a screenshot artifact.
The mistake it prevents
Mixing implicit and explicit waits or adding sleeps, which makes timeouts unpredictable and hides the real synchronisation problem.
A Selenium suite is healthy when every interaction waits for a specific condition rather
than a fixed time, locators target stable attributes, each test owns its browser session
and data, failures leave a screenshot and page source behind, and the same tests run
against a pinned Grid in CI. Selenium is the right tool for existing suites, Java-heavy
teams, real-browser vendor coverage and device clouds; for a new JavaScript project,
consider playwright-e2e first.
Inspect first
- Read
AGENTS.md, the build file (requirements*.txt/pyproject.toml,pom.xml,build.gradle,package.json) and the Selenium version. Selenium 4.6+ ships Selenium Manager, which resolves drivers and browsers automatically (it downloads them, so it needs network the first time or a pre-provisioned cache). - Find the driver factory, base test class or fixtures, page objects, wait helpers and
any
time.sleep,Thread.sleeporimplicitly_waitcalls:rg -n "sleep\(|implicitly_wait|implicitlyWait|manage\(\)\.timeouts" tests src/test. - Read CI config: headless flags, Grid URL, browser versions, parallelism, artifacts.
- Collect the failing test’s error type and screenshot before changing code.
Locators
| Prefer | Example | Avoid |
|---|---|---|
| Dedicated test attribute | By.CSS_SELECTOR, "[data-testid='checkout-submit']" | Generated classes (.css-1x2y3z) |
| Accessible id or name | By.ID, "email", By.NAME, "password" | Absolute XPath (/html/body/div[3]/...) |
| Link or button text, when copy is stable | By.XPATH, "//button[normalize-space()='Place order']" | Index-based selection ((//button)[4]) |
Add data-testid attributes to the app when no stable attribute exists; it is cheaper than
maintaining brittle XPath.
Waits
- Use explicit waits (
WebDriverWait+ expected conditions) for every element that appears, changes or becomes clickable after page load. - Leave the implicit wait at 0. Mixing implicit and explicit waits makes timeouts add up unpredictably; Selenium’s own documentation warns against it.
- Never
sleepto wait for the UI. If no expected condition fits, write a custom one (a function taking the driver and returning a truthy value).
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
wait = WebDriverWait(driver, timeout=10)
wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-testid='checkout-submit']"))).click()
wait.until(EC.text_to_be_present_in_element((By.CSS_SELECTOR, "[data-testid='order-status']"), "Confirmed"))
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector("[data-testid='checkout-submit']"))).click();
wait.until(ExpectedConditions.textToBePresentInElementLocated(By.cssSelector("[data-testid='order-status']"), "Confirmed"));
const { Builder, Browser, By, until } = require("selenium-webdriver");
const el = await driver.wait(until.elementLocated(By.css("[data-testid='checkout-submit']")), 10000);
await driver.wait(until.elementIsEnabled(el), 10000);
await el.click();
Driver lifecycle (pytest)
import os
import pytest
from selenium import webdriver
@pytest.fixture
def driver(request):
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1366,900")
grid = os.environ.get("SELENIUM_REMOTE_URL")
drv = webdriver.Remote(command_executor=grid, options=options) if grid else webdriver.Chrome(options=options)
yield drv
if getattr(request.node, "rep_call", None) and request.node.rep_call.failed:
os.makedirs("artifacts", exist_ok=True)
drv.save_screenshot(f"artifacts/{request.node.name}.png")
with open(f"artifacts/{request.node.name}.html", "w", encoding="utf-8") as f:
f.write(drv.page_source)
drv.quit()
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
rep = outcome.get_result()
setattr(item, f"rep_{rep.when}", rep)
One driver per test (or per worker with a fresh profile per test) keeps cookies and storage
from leaking. Always quit() in teardown, or Grid slots and local processes leak.
Page objects
class CheckoutPage:
SUBMIT = (By.CSS_SELECTOR, "[data-testid='checkout-submit']")
STATUS = (By.CSS_SELECTOR, "[data-testid='order-status']")
def __init__(self, driver):
self.driver = driver
self.wait = WebDriverWait(driver, 10)
def place_order(self) -> "CheckoutPage":
self.wait.until(EC.element_to_be_clickable(self.SUBMIT)).click()
self.wait.until(EC.text_to_be_present_in_element(self.STATUS, "Confirmed"))
return self
def status(self) -> str:
return self.driver.find_element(*self.STATUS).text
Page objects expose user actions and queries, not WebElements; assertions stay in tests.
Selenium Grid 4
| Need | Setup |
|---|---|
| One browser in CI | docker run -d -p 4444:4444 --shm-size=2g selenium/standalone-chrome:<pinned tag> |
| Several browsers, parallel sessions | Hub + nodes with Docker Compose |
| Elastic, per-session containers | Dynamic Grid (node-docker) or Kubernetes with the Selenium Helm chart |
| Real Safari, mobile devices, many OS versions | A device cloud that speaks WebDriver; credentials as CI secrets |
# docker-compose.grid.yml
services:
selenium-hub:
image: selenium/hub:4.35.0 # pin to the version you test with
ports: ["4442:4442", "4443:4443", "4444:4444"]
chrome:
image: selenium/node-chrome:4.35.0
shm_size: 2gb
depends_on: [selenium-hub]
environment:
SE_EVENT_BUS_HOST: selenium-hub
SE_EVENT_BUS_PUBLISH_PORT: "4442"
SE_EVENT_BUS_SUBSCRIBE_PORT: "4443"
SE_NODE_MAX_SESSIONS: "2"
firefox:
image: selenium/node-firefox:4.35.0
shm_size: 2gb
depends_on: [selenium-hub]
environment:
SE_EVENT_BUS_HOST: selenium-hub
SE_EVENT_BUS_PUBLISH_PORT: "4442"
SE_EVENT_BUS_SUBSCRIBE_PORT: "4443"
Check curl -s http://localhost:4444/status reports "ready": true before starting tests,
and keep client library and Grid on the same minor version. Image tags above are examples;
use the current release.
Common errors
| Error | Cause | Fix |
|---|---|---|
NoSuchElementException | Looked before it rendered, wrong frame, wrong locator | Explicit wait; switch_to.frame; check locator in DevTools |
StaleElementReferenceException | DOM re-rendered after you found the element | Re-locate inside the action; wait for the re-render to finish |
ElementClickInterceptedException | Overlay, sticky header, animation, cookie banner | Wait for overlay invisibility; scroll into view; dismiss banner in setup |
TimeoutException on a wait | Condition never met: real bug, or the wrong condition | Read screenshot and page source before raising the timeout |
SessionNotCreatedException | Driver/browser mismatch, Grid has no matching node | Let Selenium Manager resolve; check Grid capabilities |
| Chrome crashes in Docker | /dev/shm too small | --shm-size=2g or --disable-dev-shm-usage |
Verification
- Run the changed tests five times headless locally or against the Grid, and once with the CI worker count; all green.
- Break the feature on purpose (rename the test id) and confirm a fast, clear failure with a screenshot artifact.
rg "sleep\(|implicitly_wait"in the suite returns nothing new.
Gotchas
--headless=newis Chrome’s current headless mode; the old headless mode behaved differently and has been removed from Chrome.- Grid sessions that are never quit hold slots until
SE_NODE_SESSION_TIMEOUT; parallel runs then queue and time out. - Tests that log in through the UI every time are slow; set the session cookie via
add_cookieafter an API login where the app allows it. - Running against shared staging with real accounts can send real emails or payments; use dedicated test accounts and sandboxed integrations.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Selenium WebDriver"
short_description: "Explicit waits, stable locators, a pinned Grid"
default_prompt: "$selenium-webdriver Our Selenium tests fail with StaleElementReferenceException in CI, fix them" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.