Selector Steps

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

StepWhat it does
When I add "mobile logo" selector for "header img#logo" css selectorRegister a named CSS selector at runtime.
When I add "page title" selector for "//h1[contains(@class,'page-header')]" xpath selectorRegister a named XPath selector at runtime.
When I add selectors from "selectors.json" fileLoad CSS and XPath selectors from a JSON file.
Then I print css selectorsPrint all registered CSS selectors to console (debug).
Then I print xpath selectorsPrint 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 screenResize the viewport to a named breakpoint.
Then I see header above footerAssert a component is above one or more others.
Then I see footer below headerAssert a component is below one or more others.
Then I see logo to the left of navAssert a component is left of one or more others.
Then I see nav to the right of logoAssert a component is right of one or more others.
Then I see logo inside of headerAssert a bounding box is fully inside another's.
Then I see header outside of logoAssert a bounding box fully contains another's.
Then I see modal over contentAssert overlap via intersection and z-index.
Then I see header not over contentAssert two components do NOT overlap.
Then I see visible headerAssert one or more named locators are visible.
Then I don't see modalAssert one or more named locators are hidden.
Then I see search has focusAssert a named locator has keyboard focus.
When I move focus to "Title" fieldMove keyboard focus to a field by label, name, or id.
When I select all text in "Title" fieldSelect all text inside a field.
When I select from 0 to 5 text in "Title" fieldSelect a character range inside a field.
When I select "title name" text in "Title" fieldSelect a specific substring inside a field.
When I click navClick 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:

NameSizeRepresents
xs375 x 667phone portrait
sm576 x 800large phone / phablet
md768 x 1024tablet portrait
lg992 x 768small laptop / tablet landscape
xl1200 x 900desktop (default)
xxl1400 x 900wide desktop / HD
xxxl1920 x 1080Full 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 varworldParameters keyDefaultPurpose
WEBSHIP_SELECTORS_OFFSETselectors.offset60Scroll offset in pixels.
WEBSHIP_SELECTORS_BREAKPOINTSselectors.breakpoints7 built-ins (xs-xxxl)Named breakpoints as a JSON object.

Tips

  • Files listed in selectors.files are 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 try cms-drupal-core-claro.json or cms-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 selectors when 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.

Back to all step groups