I originally wrote this as a quick reference while moving between CSS and XPath selectors in a Capybara test suite.
I had been using:
1
page.has_selector?(:xpath, selectable).should == false
and:
1
page.has_css?(selectable).should == true
Both are asking the same broad question—does the current page contain an element matching this locator?—but they select different query languages. Capybara’s current selector documentation still describes that common model: finder and matcher methods accept a selector name, a locator and optional filters.
Why XPath appeared in the first place
CSS selectors are normally the clearest option for classes, IDs, attributes and straightforward document structure. XPath becomes useful when the relationship between elements matters: finding an element by an ancestor, navigating from a label to a related control, or describing a condition that would be awkward in CSS.
The temptation is to treat the locator as the test. It is not. The locator is only the mechanism used to observe behaviour. A useful feature test should still make the user-visible outcome obvious.
The important Capybara detail: waiting
Capybara’s matchers are more valuable than a raw DOM lookup because they understand asynchronous pages. They retry for a bounded period while the application changes, which avoids replacing real synchronisation with arbitrary sleep calls.
That matters particularly for negative assertions. A direct negation can stop as soon as the first lookup returns false, even if the page is still changing. Using Capybara’s negative matcher allows it to apply the intended waiting semantics.
How I would write it now
With contemporary RSpec syntax, I would make the expectation explicit:
1
2
expect(page).to have_no_selector(:xpath, selectable)
expect(page).to have_css(selectable)
If the element has a user-facing role, I would go one step further and prefer a semantic matcher:
1
2
expect(page).to have_button("Deploy")
expect(page).to have_link("View incident")
Those expectations tell the next engineer what behaviour matters. They are less coupled to the page’s implementation and survive a styling refactor far better than a deeply nested selector.
What has held up since 2012
The syntax has evolved, but the testing principle has not:
- Choose CSS or XPath according to the relationship you need to express.
- Prefer Capybara matchers over manually interrogating the rendered HTML.
- Let the matcher manage asynchronous waiting.
- Describe user intent whenever a semantic selector is available.
- Keep complex locators behind a well-named helper rather than repeating them.
The best test is not the cleverest selector. It is the one that fails with a message that immediately explains which user-visible promise was broken.