The selectors steps provide a named CSS and XPath registry plus layout assertions built on it. Instead of scattering raw selectors through feature files, you register human-readable names - header, main nav, cta button - once, then use those names everywhere: to assert relative position (above, below, left, right, inside, outside, over), visibility, and focus, and to click. When a redesign changes the markup, you update one registry entry and every scenario keeps passing, which is why test maintenance teams gain stable test artifacts across redesigns.
Selectors come from four places, merged in order: the selectors.css / selectors.xpath blocks in cucumber.js worldParameters, JSON selector files auto-loaded via selectors.files, files loaded mid-scenario with the "add selectors from file" step, and inline registrations made by steps in the scenario itself - latest registration wins. The project ships more than 20 ready-made CMS and framework presets under tests/selectors/ - Drupal (Claro and Gin), WordPress, Joomla, TYPO3, Magento 2, Shopify, Strapi, and more, plus front-end frameworks from Bootstrap and Tailwind to Material UI - so admin-UI names work out of the box.
Resolution is forgiving: names are looked up in the CSS registry first, then XPath, and an unknown name fails with a "Did you mean ...?" suggestion plus the list of registered names. Position assertions use Playwright's locator.boundingBox() in absolute page coordinates (scrolling the element into view first), visibility assertions use auto-retrying waitFor checks (5 s), and clicks use locator.click(), which auto-scrolls - all in line with webship-js web-first assertions.
Quick reference
| Step | What it does |
|---|---|
When I add "mobile logo" selector for "header img#logo" css selector | Register a named CSS selector at runtime. |
When I add "page title" selector for "//h1[contains(@class,'page-header')]" xpath selector | Register a named XPath selector at runtime. |
When I add selectors from "selectors.json" file | Load CSS and XPath selectors from a JSON file. |
Then I print css selectors | Print all registered CSS selectors to console (debug). |
Then I print xpath selectors | Print all registered XPath selectors to console (debug). |
Given I define css selectors: | Define named CSS selectors in bulk (data table). |
Given I define xpath selectors: | Define named XPath selectors in bulk (data table). |
Given I am viewing the site on a xl screen | Resize the viewport to a named breakpoint. |
Then I see header above footer | Assert a component is above one or more others. |
Then I see footer below header | Assert a component is below one or more others. |
Then I see logo to the left of nav | Assert a component is left of one or more others. |
Then I see nav to the right of logo | Assert a component is right of one or more others. |
Then I see logo inside of header | Assert a bounding box is fully inside another's. |
Then I see header outside of logo | Assert a bounding box fully contains another's. |
Then I see modal over content | Assert overlap via intersection and z-index. |
Then I see header not over content | Assert two components do NOT overlap. |
Then I see visible header | Assert one or more named locators are visible. |
Then I don't see modal | Assert one or more named locators are hidden. |
Then I see search has focus | Assert a named locator has keyboard focus. |
When I move focus to "Title" field | Move keyboard focus to a field by label, name, or id. |
When I select all text in "Title" field | Select all text inside a field. |
When I select from 0 to 5 text in "Title" field | Select a character range inside a field. |
When I select "title name" text in "Title" field | Select a specific substring inside a field. |
When I click nav | Click one or more named components (auto-scrolls). |
Steps in detail
When I add "mobile logo" selector for "header img#logo" css selector
Registers one named CSS selector for the rest of the scenario. Registering the same name again overrides the earlier value - latest wins - so you can load a preset file and then patch a single entry inline. Names and selectors are trimmed; empty values are rejected with a clear error.
When I add "breadcrumb" selector for ".breadcrumb" css selector
And I add "cta button" selector for ".cta .btn-primary" css selector
Then I see visible breadcrumb, cta button
When I add "page title" selector for "//h1[contains(@class,'page-header')]" xpath selector
Registers a named XPath selector. At resolution time the expression is passed to Playwright with an xpath= prefix and only the first match is used, so axis predicates like //table//tr[1] or text matches like //a[contains(normalize-space(.),'Login')] behave predictably. The CSS registry is checked before the XPath registry when both hold the same name.
When I add "login link" selector for "//a[contains(normalize-space(.),'Login')]" xpath selector
Then I see visible login link
When I click login link
When I add selectors from "selectors.json" file
Loads a JSON file with the shape {"css": {...}, "xpath": {...}} and merges both maps into the scenario's registries. The path is resolved against worldParameters.selectors.filesPath (which must be set); only .json files are accepted. Loading several files in sequence is fine - later files override earlier keys with the same name.
When I add selectors from "front-end-selectors.json" file
And I add selectors from "cms-drupal-core-claro.json" file
Then I see visible page heading
Then I print css selectors / Then I print xpath selectors
Dumps every registered name and its selector to the console - a quick way to inspect what a preset file actually registered, or to debug an "Unknown selector" failure. Works under any Gherkin keyword.
When I add selectors from "homepage-selectors.json" file
Then I print css selectors
And I print xpath selectors
Given I define css selectors: / Given I define xpath selectors:
Bulk registration via a two-column data table (name, selector) - the idiomatic way to declare a scenario's page vocabulary in a Background. Entries merge into the same registries as the one-at-a-time steps, latest wins.
Given I define css selectors:
| header | #header |
| nav | nav.primary |
| footer | footer |
And I define xpath selectors:
| first heading | //h1[1] |
Then I see header above footer
Given I am viewing the site on a xl screen
Resizes the viewport to a named breakpoint before layout assertions. The name can be bare or quoted, followed by screen or device, and custom breakpoints can be defined under worldParameters.selectors.breakpoints. Built-ins:
| Name | Size | Represents |
|---|---|---|
xs | 375 x 667 | phone portrait |
sm | 576 x 800 | large phone / phablet |
md | 768 x 1024 | tablet portrait |
lg | 992 x 768 | small laptop / tablet landscape |
xl | 1200 x 900 | desktop (default) |
xxl | 1400 x 900 | wide desktop / HD |
xxxl | 1920 x 1080 | Full HD / large monitor |
Given I am viewing the site on a xs screen
Then I see visible heading
Given we are viewing the site on a "xxl" device
Then I see heading above list
Then I see header above footer / below / to the left of / to the right of
Directional position assertions in absolute page coordinates (bounding box plus scroll offset, after scrolling the element into view). Both sides accept comma-and-and-separated lists and every subject is checked against every target - Then I see top bar above nav, content and footer runs the full cartesian product and reports all failures at once. "Above" means the subject's bottom edge is at or above the target's top edge, and the other directions mirror that.
Then I see heading above subline, list and footer
And I see footer below header, nav and content
And I see logo to the left of nav
And I see search button to the right of search
Then I see logo inside of header / Then I see header outside of logo
Containment assertions: inside of passes when the subject's bounding box is fully within the target's; outside of is the inverse claim - the subject fully contains the target. Both accept multiple names on either side.
Then I see logo inside of header
And I see nav inside of header
And I see page outside of content
Then I see modal over content / Then I see header not over content
Overlap assertions combining box intersection with effective z-index (computed by walking positioned ancestors). over passes when the boxes intersect and stacking allows the subject to sit over the target - ideal for modals, dropdowns, tooltips, and sticky headers. not over asserts the two components do not overlap at all, catching layout collisions.
Then I see modal over content
And I see cookie banner over footer
And I see nav not over sidebar
Then I see visible header / Then I don't see modal
Visibility assertions on one or more named locators, using Playwright's auto-retrying waitFor with a 5-second timeout - visible waits for the element to appear; the negative form (don't see / do not see) waits for it to be hidden or detached. All failing names are reported together with their resolved selectors.
Then I see visible hero, content and sidebar
And I don't see modal, overlay and popup
Then I see search has focus
Asserts a single named locator is the document's active element, waiting up to 5 seconds via waitForFunction. Useful after keyboard navigation, skip links, or modal-open focus management.
When I click search button
Then I see search has focus
When I move focus to "Title" field
Moves keyboard focus to a form field resolved by the accessibility-first chain label → placeholder → role=textbox name → [name]/#id (Playwright's getByLabel/getByPlaceholder/getByRole with fallbacks). This is the same field-resolution used by the text-selection steps below.
Given I am on "/field.html"
When I move focus to "Email" field
Then I see email input has focus
When I select all text in "Title" field / ...from 0 to 5 text... / ...select "title name" text...
Text-selection steps on a field resolved by label, name, or id. The "all text" form focuses the field and selects its whole value; the numeric form selects a character range via setSelectionRange; the substring form finds the given text in the field's current value (failing clearly if absent) and selects exactly that range. Follow with keyboard steps to replace, copy, or delete the selection.
And I fill in "Title" with "Release 2.0 notes"
When I select from 0 to 7 text in "Title" field
When I select "notes" text in "Title" field
And I select all text in "Title" field
When I click nav
Clicks one or more named components - on/a are optional (When I click on logo), and a comma-separated list clicks each in sequence: When I click tab 1, tab 2. Uses locator.click(), which auto-scrolls the target into view and waits for it to be actionable. Works with CSS- and XPath-registered names alike.
Given I define css selectors:
| tab 1 | [data-tab='1'] |
| tab 2 | [data-tab='2'] |
When I click tab 1, tab 2
Complete example
Adapted from tests/features/test--then--i-see-locator.feature:
Feature: Relative element positioning assertions and advanced selector system.
As a tester,
I want to assert that page components are positioned correctly relative to each other
and resolve elements via a named CSS/XPath selector registry
so that I can catch layout regressions without brittle pixel comparisons.
Background:
Given I am on homepage
And I define css selectors:
| heading | h1 |
| subline | h3 |
| list | ul |
Scenario: Layout holds on a phone-sized viewport.
Given I am viewing the site on a xs screen
Then I see visible heading
And I see heading above subline, list
And I see subline below heading
Scenario: Mix file-based, inline CSS, and inline XPath selectors.
When I add selectors from "homepage-selectors.json" file
And I add "first para" selector for "//p[1]" xpath selector
Then I see visible page heading, first para
And I print css selectors
Configuration
Everything lives under one selectors block in cucumber.js worldParameters; two settings are also overridable from the shell.
selectors: {
filesPath: './tests/selectors/',
files: ['front-end-selectors.json', 'back-end-selectors.json'],
css: {
'page header': 'header.page-header',
'main nav': 'nav[role="navigation"]',
},
xpath: {
'page title': '//h1[contains(@class,"page-title")]',
},
offset: 60,
breakpoints: {
xs: { width: 375, height: 667 },
xl: { width: 1200, height: 900, default: true },
},
}
| Env var | worldParameters key | Default | Purpose |
|---|---|---|---|
WEBSHIP_SELECTORS_OFFSET | selectors.offset | 60 | Scroll offset in pixels. |
WEBSHIP_SELECTORS_BREAKPOINTS | selectors.breakpoints | 7 built-ins (xs-xxxl) | Named breakpoints as a JSON object. |
Tips
- Files listed in
selectors.filesare auto-loaded at the start of every scenario - no step needed. Use the "add selectors from file" step only for scenario-specific presets. - Preset names for popular stacks live in
tests/selectors/- for a Drupal admin theme trycms-drupal-core-claro.jsonorcms-drupal-cms-gin.json. - An unknown name fails with a "Did you mean ...?" suggestion and the list of registered names - run
Then I print css selectorswhen in doubt. - Position assertions compare absolute page coordinates, so they are stable regardless of the current scroll position; combine them with breakpoint resizes to lock down responsive layouts.