skip to content

In Selenium 4, why does By.className("recall-row overdue") throw before any command is sent?

level: middleimportance: should knowfreq 58%

answer

  1. The failure is earlier than you expect
  2. One token, because of one format string
  3. The alternative would match nothing at all
  4. A static field turns it into initialiser error
  5. Write the two classes as CSS instead

basics

~10 s

Compound class names are rejected. The By.className constructor checks the value for whitespace and throws InvalidSelectorException immediately, because its CSS fallback is a single class selector that could never match two tokens.

solid answer

~40 s

`By.className` has no wire strategy of its own in Selenium 4, so the client builds a CSS fallback from the format `.%s`. A value containing whitespace cannot work there: whitespace is escaped along with the other CSS metacharacters, so `"recall-row overdue"` would serialise to `.recall-row\ overdue`, a request for one class token literally containing a space. No element can carry such a class, so the query would be valid CSS that silently matches nothing. Rather than allow that, the `ByClassName` constructor matches the value against a whitespace pattern and throws `InvalidSelectorException("Compound class names not permitted")` on the spot, before a driver is involved. `By.cssSelector(".recall-row.overdue")` expresses "both classes on one element" directly.

code

java · 14 lines
java
import org.openqa.selenium.By;
import org.openqa.selenium.InvalidSelectorException;

public class RecallRowClasses {
  public static void main(String[] args) {
    try {
      By.className("recall-row overdue");
    } catch (InvalidSelectorException e) {
      System.out.println("rejected: " + e.getMessage());
    }
    System.out.println(By.className("overdue"));
    System.out.println(By.cssSelector(".recall-row.overdue"));
  }
}

go deeper

for a junior

Remember that By.className takes exactly one class name and that two names separated by a space is an error, not a filter. Reach for a CSS selector when you need both.

for a middle

Explain the mechanism: no wire keyword, a class-selector fallback built from a format string, and a constructor guard that rejects whitespace because the escaped alternative could never match.

for a senior

An interviewer expects you to place the failure precisely, including the static-field case where it becomes an initialiser error, and to distinguish it from a locator that runs and finds nothing.

for a principal

Own the general principle it illustrates: a library should fail loudly on input it can only interpret wrongly, rather than emitting a technically valid query that quietly matches nothing.

## The rule, and where it is enforced `By.className` in Selenium 4 has no wire keyword of its own. The **W3C WebDriver** table of location strategies lists only `css selector`, `link text`, `partial link text`, `tag name` and `xpath`, so the client builds a CSS fallback from the format string `.%s` and searches with that instead. Because the fallback is a single class selector, the value has to be a single class token. The `ByClassName` constructor enforces it directly: it matches the value against a whitespace pattern and, if there is any whitespace at all, throws `InvalidSelectorException` with the message *Compound class names not permitted*. Nothing is queued, nothing is sent, and no `WebDriver` needs to exist for it to happen. ## Why a guard rather than a best effort The interesting part is what the client would otherwise produce. The value is run through a CSS escape pass before it is formatted, and whitespace is in the escape set. So without the guard, `By.className("recall-row overdue")` would serialise to `.recall-row\ overdue` — a request for **one class token whose name literally contains a space**. No HTML element can carry such a class, because the `class` attribute is split on whitespace into separate tokens. The result would therefore be a perfectly valid CSS selector that can never match anything, on a page where the row visibly carries both classes. The guard converts that silent zero-match into a named, immediate failure. Selenium's Python client makes the same call for the same reason, raising `InvalidSelectorException` from its locator converter. ## When the failure surfaces Because the check is in the constructor, the throw happens the moment the locator is created, which is often not where you would look for it: 1. Written inline in a step, it fails on that line, before the driver is touched. 2. Written as a field of a page object, it fails when that object is constructed. 3. Written as a `static final By` constant, it fails during **class initialisation**, which Java reports as an `ExceptionInInitializerError` wrapping the `InvalidSelectorException` — a stack trace that names the class, not the locator line. The third case is the one that wastes time, because the symptom looks like a loading problem rather than a bad selector. ## Two locators that look equivalent and are not | Locator | Outcome | |---|---| | `By.className("recall-row overdue")` | throws `InvalidSelectorException` at construction | | `By.cssSelector(".recall-row.overdue")` | matches an element carrying **both** classes | | `By.cssSelector(".recall-row .overdue")` | matches an `overdue` element **inside** a `recall-row` | | `By.className("recall-row")` | matches every element whose class list **contains** that token | The last row is worth stating explicitly: a single class name is a containment test on the class list, not an equality test on the whole `class` attribute. On a recall row declared as `<tr class="recall-row overdue">`, `By.className("recall-row")` matches it, and so does `By.className("overdue")`. ## The recall-list version Suppose a dental-practice recall table renders rows like this: ```html <tr class="recall-row overdue" data-patient="4821"> <td class="patient-name">Amelia Fox</td> </tr> ``` - `By.className("overdue")` finds the row, along with any other element carrying that token anywhere on the page. - `By.className("recall-row overdue")` never runs at all — it throws while the locator is being built. - `By.cssSelector(".recall-row.overdue")` is the direct expression of "both tokens on one element". - `By.cssSelector("tr.recall-row.overdue")` narrows the same query to the row element itself. ## The related asymmetry worth knowing `By.id` does **not** carry the same guard. `By.id("one two")` is accepted and serialises to `#one\ two`, with the space escaped into the identifier. The Selenium test suite covers this case explicitly, noting that although whitespace in an id is not legal HTML, the client handles it rather than rejecting it. So the two sibling factories built on the same fallback machinery behave differently on the same input: - **`By.className`** rejects whitespace, because a compound class is what a user almost always means, and silently matching nothing would be worse than an error. - **`By.id`** accepts whitespace, because there is no plausible alternative interpretation to protect the user from — an id with a space is simply an odd id. - Neither behaviour is about the browser. Both decisions are made in the client, before a single byte goes on the wire.

  • Does By.id reject a value containing a space as well?
    No. `By.id("one two")` is accepted and serialises to `#one\ two`, with the space escaped so it stays inside a single identifier. Only `ByClassName` carries the whitespace guard, because a compound class is what the author almost certainly meant and matching nothing would hide the mistake.
  • When exactly does the exception surface in a page object?
    At the moment the locator is constructed. A `static final By ROW = By.className("recall-row overdue")` throws during class initialisation, which Java surfaces as an `ExceptionInInitializerError` naming the page-object class rather than the locator line. Nothing has contacted the browser at that point.
  • What does a single class name actually match?
    Any element whose class list contains that token. `By.className("overdue")` becomes the CSS selector `.overdue`, so a row declared as `class="recall-row overdue"` matches, and so would a badge elsewhere on the page carrying the same token. It is containment, not equality on the whole attribute.

saying these in an interview costs you the question

  • Says the driver rejects compound class names at the remote end
  • Thinks By.className splits the value and matches both tokens
  • Believes the failure only surfaces when findElement is called
  • Claims a space in the value silently matches a descendant element
  • Assumes By.id rejects whitespace the same way className does