This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Guides

Task-based guides: start from the problem you have and follow the links into the reference pages that solve it.

The rest of this documentation is organised by API surface: elements, interactions, waits, BiDi, and so on. That is the right shape for looking something up once you know what it is called, but most people arrive with a problem rather than a method name.

The guides in this section are organised by task instead. Each one states the problem, describes the approach the Selenium project recommends, shows the shortest working example for each language binding, lists the errors you are most likely to hit along the way, and links to the reference pages that cover the detail. They do not replace the reference pages; they are the fastest route into them.

If you are new to Selenium altogether, start with Getting started and come back here once you have a first script running.

1 - Waiting for things to happen

Synchronising your code with the page using explicit waits and expected conditions, and why a fixed sleep is the wrong tool.

Your script clicks a button, and the next line fails because the element it needs has not appeared yet. Run it again and it passes. Modern applications update the page asynchronously, so the browser and your code are in a race, and the loser is a flaky test.

Use an explicit wait: a loop that polls the application for a specific condition and continues as soon as that condition is true, or raises a timeout error if it never is. You decide what to wait for at each point where synchronisation matters, so the wait is exactly as long as it needs to be and no longer.

Most bindings ship a library of ready-made expected conditions (element visible, title contains, alert present, and so on) that you can pass to the wait instead of writing your own lambda. Each of them is a function that is called repeatedly until it returns a truthy value.

A fixed sleep is the first thing many people reach for, and it is nearly always wrong. It is either too short, and the test fails on a slow machine, or too long, and every test in the suite pays the full price every time. Likewise, do not combine an implicit wait with explicit waits: the two timeouts add up in ways that are hard to predict.

This example waits for an element that is revealed a couple of seconds after a click:

    Wait<WebDriver> wait = new WebDriverWait(driver, Duration.ofSeconds(2));
    wait.until(d -> revealed.isDisplayed());
    wait = WebDriverWait(driver, timeout=2)
    wait.until(lambda _ : revealed.is_displayed())
            WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(2));
            wait.Until(d => revealed.Displayed);
    wait = Selenium::WebDriver::Wait.new
    wait.until { revealed.displayed? }
        await driver.wait(until.elementIsVisible(revealed), 2000);
        val wait =  WebDriverWait(driver, Duration.ofSeconds(2))
        wait.until { revealed.isDisplayed }

The same wait expressed with a built-in expected condition:

    wait = WebDriverWait(driver, timeout=2)
    wait.until(EC.visibility_of_element_located((By.ID, "revealed")))

Common errors

Reference

2 - Finding elements reliably

Choosing a locator strategy that survives the next release of the application, and avoiding the ones that do not.

A locator copied out of the browser’s developer tools works today and breaks the first time a designer moves a div. Tests then fail for reasons that have nothing to do with the behaviour they were written to check, and the suite stops being trusted.

Prefer locators that describe what an element is over locators that describe where it happens to sit in the document, roughly in this order:

  1. id, when the page has stable, unique ids. This is the fastest and most readable choice.
  2. name, for form fields, which tend to keep their names because the server depends on them.
  3. CSS selector, for everything else. A short selector anchored on an id, a data attribute, or a meaningful class is both fast and expressive.
  4. Relative locators, when the target has no stable attributes of its own but a nearby element does: find the input below a given label, or the button to the right of a field.

Avoid absolute XPath (/html/body/div[3]/...). It encodes the entire structure of the page, so any change above the element breaks it, and it is slow in some browsers. XPath itself is fine when you need it, for example to match on text content, but keep the expression short and anchored.

If the application does not expose stable attributes, ask for them. A data-test attribute costs the developers nothing and removes an entire class of failures.

Locate the first matching element:

    WebElement firstInput = driver.findElement(By.className("information"));
# same examples are shown for the other language bindings:
#
            driver.Url = LocatorsPage;
            IWebElement firstInput = driver.FindElement(By.ClassName("information"));
      driver.find_element(class: 'tomatoes')
    end
    await driver.get(LOCATORS_PAGE);
    const firstInput = await driver.findElement(By.className('information'));
        driver.get(locatorsPage)
        val firstInput = driver.findElement(By.className("information"))

Locate by id, the preferred strategy when one is available:

    }
    element = driver.find_element(By.ID, "lname")
    driver.find_element(id: 'lname')
    const element = await driver.findElement(By.id('lname'));

Locate relative to another element:

      driver.find_element({relative: {tag_name: 'input', above: {id: 'password'}}})

Common errors

Reference

3 - Authentication and sessions

Getting past a login once, answering HTTP basic authentication prompts, and reusing a signed-in state across tests without driving the login form every time.

Almost every test starts behind a login. Driving the login form in each test is slow, it makes every test depend on the login page working, and some forms (two-factor prompts, third-party identity providers, CAPTCHAs) cannot be automated at all. HTTP basic authentication is a different problem again: the browser shows a native dialog that WebDriver cannot see.

Log in through the user interface in exactly one test, the one that tests the login form. Everywhere else, establish the session some other way and start the test already signed in.

For cookie-based sessions, navigate to the application’s domain, add the session cookie with WebDriver’s cookie API, and reload. The cookie must be added while the browser is on a page in the domain the cookie belongs to, otherwise the browser rejects it. Where the cookie comes from is up to you: an API call that logs in, a fixture created on the server, or a value captured from a single interactive login.

For HTTP basic authentication, use the network module of WebDriver BiDi. Register a handler for the authRequired phase and answer it with the credentials; the browser never shows the dialog. Embedding credentials in the URL (https://user:pass@host/) is blocked by modern browsers and should not be relied on.

Keep tests independent: each test should set up the state it needs rather than inheriting it from the one before, and a fresh browser per test is the simplest way to guarantee that.

Add a cookie to the current browsing context:

        driver.get("https://www.selenium.dev/selenium/web/blank.html");
        // Add cookie into current browser context
        driver.manage().addCookie(new Cookie("key", "value"));
    driver = webdriver.Chrome()
    driver.get("http://www.example.com")

    # Adds the cookie into current browser context
    driver.add_cookie({"name": "key", "value": "value"})
         driver.Url="https://www.selenium.dev/selenium/web/blank.html";
         // Add cookie into current browser context
         driver.Manage().Cookies.AddCookie(new Cookie("key", "value"));
    driver.navigate.to 'https://www.selenium.dev/selenium/web/blank.html'
    # Add cookie into current browser context
    driver.manage.add_cookie(name: 'key', value: 'value')
    await driver.get('https://www.selenium.dev/selenium/web/blank.html');

    // set a cookie on the current domain
    await driver.manage().addCookie({ name: 'key', value: 'value' });

Answer a basic authentication challenge with BiDi:

            network.addIntercept(new AddInterceptParameters(InterceptPhase.AUTH_REQUIRED));
            network.onAuthRequired(
                    responseDetails ->
                            network.continueWithAuth(
                                    responseDetails.getRequest().getRequestId(),
                                    new UsernameAndPassword("admin", "admin")));
    await network.authRequired(async (event) => {
      await network.continueWithAuth(event.request.request, 'admin','admin')
    })

Common errors

Reference

4 - Files: upload and download

Uploading a file through a form without touching the operating system’s file dialog, and why downloading through the browser is not the right way to test a download.

Clicking a file input opens the operating system’s file chooser, which is outside the browser and invisible to WebDriver. Clicking a download link starts a download the API cannot watch: there is no event for “finished”, no progress, and no portable way to find the file afterwards.

Uploads. Do not click the input. Send the absolute path of the file to the <input type="file"> element with sendKeys, then submit the form as usual. The browser attaches the file exactly as if it had been chosen through the dialog. The path must exist on the machine where the browser is running; when the browser is remote, on a Grid, set a local file detector so the client uploads the file to the node first.

Downloads. Testing a download through the browser is discouraged. The browser’s download behaviour is configurable per browser and per platform, the file lands in a directory you then have to poll, and none of this is what a user actually cares about. Instead, use Selenium to get to the point where the download link exists, read its href (and any cookies the request needs), and fetch it with an HTTP client in your test language. That gives you the bytes directly, with a proper status code, and no polling.

If you are running on Selenium Grid and really need the browser to perform the download, Grid can be configured to collect downloaded files from the node and the client can retrieve them; see the remote WebDriver page below.

Upload a file by sending its path to the input:

    WebElement fileInput = driver.findElement(By.cssSelector("input[type=file]"));
    fileInput.sendKeys(uploadFile.getAbsolutePath());
    driver.findElement(By.id("file-submit")).click();
    file_input = driver.find_element(By.CSS_SELECTOR, "input[type='file']")
    file_input.send_keys(upload_file)
    driver.find_element(By.ID, "file-submit").click()
            IWebElement fileInput = driver.FindElement(By.CssSelector("input[type=file]"));
            fileInput.SendKeys(uploadFile);
            driver.FindElement(By.Id("file-submit")).Click();
    file_input = driver.find_element(css: 'input[type=file]')
    file_input.send_keys(upload_file)
    driver.find_element(id: 'file-submit').click
    await driver.findElement(By.id("file-upload")).sendKeys(image);
    await driver.findElement(By.id("file-submit")).submit();

Common errors

  • InvalidArgumentException: the path does not exist on the machine running the browser. Use an absolute path, and a file detector when the browser is remote.
  • ElementNotInteractableException: the file input is hidden behind a styled button. Sending keys to a hidden file input is allowed in most browsers; if it is refused, check the strictFileInteractability capability.

Reference

5 - Dialogs and alerts

Reading, accepting, dismissing and typing into JavaScript alerts, confirms and prompts, and deciding what should happen to a dialog your test did not expect.

A page calls alert(), confirm() or prompt(), the browser shows a native dialog, and every subsequent WebDriver command fails until something closes it. If the dialog appears when your test is not expecting it, the failure is an unexpected alert open error several lines away from the cause.

All three dialogs are handled through the same object: switch to the alert, then read its text, accept it, dismiss it, or send keys to it. alert() can only be accepted; confirm() returns true to the page when accepted and false when dismissed; prompt() additionally accepts typed text before being accepted.

Treat the dialog like any other asynchronous event and wait for it. The bindings ship an alertIsPresent expected condition that returns the alert as soon as it exists, which is both safer and simpler than switching and catching the error.

For dialogs that may appear when no test code is ready to handle them, set the unhandledPromptBehavior capability when starting the session. dismiss and accept close the dialog silently; dismiss and notify (the default) and accept and notify close it and raise an error so you find out; ignore leaves it open for you to handle. Recent drivers close beforeunload prompts automatically.

With WebDriver BiDi enabled, the browsing context module also emits userPromptOpened and userPromptClosed events, so a test can react to a dialog the moment it appears rather than discovering it on the next command.

Switch to an alert, read its text, and close it:

        wait.until(ExpectedConditions.alertIsPresent());
        Alert alert = driver.switchTo().alert();
        Assertions.assertEquals("cheese", alert.getText());
        alert.accept();
    alert = wait.until(lambda d : d.switch_to.alert)
    text = alert.text
    alert.accept()
    assert text == "Sample alert"
    alert = driver.switch_to.alert

    # Get the text of the alert
    alert.text

    # Press on Cancel button
    alert.dismiss
        let alert = await driver.switchTo().alert();
        let alertText = await alert.getText();
        await alert.accept();

Type into a prompt before accepting it:

        wait.until(ExpectedConditions.alertIsPresent());

        Alert alert = driver.switchTo().alert();
        Assertions.assertEquals("Enter something", alert.getText());

        alert.sendKeys("Selenium");
        alert.accept();
    alert = wait.until(lambda d : d.switch_to.alert)
    alert.send_keys("Selenium")
    text = alert.text
    alert.accept()
    assert text == "What is your tool of choice?"
    alert = driver.switch_to.alert

    # Type a message
    alert.send_keys('selenium')

    # Press on Ok button
    alert.accept
        let alert = await driver.switchTo().alert();
        //Type your message
        await alert.sendKeys(text);
        await alert.accept();

Decide up front what happens to an unexpected dialog:

	chromeOptions.setUnhandledPromptBehaviour(UnexpectedAlertBehaviour.DISMISS_AND_NOTIFY);
	//verify the capability object is not null
    options = get_default_chrome_options()
    options.unhandled_prompt_behavior = 'accept'
    driver = webdriver.Chrome(options=options)
      options = Selenium::WebDriver::Options.chrome
      options.unhandled_prompt_behavior = :accept

Common errors

  • NoAlertPresentException: you switched to an alert before it appeared, or the session’s prompt handler already closed it. Wait for it with alertIsPresent, and check unhandledPromptBehavior.
  • UnhandledAlertException: a dialog opened that nothing handled, and the driver closed it before running your command. Either handle the dialog in the test or change the prompt handler.

Reference

6 - Frames and windows

Reaching elements inside an iframe, following a link that opens a new tab or window, and getting back to where you started.

The element is plainly visible on the page, the locator is correct, and findElement still reports that it does not exist. The element is inside an <iframe>, or on a tab the link just opened, and WebDriver is still looking at the document it started in.

WebDriver always has exactly one current browsing context: one window or tab, and within it one frame. Commands only see that context. Switching is explicit, and you are responsible for switching back.

Frames. Locate the <iframe> element like any other element and switch to it. You can also switch by the frame’s name or id, or by index, but a located element is the least ambiguous. When you are done, switch to the default content to return to the top-level document. A frame reference goes stale when the page reloads, so switch again after navigation.

Windows and tabs. Each window or tab has a handle, an opaque string. Record the current handle before clicking a link that opens a new one, wait until the number of handles grows, switch to the handle you have not seen before, and switch back to the original handle when you are finished. WebDriver makes no distinction between a tab and a window. If you just need a clean tab, open one yourself with newWindow; it is created already switched to.

Closing the current window leaves the session pointing at nothing, so switch to another handle immediately after close. Call quit, not close, at the end of the test; it closes every window and ends the session.

Switch into a frame using a located element:

         WebElement iframe = driver.findElement(By.id("iframe1"));
         //Switch to the frame
         driver.switchTo().frame(iframe);
iframe = driver.find_element(By.ID, "iframe1")
driver.switch_to.frame(iframe)
            IWebElement iframe = driver.FindElement(By.Id("iframe1"));
            //Switch to the frame
            driver.SwitchTo().Frame(iframe);
    iframe = driver.find_element(:id, 'iframe1')
    driver.switch_to.frame(iframe)

Return to the top-level document:

         driver.switchTo().defaultContent();
driver.switch_to.default_content()
            driver.SwitchTo().DefaultContent();
    driver.switch_to.default_content

Follow a link into a new window and switch to it:

        //click on link to open a new window
        driver.findElement(By.linkText("Open new window")).click();
        //fetch handles of all windows, there will be two, [0]- default, [1] - new window
        Object[] windowHandles=driver.getWindowHandles().toArray();
        driver.switchTo().window((String) windowHandles[1]);
        //assert on title of new window
        String title=driver.getTitle();
        assertEquals("Simple Page",title);
    original_window_handle = driver.current_window_handle

    driver.find_element(By.LINK_TEXT, "Open new window").click()
    wait.until(EC.number_of_windows_to_be(2))

    new_window_handle = (set(driver.window_handles) - {original_window_handle}).pop()
    driver.switch_to.window(new_window_handle)
            //click on link to open a new window
            driver.FindElement(By.LinkText("Open new window")).Click();
            //fetch handles of all windows, there will be two, [0]- default, [1] - new window
            IList<string> windowHandles = new List<string>(driver.WindowHandles);
            driver.SwitchTo().Window(windowHandles[1]);
            //assert on title of new window
            String title = driver.Title;
            Assert.AreEqual("Simple Page", title);

Open a fresh tab and switch to it in one step:

    driver.switch_to.new_window(:tab)
    await driver.switchTo().newWindow('tab');

Common errors

Reference

7 - Screenshots and PDFs

Capturing the viewport, a single element or the whole page as an image, and printing a page to PDF.

A test fails on the CI server and the stack trace says an element was not found. What was on the screen at the time? Without a picture, the first debugging step is to re-run the test locally and hope it fails the same way. A screenshot taken at the moment of failure turns that guess into evidence, and the same API is the basis of visual comparison and archiving.

WebDriver’s screenshot command returns the current viewport as a PNG, Base64-encoded. Every binding wraps that so you can save it straight to a file. Calling the same command on an element rather than the driver crops the image to that element’s bounding box, which is the right choice when you want to compare a component rather than a whole page.

A viewport screenshot captures only what is visible without scrolling. Firefox supports a full-page screenshot through its own driver API; with WebDriver BiDi enabled, the browsing context module’s captureScreenshot command works across browsers and can also capture a box or an element.

To produce a PDF rather than an image, use the print command. It takes a PrintOptions object for orientation, page ranges, size, margins, scale and whether to print background graphics, and returns the document Base64-encoded, ready to decode and write to a file.

Attach a screenshot to every failure in your test framework’s teardown hook: it is cheap, and the one time you need it you will be glad it is there.

Take a screenshot of the viewport:

    let encodedString = await driver.takeScreenshot();

Take a screenshot of one element:

    let header = await driver.findElement(By.css('h1'));
    // Captures the element screenshot
    let encodedString = await header.takeScreenshot(true);

Take a full-page screenshot in Firefox:

    File screenshot = driver.getFullPageScreenshotAs(OutputType.FILE);
    driver.save_full_page_screenshot("full_page_screenshot.png")
        screenshot = driver.save_full_page_screenshot(File.join(dir, 'screenshot.png'))

Capture a screenshot through BiDi:

        Assertions.assertFalse(screenshot.isEmpty());
        var screenshot = await context.CaptureScreenshotAsync();
    assert.equal(base64code, pngMagicNumber)

Print the page to PDF:

        PrintsPage printer = (PrintsPage) driver;
        PrintOptions printOptions = new PrintOptions();
        Pdf printedPage = printer.print(printOptions);
    print_options = PrintOptions()
    pdf = driver.print_page(print_options)
            PrintOptions printOptions = new PrintOptions();
            PrintDocument printedPage = driver.Print(printOptions);
    let base64 = await driver.printPage({pageRanges: ["1-2"]});

Common errors

  • ScreenshotException: the browser could not capture the screen, usually because the window is minimised or the element is not rendered. Maximise the window or scroll the element into view.
  • StaleElementReferenceException: the element being captured was re-rendered between locating it and taking the screenshot.

Reference

8 - Network interception and mocking

Watching the requests a page makes, blocking or failing some of them, and answering authentication challenges, using the W3C WebDriver BiDi network module.

You want to assert that the page called an API, fail a request to see how the application copes, block the analytics beacon that slows every test down, or supply credentials to a basic authentication challenge. Plain WebDriver only sees the DOM; it has no view of the traffic behind it.

Use the network module of WebDriver BiDi. It is part of the W3C standard, it works in every browser that implements BiDi, and it is the Selenium project’s supported route for anything that needs to observe or change network traffic.

The module has two halves. Events (beforeRequestSent, responseStarted, responseCompleted, authRequired) let you subscribe to traffic and record or assert on it. Intercepts pause requests at a chosen phase so your handler can continue them, fail them, or, at the authRequired phase, continue them with a username and password. Enable BiDi in the browser options when starting the session; the reference page shows the capability for each binding.

Keep interception narrow. Match the URL patterns you care about, remove the intercept when the test is done, and prefer failing or stubbing an external dependency over letting the test talk to a live third-party service.

The Chrome DevTools Protocol integration that Selenium offered before BiDi existed is Chromium-only, tied to specific browser versions, and being phased out. Do not start new work on it.

Subscribe to requests as they are sent:

        try (Network network = new Network(driver)) {
            CompletableFuture<BeforeRequestSent> future = new CompletableFuture<>();
            network.onBeforeRequestSent(future::complete);
            driver.get("https://www.selenium.dev/selenium/web/bidi/logEntryAdded.html");

            BeforeRequestSent requestSent = future.get(5, TimeUnit.SECONDS);
        requests.append(request)

    callback_id = driver.network.add_request_handler("before_request", callback)
    assert callback_id is not None
    const network = await Network(driver)
    await network.beforeRequestSent(function (event) {
      beforeRequestEvent.push(event)
    })

Intercept a request and fail it:

            network.addIntercept(new AddInterceptParameters(InterceptPhase.BEFORE_REQUEST_SENT));
            network.onBeforeRequestSent(
                    responseDetails -> network.failRequest(responseDetails.getRequest().getRequestId()));
        request.fail()

    driver.network.add_request_handler(["**/blank.html"], block_request)

Answer a basic authentication challenge:

            network.addIntercept(new AddInterceptParameters(InterceptPhase.AUTH_REQUIRED));
            network.onAuthRequired(
                    responseDetails ->
                            network.continueWithAuth(
                                    responseDetails.getRequest().getRequestId(),
                                    new UsernameAndPassword("admin", "admin")));
    await network.authRequired(async (event) => {
      await network.continueWithAuth(event.request.request, 'admin','admin')
    })

Common errors

  • TimeoutException: an intercepted request was never continued or failed, so navigation hung until the page load timeout. Every intercepted request needs a handler that resolves it.
  • UnsupportedCommandException: BiDi was not enabled for the session, or the browser does not implement the command yet.

Reference

9 - Console and logs

Capturing the browser’s console output and JavaScript errors with BiDi, and turning up Selenium’s own logging to see what the bindings and the driver are doing.

Something went wrong, and the only evidence is a failed assertion. There are three separate streams of information that could explain it: what the page wrote to the browser console, what exception JavaScript threw, and what Selenium itself was sending to and receiving from the driver. By default you see none of them.

Browser console and JavaScript errors. With WebDriver BiDi enabled, the log module streams console entries and uncaught JavaScript exceptions to your test as they happen. Register a handler before the action you want to observe, then assert on the entries it collects. This is the cross-browser, standards-based replacement for the old Chromium-only log retrieval, and it catches errors the page swallowed before a user would have seen them.

Selenium’s own logging. Each binding logs through its language’s standard logging facility: java.util.logging in Java, the logging module in Python, Selenium’s Log class in .NET, and Selenium::WebDriver.logger in Ruby. Raising the level to debug shows every command sent to the driver and every response, which is usually enough to tell whether the problem is in your code, the driver or the browser. Logs go to the console by default; add a file handler to keep them with the rest of your test artifacts.

Driver logs. The browser driver (chromedriver, geckodriver, and so on) keeps its own log, configured through the driver’s service class. Reach for it when the browser fails to start or crashes mid-session.

Collect console messages with BiDi:

    @Test
    void testListenToConsoleLog() throws ExecutionException, InterruptedException, TimeoutException {
        try (LogInspector logInspector = new LogInspector(driver)) {
            CompletableFuture<ConsoleLogEntry> future = new CompletableFuture<>();
            logInspector.onConsoleEntry(future::complete);

            driver.get("https://www.selenium.dev/selenium/web/bidi/logEntryAdded.html");
            driver.findElement(By.id("consoleLog")).click();

            ConsoleLogEntry logEntry = future.get(5, TimeUnit.SECONDS);
    driver.script.add_console_message_handler(log_entries.append)

    driver.find_element(By.ID, "consoleLog").click()
    WebDriverWait(driver, 5).until(lambda _: log_entries)
    assert log_entries[0].text == "Hello, world!"
    const inspector = await LogInspector(driver)
    await inspector.onConsoleEntry(function (log) {
      logEntry = log
    })

    await driver.get('https://www.selenium.dev/selenium/web/bidi/logEntryAdded.html')
    await driver.findElement({id: 'consoleLog'}).click()

    assert.equal(logEntry.text, 'Hello, world!')

Collect JavaScript exceptions with BiDi:


            driver.get("https://www.selenium.dev/selenium/web/bidi/logEntryAdded.html");
            driver.findElement(By.id("jsException")).click();

            JavascriptLogEntry logEntry = future.get(5, TimeUnit.SECONDS);
    driver.script.add_javascript_error_handler(log_entries.append)

    driver.find_element(By.ID, "jsException").click()
    WebDriverWait(driver, 5).until(lambda _: log_entries)
    assert log_entries[0].text == "Error: Not working"
    const inspector = await LogInspector(driver)
    await inspector.onJavascriptException(function (log) {
      logEntry = log
    })

    await driver.get('https://www.selenium.dev/selenium/web/bidi/logEntryAdded.html')
    await driver.findElement({id: 'jsException'}).click()

    assert.equal(logEntry.text, 'Error: Not working')

Turn up Selenium’s own logging:

        Logger logger = Logger.getLogger("");
        logger.setLevel(Level.FINE);
        Arrays.stream(logger.getHandlers()).forEach(handler -> {
            handler.setLevel(Level.FINE);
    logger = logging.getLogger('selenium')

    logger.setLevel(logging.DEBUG)
            Log.SetLevel(LogEventLevel.Trace);
    logger = Selenium::WebDriver.logger

    logger.level = :debug

Common errors

  • JavascriptException: a script you executed through WebDriver threw. The BiDi log module shows the page’s own errors, which this exception does not cover.
  • UnsupportedCommandException: BiDi was not enabled for the session, so the log module is unavailable.

Reference

10 - Running in CI

Running a Selenium suite on a build server: headless browsers, driver management without manual downloads, and when to move to Grid or Docker.

The suite passes on your laptop and fails on the build server. The server has no display, a different browser version, no driver on the PATH, and a locked-down network. Each of those is a separate failure with a separate fix, and the error messages rarely say which one you have hit.

No display. Run the browser headless. Chrome and Edge take the --headless=new argument, Firefox takes -headless. The page renders the same way, so only the browser options change. Keep headless behind a configuration flag so you can still watch a test locally.

No driver. Do not download driver binaries in your pipeline. Selenium Manager, which ships with Selenium 4.6 and later, detects the installed browser, downloads the matching driver, and caches it in ~/.cache/selenium; cache that directory between runs. Some hosted runners pre-install drivers on the PATH, and a stale one there takes precedence over Selenium Manager, so remove it or keep it current.

Locked-down network. Selenium Manager needs to reach the browser vendors’ download endpoints the first time. Behind a proxy, set it with the SE_PROXY environment variable or the Selenium Manager configuration file.

Scale. When one runner starting one browser per test becomes too slow, point the same tests at a Selenium Grid and run them in parallel without changing the test code.

Collect a screenshot and the Selenium log on every failure; a CI failure without artifacts is a re-run waiting to happen.

Start a headless browser:

    options.addArguments("-headless");
    options.add_argument("-headless")
            options.AddArgument("-headless");
      options.args << '-headless'
    let driver = new Builder()
      .forBrowser(Browser.CHROME)
      .setChromeOptions(options.addArguments('--headless=new'))
      .build();

A minimal GitHub Actions workflow, shown for Python, is a starting point only. The hosted Ubuntu runner already has Chrome and Firefox installed; the only Selenium-specific step is installing the binding:

name: tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install selenium pytest
      - run: pytest

The same shape applies to every binding: check out, install the language toolchain, install the Selenium package, run the standard test command.

Common errors

  • Unable to locate driver: Selenium Manager could not find or download a driver. Check the browser is installed and the network allows the download.
  • SessionNotCreatedException: the driver on the PATH does not match the installed browser, or the browser cannot start without a display. Remove stale drivers and run headless.
  • InvalidArgumentException: a browser option is wrong for this browser or version, for example a user data directory that is already in use by another session.

Reference

11 - Running in Docker and Grid

Moving the browser off the machine that runs the tests, with Selenium Grid and the project’s Docker images, so a suite can run in parallel and against several browsers.

A suite that takes an hour because every test starts its own browser on the same machine. A requirement to test on a browser that is not installed where the tests run. A build server that must not have a browser on it at all. In each case the browser needs to be somewhere other than the test process.

Selenium Grid is a server that accepts WebDriver sessions over HTTP and routes them to browsers it manages. Your test code changes in exactly one place: instead of constructing a local driver, construct a remote one with the Grid URL and the browser options. Everything else stays the same, so a suite can run locally during development and on a Grid in CI with a single configuration switch.

Standalone mode runs the whole Grid in one process and is the right starting point. It needs a Java runtime and the Selenium Server jar, listens on http://localhost:4444, and can use Selenium Manager to fetch drivers for the browsers it finds. Hub and Node spreads browsers across several machines, and distributed mode separates each Grid component for large installations.

The quickest way to get a Grid, and the only way to get one with no browser on the host, is the project’s own Docker images in the docker-selenium repository. They package the Grid, a browser and its driver, and are published for each Selenium release. A single docker run gives you a standalone Grid with Chrome, Firefox or Edge; the repository also has Docker Compose and Kubernetes examples, video recording, and VNC for watching a session.

When the browser is remote, files to upload must reach the node first, through a file detector, and downloads must be enabled on the Grid and fetched back through the client. Both are covered on the remote WebDriver page.

Point a test at a Grid:

    ChromeOptions options = getDefaultChromeOptions();
    driver = new RemoteWebDriver(gridUrl, options);
    options = get_default_chrome_options()
    driver = webdriver.Remote(command_executor=server, options=options)
            var options = new ChromeOptions();
            driver = new RemoteWebDriver(GridUrl, options);
    options = Selenium::WebDriver::Options.chrome
    driver = Selenium::WebDriver.for :remote, url: grid_url, options: options
    const options = new Chrome.Options().addArguments('--no-sandbox')
    driver = new Builder().forBrowser(Browser.CHROME).setChromeOptions(options).usingServer(gridUrl).build()

Common errors

  • SessionNotCreatedException: no node on the Grid can satisfy the requested browser and version. Check the Grid’s status page at the Grid URL for the capabilities it offers.
  • InvalidArgumentException: a local file path was sent to a remote browser. Set a file detector so the file is uploaded.
  • WebDriverException with a connection error: the Grid URL is wrong, the server is not running, or a firewall is between the client and the Grid.

Reference

12 - Debugging a failing test

A short procedure for working out why a Selenium test failed: read the exception, gather evidence, and narrow the cause before changing anything.

A test that used to pass now fails, or passes locally and fails in CI, or fails one run in five. The temptation is to add a sleep, re-run, and move on. That hides the cause, and the test comes back.

Read the exception first. The W3C WebDriver specification defines a fixed set of errors, and each one points at a specific cause. No such element means the locator matched nothing in the current frame at that moment. Stale element reference means the page replaced the element after you found it. Element click intercepted means something was drawn on top of it. Session not created means the browser never started. The common errors page lists every one with its likely cause and fixes; start there before reading any code.

Gather evidence at the moment of failure. Add a hook to your test framework that, on failure, saves a screenshot alongside the Selenium log. The screenshot shows what the page looked like; the log shows what Selenium sent and what the driver replied. With BiDi enabled you can also collect the browser console and any JavaScript exceptions, which often explain a page that looks right but does not respond.

Narrow the cause. Ask, in order:

  1. Is it timing? Most intermittent failures are. Replace any sleep with an explicit wait for the condition the next line depends on, and check that an implicit wait is not also set.
  2. Is it the locator? Try it in the browser’s developer tools, and check whether the element is inside a frame or a shadow root.
  3. Is it the environment? A different browser version, a stale driver on the PATH, no display on the build server, or a blocked download endpoint all produce failures that never happen locally.
  4. Is it the application? Then the test has found a bug, which is what it was for.

Reproduce before you fix. Run the test on its own, then in the full suite. A test that only fails in the suite is sharing state with another test, usually through a reused browser or a cookie left behind.

Common errors

The common errors page covers each error in detail. The ones you will meet most often:

Reference

13 - Migrating from Selenium 3

What changes when a Selenium 3 suite moves to Selenium 4, and where to find the step-by-step upgrade guide.

A suite written against Selenium 3 still compiles, or still installs, but sessions fail to start, deprecation warnings fill the log, and the driver you used to download by hand no longer matches the browser. The fix is to upgrade, and for the officially supported languages the upgrade is usually painless.

Follow the upgrade guide. It walks through preparing the test code, updating the dependency for each binding, and the deprecation messages you may see afterwards. The changes it covers fall into a few groups.

Capabilities. Selenium 4 removed the legacy JSON Wire Protocol and speaks only the W3C WebDriver standard. Capabilities must be W3C compliant: the standard ones (browserName, browserVersion, platformName, acceptInsecureCerts, pageLoadStrategy, proxy, timeouts, unhandledPromptBehavior) are allowed as they are, and anything else, including browser-specific and cloud-vendor options, needs a vendor prefix. Desired Capabilities are gone; build the session from the browser’s Options class instead.

Finding elements in Java. The findElementBy* and findElementsBy* convenience methods were removed. Use findElement(By.id(...)) and the other By locators.

Deprecations by binding. In Java, timeouts take a Duration rather than a number and a unit, merge on capabilities returns a new object instead of mutating the caller, the legacy Firefox driver option is deprecated, and BrowserType gives way to Browser. In C#, AddAdditionalCapability is replaced by AddAdditionalOption. In Python, the driver’s executable_path argument is deprecated in favour of a Service object, or no path at all.

Drivers. Once on a recent Selenium 4 release you no longer need to download drivers or use a third-party driver manager: Selenium Manager resolves and installs the matching driver automatically. Remove any manual driver management from the suite once the upgrade is done.

Older material on the web, and the legacy section of this documentation, describes Selenium 3 ways of working. It is kept for historical reference and should not be used as a basis for new code.

Common errors

Reference