Troupeby AliveDocsSign in

Testing

Assertions

expect checks what the screen shows. An assertion on a locator retries until it holds or its timeout passes, so you never wait by hand. This page lists the matchers, how retrying works, soft assertions, polling, and accessibility and screenshot snapshots.

Assertions on a locator

Pass a locator to expect and await the matcher.

TypeScript
await expect(screen.getByRole('heading', { name: 'Welcome back' })).toBeVisible()
await expect(screen.getByTestId('count')).toHaveText('Count: 1')
await expect(screen.getByRole('listitem')).toHaveCount(3)
await expect(screen.getByRole('switch', { name: 'Remember me' })).not.toBeChecked()
MatcherHolds when the element
toBeVisible()is on screen. { visible: false } asks for the opposite
toBeHidden()does not exist, or is not visible
toBeAttached()exists in the tree. { attached: false } asks for the opposite
toBeEnabled(), toBeDisabled()is enabled, or disabled
toBeChecked()is checked. { checked: false } and { indeterminate: true } ask for the other states
toBeFocused()has focus
toHaveText(expected)has exactly this text, counting its descendants' text
toContainText(expected)has text that contains this
toHaveValue(expected)has this value, as a text input or slider reports it
toHaveAccessibleName(expected)has this accessible name
toHaveRole(role)has this role
toHaveCount(n)The locator matches n elements
toMatchAriaSnapshot(template)has an accessibility subtree that matches the template
toHaveScreenshot(name?, options?)looks like the stored picture

Every matcher takes { timeout } as its last argument, and .not negates every one but toHaveScreenshot. expected is a string or a regular expression. toHaveText, toContainText and toHaveAccessibleName also take { ignoreCase: true }.

toHaveText and toContainText compare case-sensitively, after collapsing whitespace. Given a list, they check one element per entry, and the locator must match exactly that many:

TypeScript
await expect(screen.getByRole('listitem')).toHaveText(['Oat milk', 'Bread', /Eggs/])

How retrying works

A locator assertion asks the device, checks, and asks again after 100, 250, 500 and then every 1,000 milliseconds until it holds. The timeout is 10 seconds unless you change it:

TypeScript
await expect(screen.getByText('Synced')).toBeVisible({ timeout: 30_000 }) // this assertion

const slow = expect.configure({ timeout: 30_000 }) // every assertion made with `slow`
await slow(screen.getByText('Synced')).toBeVisible()

The config's expect.timeout sets it for a project or the whole run.

Locator assertions are strict. When the locator matches more than one element the assertion fails at once with LOCATOR_STRICT_VIOLATION; only toHaveCount and the list forms accept several. A failed assertion prints the locator, what it expected and received, and an excerpt of the accessibility tree, as shown in when a test fails.

Assertions on a value

Anything that is not a locator or the screen is a plain value, checked once with Jest's matchers: toBe, toEqual, toStrictEqual, toBeTruthy, toBeFalsy, toBeNull, toBeUndefined, toBeDefined, toBeNaN, toBeGreaterThan, toBeGreaterThanOrEqual, toBeLessThan, toBeLessThanOrEqual, toBeCloseTo, toBeInstanceOf, toContain, toContainEqual, toHaveLength, toHaveProperty, toMatch, toMatchObject and toThrow. .resolves and .rejects unwrap a promise first. The asymmetric matchers are expect.any, expect.anything, expect.objectContaining, expect.arrayContaining, expect.stringContaining, expect.stringMatching and expect.closeTo.

TypeScript
expect(await screen.getByRole('listitem').count()).toBeGreaterThan(2)
expect(await device.apps()).toContainEqual(expect.objectContaining({ bundleId: 'co.ex.shop' }))

Retrying anything

expect.poll retries a function and a value matcher together. toPass retries a block of assertions until none throws.

TypeScript
await expect.poll(() => screen.ariaSnapshot()).not.toBe('')

await expect(async () => {
  await screen.getByRole('button', { name: 'Refresh' }).tap()
  await expect(screen.getByText('Up to date')).toBeVisible({ timeout: 2_000 })
}).toPass({ timeout: 30_000 })

Both take timeout and intervals, and stop at the assertion timeout when you give none.

Soft assertions and messages

expect.soft records a failure and lets the test go on. The test still fails at the end, with every soft failure listed. A second argument to expect is a message printed above the failure.

TypeScript
await expect.soft(screen.getByTestId('total')).toHaveText('12.00')
await expect.soft(screen.getByTestId('tax')).toHaveText('1.20')
await expect(screen.getByTestId('pay'), 'the pay button unlocks after the form').toBeEnabled()

Accessibility snapshots

toMatchAriaSnapshot compares an element's accessibility subtree, or the whole screen, with a template. Each line is - role "name", with optional attributes in brackets and a value after a colon. Children are indented under a line that ends in a colon.

TypeScript
await expect(screen).toMatchAriaSnapshot(`
  - toolbar "Cart":
    - heading "Cart" [level=1]
  - textbox "Email": "a@b.co"
  - switch "Remember me" [checked]
  - button /Sign (in|up)/
  - text: "Total: 12.00"
`)

The match is partial. A template line matches an element with that role whose name, value and attributes all match; elements the template does not mention may sit between its lines, and its children must appear in order. The attributes are [checked], [checked=mixed], [disabled], [expanded], [selected], [focused], [pressed], [level=N] and [testId="…"]. screen.ariaSnapshot() prints the current tree in the same form to start from.

Screenshots

toHaveScreenshot compares the screen, or one element, with a PNG stored beside the spec file:

TypeScript
await expect(screen.getByTestId('count')).toHaveScreenshot('count-1.png')
await expect(screen).toHaveScreenshot('home.png', { maxDiffPixelRatio: 0.01 })

The snapshot for e2e/cart.e2e.ts in the project ios is e2e/cart.e2e.ts-snapshots/count-1-ios.png. Without a name it is the test's title and a counter.

  • The first run has nothing to compare with. It writes the snapshot from the first screenshot that is stable and fails the test once, so that you look at the picture before you commit it.
  • Later runs retry until a screenshot matches or the timeout passes. A mismatch attaches the expected, actual and difference images to the test.
  • To accept changes, run bunx troupe test --update-snapshots (or -u). It writes the snapshots that are missing or differ, and those tests pass.
OptionMeaningDefault
thresholdHow different one pixel's colour may be, from 0 to 10.2
maxDiffPixelsHow many pixels may differ0
maxDiffPixelRatioThe share of pixels that may differ, from 0 to 10
timeoutHow long to retrythe assertion timeout

The variable TROUPE_UPDATE_SNAPSHOTS chooses the mode directly: missing (the default), changed (what -u sets), all (rewrite every snapshot) or none (write nothing, and fail on a missing snapshot).

Your own matchers

expect.extend returns a new expect with your matchers added. A matcher receives the value and returns { pass, message }:

TypeScript
import { expect as base, Locator } from '@alivelabs/troupe'

export const expect = base.extend({
  async toHaveBadge(received: unknown, count: number) {
    if (!(received instanceof Locator)) throw new TypeError('toHaveBadge needs a locator')
    const name = await received.accessibleName()
    return {
      pass: name.endsWith(`, ${count} new`),
      message: () => `expected the name "${name}" to end with ", ${count} new"`,
    }
  },
})

Use it as await expect(screen.getByRole('tab', { name: /Inbox/ })).toHaveBadge(3). A matcher of your own runs once; expect.poll retries it like any value matcher.

Next

esc

    Type to search every page, the protocol reference and the cloud API.