> For AI agents: the complete documentation index is available at https://rstest.rs/llms.txt, the full documentation bundle is available at https://rstest.rs/llms-full.txt.

# Assertion

`expect.element` is the API for [Locator](https://rstest.rs/api/runtime-api/browser-mode/locator.md) assertions in Browser Mode. It accepts a `Locator` and returns a chainable assertion object. Unlike `expect(value)` which primarily compares JS values, `expect.element` targets the real element state on the page, suitable for verifying visibility, text, form values, count, and other user-observable results.

:::tip Auto-retry
With the Playwright provider, all `expect.element` matchers automatically retry until the assertion passes or the timeout is reached. You do not need to write manual `waitFor` or polling logic — just assert the expected state and the framework handles the waiting.
:::

When you import `@rstest/browser` in a Browser Mode test (e.g., importing `page`), `expect` automatically gains the `element` capability. This allows queries, interactions, and assertions to be organized around the same `Locator`, focusing failure messages on element state.

The following minimal example demonstrates common usage: first assert visibility, then verify a checked state change, and finally validate an element attribute.

```ts title="src/assertion.test.ts"
import { page } from '@rstest/browser';
import { expect, test } from '@rstest/core';

test('asserts element states in browser mode', async () => {
  await expect
    .element(page.getByRole('button', { name: 'Save' }))
    .toBeVisible();

  await expect.element(page.getByLabel('Agree')).toBeUnchecked();
  await page.getByLabel('Agree').check();
  await expect.element(page.getByLabel('Agree')).toBeChecked();

  await expect
    .element(page.getByRole('button', { name: 'Save' }))
    .toHaveAttribute('id', 'save-btn');
});
```

## Type signature

```ts
expect.element(locator: Locator): BrowserElementExpect
```

## Auto-retry and timeout

Assertion behavior in `expect.element` depends on the provider. With the Playwright provider, `expect.element` matchers are **web-first assertions**: they continuously retry within the timeout duration until the assertion passes or times out. You do not need to write manual `waitFor` or polling logic.

### Timeout priority

The assertion timeout is determined by the following rules:

1. **Per-assertion timeout**: The `timeout` parameter passed directly to the matcher, highest priority. This includes `0`; with Playwright, an explicit `0` means no matcher-specific deadline when there is no finite active test or hook deadline. Inside a finite non-concurrent test or hook, it is capped by the remaining scope timeout.
2. **Configured poll timeout**: When no matcher timeout is provided, the assertion uses `expect.poll.timeout`. Its default is `1000ms` in Node Mode and `5000ms` in Browser Mode.
3. **Remaining test or hook timeout**: Outside concurrent flows, an implicit assertion timeout is capped by the active test or hook deadline, so it cannot outlive that scope. Browser Mode tests default to `15000ms`.

Outside concurrent flows, the effective timeout is capped by the remaining active test or hook timeout. With no per-assertion override, it is the smaller of `expect.poll.timeout` and that remaining time. With an explicit matcher timeout, it is the smaller of the requested timeout and that remaining time. The Browser Mode `5000ms` default gives asynchronous UI updates more time without making a failed assertion wait for the full `testTimeout`. If there is no finite active test or hook deadline, an explicit `timeout: 0` retains Playwright's no-deadline behavior.

For `test.concurrent`, use the test-scoped `expect` fixture (`async ({ expect }) => ...`) when the assertion must use that test's remaining deadline. The shared file-level `expect` uses the configured poll timeout because browsers cannot identify concurrent async flows reliably. The same limitation applies to hooks inside `describe.concurrent`; their assertions use the poll timeout and can still be ended by the enclosing hook timeout.

Assertion timeout is independent from the browser RPC transport timeout. During test execution, browser and snapshot RPC requests do not add another timeout. Framework setup and file-cleanup RPCs use a separate 30-second watchdog.

```ts
// Uses the Browser Mode expect.poll.timeout default (5000ms),
// capped by the remaining time of the current test
await expect.element(page.getByText('Done')).toBeVisible();

// Override with a longer timeout
await expect.element(page.getByText('Done')).toBeVisible({ timeout: 10000 });

// With testTimeout: 0, element assertions use the configured poll timeout
// (5000ms by default in Browser Mode).
```

### Failure output

When an assertion times out, the error message includes: the expected element state, the actual state, and the timeout duration. For example:

```
Error: Expected element to be visible
  Locator: getByText('Loading complete')
  Timeout: 5000ms
```

### Use cases

Auto-retry is especially useful for handling async rendering scenarios — such as waiting for a loading indicator to disappear or UI changes after data finishes loading:

```ts
// Click and wait for async result to appear
await page.getByRole('button', { name: 'Submit' }).click();
await expect.element(page.getByText('Submitted!')).toBeVisible();

// Wait for loading to disappear
await expect.element(page.getByTestId('spinner')).toBeHidden();
```

## Input constraints

- `locator` must be a `Locator` returned by `@rstest/browser`. This allows the runtime to recognize and replay the complete query chain; hand-crafted objects or locators from other libraries cannot be parsed by this mechanism.
- `expect.element` is not available outside of Browser Mode. It relies on Browser Mode's browser runtime and communication channel to execute element assertions; pure Node test environments only support regular `expect(value)` assertions.

## Screenshot matching

### toMatchScreenshot \{#toMatchScreenshot}


[Added in v0.12.4](https://github.com/web-infra-dev/rstest/releases/tag/v0.12.4)

With the Playwright provider, `toMatchScreenshot()` captures either a Locator or the page and compares it with a PNG reference. The first run creates a reference when snapshot updates allow new snapshots. Later runs compare against that reference; use `--update` to replace a mismatched reference.

The matcher accepts an optional reference name and capture or comparison options.

Pass a Locator or the `page` object from `@rstest/browser`. The matcher is only available with the Playwright provider and does not support `.not`.

By default, the current test name identifies the reference. Pass a name to share a reference across assertions or give multiple screenshots in one test distinct names:

```ts
await expect(page.getByRole('button', { name: 'Save' })).toMatchScreenshot(
  'primary-save-button',
);
```

Unnamed assertions in the same test are assigned distinct references automatically. In `test.concurrent`, use the test-scoped `expect` fixture (`async ({ expect }) => ...`) to use the test name; if you use the shared file-level `expect`, provide a name explicitly.

The second argument controls comparison and capture. For example, allow up to 20 differing pixels, wait up to one second for a stable capture, and mask a timestamp that changes between runs:

```ts
await expect(page.locator('#profile-card')).toMatchScreenshot('profile-card', {
  maxDiffPixels: 20,
  stabilityTimeout: 1000,
  mask: [page.locator('[data-testid="updated-at"]')],
});
```

`threshold` sets the per-pixel color distance from `0` (strict) to `1` (loose). `maxDiffPixels` sets an absolute limit on differing pixels; use `maxDiffPixelRatio` instead to set a fraction of the image. The defaults allow no differing pixels after applying the default `threshold` of `0.1`. Set either `maxDiffPixels` or `maxDiffPixelRatio`, not both.

`stabilityTimeout` defaults to `2000` ms. Set it to `0` to skip stability checks; positive values must be at least `100` ms so Rstest can take a follow-up capture. Rstest disables animations during capture. Use `mask` for dynamic content, `maskColor` to change the overlay color, or `style` to inject CSS before capture. The remaining capture options (`caret`, `scale`, and `omitBackground`) are passed to Playwright's screenshot API. `fullPage` is available only for page assertions.

#### Element screenshots

Pass a Locator to capture only the matched element:

```ts
await expect(page.getByRole('button', { name: 'Save' })).toMatchScreenshot();
```

#### Page screenshots

Pass the `page` object from `@rstest/browser` to capture the current viewport. Set `fullPage: true` to capture the full page:

```ts
await expect(page).toMatchScreenshot();
await expect(page).toMatchScreenshot('full-page', { fullPage: true });
```

References are PNG files under `__screenshots__`, grouped by project and test file. Their names include the test name, browser, and operating system. On a mismatch, Rstest reports the reference, actual screenshot, and pixel diff; the actual and diff images are written to a sibling `__failures__` directory. Screenshot capture uses the remaining active test timeout. With `testTimeout: 0`, there is no test deadline, but `stabilityTimeout` still applies.

Set project-wide matcher defaults and reference paths with [`browser.expect.toMatchScreenshot`](https://rstest.rs/config/test/browser.md#expecttomatchscreenshot).

## Element assertions

The matchers listed below represent the currently supported subset. Matchers not listed here are not yet available.

### not

- **Type:** `BrowserElementExpect`

Negates the subsequent assertion.

```ts
await expect
  .element(page.getByRole('button', { name: 'Submit' }))
  .not.toBeDisabled();
```

All the following matchers support an optional `options?: { timeout?: number }` parameter (unless the type signature states otherwise).

### toBeVisible

- **Type:** `(options?: { timeout?: number }) => Promise<void>`

The `Locator` resolves to a mounted and visible element within the `timeout`.

Visibility is determined by: the element has a non-empty bounding box and `visibility` is not `hidden`. For example, `display: none` or zero dimensions are not considered visible; `opacity: 0` is still considered visible.

```ts
await expect.element(page.getByRole('button', { name: 'Save' })).toBeVisible();
```

### toBeHidden

- **Type:** `(options?: { timeout?: number }) => Promise<void>`

The `Locator` meets any of the following conditions within the `timeout`: does not match any DOM node, or matches a non-visible node.

Think of it as the opposite of `toBeVisible`; for example, setting only `opacity: 0` typically will not cause `toBeHidden` to pass.

```ts
await expect.element(page.getByTestId('loading')).toBeHidden();
```

### toBeEnabled

- **Type:** `(options?: { timeout?: number }) => Promise<void>`

The element is not in a disabled state.

Disabled determination: a native form control with the `disabled` attribute, within a disabled `fieldset`, or within an `aria-disabled=true` semantic context may all be considered disabled.

```ts
await expect
  .element(page.getByRole('button', { name: 'Submit' }))
  .toBeEnabled();
```

### toBeDisabled

- **Type:** `(options?: { timeout?: number }) => Promise<void>`

The element is determined to be disabled.

The determination rules are the same as `toBeEnabled`, just with the opposite result. Recommended for scenarios like button submission and preventing duplicate clicks.

```ts
await expect
  .element(page.getByRole('button', { name: 'Submit' }))
  .toBeDisabled();
```

### toBeChecked

- **Type:** `(options?: { timeout?: number }) => Promise<void>`

The `checked` state of a checkbox or radio is `true`.

Commonly used to verify user check actions or whether a default checked state has taken effect.

```ts
await expect.element(page.getByLabel('Agree')).toBeChecked();
```

### toBeUnchecked

- **Type:** `(options?: { timeout?: number }) => Promise<void>`

The `checked` state of a checkbox or radio is `false`.

Suitable for use with `check` / `uncheck` to verify state changes before and after interaction.

```ts
await expect.element(page.getByLabel('Agree')).toBeUnchecked();
```

### toBeAttached

- **Type:** `(options?: { timeout?: number }) => Promise<void>`

The node pointed to by the `Locator` is connected to a `Document` or `ShadowRoot` (equivalent to `Node.isConnected === true`).

This assertion only checks "whether it is in the DOM tree" and does not require the element to be visible.

```ts
await expect.element(page.locator('#toast')).toBeAttached();
```

### toBeDetached

- **Type:** `(options?: { timeout?: number }) => Promise<void>`

The `Locator` no longer points to a connected DOM node.

Common for verification after conditional rendering, async unmounting, or delete operations.

```ts
await expect.element(page.locator('#toast')).toBeDetached();
```

### toBeEditable

- **Type:** `(options?: { timeout?: number }) => Promise<void>`

The element is both enabled and not readonly.

Readonly determination includes both the native `readonly` attribute and `aria-readonly=true` semantics.

```ts
await expect.element(page.getByLabel('Bio')).toBeEditable();
```

### toBeFocused

- **Type:** `(options?: { timeout?: number }) => Promise<void>`

The element is the current document's focus target (active element).

Suitable for verifying keyboard navigation, auto-focus, or form focus-switching behavior.

```ts
await expect.element(page.getByLabel('Username')).toBeFocused();
```

### toBeEmpty

- **Type:** `(options?: { timeout?: number }) => Promise<void>`

An editable element's content is empty, or a regular DOM node has no text content.

It checks "whether the content is empty", not "whether the element exists" or "whether it is visible".

```ts
await expect.element(page.locator('#empty-state')).toBeEmpty();
```

### toBeInViewport

- **Type:** `(options?: { timeout?: number; ratio?: number }) => Promise<void>`

The element intersects with the viewport (based on Intersection Observer semantics).

`ratio` represents the minimum intersection ratio; for example, `ratio: 0.5` means at least half of the area is within the viewport.

```ts
await expect.element(page.locator('#hero')).toBeInViewport({ ratio: 0.5 });
```

### toHaveText

- **Type:** `(text: string | RegExp, options?: { timeout?: number }) => Promise<void>`

The element's text fully matches the expected value (supports `string` / `RegExp`).

Text computation includes nested child element content. When the expected value is a `string`, whitespace and line breaks are normalized before matching.

```ts
await expect.element(page.getByRole('status')).toHaveText('Saved');
```

### toContainText

- **Type:** `(text: string | RegExp, options?: { timeout?: number }) => Promise<void>`

The element's text contains the expected substring, or matches the given regex.

The difference from `toHaveText` is: `toContainText` does substring matching, while `toHaveText` does full matching.

```ts
await expect.element(page.getByRole('status')).toContainText('Save');
```

### toHaveValue

- **Type:** `(value: string | RegExp, options?: { timeout?: number }) => Promise<void>`

The form control's current `value` matches the expected value (supports `string` / `RegExp`).

Applicable to `input`, `textarea`, `select`, and other elements with retrievable values.

```ts
await expect.element(page.getByLabel('Email')).toHaveValue('dev@rstest.rs');
```

### toHaveId

- **Type:** `(value: string | RegExp, options?: { timeout?: number }) => Promise<void>`

The element's `id` matches the expected value (supports `string` / `RegExp`).

Suitable for validating dynamically generated IDs or fixed IDs injected after component mounting.

```ts
await expect
  .element(page.getByRole('button', { name: 'Save' }))
  .toHaveId('save-btn');
```

### toHaveClass

- **Type:** `(value: string | RegExp, options?: { timeout?: number }) => Promise<void>`

The element's `class` attribute matches the expected value (supports `string` / `RegExp`).

When passing a `string`, it matches against the entire `class` string. If you only care about a specific class, consider using a more targeted regex.

```ts
await expect.element(page.getByRole('alert')).toHaveClass(/error/);
```

### toHaveAttribute

- **Type:**
  - `(name: string, options?: { timeout?: number }) => Promise<void>`
  - `(name: string, value: string | RegExp, options?: { timeout?: number }) => Promise<void>`

When only `name` is passed, it asserts the attribute exists. When `name + value` is passed, it asserts the attribute value matches (`value` supports `string` / `RegExp`).

Common use cases include validating structural attributes like `type`, `disabled`, `aria-*`, etc.

```ts
await expect
  .element(page.getByRole('button', { name: 'Save' }))
  .toHaveAttribute('type');
await expect
  .element(page.getByRole('button', { name: 'Save' }))
  .toHaveAttribute('type', 'submit');
```

### toHaveCount

- **Type:** `(count: number, options?: { timeout?: number }) => Promise<void>`

The number of elements resolved by the `Locator` exactly matches `count`.

Suitable for list rendering, filter results, pagination item counts, and similar scenarios.

```ts
await expect.element(page.getByRole('listitem')).toHaveCount(3);
```

### toHaveCSS

- **Type:** `(name: string, value: string | RegExp, options?: { timeout?: number }) => Promise<void>`

The specified CSS property value in the element's computed style matches the expected value.

`name` must be a non-empty string; `value` supports `string` / `RegExp`.

```ts
await expect.element(page.getByRole('alert')).toHaveCSS('display', 'block');
```

### toHaveJSProperty

- **Type:** `(name: string, value: unknown, options?: { timeout?: number }) => Promise<void>`

The JS property on the element matches the expected value.

`name` must be a non-empty string; `value` must be JSON-serializable (assertion parameters are transmitted through the Browser Mode channel).

```ts
await expect
  .element(page.getByLabel('Agree'))
  .toHaveJSProperty('checked', true);
```

## Related API

- [Locator](https://rstest.rs/api/runtime-api/browser-mode/locator.md)
- [Page (Locator entry point)](https://rstest.rs/api/runtime-api/browser-mode/locator.md#page)
- [Expect](https://rstest.rs/api/runtime-api/test-api/expect.md)
