Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use Selenium’s By locator with findElement when you need one match, or findElements when zero or more matches are valid. In Selenium 3, the practical order is usually a unique ID, a concise CSS selector, a stable name or class, link text, tag name, and finally XPath for relationships or conditions CSS cannot express.

The examples below show the legacy PhantomJS 2.1.1 integration in JavaScript and Python, explain why elements are missed, and show how to migrate the same locators to a maintained headless browser.

What “finding an element” means in Selenium 3

Selenium does not search for visible text in the abstract. It queries the page’s DOM using a locator strategy. The By object describes that strategy, and the WebDriver call performs the search.

  • findElement returns the first matching element. If nothing matches, Selenium raises a no-such-element error.
  • findElements returns a collection. If nothing matches, the collection is empty, which is useful when zero matches is a normal result.

Finding and interacting are separate operations. An element can exist in the DOM yet be hidden, covered by another element, disabled, or outside the current iframe, so a successful lookup does not guarantee that a click or keystroke will work.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Legacy setup: Selenium 3 with PhantomJS 2.1.1

PhantomJS 2.1.1 is a headless browser built on an older WebKit engine. Its embedded GhostDriver can expose a WebDriver endpoint with:

phantomjs --webdriver=8910

The documented default endpoint is 127.0.0.1:8910. PhantomJS 2.1 was released on January 23, 2016. Selenium later deprecated and removed native PhantomJS support because its WebDriver implementation was no longer actively developed. Treat the recipes here as maintenance guidance for an existing Selenium 3 suite, not a good foundation for a new test project.

For a new suite, use a maintained headless Chrome or Firefox driver. The locator concepts—By, findElement, and findElements—transfer directly, so migration normally changes browser setup rather than every selector.

JavaScript: locate IDs, CSS selectors, and collections

This Selenium 3-style example opens a page, finds a username by ID, finds a password field with CSS, and collects every result element.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const {Builder, By} = require('selenium-webdriver');

(async function () {
  const driver = await new Builder().forBrowser('phantomjs').build();
  try {
    await driver.get('https://example.test/login');

    const username = await driver.findElement(By.id('username'));
    const password = await driver.findElement(By.css('input[name="password"]'));
    const results = await driver.findElements(By.css('.result'));

    await username.sendKeys('alice');
    await password.sendKeys('secret');
    console.log(`Found ${results.length} result elements`);
  } finally {
    await driver.quit();
  }
})();

The phantomjs browser target depends on a Selenium 3-era JavaScript binding and a compatible PhantomJS installation. A current Selenium package may no longer include this target, so pin the versions required by the legacy project instead of assuming that the newest package will work.

Python: the Selenium 3 PhantomJS binding

In Python environments that still expose the PhantomJS binding, use the By-based API:

from selenium import webdriver
from selenium.webdriver.common.by import By

# In Selenium 3 environments that still expose the PhantomJS binding:
driver = webdriver.PhantomJS(executable_path='/path/to/phantomjs')
try:
    driver.get('https://example.test/login')
    username = driver.find_element(By.ID, 'username')
    password = driver.find_element(By.CSS_SELECTOR, 'input[name="password"]')
    results = driver.find_elements(By.CSS_SELECTOR, '.result')
    print(len(results))
finally:
    driver.quit()

Use the current project’s exact Selenium 3-compatible package and binary versions. If webdriver.PhantomJS is missing, that is a support-status problem rather than a selector problem; migrate the driver rather than trying random locator changes.

Choose the locator that will survive page changes

Strategy JavaScript Python Best use Maintenance notes
ID By.id('username') By.ID, 'username' A unique, stable HTML id Preferred when the ID is genuinely unique and does not change on every render.
CSS selector By.css('form input[name="email"]') By.CSS_SELECTOR, 'form input[name="email"]' Compact combinations of tags, classes, IDs, and attributes Usually the best fallback; scope it to a stable container.
Name By.name('email') By.NAME, 'email' Inputs with a stable name attribute Confirm that repeated names are intentional.
Class name By.className('information') By.CLASS_NAME, 'information' One class token shared by a component Do not pass a compound string such as "card primary" to the traditional class-name strategy; use CSS for multiple classes.
Link text By.linkText('Sign in') By.LINK_TEXT, 'Sign in' An anchor whose complete rendered text is stable Applies to links, and exact wording changes can break it.
Partial link text By.partialLinkText('Sign') By.PARTIAL_LINK_TEXT, 'Sign' An anchor when only part of its text is stable Broad matches can select the wrong link.
Tag name By.tagName('button') By.TAG_NAME, 'button' Collecting a set of elements by semantic tag Often matches many nodes; combine with a container or filter.
XPath By.xpath('//form//input[@name="email"]') By.XPATH, '//form//input[@name="email"]' Relationships, conditions, and structures CSS cannot express Powerful but generally harder to read and debug.

Prefer a stable ID

A unique ID is direct and readable:

const save = await driver.findElement(By.id('save')); // JavaScript
# driver.find_element(By.ID, 'save')              # Python

Do not choose an ID that is generated differently on every load. Inspect several real page loads before treating it as stable.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use CSS for a precise fallback

CSS can express an element, an attribute, and its scope without a long DOM path:

By.css('#checkout button.submit')
By.css('[data-testid="save"]')
By.css('form input[name="email"]')

A selector anchored to a stable component is less fragile than a selector that walks through every intermediate div.

Use class names correctly

By.className accepts one class token. For an element carrying both card and primary, use By.css('.card.primary') rather than a space-separated class-name argument.

Reserve XPath for relationships

XPath is appropriate when you need an ancestor, sibling, or conditional relationship, for example:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
By.xpath('//form//input[@name="email"]')
By.xpath('//label[normalize-space()="Email"]/following::input[1]')

Keep XPath short enough to explain. Absolute paths such as /html/body/div[2]/div[1]/form/input break whenever layout markup changes.

Waiting for elements inserted after navigation

driver.get() means navigation was requested and the document became available; it does not guarantee that an application has finished rendering an asynchronous component. If a result is inserted later, wait for a condition instead of adding an arbitrary long sleep.

In a Selenium 3 binding, use its explicit-wait facility to wait for presence or visibility. The exact helper names vary by language version, but the condition should describe the state you need:

  • Presence: the node exists in the DOM and can be read.
  • Visibility: the node is displayed and has usable dimensions.
  • Element to be clickable: visibility plus an enabled state, where supported.

Keep the timeout finite and capture the page state when it expires. A wait cannot fix a selector that never matches.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frames, shadow boundaries, and page context

Switch into an iframe

If the target is inside an iframe, searching from the top-level document will always fail. Locate the frame, switch into it, and then search:

const frame = await driver.findElement(By.css('iframe#payment'));
await driver.switchTo().frame(frame);
const cardNumber = await driver.findElement(By.name('cardnumber'));
await driver.switchTo().defaultContent();

Switch back to the default content before looking for elements outside that frame. A frame’s DOM is a separate document.

Account for older browser-engine behavior

PhantomJS uses an old WebKit engine. Modern JavaScript, layout, promises, and browser APIs may behave differently or fail before Selenium ever evaluates your locator. If the same page works in current Chrome but not PhantomJS, inspect console errors and the rendered DOM in PhantomJS before rewriting selectors.

Shadow DOM support and other modern platform features are another reason to prefer a maintained browser for new automation. If the application depends on features PhantomJS cannot represent, a locator change is not a reliable solution.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why Selenium says it cannot find an element

The selector does not match the rendered DOM

Check spelling, quoting, case, attribute values, and whether the element is actually present after scripts run. Test the smallest stable selector first, then add scope.

The page has not finished rendering

Use an explicit wait for the relevant condition. Avoid making every test sleep for a fixed number of seconds: it slows fast runs and still fails on slower pages.

You are in the wrong document

Switch into the correct iframe before searching, and return to the default content afterward.

The element exists but is hidden

A hidden template node can be found but cannot be interacted with. Wait for visibility or the application’s displayed state. Also check whether a cookie banner, modal, or overlay is covering the control.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The selector is too broad

By.tagName('button') can return many buttons, and partial link text can match an unintended anchor. Scope the search to a stable form, panel, or component.

The page is blocked or different in automation

Bot defenses, authentication redirects, user-agent checks, and network failures can produce a page that does not contain the expected markup. Save the current URL and page source on failure so you can distinguish a bad selector from a different response.

The PhantomJS driver itself is unsupported

Selenium removed native PhantomJS support because its implementation was no longer actively developed. Errors during session creation, missing browser targets, or incompatibilities with modern pages should trigger a migration to headless Chrome or Firefox, not an endless search for another locator syntax.

Debugging with findElements

When you are unsure whether a component is present, use a collection lookup and inspect its length:

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const matches = await driver.findElements(By.css('[data-testid="notice"]'));
if (matches.length === 0) {
  console.log('Notice is not present on this response');
}

This avoids treating an expected absence as an exception. Use findElement when the element is required and its absence should fail the test immediately.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance and reliability practices

  • Prefer one stable locator over a long chain of DOM descendants.
  • Search from a stable container when a page contains repeated components.
  • Wait for a meaningful state rather than polling with multiple unrelated selectors.
  • Use findElements for optional collections and check the returned count.
  • Quit the driver in a finally block so failed tests do not leave PhantomJS processes running.
  • Record the URL, selector, page source, and screenshot when a lookup fails.
  • Run the legacy suite against a pinned browser binary; changing PhantomJS or Selenium versions can alter parsing and WebDriver behavior.

Migration path to headless Chrome or Firefox

Keep your locator calls and replace the browser construction with the maintained browser’s WebDriver setup. Start with a small representative test: navigation, an ID lookup, a CSS lookup, an iframe switch, and an asynchronous wait. Compare the resulting DOM and timing before migrating the full suite.

Expect some selectors to expose genuine browser differences. A current engine may execute scripts PhantomJS cannot, while an old page may rely on WebKit behavior that no longer exists. Treat those as application-compatibility findings and update the test intentionally.

Or skip the browser setup

If your actual goal is a clean image or PDF of a page rather than DOM interaction, ScreenshotNeo provides a single HTTP request. It accepts consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be disabled. Only clean shots are billed: bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the result with X-Page-Verdict and X-Billed headers.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

See the complete parameter reference in the ScreenshotNeo documentation. A cURL capture is:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

The equivalent Python request is:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

And Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. Other available controls include full-page screenshots with lazy images loaded, CSS-selector element capture, dark mode, device presets and custom viewports, retina scale, PDF paper size and page ranges, custom CSS and JavaScript, clicks, selector waits, delays, network-idle waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed image links, asynchronous webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. Parameter names used by other screenshot APIs also work to ease switching.

The Free plan includes 1,000 screenshots each month with no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free, and every feature is available on every plan. Create a free ScreenshotNeo account to try it.

FAQ

Can PhantomJS run without a visible desktop?

Yes. PhantomJS is headless, and its embedded GhostDriver exposes WebDriver through the --webdriver option. The limitation is its age and discontinued Selenium integration, not the absence of headless operation.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Should I rewrite every locator when moving to Chrome or Firefox?

No. Keep the By, findElement, and findElements calls first. Change the driver setup, then investigate only selectors affected by genuine browser or application differences.

Frequently Asked Questions

Can PhantomJS run without a visible desktop?

Yes. PhantomJS is headless, and its embedded GhostDriver exposes WebDriver through the --webdriver option. The limitation is its age and discontinued Selenium integration, not the absence of headless operation.

Should I rewrite every locator when moving to Chrome or Firefox?

No. Keep the By, findElement, and findElements calls first. Change the driver setup, then investigate only selectors affected by genuine browser or application differences.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.