skip to content

In Selenium, why can no CSS locator find a ::before pseudo-element, and how do you read its content?

level: middleimportance: should knowfreq 38%

answer

  1. It is painted, not parented
  2. Style engine output, not document content
  3. querySelectorAll returns nodes only
  4. Host element plus computed style
  5. getComputedStyle takes a second argument

basics

~20 s

A pseudo-element is generated by the style engine and never becomes a DOM node, so the browser's querySelectorAll cannot return one and Selenium has nothing to wrap in a WebElement. Read it with getComputedStyle inside an injected script instead.

solid answer

~40 s

Selenium's `css selector` strategy is defined as a `querySelectorAll` call, and `querySelectorAll` returns element nodes. A `::before` or `::after` box is created by the style engine while it builds the render tree; it is never inserted into the DOM, so there is no node to turn into a `WebElement`. `findElement` therefore reports `NoSuchElementException` even though the glyph is visible on screen, and waiting or retrying cannot help. To assert on it, find the host element with a normal CSS locator and read the pseudo-element's computed style through `JavascriptExecutor`: `window.getComputedStyle(arguments[0], '::before').content`. Note that `WebElement.getCssValue` reads only the host element's own style — the W3C Get Element CSS Value command takes a property name and has no pseudo-element parameter.

code

java · 8 lines
java
WebElement seatCell = driver.findElement(
    By.cssSelector("tr.listing[data-status='sold'] td.seat"));

String generated = (String) ((JavascriptExecutor) driver).executeScript(
    "return window.getComputedStyle(arguments[0], '::before').content;", seatCell);

// computed content keeps its quote characters: "\"SOLD \""
assertTrue(generated.contains("SOLD"));

go deeper

for a junior

Be ready to say that a CSS locator can only return real DOM elements, and that text drawn by a stylesheet is not one of them. Knowing the failure is NoSuchElementException, not a timing problem, is enough here.

for a middle

Explain the mechanism: the style engine builds the pseudo-element while producing the render tree, so querySelectorAll never sees it. Then show the workaround with getComputedStyle and a second pseudo-element argument through JavascriptExecutor.

for a senior

An interviewer expects you to diagnose this from a failing test: a visible label that no locator finds, an empty findElements result, and a stylesheet rule that explains both. Say which fact in the DOM you would assert on instead.

for a principal

Own the guidance that assertions should target state the document actually carries rather than decoration produced by styling, and be clear about when a scripted read of computed style is worth the extra hop your suite has to maintain.

## Why a pseudo-element is not a node A **pseudo-element** — `::before`, `::after`, `::marker`, `::first-line`, `::placeholder` — is produced by the browser's **style engine** while it builds the render tree out of the DOM. It is a box that gets laid out and painted, and that is all it is. It is never inserted into the document tree, it has no entry among an element's child nodes, and no script can take a reference to it. On a stadium ticket exchange whose stylesheet says `tr.listing[data-status="sold"] td.seat::before { content: "SOLD "; }`, the word SOLD is plainly on screen and completely absent from the DOM. Selenium's `css selector` locator strategy is defined in terms of the DOM and nothing else. The remote end calls `querySelectorAll` on the start node with the string you handed to `By.cssSelector`, then returns the element nodes that came back. A pseudo-element is not among them, so the chain breaks at the first link: there is no node to give a web element reference to, and therefore no `WebElement` for your client to receive. ## What the failure actually looks like - `driver.findElement(By.cssSelector("td.seat::before"))` raises `NoSuchElementException`, and the message is maddening because the text it cannot find is visible in the screenshot. - `driver.findElements(By.cssSelector("td.seat::before"))` returns an empty list and throws nothing, so a check that only counts matches quietly reports zero. - Some engines reject the pseudo-element part while parsing and you get an `invalid selector` error instead. Either way, no element comes back. - Polling changes nothing. A wait re-runs the same find, and a node the DOM never contains will not appear on the next attempt. - There is nothing to click either. A click has to be sent to the **host element** that owns the pseudo-element. ## Reading the generated content anyway The information is real — it just lives in the **CSSOM** rather than the DOM. `window.getComputedStyle` accepts an optional second argument naming a pseudo-element, and Selenium can reach it through `JavascriptExecutor`: 1. Locate the host element with an ordinary CSS locator (`td.seat`); that one is a genuine node. 2. Pass the resulting `WebElement` into `executeScript` as `arguments[0]` — the driver turns it back into a live element on the page. 3. Call `window.getComputedStyle(arguments[0], "::before")` and read the property you care about, usually `content`. 4. Assert on the returned string, remembering that a computed `content` value arrives **with its quote characters included**, so you get `"SOLD "` rather than a bare `SOLD`. ```java String generated = (String) ((JavascriptExecutor) driver) .executeScript("return window.getComputedStyle(arguments[0], '::before').content;", seatCell); ``` ## `getCssValue` is a different call, not a shortcut | Call | What it reads | Result for a `::before` glyph | |---|---|---| | `WebElement.getCssValue("content")` | the host element's own computed style | usually `none`, because the host declares no `content` | | `executeScript` with `getComputedStyle(el, "::before")` | the pseudo-element's computed style | the generated string, quote characters included | `getCssValue` maps onto the W3C **Get Element CSS Value** command, whose URL carries an element id and a property name and nothing else. The protocol gives it nowhere to put a pseudo-element argument, which is why no Selenium client offers a `getCssValue("content", "::before")` overload. This is a protocol-level limit, not a missing convenience. ## What to assert instead Generated content is decoration driven by something the DOM does hold. The rule above fires only because the row carries `data-status="sold"`, and that attribute is a first-class part of the document: `tr.listing[data-status="sold"] td.seat` is an ordinary CSS locator the driver can find, count and act on. So a check that wants to prove "this listing shows as sold" has two honest routes — assert on the state that produced the glyph, or take the script hop and assert on the computed `content`. Both are defensible. The only mistake is expecting `findElement` to return a pseudo-element. Two further limits fall straight out of the same rule: - `::selection`, `::marker`, `::placeholder` and `::first-line` are equally unreachable, for exactly the reason above — none of them is a node. - A locator string containing `::before` is not a syntax error you will catch in review. It reads perfectly naturally, and it fails only at run time with a message about a missing element, which is why this shows up in interviews as a debugging story rather than a rule.

  • Would an explicit wait eventually find the pseudo-element once the page settles?
    No. A wait re-runs the same find, and the find is a `querySelectorAll` call over the DOM. A pseudo-element is never in the DOM, so no number of polls produces one. The only thing a wait achieves here is turning an instant `NoSuchElementException` into a slow timeout.
  • What does WebElement.getCssValue("content") return when the element has a ::before rule?
    The host element's own computed `content`, which is normally `none` because the rule declared `content` on the pseudo-element, not on the element. `getCssValue` maps onto the W3C Get Element CSS Value command, whose parameters are an element id and a property name; there is nowhere in the protocol to name a pseudo-element.

saying these in an interview costs you the question

  • Claims findElement returns the pseudo-element's parent when you select ::before
  • Thinks getText includes content generated by ::before and ::after
  • Believes a longer wait will eventually make the pseudo-element appear
  • Says getCssValue can read a pseudo-element's computed content
  • Tries to click a ::after glyph instead of the host element