TestCafe Selectors: How to Find and Interact with Elements
Learn to build stable TestCafe selectors, refine matches, interact with elements, and diagnose timing, visibility, and Shadow DOM issues.
A TestCafe selector is an asynchronous query over the page DOM. Start with a stable CSS selector, a client-side function, or another selector; narrow the result with attributes, text, filters, or traversal; then pass it to an action or assertion. Before relying on it, check that it identifies the intended element and account for visibility, waiting, and Shadow DOM behavior.
Selectors locate elements; actions such as click and typeText operate on them. A CSS selector string can also be passed directly as an action target. The examples below use data-test-id attributes because they can remain independent of styling and layout. Verify that your application actually renders the attributes you select.
1. Create and use a basic selector
Import Selector from TestCafe to compose a reusable query. This example defines a checkout test and clicks a uniquely identified button:
import { Selector } from 'testcafe';
fixture`Checkout`
.page`https://example.com/checkout`;
test('submit checkout', async t => {
const submit = Selector('[data-test-id="submit"]');
await t.click(submit);
});
The selector variable represents a query, not a frozen snapshot of the DOM. When the query is used by an action, assertion, or await, TestCafe evaluates it against the page. Reusing it after an action can therefore return different results if the page changed.
For a simple target, a CSS string is also valid: await t.click('[data-test-id="submit"]'). Use a Selector when you need to compose, inspect, or reuse the query.
2. Choose a selector strategy
| Approach | Use it when | Trade-off |
|---|---|---|
| CSS selector | A stable ID, custom attribute, tag, or CSS relationship identifies the target. | Familiar and concise; mutable classes and deep layout paths can be brittle. |
| Function-based selector | Client-side DOM logic or page state is needed to derive the target. | Flexible, but the function must follow TestCafe’s serialization restrictions. Do not use async/await or generators inside the selector function. |
| Selector query and methods | An existing query needs filtering or traversal to a related element. | Can avoid a long CSS path, but still verify the final match. |
Prefer a stable test attribute over a class whose purpose is visual styling. For example, data-test-id="submit" expresses test intent more clearly than a selector coupled to a CSS class or several nested containers. Keep the attribute meaningful and unique within the relevant scope.
3. Refine selectors by attributes, descendants, and text
Match an attribute
withAttribute(name, value) narrows a selector by attribute. The value is optional; string values require a strict match, and regular expressions are supported.
const submit = Selector('button')
.withAttribute('data-test-id', 'submit');
This is useful when the attribute alone could match a non-button element. Combine constraints only when they reflect meaningful properties of the target.
Find a descendant
find searches descendants of the starting selector. It accepts a CSS selector or a filter function:
const checkout = Selector('form')
.withAttribute('data-test-id', 'checkout');
const email = checkout.find('input[type="email"]');
await t.typeText(email, 'dev@example.com');
Keep the starting scope as narrow as possible while still matching the intended container. This helps prevent a similarly named input elsewhere on the page from being selected.
Match text carefully
withText matches a case-sensitive substring in text content or a regular expression. withExactText matches the exact case-sensitive text. Text in a descendant can also cause an ancestor to match, so pair text with a tag, attribute, or relationship when multiple elements may contain it.
const continueButton = Selector('button')
.withExactText('Continue');
await t.click(continueButton);
Use text when the wording is part of the behavior being tested. If copy changes independently of the control’s purpose, a stable test attribute may be a better locator.
4. Check matches, visibility, and timing
A broad query can match more than one element. TestCafe uses the first matching element for an action or assertion, which can make an ambiguous selector act on the wrong target without indicating that the query was too broad. The official guide states: “If a page action / assertion Selector matches multiple DOM elements, TestCafe performs the action / assertion with the first matching element.” [TestCafe Element Selectors]
const submit = Selector('button')
.withAttribute('data-test-id', 'submit');
const matches = await submit.count;
const exists = await submit.exists;
if (!exists || matches !== 1) {
throw new Error(`Expected one submit button, found ${matches}`);
}
exists and count are calculated immediately; selector timeout does not make them wait for a future match. Actions automatically wait for their targets to appear and become visible up to the selector timeout. Assertions have a separate assertion timeout. Use an assertion when you need to wait for an expected state to become true; do not treat an immediate count check as a wait.
TestCafe does not interact with invisible elements. Its documented visibility criteria include display: none, visibility: hidden or collapse, and zero width or height on the element or an ancestor. Opacity, z-index, and page position are not part of that stated classification. These rules describe TestCafe’s check; they do not guarantee a person would perceive the element as visible. [TestCafe filterVisible()]
If you need to select among visible matches, use filterVisible() as part of a selector query, then still verify the match is specific enough. Visibility filtering is not a substitute for a stable locator.
5. Handle special DOM cases
Shadow DOM
For content inside a shadow tree, locate the shadow root and traverse from it with selector methods. The shadow-root result is an entry point; do not pass that result itself to an action or assertion. Select the target inside the shadow tree, then act on or assert against that target. See the Selector constructor documentation for the documented selector behavior.
Pseudo-elements
Pseudo-elements such as ::before and ::after are generated by CSS and are not DOM elements that TestCafe actions can target. Interact with the actual element that owns the styling, or test the relevant rendered behavior through an assertion appropriate to your application.
Framework component selectors
Framework-specific selector libraries may be available as additional integrations. Do not assume that a base CSS selector can identify a React or Angular component as a component; use the relevant integration and verify its current documentation for your framework and version.
6. Troubleshooting common selector failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Action fails because no element was found | The locator is wrong, the attribute is absent, or the target did not appear before the selector timeout. | Inspect the rendered DOM and attribute value; scope the selector correctly. If the page loads the target asynchronously, use an action or assertion that waits for the expected state. |
| Action affects the wrong matching element | The selector matches several elements and TestCafe uses the first. | Add a stable attribute, scope with find, or refine with text and relationships. Check count when uniqueness matters. |
| Element exists but action cannot interact with it | It fails TestCafe’s visibility criteria, or the match is an unsupported target such as a pseudo-element or shadow root. | Wait for the real interactive element to become visible; target the element inside the shadow tree; target the owning DOM element instead of a pseudo-element. |
| Text locator matches a container unexpectedly | A descendant’s text also contributes to an ancestor’s text match. | Constrain by tag, test attribute, or relationship; use withExactText if exact text is the intended condition. |
exists is false just before content appears |
exists is an immediate query and does not wait for selector timeout. |
Use a waiting assertion for the expected condition or proceed with an action whose target is expected to appear. |
| Function-based selector fails to evaluate | The selector function may use unsupported constructs such as async/await or generators. |
Keep selector functions within the documented restrictions and move asynchronous work into the test flow. |
7. Keep tests reliable and efficient
- Prefer stable hooks. Ask the application team to expose test attributes on important controls instead of binding tests to incidental layout or styling.
- Scope locally. Start from a uniquely identified form or component and find the target inside it.
- Assert meaningful states. Wait for a state the test cares about, such as a confirmation element, rather than adding arbitrary delays as a substitute for synchronization.
- Use text intentionally. Text locators are useful when wording is part of the requirement, but can be sensitive to copy and localization changes.
- Check cardinality where ambiguity is a defect. A query finding one target makes accidental first-match behavior less likely.
- Avoid unnecessary repeated inspection. Each awaited selector property evaluates against the page. Keep diagnostics targeted, especially in larger suites.
Selector reliability depends on the page’s DOM and the stability of the chosen hooks. No benchmark or universal timeout value is implied here; tune timeouts to the application’s actual loading behavior and use assertions to express expected waits.
8. Or skip the browser setup
If your next step is capturing a page rather than testing an interactive control, ScreenshotNeo offers a one-call screenshot API. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Visit ScreenshotNeo or sign up free for 1,000 screenshots a month with no card.
9. FAQ
Does declaring a selector variable query the page immediately?
No. The selector represents a query that is evaluated when it is awaited or used by an action or assertion.
Does TestCafe click every element a selector matches?
No. An action or assertion uses the first matching element. Make the selector specific when multiple matches are possible.
Can I click a CSS pseudo-element?
No. Pseudo-elements are not action targets; select the actual DOM element instead.
Are opacity and z-index part of TestCafe’s visibility criteria?
They are not among the documented criteria listed for visibility. The guide identifies display, visibility, and zero dimensions on the element or an ancestor.


