PRACTICAL GUIDE / InvalidSelectorException CSS contains Selenium
Why Selenium rejects CSS :contains before it searches the page
Diagnose Selenium InvalidSelectorException errors caused by CSS :contains, choose valid attribute or XPath text matching, and prove the selector in DevTools.
In this guide6 sections
What you will learn
- What the browser does with a CSS locator
- Run the failing and corrected selectors together
- Prove it is a parse error, not a timing error
- Pick the selector that matches the requirement
Your Java test fails on button:contains('Save'), even though the Save button is plainly visible in the screenshot. Selenium never looked for the button. The browser rejected the locator as invalid CSS before any DOM match could happen.
That difference changes the fix: waiting helps an element that is late, but it cannot repair a selector the browser cannot parse.
What the browser does with a CSS locator
By.cssSelector(...) tells Selenium which locator strategy to send with the find-element command. For web content, the browser evaluates that value as a CSS selector. The string must follow the selector grammar understood by browser APIs such as querySelector() and querySelectorAll().
The functional pseudo-class :contains() is not part of that CSS grammar. Many engineers remember it from jQuery, where it is an extension implemented by the library. Copying a jQuery selector into By.cssSelector removes the library that gave the expression meaning. A standards-based browser parser sees an unsupported pseudo-class and returns an invalid-selector error.
This failure occurs before matching. It does not say the element is hidden, late, stale, inside a frame, or absent. Those are separate conditions:
InvalidSelectorExceptionmeans the locator syntax or strategy is invalid.NoSuchElementExceptionmeans a valid locator produced no element at that time.StaleElementReferenceExceptionmeans a previously returned element reference no longer points to an element in the current DOM context.
CSS does have a contains operator for attribute values. [data-action*='save'] matches an element whose data-action attribute contains the substring save. The *= operator does not inspect rendered text or descendant text nodes. These two requirements may sound similar in English, but they ask different engines to inspect different data.
XPath supports string functions, so //button[contains(normalize-space(.), 'Save')] can match a button by its string value. The dot includes descendant text, and normalize-space(.) collapses runs of whitespace. That is usually more useful than text(), which targets direct text nodes and can miss wording split across nested spans.
Run the failing and corrected selectors together
This JUnit 5 test builds its own page as a base64 data URL, so it does not depend on an application server. Selenium Manager can resolve the local Chrome driver when the environment supports it.
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.InvalidSelectorException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
class CssContainsTest {
private WebDriver driver;
@BeforeEach
void openBrowser() {
driver = new ChromeDriver();
}
@AfterEach
void closeBrowser() {
if (driver != null) {
driver.quit();
}
}
@Test
void choosesTheLocatorStrategyForTheValueBeingMatched() {
String html = """
<!doctype html>
<html lang="en">
<body>
<button id="save-invoice" data-action="invoice-save">
<span>Save</span> invoice
</button>
<button id="save-draft" data-action="draft">Save draft</button>
</body>
</html>
""";
String encoded = Base64.getEncoder().encodeToString(
html.getBytes(StandardCharsets.UTF_8));
driver.get("data:text/html;base64," + encoded);
assertThrows(
InvalidSelectorException.class,
() -> driver.findElement(
By.cssSelector("button:contains('Save invoice')")));
WebElement byAttribute = driver.findElement(
By.cssSelector("button[data-action*='save']"));
assertEquals("save-invoice", byAttribute.getAttribute("id"));
WebElement byVisibleText = driver.findElement(
By.xpath("//button[contains(normalize-space(.), 'Save invoice')]"));
assertEquals("save-invoice", byVisibleText.getAttribute("id"));
}
}Run only this class while diagnosing the locator:
mvn -Dtest=CssContainsTest -DtrimStackTrace=false testThe first assertion proves that the syntax is rejected. The next two prove two legitimate but different contracts: an attribute contains a token, and the user-facing text contains a phrase. Keeping them in one test makes the choice visible instead of presenting XPath as a magical spelling change.
Prove it is a parse error, not a timing error
Copy the raw CSS value from the exception, including punctuation. Do not retype it from memory. A quote changed while formatting logs can turn one defect into another.
Open the same page and run this in the browser console:
document.querySelector("button:contains('Save')");A standards-based browser reports a SyntaxError because the selector is invalid. Now try a valid attribute substring selector:
document.querySelector("button[data-action*='save']");That expression returns the first matching element or null. Both outcomes prove that parsing succeeded. null means you should investigate DOM state, timing, shadow roots, or frame context. A syntax error means you should stay focused on the selector.
You can evaluate the XPath independently as well:
document.evaluate(
"//button[contains(normalize-space(.), 'Save invoice')]",
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null,
).singleNodeValue;Run these checks against the DOM from the failing state, not a later page that happens to look similar. Confirm the test is in the correct frame before comparing results. DevTools evaluates against the context currently selected in the console, while WebDriver evaluates against its current browsing context.
The WebDriver stack trace should name InvalidSelectorException and usually includes the browser's parser message. Preserve the locator strategy and exact value in test logs. A screenshot has little diagnostic value here because it can prove the button was drawn but cannot prove the locator grammar was valid.
Log the strategy separately from the value. css selector plus //button[...] shows that valid XPath was sent to the wrong parser, while xpath plus button[data-action='save'] reveals the opposite mismatch. Logging only the expression encourages reviewers to paste it into whichever DevTools tool they normally use and reach the wrong conclusion.
Generated locators need inspection after interpolation. A missing quote, an empty attribute value, or user data containing punctuation can make the final string different from the source template. Capture the fully rendered selector from the failing attempt, but redact sensitive data before attaching it to a shared report. Then add a focused unit test for the locator builder so the same input cannot silently produce invalid syntax again.
An explicit wait provides another useful signal. If the exception escapes immediately rather than after the configured timeout, an unignored failure terminated the wait. Do not add InvalidSelectorException to the ignored list. Doing so changes a precise parser error into a delayed timeout with the same broken input.
Pick the selector that matches the requirement
If the application exposes a stable unique attribute, use it directly. By.id("save-invoice") or By.cssSelector("[data-testid='save-invoice']") states a clearer contract than a substring. The cost is coordination with developers and an attribute that becomes part of the test interface.
Use CSS *= only when substring semantics are intentional. It can overmatch save, autosave, and saved-copy. Add the element type or another stable attribute to narrow the set, then verify uniqueness. findElement silently returns the first match when several elements qualify, which can turn an overly broad selector into a test that clicks the wrong control.
During diagnosis, count matches for the corrected locator in the failing state:
By saveAction = By.cssSelector("button[data-action*='save']");
assertEquals(1, driver.findElements(saveAction).size());That assertion is useful while establishing the locator contract. Whether it belongs in every product test is a trade-off: it catches duplicate controls early, but it adds another browser command and may reject pages where several matching controls are valid by design.
Choose XPath when visible text or ancestry is the requirement and the markup offers no better hook. Prefer an exact normalized comparison when wording should be exact:
By.xpath("//button[normalize-space(.)='Save invoice']")Use contains(...) only when extra text is allowed by the requirement. The trade-off is that translated copy, inserted badges, and wording changes can break the test. That may be correct if the text is a contractual label, or needless churn if the test only needs the save action.
Selenium also provides partialLinkText, but it applies to links. It is not a general text locator for buttons, labels, or arbitrary containers. Selecting a locator strategy because its name sounds close to the requirement is how invalid and misleading selectors enter a framework.
Account for quoting, whitespace, and nested text
Text matching becomes complicated when the expected phrase contains both single and double quotes. Building XPath by string concatenation can produce another invalid expression or, with untrusted input, an injection problem. Prefer a stable attribute. If dynamic XPath is unavoidable, centralize a tested XPath-literal encoder rather than scattering escaping tricks through tests.
Whitespace is another source of false conclusions. A button rendered as Save invoice may contain newlines and indentation in the HTML. normalize-space(.) reduces that variation, but it also treats multiple spaces as one. Use it because that normalization matches the requirement, not because it makes every failing expression pass.
Nested markup explains many text() surprises. In <button><span>Save</span> invoice</button>, the button has more than one text node. contains(text(), 'Save invoice') does not combine them the way a person reading the control does. The element string value represented by . is the better fit for this case.
Every broader match carries ambiguity. A case-insensitive regular expression in application code, a substring attribute selector, and an XPath contains call can all find more than intended. Follow the lookup with an assertion about the chosen control's meaningful state, and keep selectors narrow enough that duplicate UI labels cause a clear failure rather than a first-match pass.
Do not use text contains as a default locator policy
Avoid XPath text matching for controls whose copy is localized or frequently edited when the action has a stable semantic identifier. Otherwise routine content work creates automation churn without revealing a product defect.
Do not replace every invalid :contains with [class*='word']. Generated class names and styling tokens are implementation details, and substring matching makes accidental matches likely. A test-specific attribute is less clever and more dependable.
Leave waits out of parser diagnosis. Once the selector is valid, a wait may be appropriate if the element appears asynchronously. That is a second decision supported by a different exception and different evidence.
Finally, do not assume a selector validated in one console will work in another context. Shadow DOM boundaries, iframes, XML documents, and browser support can change what is reachable or valid. Reproduce the actual page, browser, and context first. The quickest fix is the one that answers the correct question: did parsing fail, did matching return nothing, or did the page change after the match?
// FIELD DISPATCH
Get the QA Field Notes
Weekly QA battles, AI testing guides, and interview drills. Free on Substack.
// LIVE COURSE / THE TESTING ACADEMY
Playwright Automation Mastery
Go beyond Selenium. Master Playwright with JS/TS in 90 days.
From the instructor behind this guide.
Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.
PRIMARY REFERENCES
Verify the details at the source
QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.
- 01Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 02Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 03Official w3.org reference
w3.org
Primary documentation selected and verified for the claims in this guide.
- 04Official developer.mozilla.org reference
developer.mozilla.org
Primary documentation selected and verified for the claims in this guide.
FAQ / QUICK ANSWERS
Questions testers ask
Can a Selenium CSS selector use :contains to match text?
No. The :contains() syntax is associated with jQuery, not the CSS selector grammar used by browser query APIs. Selenium therefore reports an invalid selector instead of searching for an element.
What is the CSS equivalent of contains in Selenium?
Use an attribute substring selector such as `[data-action*='save']` when the value lives in an attribute. CSS has no equivalent operator for arbitrary descendant text, so text matching needs a different locator strategy.
Will WebDriverWait fix an InvalidSelectorException?
Waiting cannot make malformed syntax valid. Correct the selector first; retries only repeat the same parser failure and hide the distinction between invalid syntax and an element that has not appeared yet.
How do I test a Selenium CSS selector in Chrome DevTools?
Paste the selector into `document.querySelector()` in the Console. A returned element or `null` means the syntax parsed, while a SyntaxError means the browser rejected the selector itself.
Should I use XPath contains for every text locator?
Prefer a stable ID, name, or test attribute when the application provides one. XPath text matching is useful when visible wording is the real contract, but it can become brittle with localization, whitespace, and nested content.
RELATED GUIDES
Continue the learning route
GUIDE 01
How to Handle Dropdowns in Selenium
Learn how to handle dropdowns in Selenium using Select, custom lists, multi-select, dynamic options, keyboard actions, and stable click patterns.
GUIDE 02
Selenium executeAsyncScript in Java
Selenium executeAsyncScript Java uses a final callback argument and the script timeout. Learn return conversion, error handling, and tested Java patterns.
GUIDE 03
Data-Driven Testing in Selenium
Learn data-driven testing in Selenium using CSV, Excel, and TestNG DataProvider patterns to run one script across many reusable test datasets.
GUIDE 04
Handle Alerts in Selenium: Complete Guide
Handle alerts in Selenium with examples for accept, dismiss, prompt text, explicit waits, unexpected alerts, browser prompts, and common mistakes in CI.
GUIDE 05
Playwright vs Selenium for Beginners
Compare Playwright vs Selenium for beginners: setup, syntax, waits, browsers, debugging tips, and which automation tool to learn first in 2026.