Every flaky end-to-end suite I have inherited had the same disease at its core: a selector strategy vs AI intent argument that nobody had actually settled. Half the tests pinned themselves to div.MuiBox-root > button:nth-child(3), the other half used a hand-crafted data-testid, and a new wave of AI-driven tools claimed you could skip locators entirely and just describe what you want. So which approach actually breaks less when the frontend team ships a redesign on a Friday afternoon? That is the real question, and the honest answer is not "AI always wins."
I have spent years maintaining Playwright, Selenium, and Cypress suites, and more recently building tests where an agent reads a plain-English objective and drives a real browser. This guide compares the two philosophies fairly. CSS and XPath selectors are precise and fast but couple your test to markup that changes. Intent-based location asks a model to find "the checkout button" the way a human would, which survives cosmetic churn but introduces its own failure modes. Both have a place. Let me show you exactly where each one earns its keep.
What a selector strategy actually commits you to
A selector is a promise about structure. When you write page.locator('#email'), you are betting that an element with id="email" will exist, be unique, and mean the same thing on every future deploy. That bet ranges from rock-solid to reckless depending on which selector you reach for.
Here is the rough durability ladder I use, best to worst:
- Semantic role + accessible name (
getByRole('button', { name: 'Sign in' })). Tied to what the element is and says, not where it sits. Survives most restyling. - Explicit test IDs (
data-testid="checkout-submit"). A contract the frontend team promises not to break. Durable as long as that promise holds. - Stable functional attributes (
input[name="email"],[href="/cart"]). Reasonably safe because names and routes change less than classes. - Text content (
text=Add to cart). Fine until someone runs an A/B test on button copy or ships i18n. - CSS class chains (
.btn.btn-primary.checkout). Classes are styling, and styling is the single most churned layer of any app. - Positional and structural (
:nth-child(3), absolute XPath like/html/body/div[2]/div/form/button). These encode the DOM tree shape. One wrapper<div>added for a new layout and every one of them shatters.
The trouble is that real suites drift down this ladder over time. A developer needs a quick fix, the semantic option is not available, and a nth-child sneaks in. Multiply that across a few hundred tests and two years, and you have a suite where nobody trusts a red build because half the reds are selector rot, not real bugs.
Why XPath earns its brittle reputation
XPath is not inherently worse than CSS, but the way teams write it usually is. Auto-generated absolute XPath from browser devtools is the worst offender because it hardcodes the entire ancestor chain. A well-written relative XPath like //button[normalize-space()='Sign in'] is actually close to a semantic selector in durability. The reputation comes from the copy-paste-from-devtools workflow, not the query language itself.
What "AI intent" means in practice
Intent-based location flips the model. Instead of telling the runner how to find an element, you tell it what you want to accomplish. You write "click the checkout button" or "log in with the saved account and confirm the dashboard loads," and an agent inspects the page, reasons about which element matches your intent, and acts.
Under the hood this is not magic. The agent reads an accessibility snapshot of the page (roles, names, labels, visible text), reasons over that structured representation, and picks the target. It is essentially automating the same judgment a human tester applies: "the big green button that says Checkout is obviously the one." Because it works from meaning rather than markup coordinates, a refactor that renames a class or nests a new wrapper div leaves the intent untouched. The button still says Checkout, so the agent still finds it.
This is the philosophy behind BrowserBash, an open-source CLI where you write a plain-English objective and an AI agent drives a real Chrome browser step by step, no selectors and no page objects. You get a deterministic verdict back plus structured results. Here is the simplest possible version:
npm install -g browserbash-cli
browserbash run "Open example.com, click the More information link, and confirm the page heading contains 'IANA'"
No locator appears anywhere in that command. The agent figures out what "the More information link" refers to by reading the page, the same way you would.
Intent is not free of failure modes
Being honest is the whole point of a comparison like this. Intent-based location has real weaknesses. If a page has two buttons that both reasonably match "the submit button," the model has to disambiguate, and it can guess wrong. Non-determinism is the bigger concern: the same objective can occasionally take a different path than it did yesterday. And there is a cost and latency tax, because reasoning over a page is heavier than executing a compiled CSS query. Small local models (roughly 8B parameters and under) can be genuinely flaky on long multi-step flows; the sweet spot is a mid-size local model like Qwen3 or Llama 3.3 70B-class, or a capable hosted model when the flow is hard. Anyone who tells you intent-based testing is pure upside has not run it at scale.
Selector strategy vs AI intent: the honest comparison table
| Dimension | Selector strategy (CSS/XPath) | AI intent-based location |
|---|---|---|
| Survives CSS/class refactor | Depends: brittle for class chains, fine for test IDs | Yes, intent is markup-agnostic |
| Survives DOM restructure (new wrappers) | No for positional/XPath, yes for semantic | Yes |
| Survives copy/label changes | No for text selectors, yes for IDs | Partial: usually adapts, can be confused by ambiguity |
| Determinism | High, same query every run | Lower, model may choose a different path |
| Execution speed | Fast, compiled query | Slower, reasoning per step (mitigated by caching) |
| Cost per run | Near zero | Model tokens per step, unless replayed from cache |
| Handles a brand-new page never seen before | Needs new selectors written | Yes, describe the goal and go |
| Failure message clarity | "Element not found for .x > .y" |
Natural-language step trace of what it tried |
| Best for | Stable apps with disciplined test-id hygiene | Fast-changing UIs, exploratory flows, agent validation |
Read that table as "different tools for different failure modes," not "one is the future." A team with airtight data-testid discipline on a slow-moving product gets fantastic mileage from selectors and does not need an agent. A team shipping UI three times a day on a product with no test IDs will drown in selector maintenance and benefits enormously from intent.
Which one actually breaks less
The blunt answer: it depends on which kind of change your app ships most often.
Selectors break on structural and cosmetic churn. Renamed classes, new wrapper divs for a layout change, reordered elements, a component library upgrade that rewrites the DOM. If your team refactors markup frequently and does not enforce test-id hygiene, selectors are a maintenance treadmill. This is where AI intent clearly breaks less, because none of those changes touch the meaning of the button you are clicking.
Intent breaks on ambiguity and non-determinism. Two elements that plausibly match the same description, a page where the agent picks a slightly different route, or a truly novel flow the model reasons about incorrectly. If your app is stable and your selectors are semantic, a compiled query is more predictable than a model call, full stop.
There is a third category worth naming: both approaches break on genuine functional regressions, and that is exactly what you want. If the checkout button disappears, both a selector test and an intent test should fail. The goal was never zero failures; it was zero false failures. Selector rot and model non-determinism are both noise. A real broken button is signal, and the best test strategy maximizes the ratio of signal to noise.
In my experience, on a fast-moving product the noise from selector rot dwarfs the noise from model non-determinism, which is why intent-based tools win on maintenance hours saved. On a mature product with a locked design system, the opposite holds.
When a stable data-testid still wins
I promised honesty, so here it is plainly: a disciplined data-testid strategy is often the single most reliable thing you can do, and it beats AI intent in several concrete situations.
High-frequency assertions in a tight loop. If you run a smoke check every 60 seconds against a login form, you do not want a model call each time. A compiled getByTestId('login-submit') executes in microseconds and never guesses.
Elements with no meaningful label. An icon-only button with no accessible name, a canvas region, a drag handle, a specific row in an infinite-scroll grid. Intent struggles when there is no human-readable meaning to reason about. A test ID gives the runner an unambiguous anchor exactly where semantics fail.
Regulatory or safety-critical determinism. In domains where you must be able to prove the exact same interaction ran identically every time, the reproducibility of a compiled selector is a feature, not a limitation.
Cost-sensitive massive suites. Ten thousand assertions running on every commit will cost real money if each one reasons over a page. Selectors are effectively free to execute.
The pragmatic move is not to pick a religion. It is to add data-testid attributes to the elements that need bulletproof anchors, use semantic role-and-name selectors everywhere you can, and reach for intent-based validation on the flows that change constantly or that you are still exploring. You can read more about structuring durable suites in the BrowserBash tutorials.
The hybrid that most mature teams land on
The best setups I have seen use intent for the navigation and flow and deterministic checks for the assertions. The agent drives the messy multi-step journey ("log in, add two items to the cart, apply the promo code, reach the payment page") because that path changes often. Then a deterministic assertion nails down the outcome ("the order total is exactly $47.98") because you never want a model judging whether a critical number is correct.
BrowserBash bakes this hybrid into its Verify assertions. Verify steps in a test file compile to real Playwright checks (URL contains, title is, text visible, a named button or heading visible, element counts, a stored value equals a target) with no LLM judgment involved. A pass means the condition literally held; a fail comes with expected-versus-actual evidence. So the agent uses intent to get to the page, and Verify uses deterministic logic to confirm the result. You get the maintenance savings of intent and the trustworthiness of an assertion.
Writing tests that use both: a worked example
Here is where the two philosophies stop being a debate and start being a workflow. In a BrowserBash markdown test you can mix plain-English intent steps with deterministic API and Verify steps. Turn on version: 2 frontmatter and every step runs one at a time against a single browser session.
# a testmd v2 file: seed data via API, drive the UI by intent, assert deterministically
cat > checkout_test.md <<'EOF'
---
version: 2
---
# Verify a seeded order reaches the confirmation page
POST https://api.example.com/orders with body {"item":"widget","qty":2}
Expect status 201, store $.id as 'orderId'
Go to https://example.com/orders/{{orderId}}
Click the button to proceed to checkout
Fill in the shipping form with test data and continue
Verify URL contains /confirmation
Verify text "Order confirmed" visible
Verify 'Place order' button visible
EOF
browserbash testmd run ./checkout_test.md --agent
Look at what each line is doing. The POST and Expect lines are deterministic API steps that seed data with no model involved, so your UI test starts from a known state instead of clicking through a fixture setup. The plain-English lines in the middle are pure intent: the agent reads the page and figures out which button proceeds to checkout, no selector required, so a redesign of that flow will not break the test. The Verify lines at the bottom are compiled Playwright checks that fail loudly with evidence if the outcome is wrong. That is selector-grade determinism and AI intent living in one file, each doing the job it is best at.
This is also the pattern that makes tests survive. The brittle middle (navigation through a changing UI) is handled by intent that adapts. The parts that must be exact (the seed data and the final assertions) are handled deterministically. For CI, the --agent flag emits NDJSON, one JSON event per line, with clean exit codes: 0 passed, 1 failed, 2 error, 3 timeout. No prose parsing.
Migrating an existing selector suite without a rewrite
If you already have a Playwright suite full of selectors, you do not have to throw it away to experiment with intent-based location. BrowserBash can convert existing specs to plain-English tests heuristically, deterministically, and with no model calls during conversion:
# convert a Playwright spec folder to plain-English *_test.md files
browserbash import ./tests/e2e
# anything untranslatable is written to IMPORT-REPORT.md, never silently dropped
The import handles the common shapes: goto, click, fill, press, check, selectOption, getBy* locators, and standard expects. Your process.env.X references become {{X}} variables. Whatever cannot be translated cleanly lands in an IMPORT-REPORT.md instead of being dropped or, worse, invented. That gives you a side-by-side: keep the selector-based specs for the stable, test-id-clean areas, and run the intent-based conversions on the flows that keep breaking. Compare the maintenance burden over a few sprints and let the data decide, rather than the hype.
Reliability tricks that make intent-based testing predictable
The single biggest objection to AI intent is non-determinism, so it is worth explaining how a serious tool tames it rather than pretending the objection does not exist.
Replay cache. The first green run records the exact actions the agent took. The next identical run replays those recorded actions with zero model calls, and the agent only steps back in when the page has actually changed. This collapses most of the cost and most of the run-to-run variance, because a stable page replays the same path every time. It also means an always-on monitor is nearly token-free.
Saved logins. Re-authenticating on every test is both slow and a common source of flakiness. You log in once with browserbash auth save, and reuse the stored session with --auth on any run. That removes an entire class of "the login form changed" failures from every downstream test.
Cheap-model routing. You can plan on a strong model and execute the recorded steps on a cheaper one, which keeps cost down without sacrificing the reasoning quality where it matters.
Deterministic Verify for anything critical. As covered above, never let a model judge a value that must be exact. Compile it to a real assertion.
Put together, these turn "the model might do something different" into "the model does the same replayed thing every run until the page genuinely changes, and even then only the changed step re-reasons." That is a very different reliability profile from a naive prompt-the-model-every-time approach, and it is closer to how a well-maintained selector suite behaves, minus the selector maintenance.
You can watch state changes over time with monitor mode, which alerts only when a test flips between pass and fail in either direction, never on every green run:
browserbash monitor ./checkout_test.md --every 10m --notify https://hooks.slack.com/services/XXX
A decision framework you can actually use
Skip the ideology. Answer these questions about your specific app and the choice becomes obvious.
How often does your markup change? Daily UI shipping with no test-id discipline points hard toward intent. A locked design system with enforced test IDs points toward selectors.
Do your elements have meaningful labels? Buttons and links with clear accessible names are perfect for intent. Icon-only controls, canvases, and unlabeled grid cells favor a test ID anchor.
How exact must your assertions be? Anything involving money, counts, legal text, or safety needs a deterministic check regardless of how you navigate to it.
How many times per day does the test run? A ten-second smoke loop wants compiled selectors. A nightly regression of exploratory flows tolerates model reasoning happily.
Who maintains the tests? If it is a dedicated automation team fluent in locators, selectors scale fine for them. If it is developers or QA who would rather describe behavior than debug XPath, intent lowers the barrier dramatically.
For most teams the answer is "both, deliberately split." Use intent for the churny navigation and exploratory coverage, use selectors and deterministic Verify for the assertions and the high-frequency loops. The teams that struggle are the ones that pick one tool and force every test through it. If you want a longer walkthrough of building a suite this way, the BrowserBash learn hub and the open-source repo on GitHub are the right next stops.
FAQ
Is AI intent-based testing more reliable than CSS selectors?
Neither is universally more reliable; they fail on different things. AI intent survives markup refactors, renamed classes, and new wrapper elements because it works from the meaning of the page rather than its structure. CSS selectors, especially semantic role-and-name or test-id selectors, are more deterministic and far faster for stable apps and high-frequency checks. The most reliable setup usually combines both: intent for navigation through changing UI, and compiled selectors or deterministic assertions for the outcomes that must be exact.
When should I still use a data-testid instead of natural language?
Reach for a data-testid when the element has no meaningful accessible label (icon-only buttons, canvas regions, specific rows in a large grid), when you run the same assertion thousands of times and cannot afford a model call each time, or when you need provable, identical reproducibility for regulatory or safety-critical flows. Test IDs give the runner an unambiguous anchor exactly where human-readable meaning is missing. A disciplined test-id strategy remains one of the most robust things a frontend team can maintain.
Does removing selectors mean my tests become non-deterministic?
It introduces some run-to-run variability, but good tooling controls it heavily. A replay cache records a green run's actions and replays them with zero model calls until the page actually changes, which collapses most variance and cost. Saved logins, cheap-model routing, and compiling critical checks into deterministic Verify assertions further tighten predictability. The result behaves closer to a stable selector suite than to a naive prompt-the-model-every-time script.
Can I convert my existing Playwright tests to intent-based tests?
Yes. BrowserBash includes an import command that converts Playwright specs to plain-English test files heuristically and deterministically, with no model calls during conversion. It translates the common shapes like goto, click, fill, press, check, selectOption, getBy locators, and standard expects, and turns environment variables into templated variables. Anything it cannot translate cleanly is written to an import report rather than dropped or invented, so you get an honest side-by-side to evaluate.
Stop arguing selector strategy vs AI intent in the abstract and just test both on your own flows. Install with npm install -g browserbash-cli, point it at the pages that keep breaking your suite, and see which approach actually holds up on a real deploy. It is free and open-source, so there is nothing to lose. If you want the optional cloud dashboard and 15-day run history, create a free account, though an account is entirely optional and everything runs on your machine by default.