Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
donkeycode avatar

Cucumber Sentences

  • 8 installs
  • Updated May 6, 2026
  • donkeycode/skills-cucumber-sentences

cucumber-sentences is an agent skill that helps you write Cucumber features and page objects using the cucumber-sentences Ruby gem DSL.

About

cucumber-sentences is an agent skill for Ruby projects that depend on the donkeycode cucumber-sentences gem. It helps solo builders and small teams keep browser acceptance tests consistent: one shared Gherkin vocabulary built on page-object, Watir, and rspec-expectations instead of reimplementing Selenium boilerplate per page. Invoke it when you create or edit `.feature` files, add buttons or fields that must be addressable from scenarios, introduce a new page class, or debug undefined Cucumber steps. The agent learns when to reuse the gem’s ~39 generic steps versus authoring custom definitions, and how to wire PageObject lookups correctly. That matters for indie SaaS teams who want readable scenarios stakeholders can skim while still automating against real browsers. Complexity is intermediate because you need Ruby, Cucumber, and the host project’s page map. It is an integration-style skill anchored to a specific open-source gem, not a generic test generator.

  • ~39 pre-built generic Cucumber step definitions via a single ~500 LOC library file
  • Standardizes Gherkin for clicks, fields, and named elements across page-object classes
  • Guides when to reuse library sentences versus custom step definitions
  • Covers Domain helpers, typed `of <type> "<id>"`, and state holder conventions
  • Resolves undefined-step errors by aligning pages with get_button / get_field / get_element_by_name

Cucumber Sentences by the numbers

  • 8 all-time installs (skills.sh)
  • Ranked #1,571 of 2,153 Testing & QA skills by installs in the Skillselion catalog
  • Security screen: MEDIUM risk (skills.sh audit)
  • Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/donkeycode/skills-cucumber-sentences --skill cucumber-sentences

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs8
Security audit2 / 3 scanners passed
Last updatedMay 6, 2026
Repositorydonkeycode/skills-cucumber-sentences

What it does

Write and extend Cucumber feature files and page objects using the cucumber-sentences Ruby gem’s shared Gherkin DSL.

Who is it for?

Ruby/Watir teams already using or adopting the cucumber-sentences gem for acceptance tests.

Skip if: JavaScript Playwright-only stacks, unit-test-only workflows, or projects with no Cucumber dependency.

When should I use this skill?

Writing or editing Cucumber `.feature` files, page-object classes, or step definitions in a project that depends on the cucumber-sentences Ruby gem; adding scenarios, undefined steps, new pages, or UI elements addressabl

What you get

Scenarios reuse the gem’s shared sentences and correctly mapped page objects so new UI elements are feature-addressable without rewriting step glue.

  • Updated `.feature` scenarios using shared sentences where appropriate
  • Page-object classes with correct element lookup helpers for new UI

By the numbers

  • ~39 pre-built generic Cucumber step definitions
  • Single library file ~500 lines of code (lib/cucumber-sentences.rb)

Files

SKILL.mdMarkdownGitHub ↗

cucumber-sentences

A Ruby gem that ships ~39 pre-built generic Cucumber step definitions on top of page-object + watir + rspec-expectations. The whole library is a single file; once required, every page in the host project speaks the same Gherkin DSL without re-implementing selenium boilerplate.

Source: <https://github.com/donkeycode/cucumber-sentences> · gem name: cucumber-sentences · file: lib/cucumber-sentences.rb (~500 LOC).

---

When to invoke this skill

Trigger on any of:

  • A .feature file is being created or edited and you must decide whether to reuse an existing sentence or write a custom step.
  • A new button, field, or element on a page must become referenceable from features (I click on the button "...", I fill "..." field with "...", …).
  • A new page is being introduced — needs a PageObject-style class with the get_button / get_field / get_element_by_name lookups wired correctly.
  • A Cucumber::Undefined error or "undefined step" feedback appears — the phrase is probably misspelled relative to the catalogue, or the page lacks a name in its lookup hash.
  • A request to add a "kind" of object resolvable by name (I am on the "OrderPage" of order "ABC-123") — needs a typed helper class under support/helpers/.
  • A request to share state across steps in the same scenario (an extracted email link, a generated id, …) — needs a session-scoped state holder.

---

The mental model: phrase → element in five hops

Cucumber-sentences is glue, not magic. Every browser-touching phrase follows the same chain. Internalise this before writing or debugging steps.

Gherkin string
    │  ① Cucumber regex match (captures the name)
    ▼
get_button("save profile")  ← method on @current_page (a PageObject)
    │  ② Hash lookup that you wrote on the page class
    ▼
save_element                ← page-object accessor
    │  ③ Synthesised by `button(:save, :xpath => "...")`
    ▼
Watir::Element              ← live DOM handle
    │  ④ Watir verb (when_visible, click, value=, text, …)
    ▼
Browser action              ← what actually happens
Phrase familyResolver method called on @current_page
... button "<name>" ... (click, see, not see, see disabled)get_button(name)
... field "<name>" ..., `I fill "<name>" {field\autocomplete\
I should not see the element "<name>", I can [not] see "..." in element "<name>", I scroll to "<name>", I hover over the element "<name>", I should see the element "<name>"get_element_by_name(name)
I fill "<name>" ckeditor field with "...", I can see "..." in ckeditor "<name>"get_ckeditor(name)
I fill "<name>" js field with "...", I force scroll to "<name>"get_js_selector(name)
I upload a file with the filename "..." in element "<name>"get_field(name) (Watir treats file inputs as fields)

The layered indirection — Gherkin name → get_X hash → page-object accessor → Watir handle — is deliberate:

  • Gherkin names track product copy, not markup.
  • get_X hashes localise renames in a single file.
  • Page-object declarations isolate locator strategy (xpath/id/css).
  • Watir is the only stable thing the gem talks to.

Adding a new button to a feature does not require a new step. Extend the page's locator block + the get_button hash, and the existing When I click on the button "..." resolves it. Authoring custom steps is the last resort, not the first.

---

Decision tree

When asked to make a new sentence work, walk it in this order:

1. Is the sentence already in the catalogue? Open references/sentences-catalogue.md and search. If yes, the only work is on the page class (add a locator + a hash entry). Stop. 2. Is it a near-miss spelling? Compare against the regex column. The phrases use double quotes only; embedded " is not supported. "in input" vs "in element" resolve to different methods. 3. *Does it need a new kind of object (a `<type>` lookup)? Add a class in `support/helpers/<type>.rb` exposing `self.get(identifier)`. See `references/helpers-and-state.md`. 4. Does it need to share data between steps? Add a state-holder class in `support/helpers/`. See `references/helpers-and-state.md`. 5. Genuinely new behaviour? Only now write a custom step in `features/step_definitions/.rb. Compose with step "..."` whenever possible to reuse existing sentences.

---

Fast templates (copy/adapt)

Step-definitions entry point

features/step_definitions/imports.rb:

ENV['CUCUMBER_ROOT'] = File.absolute_path('../', File.dirname(__FILE__))
require "cucumber-sentences"

This must run before any other step file — ENV['CUCUMBER_ROOT'] is required for the gem's dynamic helper-loading. See templates/step_definitions_imports.rb.

support/env.rb

require 'page-object'

$api_url     = ENV['API_URL']     || "https://api.example.test/"
$front_url   = ENV['FRONT_URL']   || "https://app.example.test/"
$mailhog_url = ENV['MAILHOG_URL'] || "http://localhost:8025/"

$site_url = $front_url            # default base URL injected as params['site_url']

World PageObject::PageFactory

support/hooks.rb

require 'watir'

browser = Watir::Browser.new :chrome, options: {
    args: %w(--no-sandbox --window-size=1600,1200) + (ENV['IS_DEV'] ? [] : %w(--headless=new))
}

Before        { @browser = browser }
at_exit       { browser.close }

After do |tc|
    if tc.failed?
        @browser.driver.save_screenshot("logs/#{tc.name}.png")
        File.write("logs/#{tc.name}.log", @browser.driver.logs.get(:browser).join("\n"))
    end
end

A page class — the only file most contributors edit

class LoginPage
    include PageObject

    page_url "<%=params['site_url']%>login"   # ERB-rendered with the params hash

    text_field(:email,    :id    => "email")
    text_field(:password, :id    => "password")
    button(:submit,       :xpath => "//button[@type='submit']")
    div(:formError,       :xpath => "//global-error//div")

    # Required by `I fill "<name>" field with "..."`, `I can see "..." in input "<name>"`,
    # `I should not see the field "<name>"`, etc.
    def get_field(name)
        {
            "email"    => email_element,
            "password" => password_element,
        }[name]
    end

    # Required by `I click on the button "<name>"`, `I should [not] see the button "<name>"`.
    def get_button(name)
        {
            "log in" => submit_element,
        }[name]
    end

    # Required by `I should [not] see the element "<name>"`, `I can [not] see "..." in element "<name>"`,
    # `I scroll to "<name>"`, `I hover over the element "<name>"`.
    def get_element_by_name(name)
        {
            "form error" => formError_element,
        }[name]
    end
end

See templates/page.rb for the fuller template (CKEditor, JS-selector, custom redirect matching with is_on_page).

A typed helper for of <type> "<identifier>"

# support/helpers/order.rb
class Order
    def self.get(identifier)
        # Return a Hash of params used by the page's `page_url` ERB template.
        # Only `site_url` is added later by the gem; the rest is up to you.
        {
            "ABC-123" => { "id" => "abc-123-uuid", "tab" => "summary" },
        }[identifier]
    end
end

Then in a feature:

Given I am on the "OrderEditPage" of order "ABC-123"

Page must template the params:

class OrderEditPage
    include PageObject
    page_url "<%=params['site_url']%>orders/<%=params['id']%>?tab=<%=params['tab']%>"
end

See templates/helper_typed.rb and references/helpers-and-state.md.

A session-scoped state holder

# support/helpers/extracted_link.rb
class ExtractedLink
    @@value = nil
    def self.set(v); @@value = v; end
    def self.get;    @@value;     end
end

Use from any step:

When(/^I save the link from the email$/) do
    href = @browser.execute_script("return document.querySelector('iframe').contentDocument.querySelector('a').href")
    ExtractedLink.set(href)
end

When(/^I follow the saved link$/) do
    @browser.goto ExtractedLink.get
end

@@-style class variables persist for the whole Cucumber run unless reset in a Before hook.

A new custom step that composes existing sentences

# features/step_definitions/auth.rb
Given(/^"([^"]*)" is logged in$/) do |name|
    require File.join(ENV['CUCUMBER_ROOT'], 'support/helpers/user')
    user = User.get(name) or raise "Unknown user: #{name}"
    step %{I try visit the page "LoginPage"}
    step %{I fill "email" field with "#{user['email']}"}
    step %{I fill "password" field with "#{user['password']}"}
    step %{I click on the button "log in"}
    step %{I should be redirected on "HomePage"}
end

Composition with step "..." is the right way to build domain-specific shortcuts — never copy-paste Watir code that the gem already encapsulates.

---

The full sentence catalogue (cheat sheet)

The gem registers exactly 39 step definitions. Full table with regex, retry semantics, and examples is in references/sentences-catalogue.md. The 39, grouped:

Navigation (6)I am on the "<page>" · I try visit the page "<page>" · I am on the "<page>" of <type> "<id>" · I try visit the page "<page>" of <type> "<id>" · I should be redirected on "<page>" · I change the domain to "<name>"

Buttons (3)I click on the button "<name>" · I should not see the button "<name>" · I should see the (button|field|element) "<name>"(\| disabled)

Fill (8)I fill "<f>" field with "<v>" · I fill "<f>" autocomplete with "<v>" · I fill "<f>" datepicker with "<v>" · I fill "<f>" ckeditor field with "<v>" · I fill "<f>" contenteditable with "<v>" · I fill "<f>" js field with "<v>" · I fill "<f>" field with date · I fill "<f>" field with time

Assertions on fields (6)I should see field "<f>" filled "<v>" · I should see field "<f>" filled date · I should see field "<f>" filled time · I can see "<v>" in input "<f>" · I can see the value "<v>" selected in the select box "<f>" · I should not see the field "<f>"

Selects (1)I click on the select box "<f>" to select "<v>"

Visibility & text (5)I should not see the element "<n>" · I can see "<t>" in element "<n>"(\| exactly) · I can not see "<t>" in element "<n>" · I should see a message tell me "<t>" · I should not see a message tell me "<t>"

CKEditor assertion (1)I can see "<v>" in ckeditor "<f>"

Browser misc (7)I refresh the page · I wait <n> seconds · I make one pause · I scroll to "<n>" · I force scroll to "<n>" · I hover over the element "<n>" · I upload a file with the filename "<f>" in element "<dz>"

MailHog helpers (2)I see the last email subject "<s>" · I open on the last email link

Total: 6 + 3 + 8 + 6 + 1 + 5 + 1 + 7 + 2 = 39.

Given / When / Then keywords at registration are stylistic only — Cucumber matches against the same pool regardless of keyword. Use whichever reads naturally in the feature.

---

Fakable inline substitution (optional)

Inside any string argument routed through Fakable.fake_if_needed, the token

@('Category', 'method', 'memoize_key')

is replaced by the value of Faker::<Category>.<method>. Repeated memoize_key reuses the first value within the run — useful when two steps must refer to the same generated email or name.

Given I fill "email"         field with "@('internet', 'email', 'main-user')"
And   I fill "email confirm" field with "@('internet', 'email', 'main-user')"

⚠️ Memoisation is class-wide (@@memorized_strings) and not reset between scenarios — choose distinct mem keys per scenario or accept the shared state. See references/sentences-catalogue.md for the precise list of phrases that route through Fakable.

---

Hidden contracts (the things that bite)

1. `ENV['CUCUMBER_ROOT']` must be set before `require "cucumber-sentences"`. Otherwise the dynamic helper loaders (I change the domain to, I am on the "..." of <type> "...", I upload a file ...) blow up on nil path joins. 2. Page name → snake_case transform is gsub(" ", "_") only — non-ASCII and dashes pass through. visit_page then expects a class named in PascalCase of the snake-case (e.g. "OrderEditPage" → file/class OrderEditPage). 3. *String capture is `"([^"])"** — only double quotes, no escapes. Embedded " in test data is impossible without forking the gem. 4. **I force scroll to "<name>" (built-in)** does getElementById(get_js_selector(name)) — your get_js_selector must return an *id*, not a CSS selector. Most projects override this phrase with a CSS/XPath-aware variant in their own step_definitions/. 5. **I open on the last email link** depends on jQuery being loaded on the current page (jQuery(srcdoc)…). Replace with a pure-JS extension if you can't guarantee jQuery. 6. **Strict URL match on redirection** — unless the page implements is_on_page(url), page.page_url_value must equal @browser.url exactly (no trailing-slash leniency, no query-string tolerance). 7. **Disabled-button assertion** calls enabled? on the element returned by get_button. If the visible "button" is a <div> wrapper around the real <button>, return the inner element from get_button. 8. **Retry budget** — most assertions retry **30 times × 1 s sleep**. I fill ... datepicker and I can see ... in element cap retries at **5** when the cause is Watir::Wait::TimeoutError (the visibility check already waited). Failures past the budget propagate normally. 9. **Faker memoisation persists across scenarios** in the same cucumber invocation — see §Fakable above. 10. **Watir / Selenium / Chrome alignment** — if the suite runs in Docker against selenium/standalone-chrome, bumping Chrome out of band against an unchanged watir / selenium-webdriver` will break headlessly. Pin both.

---

Verification checklist before declaring a step "done"

  • [ ] The phrase is one of the 39 registered, OR a project-side step composed from them.
  • [ ] Every name referenced in the feature exists in the page's get_button / get_field / get_element_by_name (or the right resolver per the table above).
  • [ ] The locator (text_field/button/div/etc.) is declared at the top of the page class.
  • [ ] If the URL is templated (<%=params['…']%>), every placeholder is supplied either by $site_url or by the typed helper's .get(identifier) Hash.
  • [ ] If is_on_page is overridden, it returns truthy for every URL the redirect should accept.
  • [ ] Run the scenario locally — IS_DEV=1 bundle exec cucumber path/to.feature — and watch the actual browser; do not trust a green run-on-CI without eyes-on the first time.

---

References (load on demand)

  • references/sentences-catalogue.md — every registered sentence with its exact regex, parameters, retry semantics, and an example. Use when debugging an "undefined step" or when picking the closest match for a new feature line.
  • references/page-class-contract.md — deeper dive into page-object's locator declarations, the synthesised <name>_element accessors, and which sentences call which get_X resolver.
  • references/helpers-and-state.md — patterns for Domain (named base URLs), <type> helpers (typed object lookups), and session-scoped state holders.

Templates (copy from templates/)

  • step_definitions_imports.rb — the entry point that requires the gem.
  • page.rb — full skeleton of a page class with all get_X resolvers stubbed.
  • helper_domain.rbDomain helper template (named base URLs).
  • helper_typed.rb — typed helper template for of <type> "<id>" flows.
  • state_holder.rb — session-scoped state holder template.

Related skills

How it compares

Opinionated gem integration for shared Gherkin steps—not a greenfield BDD framework generator for every language.

FAQ

Who is cucumber-sentences for?

Developers and small teams on Ruby Cucumber projects that use page-object and Watir and want the donkeycode sentence library applied correctly.

When should I use cucumber-sentences?

When editing `.feature` files, adding feature-referenceable UI elements, creating pages, extending Domain/state helpers, or fixing undefined steps in a cucumber-sentences-based repo.

Is cucumber-sentences safe to install?

It guides test code against your repo; review the Security Audits panel on this Prism page and pin the gem version you trust in Gemfile.

Testing & QAtestingfrontend

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.