Assertion Steps

Assertion steps are the Then half of every scenario: after navigating and interacting, they verify that the user actually saw the right thing. This group covers page-level checks - plain text on the page, regex pattern matching, text inside a table row or a specific element, element existence, CSS properties, element counts, link destinations, the raw HTML response, and the HTTP status code. Field-level checks (checkbox state, field values) live in the field steps group, URL/path checks in the navigation steps group, and modal checks in the modal steps group.

Most of these steps are web-first: instead of taking a single snapshot of the page, they wait on a Playwright locator, so the assertion passes the moment the expected content appears. For example, I should see "Welcome" waits up to 5 seconds for the body to contain the text, element-existence checks wait up to 3 seconds for visibility, and element-content checks wait up to 5-10 seconds for the element to attach before reading its text. That means you rarely need an explicit wait between an action and its assertion; for slow AJAX flows, combine them with the wait steps (BBR smart waits) or the auto-retrying matchers on the web-first assertion steps page.

Steps that take an element "by attr" run the identifier through the selector registry: a bare word like uname expands to a tolerant selector that tries the tag name, #id, .class, name, data-testid, data-test-id, data-test, data-cy, aria-label, value, placeholder, and title - so the same phrasing works whether your markup uses ids, classes, or test attributes. Naming an explicit attribute (by its "id" attr) pins the lookup to that attribute with a case-insensitive match. Every step also accepts I, we, or no pronoun, and (where shown) a not variant for the negative check.

Quick reference

StepWhat it does
Then I should see "Welcome"Assert that text is or is not present on the page.
Then I should see text matching "^T\w+"Assert that page text does or does not match a regex pattern.
Then I should see "Active" in the "John Smith" rowAssert text is or is not inside a table row identified by text.
Then I should see a "Username" elementAssert that an element with a given label exists (or does not).
Then I should see a "uname" element by its "id" attrAssert that an element, identified by attribute, exists (or does not).
Then I should see "John Smith" in the "Username" elementAssert that an element found by its label contains (or not) text.
Then I should see "John Smith" in the "uname" element by its "id" attrAssert that an element found by attribute contains (or not) text.
Then I should see text matching "\d{4}" in the "#year" elementAssert that an element's text does or does not match a regex.
Then the "body" element should contain "color:white;"Assert that an element has (or lacks) a computed CSS property.
Then I should see 3 "li" elementsAssert the exact number of elements matching a CSS selector.
Then the "Login" link should contain "/log-in"Assert that a link located by visible text points to a URL.
Then the "#about-us-id" link should contain "about" by attrAssert that a link located by attribute points to a URL.
Then the response should contain "Welcome visitor"Assert that the full HTML response contains (or not) text.
Then the response status code should be 200Assert the current page's HTTP status code is (or is not) a value.

Steps in detail

Then I should see "Welcome"

The workhorse assertion: check that the given text appears anywhere in the page body. The positive form is web-first - it waits up to 5 seconds for the text to appear, so it tolerates content that arrives after an AJAX call or a client-side render. The negative form (should not see) takes an immediate snapshot of the body text.

When I press "Register"
Then I should see "Registration Done Successfully"
 And I should not see "Access denied"

Then I should see text matching "^T\w+"

Check the page's rendered text (document.body.innerText) against a JavaScript regular expression. Use it for dynamic values - dates, order numbers, prices - where the exact text can't be hard-coded. Because it reads innerText, hidden elements are excluded.

Given I am on "/test--then--i-should--see-text-matching-pattern.html"
Then I should see text matching "^T\w+"
 And I should see text matching "[a-z0-9]+@[a-z]+\.[a-z]{2,3}"
 And I should not see text matching "^O\w+"

Then I should see "Active" in the "John Smith" row

Find every <tr> whose text contains the row identifier, then assert the expected text appears in (at least) one of them. This scopes the check to a single table row, so "Active" next to John Smith passes even when "Inactive" appears elsewhere in the table. Pairs naturally with When I click "Edit" in the "John Smith" row from the interaction steps.

Given I am on "/test--then--i-should-see-text-in-table-row.html"
Then I should see "Admin" in the "John Smith" row
 And I should see "$85,000" in the "John Smith" row
 And I should not see "Admin" in the "Jane Doe" row

Then I should see a "Username" element / ... element by its "id" attr

Existence checks. The label form locates the exact text on the page; if that text is a <label for="...">, the step follows the for attribute and asserts the associated control itself is visible - so I should see a "Username" element proves the input exists, not just its label. The attribute form goes through the selector registry: by attr alone tries id, class, name, data-testid and friends, while by its "id" attr pins it to one attribute. Both wait up to 3 seconds for visibility; the negative forms assert absence immediately.

Given I am on "/test--then--i-should--see-text-in-element.html"
Then I should see a "Username" element
 And I should see a "uname" element by its "id" attr
 And I should see a "pwordcss" element by attr
 And I should not see an "emailId" element by its "id" attr

Then I should see "John Smith" in the "Username" element / ... element by its "id" attr

Scoped text checks: assert that a specific element contains (or does not contain) the expected text. The label form resolves <label for> the same way as the existence check, which makes it ideal for asserting the value shown in a form control. The attribute form accepts a CSS selector or a registry-expanded shorthand. For inputs, the element's value counts as its text, so you can verify pre-filled fields.

Given I am on "/test--then--i-should--see-text-in-element.html"
When I fill in "user1" for "Username"
Then I should see "user1" in the "Username" element
 And I should see "1234" in the "pwordcss" element by attr
 And I should not see "John Smith" in the "uname" element by its "id" attr

Then I should see text matching "\d{4}" in the "#year" element

Regex matching scoped to one element, located by CSS selector. This is the tool for format checks - timestamps, currencies, dates - inside a known container.

Given I am on "/date.html"
Then I should see text matching "(0?[1-9]|[12][0-9]|3[01])[\/\-](0?[1-9]|1[012])[\/\-]\d{4}" in the "#date" element
 And I should see text matching "\$\d+\.\d{2}" in the "#price" element
 And I should not see text matching "Error" in the "#status" element

Then the "body" element should contain "color:white;"

A CSS assertion, not a text assertion: the quoted string is parsed as property:value and compared against the element's computed style. Color values are normalized before comparison, so white, #fff, and rgb(255, 255, 255) all match each other - write whichever form your stylesheet uses. Shorthand values are token-matched, so border:solid 5px red; matches regardless of token order in the computed value. The trailing semicolon is optional.

Given I am on "/test--then--the-element-should_not-contain-cssproperty.html"
Then the "body" element should contain "background-color:white;"
 And the "#uname" element should not contain "border:solid 5px red;"
 And the "pword" element should not contain "font-size: 28px;"

Then I should see 3 "li" elements

Assert the exact count of elements matching a CSS selector - including compound selectors and attribute selectors. A count of 0 is a valid expectation for asserting absence. The failure message reports the actual count, and the singular/plural element/elements are both accepted. Note this counts immediately; if the list is still loading, precede it with When I wait until 3 elements match "li" from the wait steps, or use the auto-retrying should have a count of web-first matcher.

Given I am on "/element.html"
Then I should see 1 "h1" element
 And I should see 13 "ul:first-of-type li" elements
 And I should see 0 ".does-not-exist" elements

Then the "Login" link should contain "/log-in" / ... by attr

Verify where a link points without clicking it. The first form locates the link by its exact visible text; the by attr form locates it by CSS selector or by a named attribute (by its "class" attribute, by its "title" attribute, …). The check is a substring match against the link's resolved href, so "about" matches /about-us and absolute URLs alike.

Given I am on "/test--then--the-link-should-contain.html"
Then the "About us" link should contain "about"
 And the "#aboutUsid" link should contain "about" by attr
 And the "contactUsid" link should contain "/contact-" by its "id" attribute
 And the "aboutUs" link should contain "about" by its "class" attribute

Then the response should contain "Welcome visitor"

Assert against the text of the entire <html> element rather than just the visible body. Reach for this when the text lives outside normal view: <head> content such as titles and JSON-LD, <noscript> blocks, or hidden regions. The negative form guards against content that must never ship (debug output, "TODO" markers).

Given I am on "/"
Then the response should contain "Welcome to the homepage"
 And the response should not contain "TODO"

Then the response status code should be 200

Re-request the current URL and assert its HTTP status. Because the check issues a fresh GET (via axios) rather than reusing the browser's original response, it also works after client-side navigation, and non-2xx codes (404, 301, 500) are captured rather than thrown. Use it to confirm error pages really are errors and real pages really are 200s.

When I go to "/does-not-exist.html"
Then the response status code should be 404
 And the response status code should not be 200
When I go to "/existing-page.html"
Then the response status code should be 200

Complete example

Adapted from the suite's own feature files, running against the bundled fixture pages in examples/.

Feature: Verify a user directory page
  As a site owner
  I want the user table, links, and page content verified
  So that I know visitors see the right thing

  Scenario: Page renders the expected content and structure
    Given I am on "/test--then--i-should-see-text-in-table-row.html"
     Then the response status code should be 200
      And I should see "Admin" in the "John Smith" row
      And I should see "john.smith@company.com" in the "John Smith" row
      And I should not see "Admin" in the "Jane Doe" row
      And I should see text matching "[a-z0-9.]+@[a-z]+\.[a-z]{2,3}"

  Scenario: Login form has the expected fields and styling
    Given I am on "/test--then--i-should--see-text-in-element.html"
     Then I should see a "Username" element
      And I should see a "uname" element by its "id" attr
      And I should not see an "Eamil" element
      And the "#uname" element should not contain "border:solid 5px red;"
     When I fill in "user1" for "Username"
     Then I should see "user1" in the "Username" element

  Scenario: Navigation links point to the right pages
    Given I am on "/test--then--the-link-should-contain.html"
     Then the "About us" link should contain "about"
      And the ".contactUs" link should contain "/contact-" by attr
      And I should see 1 "h1" element

Tips

  • Prefer an assertion over a sleep: Then I should see "Done" already waits up to 5 seconds, and Then eventually I should see "Done" within 10 seconds (wait steps) polls even longer. A fixed I wait N seconds before an assertion is almost never needed.
  • Scope your checks. I should see "Active" passes if the word appears anywhere; I should see "Active" in the "John Smith" row or ... in the "#status" element proves it appears in the right place.
  • Negative checks are snapshots. should not see runs immediately, so if content is still loading it can pass spuriously - settle the page first with When I wait for AJAX to finish or a targeted disappear wait.
  • For state checks (visible, hidden, enabled, focused, count) with configurable within N seconds budgets, see the auto-retrying matchers on the web-first assertion steps page.

Back to all step groups