e2e 0.21.1 → 0.21.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +2 -0
  2. package/dist/.build.json +1 -1
  3. package/dist/agent/act-validation.js +1 -0
  4. package/dist/agent/act.js +7 -3
  5. package/dist/agent/action-dispatcher.js +30 -8
  6. package/dist/agent/actions.d.ts +2 -0
  7. package/dist/agent/actions.js +5 -2
  8. package/dist/agent/error.js +1 -0
  9. package/dist/agent/executor.d.ts +14 -6
  10. package/dist/agent/index.js +5 -1
  11. package/dist/agent/invocation.d.ts +3 -0
  12. package/dist/agent/model/sdk.d.ts +5 -0
  13. package/dist/agent/model/sdk.js +18 -1
  14. package/dist/agent/observation.js +1 -0
  15. package/dist/agent/primitives.js +14 -3
  16. package/dist/agent/replay.js +9 -3
  17. package/dist/agent/step-cache.d.ts +3 -2
  18. package/dist/agent/step-cache.js +7 -4
  19. package/dist/agent/tool-loop.js +4 -1
  20. package/dist/cache/context.js +1 -1
  21. package/dist/cache/recorder.d.ts +2 -1
  22. package/dist/cache/recorder.js +11 -2
  23. package/dist/cache/trace-fields.d.ts +10 -0
  24. package/dist/cache/trace-fields.js +17 -0
  25. package/dist/cache/trace.d.ts +9 -1
  26. package/dist/cache/trace.js +26 -7
  27. package/dist/cli/feedback.js +2 -1
  28. package/dist/cli/init/sibling-versions.json +1 -1
  29. package/dist/collect/select.js +12 -2
  30. package/dist/config/agent.d.ts +1 -1
  31. package/dist/config/diagnose.js +9 -3
  32. package/dist/config/esm-hooks.js +3 -2
  33. package/dist/config/resolve.js +3 -2
  34. package/dist/config/typescript.js +52 -2
  35. package/dist/engine/contract.d.ts +3 -2
  36. package/dist/engine/manifest.js +2 -2
  37. package/dist/internal/errors.js +5 -2
  38. package/dist/internal/source.js +2 -0
  39. package/dist/internal/unserved-model.d.ts +5 -0
  40. package/dist/internal/unserved-model.js +22 -0
  41. package/dist/locator/screen.js +1 -2
  42. package/dist/oauth/copilot.js +23 -5
  43. package/dist/oauth/device-code.js +35 -19
  44. package/dist/oauth/errors.d.ts +3 -0
  45. package/dist/oauth/errors.js +26 -11
  46. package/dist/oauth/providers/github-copilot.d.ts +4 -0
  47. package/dist/oauth/providers/github-copilot.js +64 -16
  48. package/dist/oauth/providers/openai.d.ts +2 -2
  49. package/dist/oauth/providers/openai.js +47 -19
  50. package/dist/oauth/providers/opencode-console.js +36 -15
  51. package/dist/oauth/providers/xai.js +18 -9
  52. package/dist/oauth/token-endpoint.d.ts +15 -6
  53. package/dist/oauth/token-endpoint.js +18 -6
  54. package/dist/report/code-frame.js +9 -3
  55. package/dist/report/failure-text.d.ts +10 -2
  56. package/dist/report/failure-text.js +24 -8
  57. package/dist/report/list-model.d.ts +13 -0
  58. package/dist/report/list-steps.d.ts +6 -1
  59. package/dist/report/list-steps.js +29 -0
  60. package/dist/report/list.d.ts +1 -0
  61. package/dist/report/list.js +16 -7
  62. package/dist/report/live-window.js +2 -1
  63. package/dist/report/markdown.js +1 -1
  64. package/dist/run/fixtures.js +2 -0
  65. package/dist/run/host-trace.d.ts +3 -2
  66. package/dist/run/host-trace.js +2 -1
  67. package/dist/run/last-run.d.ts +9 -0
  68. package/dist/run/last-run.js +28 -0
  69. package/dist/run/open-session.d.ts +3 -1
  70. package/dist/run/open-session.js +1 -1
  71. package/dist/run/runner.js +6 -2
  72. package/dist/run/sessions.js +3 -3
  73. package/dist/run/standalone-attempt.d.ts +2 -1
  74. package/dist/run/standalone-attempt.js +3 -1
  75. package/dist/run/steps.d.ts +3 -1
  76. package/dist/run/units.d.ts +8 -0
  77. package/dist/run/units.js +14 -0
  78. package/dist/run/worker-models.d.ts +2 -0
  79. package/dist/run/worker-models.js +7 -0
  80. package/dist/types.d.ts +2 -2
  81. package/docs/authentication.mdx +26 -23
  82. package/docs/browser.mdx +7 -4
  83. package/docs/bug-bash.mdx +2 -2
  84. package/docs/cache.mdx +6 -1
  85. package/docs/ci/bitrise.mdx +1 -1
  86. package/docs/ci/codemagic.mdx +2 -2
  87. package/docs/ci/eas-workflows.mdx +1 -1
  88. package/docs/coding-agents.mdx +7 -35
  89. package/docs/debugging.mdx +24 -36
  90. package/docs/executors.mdx +1 -1
  91. package/docs/github.mdx +2 -1
  92. package/docs/integrations/eas.mdx +1 -1
  93. package/docs/integrations/index.mdx +6 -0
  94. package/docs/integrations/lightpanda.mdx +192 -0
  95. package/docs/integrations/testmu.mdx +152 -0
  96. package/docs/mcp.mdx +123 -0
  97. package/docs/migrate/detox.mdx +1 -1
  98. package/docs/migrate/maestro.mdx +101 -105
  99. package/docs/migrate/playwright.mdx +65 -70
  100. package/docs/mobile.mdx +18 -3
  101. package/docs/models.mdx +4 -2
  102. package/docs/quickstart.mdx +7 -0
  103. package/docs/reference/agent.mdx +369 -277
  104. package/docs/reference/app.mdx +141 -32
  105. package/docs/reference/cli.mdx +674 -558
  106. package/docs/reference/config.mdx +1406 -752
  107. package/docs/reference/engine.mdx +1062 -256
  108. package/docs/reference/environment.mdx +156 -84
  109. package/docs/reference/errors.mdx +106 -72
  110. package/docs/reference/expect.mdx +752 -372
  111. package/docs/reference/mcp.mdx +366 -141
  112. package/docs/reference/mobile.mdx +844 -403
  113. package/docs/reference/reporters.mdx +312 -106
  114. package/docs/reference/runner.mdx +373 -174
  115. package/docs/reference/screen.mdx +931 -398
  116. package/docs/reference/security.mdx +106 -40
  117. package/docs/reference/test.mdx +626 -273
  118. package/docs/reference/web.mdx +956 -533
  119. package/docs/security.mdx +6 -5
  120. package/docs/snippets/model-provider.jsx +763 -0
  121. package/docs/snippets/setup-prompt-card.jsx +22 -0
  122. package/docs/snippets/show-more.jsx +22 -0
  123. package/docs/snippets/video-card.jsx +34 -0
  124. package/docs/snippets/video.jsx +25 -0
  125. package/docs/subscriptions.mdx +10 -3
  126. package/docs/telemetry.mdx +39 -39
  127. package/package.json +3 -2
  128. package/schema/report-v1.schema.json +6 -0
  129. package/skills/e2e/references/agent.md +8 -3
  130. package/skills/e2e/references/debugging.md +1 -1
  131. package/skills/e2e/references/mcp.md +1 -1
  132. package/skills/e2e/references/setup.md +12 -3
  133. package/skills/e2e/references/writing-tests.md +1 -1
@@ -6,14 +6,755 @@ icon: "circle-check"
6
6
  "twitter:image": "https://e2e.tester.army/docs/images/og/reference/expect.png?v=118436cc41"
7
7
  ---
8
8
 
9
- Use `expect(locator)` to wait for an app condition, `expect(screen)` to
10
- compare the screen against a stored screenshot, and `expect(value)` to check
11
- a value immediately. `expect.poll` repeatedly reads a value until its
12
- matcher passes. `expect.soft` keeps a failure for the end of the test body
13
- instead of throwing it, and `expect.any` and the other
9
+ `expect(locator)` waits for an app condition, `expect(screen)` compares the
10
+ screen against a stored screenshot, and `expect(value)` checks a value
11
+ immediately. `expect.poll` re-reads a value until its matcher passes,
12
+ `expect.soft` keeps a failure for the end of the test body instead of
13
+ throwing it, and `expect.any` and the other
14
14
  [asymmetric matchers](#asymmetric-matchers) stand in for values inside a
15
15
  structural comparison.
16
16
 
17
+ ```ts title="tests/checkout.e2e.ts"
18
+ import { expect, test } from 'e2e';
19
+
20
+ test('checkout summary', async ({ screen }) => {
21
+ await expect(screen.getByRole('heading', 'Checkout')).toBeVisible();
22
+ await expect(screen.getByTestId('line-item')).toHaveCount(2);
23
+ const total = await screen.getByTestId('total').textContent();
24
+ expect(total).toContain('$');
25
+ });
26
+ ```
27
+
28
+ ## Reference
29
+
30
+ | Call | Returns |
31
+ | --- | --- |
32
+ | [`expect(locator)`](#locator-matchers) | `AsyncExpectation` |
33
+ | [`expect(screen)`](#screenshot-matchers) | `ScreenExpectation` |
34
+ | [`expect(fixture)`](#browser-matchers) | The engine's matcher set |
35
+ | [`expect(value, message?)`](#value-matchers) | `ValueExpectation<T>` |
36
+ | [`expect.poll(read, options?)`](#polling-a-value) | `PollExpectation<T>` |
37
+ | [`expect.soft(actual)`](#soft-assertions) | The same matchers, failures kept |
38
+ | [`expect.any(Class)` and others](#asymmetric-matchers) | `AsymmetricMatcher` |
39
+
40
+ `expect` tells the argument apart by what it is:
41
+
42
+ - Engine fixtures can provide matchers, such as `expect(browser).toHaveURL()`.
43
+ Such a fixture is an `Expectable<E>`: its engine attached the matcher set
44
+ `E` through [`expectable`](/reference/engine#contributed-fixtures), and
45
+ `expect(fixture)` returns it.
46
+ - Every other argument gets the value matchers, a promise included. Await a
47
+ promise before passing its result to `expect`.
48
+
49
+ The declarations are under [Types](#types).
50
+
51
+ ### Locator matchers
52
+
53
+ `expect(locator)` returns an `AsyncExpectation`. Each matcher polls until it
54
+ holds or its timeout passes, under the rules in [Behavior](#behavior):
55
+ [Strictness](#strictness), [Text matching](#text-matching), and
56
+ [Secure fields](#secure-fields) among them. The options are in
57
+ [`AsyncExpectation`](#asyncexpectation-and-textmatcheroptions).
58
+
59
+ | Matcher | Description |
60
+ | --- | --- |
61
+ | [`toBeVisible`](#tobevisible-and-tobehidden) | Waits for visibility |
62
+ | [`toBeHidden`](#tobevisible-and-tobehidden) | Waits for hidden or absent state |
63
+ | [`toBeAttached`](#tobeattached) | Waits for one match to exist |
64
+ | [`toBeEnabled`](#tobeenabled-and-tobedisabled) | Waits for enabled state |
65
+ | [`toBeDisabled`](#tobeenabled-and-tobedisabled) | Waits for disabled state |
66
+ | [`toBeChecked`](#tobechecked-tobeselected-and-tobeexpanded) | Waits for checked state |
67
+ | [`toBeSelected`](#tobechecked-tobeselected-and-tobeexpanded) | Waits for selected state |
68
+ | [`toBeExpanded`](#tobechecked-tobeselected-and-tobeexpanded) | Waits for expanded state |
69
+ | [`toBeFocused`](#tobefocused) | Waits for focused state |
70
+ | [`toHaveText`](#tohavetext-and-tocontaintext) | Waits for exact normalized text |
71
+ | [`toContainText`](#tohavetext-and-tocontaintext) | Waits for contained normalized text |
72
+ | [`toHaveValue`](#tohavevalue) | Waits for a form control's value |
73
+ | [`toHaveAttribute`](#tohaveattribute) | Waits for an attribute |
74
+ | [`toHaveCount`](#tohavecount) | Waits for an exact match count |
75
+ | [`toHaveAccessibleName`](#tohaveaccessiblename) | Waits for an accessible name |
76
+ | [`toHaveScreenshot`](#screenshot-matchers) | Compares with a stored screenshot |
77
+
78
+ #### `toBeVisible` and `toBeHidden`
79
+
80
+ `toBeVisible` waits for the one match to be shown. `toBeHidden` waits for a
81
+ hidden or absent state.
82
+
83
+ ```ts
84
+ await expect(screen.getByRole('dialog')).toBeVisible();
85
+ await expect(screen.getByRole('dialog')).not.toBeVisible({ timeout: 10_000 });
86
+ await expect(screen.getByRole('dialog')).toBeVisible({ visible: false }); // as toBeHidden()
87
+ ```
88
+
89
+ #### `toBeAttached`
90
+
91
+ Waits for one match to exist, hidden or shown, where `toBeVisible` needs a
92
+ shown one.
93
+
94
+ ```ts
95
+ await expect(screen.getByText('Loaded')).toBeAttached();
96
+ await expect(screen.getByText('Loading')).toBeAttached({ attached: false });
97
+ ```
98
+
99
+ Negated, or with `attached: false`, it waits for no match and passes once
100
+ nothing matches.
101
+
102
+ #### `toBeEnabled` and `toBeDisabled`
103
+
104
+ `toBeEnabled` waits for an enabled node; `toBeDisabled` waits for a disabled
105
+ one.
106
+
107
+ ```ts
108
+ await expect(screen.getByRole('button', 'Pay')).toBeEnabled();
109
+ await expect(screen.getByRole('button', 'Pay')).toBeEnabled({ enabled: false }); // as toBeDisabled()
110
+ ```
111
+
112
+ A node is enabled unless it reports `disabled`. On the web, `disabled` is
113
+ the state the browser applies, not the attribute alone. That is the reading
114
+ Playwright's `toBeDisabled` gives:
115
+
116
+ - A control inside a `<fieldset disabled>` is disabled unless it sits in
117
+ that fieldset's first `<legend>`.
118
+ - `aria-disabled="true"` on an ancestor disables the widgets below it until
119
+ an `aria-disabled="false"` cuts the chain.
120
+
121
+ #### `toBeChecked`, `toBeSelected`, and `toBeExpanded`
122
+
123
+ Each waits for its state: checked, selected, or expanded.
124
+
125
+ ```ts
126
+ await expect(screen.getByLabel('Subscribe')).toBeChecked({ checked: false });
127
+ await expect(screen.getByRole('tab', 'Billing')).toBeSelected();
128
+ ```
129
+
130
+ A missing `checked`, `selected`, or `expanded` state counts as `false`, so
131
+ `.not.toBeChecked()` passes on a button or a heading.
132
+
133
+ #### `toBeFocused`
134
+
135
+ Waits for focused state.
136
+
137
+ ```ts
138
+ await expect(screen.getByLabel('Email')).toBeFocused();
139
+ ```
140
+
141
+ #### `toHaveText` and `toContainText`
142
+
143
+ `toHaveText` waits for exact normalized text; `toContainText` waits for
144
+ contained normalized text. Both take a single `TextMatch` or a list.
145
+
146
+ ```ts
147
+ await expect(screen.getByRole('status')).toHaveText('Saved');
148
+ await expect(screen.getByRole('alert')).not.toContainText('error', { ignoreCase: true });
149
+ await expect(screen.getByTestId('todo')).toHaveText(['Write spec', /^Ship/, 'Release']);
150
+ await expect(screen.getByTestId('todo')).toContainText(['spec', 'runner', 'Release']);
151
+ await expect(screen.getByTestId('todo')).toContainText(['spec', 'Release']);
152
+ ```
153
+
154
+ A list form's failure message lists every text observed.
155
+
156
+ | Form | Holds when |
157
+ | --- | --- |
158
+ | `toHaveText([...])` | The match count equals the list length, and the match at each position satisfies the entry there: a string exactly after normalization, a `RegExp` by test. An empty list holds when nothing matches |
159
+ | `toContainText([...])` | Each entry is contained by a distinct match, in list order, as Playwright's does. Extra matches before, between, or after are fine, a repeated entry needs a match of its own, and an empty list always holds |
160
+
161
+ <Note>
162
+ Negated, `toContainText([...])` fails as soon as that ordered subsequence
163
+ exists, so `not.toContainText(['Deleted'])` fails while any match contains
164
+ `Deleted`.
165
+ </Note>
166
+
167
+ #### `toHaveValue`
168
+
169
+ Waits for a form control's value, compared as it is.
170
+
171
+ ```ts
172
+ await expect(screen.getByLabel('Email')).toHaveValue('ada@example.test');
173
+ await expect(screen.getByLabel('Search')).toHaveValue('');
174
+ ```
175
+
176
+ - No normalization: newlines and trailing spaces count, and the failure
177
+ message prints the value that way.
178
+ - A `contenteditable` editor's value is its rendered text, kept the same way.
179
+ An empty editor reads as `''`.
180
+ - A control the platform reports without a value, such as a cleared field on
181
+ a device, reads as `''`.
182
+ - A node whose role carries no value, such as a heading or a link, never
183
+ satisfies `toHaveValue`, negated or not. The message reads
184
+ `no value (not a form control)`.
185
+
186
+ #### `toHaveAttribute`
187
+
188
+ Waits for the attribute to be present; with `value`, for it to match.
189
+
190
+ ```ts
191
+ await expect(screen.getByRole('link', 'Help')).toHaveAttribute('target');
192
+ await expect(screen.getByRole('link', 'Help')).toHaveAttribute('href', '/help');
193
+ ```
194
+
195
+ #### `toHaveCount`
196
+
197
+ Waits for an exact match count.
198
+
199
+ ```ts
200
+ await expect(screen.getByTestId('todo')).toHaveCount(3);
201
+ ```
202
+
203
+ #### `toHaveAccessibleName`
204
+
205
+ Waits for an accessible name.
206
+
207
+ ```ts
208
+ await expect(screen.getByRole('button', { name: /close/i })).toHaveAccessibleName('Close');
209
+ ```
210
+
211
+ ### Screenshot matchers
212
+
213
+ `expect(screen)` and `expect(locator)` compare against a PNG stored beside
214
+ the test file, on every engine that captures pixels. The
215
+ [Visual comparison](/visual-testing) guide shows the workflow.
216
+
217
+ | Matcher | Description |
218
+ | --- | --- |
219
+ | [`expect(screen).toHaveScreenshot`](#tohavescreenshot) | Compares the whole screen |
220
+ | [`expect(locator).toHaveScreenshot`](#tohavescreenshot) | Compares a locator's one match |
221
+
222
+ #### `toHaveScreenshot`
223
+
224
+ Waits for the screen, or the one match of a locator, to look like its stored
225
+ screenshot: `name`, or one named after the test.
226
+
227
+ ```ts
228
+ await expect(screen).toHaveScreenshot('home.png');
229
+ await expect(screen.getByRole('navigation')).toHaveScreenshot();
230
+ await expect(screen).toHaveScreenshot('feed.png', { mask: [screen.getByTestId('timestamp')], maxDiffPixels: 50 });
231
+ ```
232
+
233
+ Both forms take the [Screenshot options](#screenshot-options).
234
+
235
+ | Property | Value |
236
+ | --- | --- |
237
+ | Stored at | `<test file>-snapshots/<name>-<target>-<os>.png`; `.png` is added to a name without it |
238
+ | Unnamed | The test's title path, lowercase, and a count: `checkout-pays-by-card-1` |
239
+ | Missing | Taken from two screenshots in a row that are the same within the tolerance, then `ASSERTION_FAILED`. Written beside the test; in CI without `--update-snapshots` only attached to the results, at `snapshots/<path in the project>` |
240
+ | Written earlier in the run | Without `--update-snapshots`, and always for `.not`, no comparison passes against a screenshot the same run wrote (a retry, a repeat, another test naming it): `ASSERTION_FAILED` until the next run. With `--update-snapshots` a positive matcher compares and may write it again |
241
+ | Different | Retried until one matches or the timeout; then `ASSERTION_FAILED` with `-expected`, `-actual`, and `-diff` images on the step |
242
+ | `--update-snapshots`, `-u` | A missing or different stored screenshot is written from a screen that holds still, and the matcher passes |
243
+ | `.not` | Passes once the screen differs; with nothing stored it is `ASSERTION_FAILED` |
244
+ | Tolerance | Both `maxDiffPixels` and `maxDiffPixelRatio` set: the smaller wins |
245
+
246
+ | Throws | When |
247
+ | --- | --- |
248
+ | `INVALID_ARGUMENT` | Before the screen is read: a name with a path separator, an option the matcher does not take (Playwright's `fullPage`, `animations`, `clip`), a value out of range, or `expect` on a screen scoped to a frame. Compare a locator inside the frame instead |
249
+ | `ASSERTION_FAILED` | A missing, different, or same-run stored screenshot, as in the table above |
250
+ | `POLICY_DENIED` | A `Secret` was filled earlier, as for `app.screenshot()` |
251
+
252
+ #### Screenshot options
253
+
254
+ | Option | Type | Default |
255
+ | --- | --- | --- |
256
+ | `threshold` | `number` | `0.2` |
257
+ | `maxDiffPixels` | `number` | `0` |
258
+ | `maxDiffPixelRatio` | `number` | `0` |
259
+ | `mask` | `readonly Locator[]` | - |
260
+ | `maskColor` | `string` | `#ff00ff` |
261
+ | `timeout` | `number` | `config.assertionTimeout` |
262
+
263
+ [`ScreenshotOptions`](#screenexpectation-and-screenshotoptions) describes
264
+ each option.
265
+
266
+ ### browser matchers
267
+
268
+ Contributed by `@e2e-dev/web`; the `BrowserExpectation` type is exported
269
+ from that package. Every matcher call is a recorded `expect.<matcher>`
270
+ assertion step.
271
+
272
+ | Matcher | Description |
273
+ | --- | --- |
274
+ | [`toHaveURL`](#tohaveurl) | Waits for the current URL |
275
+ | [`toHaveTitle`](#tohavetitle) | Waits for the current title |
276
+ | [`toHaveClass`](#tohaveclass) | Waits for a target's `class` |
277
+
278
+ Any other option key is `INVALID_ARGUMENT`, such as `ignoreCase` on
279
+ `toHaveTitle`, which Playwright does not take either.
280
+
281
+ #### `toHaveURL`
282
+
283
+ Waits for the current URL to match.
284
+
285
+ ```ts
286
+ await expect(browser).toHaveURL('/dashboard');
287
+ await expect(browser).toHaveURL(/\/orders\/\d+$/);
288
+ ```
289
+
290
+ - A relative string resolves against the base URL and matches exactly after
291
+ normalization.
292
+ - A `RegExp` tests the full URL.
293
+ - Anything else, such as a predicate function, is `INVALID_ARGUMENT`.
294
+ - It takes Playwright's `ignoreCase`, as the locator matchers do
295
+ ([Text matching](#text-matching)).
296
+
297
+ #### `toHaveTitle`
298
+
299
+ Waits for the current title to match.
300
+
301
+ ```ts
302
+ await expect(browser).toHaveTitle(/Dashboard/);
303
+ ```
304
+
305
+ #### `toHaveClass`
306
+
307
+ Waits for the target's `class` attribute to match: a string is the whole
308
+ normalized class list, a `RegExp` is tested against it.
309
+
310
+ ```ts
311
+ await expect(browser).toHaveClass(browser.locator('.card'), 'card active');
312
+ ```
313
+
314
+ - It polls for the node too: a target that matches nothing yet is one more
315
+ poll, not a failure, until the assertion timeout runs out.
316
+ - An ambiguous target still fails at once with `LOCATOR_AMBIGUOUS`.
317
+ - It reads the `class` attribute, so `''` matches `class=""` and not an
318
+ element with no `class` attribute at all. Playwright's `toHaveClass('')`
319
+ accepts both.
320
+
321
+ ### Value matchers
322
+
323
+ `expect(value)` runs synchronously without retrying. For a value that may
324
+ still change, use [Polling a value](#polling-a-value).
325
+
326
+ | Matcher | Description |
327
+ | --- | --- |
328
+ | [`toBe`](#tobe) | Compares with `Object.is` |
329
+ | [`toEqual`](#toequal) | Recursive structural equality |
330
+ | [`toMatchObject`](#tomatchobject) | Recursive subset match |
331
+ | [`toBeTruthy`, `toBeFalsy`](#tobetruthy-and-tobefalsy) | Truthy or falsy value |
332
+ | [`toBeNull`, `toBeUndefined`, `toBeDefined`](#tobenull-tobeundefined-and-tobedefined) | `null`, `undefined`, or defined |
333
+ | [`toHaveLength`](#tohavelength) | `length` equals `expected` |
334
+ | [`toHaveProperty`](#tohaveproperty) | Property at a path |
335
+ | [`toContain`](#tocontain) | String or collection containment |
336
+ | [`toMatch`](#tomatch) | String or `RegExp` match |
337
+ | [`toBeGreaterThan` and others](#numeric-comparisons) | Numeric bound holds |
338
+ | [`toBeCloseTo`](#tobecloseto) | Within `10 ** -digits / 2` |
339
+ | [`toMatchSchema`](#matching-a-schema) | Standard Schema validation |
340
+
341
+ A matcher that cannot evaluate its value fails as `ASSERTION_FAILED`, like a
342
+ mismatch. Examples are `toContain` on a number, or `toHaveLength` on a value
343
+ without a numeric `length`. So under `expect.poll`, a value that has not
344
+ taken its final shape yet is one more failing sample.
345
+
346
+ #### `toBe`
347
+
348
+ Compares with `Object.is`.
349
+
350
+ ```ts
351
+ expect(status).toBe('paid');
352
+ ```
353
+
354
+ #### `toEqual`
355
+
356
+ Performs recursive structural equality the way Jest's does.
357
+
358
+ ```ts
359
+ expect(payload).toEqual({ ok: true, items: [{ sku: 'a' }] });
360
+ ```
361
+
362
+ - Primitives compare with `Object.is`, so `NaN` equals `NaN` and `0` does not
363
+ equal `-0`.
364
+ - Arrays and plain objects compare by their contents, `Date` by time,
365
+ `RegExp` by source and flags, `Map` and `Set` by their entries.
366
+ - A property whose value is `undefined` is treated as absent, and a sparse
367
+ array equals one holding `undefined`.
368
+ - Class types are not compared: a class instance equals a plain object with
369
+ the same properties. There is no `toStrictEqual`; see
370
+ [Not included](#not-included).
371
+
372
+ #### `toMatchObject`
373
+
374
+ Requires every property of `expected` to match recursively.
375
+
376
+ ```ts
377
+ expect(order).toMatchObject({ customer: { name: 'Ada' }, lines: [{ sku: 'a' }, { sku: 'b' }] });
378
+ ```
379
+
380
+ - Properties `expected` does not name are ignored.
381
+ - An array in `expected` matches an array of the same length, element by
382
+ element, each element again as a subset.
383
+ - Both sides must be objects; a primitive on either side is refused.
384
+
385
+ #### `toBeTruthy` and `toBeFalsy`
386
+
387
+ Require a truthy or a falsy value.
388
+
389
+ ```ts
390
+ expect(created).toBeTruthy();
391
+ ```
392
+
393
+ #### `toBeNull`, `toBeUndefined`, and `toBeDefined`
394
+
395
+ `toBeNull` requires `null` and `toBeUndefined` requires `undefined`.
396
+ `toBeDefined` requires any value but `undefined`; `null` passes.
397
+
398
+ ```ts
399
+ expect(row).toBeDefined();
400
+ ```
401
+
402
+ #### `toHaveLength`
403
+
404
+ Requires a `length` property equal to `expected`: a string, an array, or
405
+ anything array-like.
406
+
407
+ ```ts
408
+ expect(items).toHaveLength(3);
409
+ ```
410
+
411
+ #### `toHaveProperty`
412
+
413
+ Requires the property at `path` to exist; with `expected`, to equal it by
414
+ `toEqual` rules.
415
+
416
+ ```ts
417
+ expect(order).toHaveProperty('customer.email', 'ada@example.test');
418
+ expect(order).toHaveProperty(['lines', 0, 'sku'], 'a');
419
+ ```
420
+
421
+ - `path` is dotted (`'user.address.city'`) or an array of keys
422
+ (`['user', 'address', 'city']`). Use the array form when a key contains a
423
+ dot, or to index an array by number.
424
+ - Inherited properties count, and a property holding `undefined` exists.
425
+ - A string, number, or boolean on the path is read through its wrapper
426
+ object, as in Jest, so `toHaveProperty('label.length', 3)` and
427
+ `toHaveProperty(['label', 0], 'a')` reach into a string.
428
+ - `null` and `undefined` have no properties.
429
+
430
+ #### `toContain`
431
+
432
+ Requires string or collection containment.
433
+
434
+ ```ts
435
+ expect(total).toContain('$');
436
+ expect(rows).toContain(expect.objectContaining({ status: 'paid' }));
437
+ ```
438
+
439
+ - On a string, `expected` must be a string, and it is a substring check. An
440
+ asymmetric matcher is refused there:
441
+ `expect('abc').toContain(expect.stringContaining('a'))` fails.
442
+ - On a collection, an element must equal `expected`. This form takes
443
+ [asymmetric matchers](#asymmetric-matchers).
444
+
445
+ #### `toMatch`
446
+
447
+ Requires a string or `RegExp` match.
448
+
449
+ ```ts
450
+ expect(id).toMatch(/^usr_/);
451
+ ```
452
+
453
+ A pattern that is neither a string nor a `RegExp` is `INVALID_ARGUMENT`.
454
+
455
+ #### Numeric comparisons
456
+
457
+ `toBeGreaterThan` and `toBeLessThan` require a numeric bound;
458
+ `toBeGreaterThanOrEqual` and `toBeLessThanOrEqual` require it inclusive.
459
+
460
+ ```ts
461
+ expect(count).toBeGreaterThan(1);
462
+ expect(count).toBeLessThanOrEqual(10);
463
+ ```
464
+
465
+ #### `toBeCloseTo`
466
+
467
+ Requires a number within `10 ** -digits / 2` of `expected`.
468
+
469
+ ```ts
470
+ expect(totals.lineTotal).toBeCloseTo(60); // digits default: 2
471
+ ```
472
+
473
+ `toBeCloseTo(60)` accepts 59.996 and rejects 59.99. Infinite values pass only
474
+ when they are the same infinity.
475
+
476
+ #### Matching a schema
477
+
478
+ `toMatchSchema` validates a value against a
479
+ [Standard Schema](https://standardschema.dev): a Zod, Valibot, or ArkType
480
+ schema, or any library that implements the spec. Pass the schema your app
481
+ already parses its API with, and the test fails when the API drifts from it.
482
+ It returns the schema's output, typed, so the rest of the test reads
483
+ properties without casting.
484
+
485
+ ```ts
486
+ import { z } from 'zod';
487
+
488
+ const User = z.object({ id: z.number(), name: z.string(), email: z.email() });
489
+
490
+ const users = expect(await response.json()).toMatchSchema(z.array(User));
491
+ expect(users.map((user) => user.name)).toContain('Ada');
492
+ ```
493
+
494
+ - A failure lists every issue by its path (`1.email: Invalid email address`).
495
+ - The output is the schema's, so defaults and transforms apply.
496
+ - A matcher returns before anything could await it, so a schema that
497
+ validates asynchronously (an async refinement) is `INVALID_ARGUMENT`.
498
+ - Negated, it passes when the value does not match and returns nothing.
499
+
500
+ #### Failure messages
501
+
502
+ Pass a second argument to describe the check in its failure message:
503
+
504
+ ```ts
505
+ expect(created, 'the company exists through the API').toBeTruthy();
506
+ // ASSERTION_FAILED: the company exists through the API: expected undefined to be truthy
507
+ ```
508
+
509
+ Only value expectations take a message; a locator or `browser` expectation
510
+ already names its locator and matcher in the failure.
511
+
512
+ #### Not included
513
+
514
+ The unit-runner matchers (`toThrow`, `toBeInstanceOf`, `toBeNaN`,
515
+ `toStrictEqual`, `resolves`, `rejects`, `expect.extend`) are not part of
516
+ `expect`. An end-to-end test checks what the app shows and what its API
517
+ returns; the value matchers cover that, see [API](/api-testing). Code-level
518
+ checks belong in the unit runner.
519
+
520
+ ### Asymmetric matchers
521
+
522
+ An asymmetric matcher stands in for a value inside `toEqual`,
523
+ `toMatchObject`, the collection form of `toContain`, and the value of
524
+ `toHaveProperty`. Where the comparison would test equality, it asks the
525
+ matcher instead. They are Jest's matchers, with Jest's rules.
526
+
527
+ | Matcher | Matches |
528
+ | --- | --- |
529
+ | `expect.any(Class)` | An instance of the class. For `String`, `Number`, `Boolean`, `BigInt`, `Symbol`, and `Function`, the primitive too; for `Object`, anything `typeof` calls an object, `null` included |
530
+ | `expect.anything()` | Anything but `null` and `undefined` |
531
+ | `expect.objectContaining(sample)` | An object whose properties include every property of `sample`, each compared by `toEqual` rules |
532
+ | `expect.arrayContaining(sample)` | An array holding every element of `sample`, in any order, each compared by `toEqual` rules |
533
+ | `expect.stringContaining(text)` | A string with `text` as a substring |
534
+ | `expect.stringMatching(text \| RegExp)` | A string the RegExp tests true on; a text sample is compiled to a RegExp, so `'a.b'` matches `'axb'` |
535
+
536
+ ```ts
537
+ expect(created).toEqual({
538
+ id: expect.stringMatching(/^usr_/),
539
+ createdAt: expect.any(String),
540
+ roles: expect.arrayContaining(['member']),
541
+ profile: expect.objectContaining({ name: 'Ada' }),
542
+ });
543
+ expect(rows).toContain(expect.objectContaining({ status: 'paid' }));
544
+ expect(response).toHaveProperty('body.total', expect.any(Number));
545
+ ```
546
+
547
+ - In a failure message a matcher prints as its description: `Any<Number>`,
548
+ `ObjectContaining {"name":"Ada"}`.
549
+ - Matchers nest: an `objectContaining` sample may hold an `expect.any`.
550
+ - The string form of `toContain` refuses them; see [`toContain`](#tocontain).
551
+
552
+ ### Soft assertions
553
+
554
+ `expect.soft(actual)` takes what `expect(actual)` takes and returns the same
555
+ matchers. A soft matcher that fails does not throw: the failure is kept on
556
+ the running attempt and the body continues.
557
+
558
+ ```ts
559
+ test('checkout summary', async ({ agent, screen }) => {
560
+ await agent.act('add the "Pro" plan to the cart and open the summary');
561
+ expect.soft(await screen.getByTestId('subtotal').textContent()).toBe('$40.00');
562
+ expect.soft(await screen.getByTestId('tax').textContent()).toBe('$8.00');
563
+ await expect.soft(screen.getByRole('button', 'Pay')).toBeEnabled();
564
+ await expect(screen.getByTestId('total')).toHaveText('$48.00');
565
+ });
566
+ ```
567
+
568
+ ```text
569
+ ASSERTION_FAILED: 2 soft assertions failed
570
+ 1. expected "$41.00" to be "$40.00"
571
+ 2. expect.toBeEnabled failed
572
+ locator: getByRole('button', 'Pay')
573
+ expected: enabled
574
+ observed: states: disabled (match count 1)
575
+ ```
576
+
577
+ - Once the body has settled, and before `afterEach`, the attempt fails with
578
+ one `ASSERTION_FAILED` that lists every kept failure in order. The error's
579
+ source line is the first soft matcher that failed.
580
+ - A soft locator or `browser` matcher is still a recorded `expect.<matcher>`
581
+ step, marked failed, so the report shows where in the flow it landed.
582
+ - A soft value matcher is not a step, the same as a hard one; its failure
583
+ appears in the attempt's error records.
584
+ - When the body throws on its own, or times out, that error stays the
585
+ attempt's verdict and the soft failures are recorded beside it under
586
+ `secondaryErrors`.
587
+ - A body that skips itself with `test.skip(...)` after a soft failure still
588
+ fails: the soft failures are the attempt's error, and the skip reason is
589
+ kept in the attempt's `skip`.
590
+ - A soft `toMatchSchema` returns the schema's output when the value matches
591
+ and `undefined` after a kept failure, and its type says so.
592
+
593
+ <Note>
594
+ Outside a test body there is nothing to keep a failure for, so `expect.soft`
595
+ throws at once like `expect`: in a standalone script, and in an `afterEach`
596
+ hook, which runs after the soft failures have been settled. There is no
597
+ `expect.soft.poll`; `expect.poll` has its own timeout to fail on.
598
+ </Note>
599
+
600
+ ### Polling a value
601
+
602
+ `expect.poll(read)` re-reads a value until a matcher passes. Use it to wait
603
+ for an API response or saved data after an app action.
604
+
605
+ ```ts
606
+ await agent.act('create a test named "AI checkout regression" described as {description}', {
607
+ params: { description },
608
+ });
609
+ await expect
610
+ .poll(() => getTest(workspace).then((row) => row?.title), { timeout: 15_000 })
611
+ .toBe('AI checkout regression');
612
+ await expect.poll(async () => (await listTests(workspace)).length).toBeGreaterThan(1);
613
+ await expect.poll(() => readStatus(), { message: 'the batch never finished' }).not.toBe('running');
614
+ ```
615
+
616
+ | Option | Type | Default |
617
+ | --- | --- | --- |
618
+ | `timeout` | `number` | `config.assertionTimeout` during an attempt, 5000 ms outside one |
619
+ | `interval` | `number` | `100` |
620
+ | `message` | `string` | - |
621
+
622
+ - `timeout` is the deadline in milliseconds, a finite number 0 or more. It is
623
+ capped by the test deadline in the body, or the hook's `cleanupTimeout`
624
+ during `afterEach`.
625
+ - `interval` is the pause between reads in milliseconds, a finite number
626
+ above 0.
627
+ - `message` is an extra line in the timeout error, after the matcher line.
628
+
629
+ Every value matcher is available, returning a promise.
630
+
631
+ - Each sample calls `read` and runs the matcher. A matcher failure or a
632
+ `read` that throws is one failing sample; a `read` that hangs is cut at the
633
+ deadline.
634
+ - `toMatchSchema` resolves to the schema's output for the passing read.
635
+ - When the attempt is cancelled, the loop stops with `CANCELLED`. At the
636
+ attempt's deadline it stops with `ASSERTION_FAILED`, naming the deadline.
637
+ A `read` in flight is abandoned, not stopped: work it started can go on.
638
+ - `expect.poll` is not recorded as a report step; a timeout surfaces only as
639
+ the test's failure.
640
+
641
+ At the deadline it throws `ASSERTION_FAILED` naming the matcher, then
642
+ `message` when given, then the last sample: the last matcher failure, the
643
+ last `read` error, or `no read completed`.
644
+
645
+ ```text
646
+ expect.poll(...).not.toBe(...) timed out after 5000 ms
647
+ the batch never finished
648
+ last: expected "running" not to be "running"
649
+ ```
650
+
651
+ | Throws | When |
652
+ | --- | --- |
653
+ | `ASSERTION_FAILED` | The deadline passed without a passing sample |
654
+ | `INVALID_CONFIG` | `timeout` or `interval` out of range, before the first read |
655
+ | `CANCELLED` | The attempt was cancelled or timed out |
656
+ | `STEP_NOT_AWAITED` | The phase that started the poll returned while it still ran |
657
+ | `HOOK_FAILED` | A `beforeAll` or `afterAll` hook returned while its poll still ran |
658
+
659
+ <Note>
660
+ Await every poll. A poll belongs to the phase that started it: the test body
661
+ (with its fixtures and `beforeEach` hooks), one `afterEach` hook or fixture
662
+ teardown, or one `beforeAll` or `afterAll` hook. When that phase returns
663
+ while the poll still runs, the poll is cancelled and the phase fails with
664
+ `STEP_NOT_AWAITED` at the line of the call, whether or not the poll would
665
+ have passed. A body that threw keeps its own error and records this one
666
+ under `secondaryErrors`, and a suite hook reports it as `HOOK_FAILED`.
667
+ </Note>
668
+
669
+ A poll never fails a later test: a poll started by work its phase left
670
+ running, such as a body past its timeout, is cancelled before its first
671
+ read.
672
+
673
+ ## Behavior
674
+
675
+ Rules that apply across the matcher groups.
676
+
677
+ ### Timeouts and failures
678
+
679
+ | Property | Value |
680
+ | --- | --- |
681
+ | Default timeout | [`config.assertionTimeout`](/reference/config#assertiontimeout) |
682
+ | Override | `{ timeout }` in milliseconds, on every locator and `browser` matcher |
683
+ | Unknown option | `INVALID_ARGUMENT`; see [Option validation](#option-validation) |
684
+ | Failure code | `ASSERTION_FAILED`, exit code 1 |
685
+
686
+ ### Negation
687
+
688
+ `.not` inverts a matcher. A negated matcher passes only once the negation
689
+ has held:
690
+
691
+ - It passes after 1000 ms of continuous truth.
692
+ - When `{ timeout }` is shorter than 1000 ms, it passes at the deadline if the
693
+ negation held for the whole budget.
694
+ - The window counts from the moment the first read that saw the negation
695
+ started, so a slow read counts toward it.
696
+
697
+ ### Strictness
698
+
699
+ - A positive locator matcher requires exactly one match. The exceptions are
700
+ `toHaveCount` and the list form of `toHaveText` and `toContainText`.
701
+ - Zero matches keep polling until the timeout.
702
+ - Multiple matches fail immediately with `LOCATOR_AMBIGUOUS`.
703
+ - `toBeHidden`, `toBeVisible({ visible: false })`,
704
+ `toBeAttached({ attached: false })`, negated visibility, and negated
705
+ `toBeAttached` accept zero matches.
706
+ - For those, and for `waitFor({ state: 'detached' })` and `'hidden'`, a
707
+ locator under a frame that is not in the document is zero matches rather
708
+ than a wait for the frame.
709
+
710
+ ### Text matching
711
+
712
+ - Text comparison is normalized: leading and trailing whitespace is trimmed
713
+ and each run of whitespace collapses to one space.
714
+ - `toHaveText` and `toContainText` read the same rendered text as
715
+ [`textContent()`](/reference/screen#locator-reads), `innerText` on the web.
716
+ - A `TextMatch` string matches exactly. A `RegExp` uses its own source and
717
+ flags. Anything else, such as a predicate function, is `INVALID_ARGUMENT`.
718
+ - `ignoreCase: true` on `toHaveText`, `toContainText`, `toHaveAccessibleName`,
719
+ and the value form of `toHaveAttribute` compares a string case-insensitively
720
+ and adds the `i` flag to a `RegExp`; `ignoreCase: false` removes it.
721
+ - `toHaveAttribute(name, value)` compares like `toHaveText`, after
722
+ normalization.
723
+ - [`toHaveValue`](#tohavevalue) is the exception: it compares a value as it
724
+ is.
725
+
726
+ ### Secure fields
727
+
728
+ A secure field, such as a password input, is never judged: the engine
729
+ withholds its value and text.
730
+
731
+ | Matcher | On a secure field |
732
+ | --- | --- |
733
+ | `toHaveValue`, `toHaveText`, `toContainText` | `POLICY_DENIED`, like `inputValue()` and `textContent()`, negated or in list form too |
734
+ | `toHaveAttribute` | `POLICY_DENIED`, like `getAttribute()`, for every attribute name and negated too |
735
+ | `toHaveAccessibleName` and the state matchers | Answer as usual |
736
+
737
+ Reading the withheld value as `''` would pass a filled password field as
738
+ cleared, and reading the withheld `value` attribute as absent would pass
739
+ `not.toHaveAttribute('value')` on a password field that has one. Assert the
740
+ outcome of the fill, not the field.
741
+
742
+ ### Option validation
743
+
744
+ - A matcher refuses any option key it does not take, and a flag that is not a
745
+ boolean, with `INVALID_ARGUMENT` before it reads the screen.
746
+ - Types catch these in TypeScript, but `e2e run` does not type-check, so the
747
+ check runs for a JavaScript test or a computed option object too.
748
+ - Playwright options e2e does not implement, such as `indeterminate` on
749
+ `toBeChecked` or `useInnerText` on `toHaveText`, fail this way instead of
750
+ passing on the default.
751
+ - The boolean options Playwright gives these matchers (`checked`, `enabled`,
752
+ `visible`, `attached`) work the same way here.
753
+
754
+ ## Types
755
+
756
+ ### `ExpectCall`, `SoftExpectCall`, and `Expect`
757
+
17
758
  ```ts
18
759
  /** The `expect(actual)` call: a locator, `screen`, an engine fixture, or a value, told apart by the argument. */
19
760
  export interface ExpectCall {
@@ -46,52 +787,7 @@ export interface Expect extends ExpectCall {
46
787
  }
47
788
  ```
48
789
 
49
- Engine fixtures can provide matchers too, such as `expect(browser).toHaveURL()`.
50
- Such a fixture is an `Expectable<E>`: its engine attached the matcher set `E`
51
- through [`expectable`](/reference/engine#contributed-fixtures), and
52
- `expect(fixture)` returns it.
53
- Other arguments use value matchers, including promises, so await a promise
54
- before passing its result to `expect`.
55
-
56
- | Property | Value |
57
- | --- | --- |
58
- | Default timeout | `config.assertionTimeout`, 5000 ms |
59
- | Override | `{ timeout }` in milliseconds, on every locator and `browser` matcher |
60
- | Unknown option | `INVALID_ARGUMENT` before the first read, from JavaScript too |
61
- | Negation grace window | A negated matcher passes after 1000 ms of continuous truth, or at the deadline when `{ timeout }` is shorter than that and the negation held for the whole budget. The window counts from the moment the first read that saw the negation started, so a slow read counts toward it |
62
- | Failure code | `ASSERTION_FAILED`, exit code 1 |
63
-
64
- Locator strictness: except for `toHaveCount` and the list form of
65
- `toHaveText` and `toContainText`, a positive matcher requires exactly one
66
- match; zero matches keep polling until the timeout, and multiple matches fail
67
- immediately with `LOCATOR_AMBIGUOUS`. `toBeHidden`, `toBeVisible({ visible:
68
- false })`, `toBeAttached({ attached: false })`, negated visibility, and
69
- negated `toBeAttached` accept zero matches; for them, and for
70
- `waitFor({ state: 'detached' })` and `'hidden'`, a locator under a frame that
71
- is not in the document is zero matches rather than a wait for the frame.
72
-
73
- Text comparison is normalized: leading and trailing whitespace is trimmed and
74
- each run of whitespace collapses to one space. `toHaveText` and
75
- `toContainText` read the same rendered text as
76
- [`textContent()`](/reference/screen#locator-reads), `innerText` on the web.
77
- A `TextMatch` string matches exactly; a `RegExp` uses its own source and
78
- flags; anything else, such as a predicate function, is `INVALID_ARGUMENT`.
79
- `toHaveValue` is the exception: a form control's value is compared as it is,
80
- newlines and trailing spaces included, and the failure message prints it that
81
- way; a `contenteditable` editor's value is its rendered text, kept the same
82
- way, and an empty editor reads as `''`. A control the
83
- platform reports without a value, such as a cleared field on a device, reads
84
- as `''`. A node whose role carries no value, such as a heading or a link,
85
- never satisfies `toHaveValue`, negated or not; the message reads
86
- `no value (not a form control)`. A secure field, such as a password input,
87
- is never judged: the engine withholds its value and text, so `toHaveValue`,
88
- `toHaveText`, and `toContainText` throw `POLICY_DENIED` there like
89
- `inputValue()` and `textContent()`, negated or in list form too, instead of
90
- reading the withheld value as `''` and passing a filled password field as
91
- cleared. `toHaveAccessibleName` and the state matchers still answer there.
92
- Assert the outcome of the fill, not the field.
93
-
94
- ## Locator matchers
790
+ ### `AsyncExpectation` and `TextMatcherOptions`
95
791
 
96
792
  ```ts
97
793
  /**
@@ -148,67 +844,7 @@ export interface TextMatcherOptions {
148
844
  }
149
845
  ```
150
846
 
151
- A missing `checked`, `selected`, or `expanded` state counts as `false`, so
152
- `.not.toBeChecked()` passes on a button or a heading; a node is enabled
153
- unless it reports `disabled`. On the web, `disabled` is the state the
154
- browser applies, not the attribute alone: a control inside a
155
- `<fieldset disabled>` is disabled unless it sits in that fieldset's first
156
- `<legend>`, and `aria-disabled="true"` on an ancestor disables the widgets
157
- below it until an `aria-disabled="false"` cuts the chain, the same reading
158
- Playwright's `toBeDisabled` gives. `toHaveAttribute(name, value)`
159
- compares like `toHaveText`, after normalization. On a secure field, such as a
160
- password input, `toHaveAttribute` throws `POLICY_DENIED` like
161
- `getAttribute()`, for every attribute name and negated too: the field's
162
- `value` attribute is withheld, so reading it as absent would pass
163
- `not.toHaveAttribute('value')` on a password field that has one.
164
-
165
- `toBeAttached` passes once one match exists, hidden or shown, where
166
- `toBeVisible` needs a shown one. Negated, it passes once nothing matches.
167
-
168
- The boolean options Playwright gives these matchers work the same way:
169
- `toBeChecked({ checked: false })` waits for an unchecked node,
170
- `toBeEnabled({ enabled: false })` for a disabled one,
171
- `toBeVisible({ visible: false })` for a hidden or absent one, as
172
- `toBeHidden` does, and `toBeAttached({ attached: false })` for no match.
173
- `ignoreCase: true` on `toHaveText`, `toContainText`, `toHaveAccessibleName`,
174
- and the value form of `toHaveAttribute` compares a string case-insensitively
175
- and adds the `i` flag to a `RegExp`; `ignoreCase: false` removes it.
176
-
177
- A matcher refuses any other option key, and a flag that is not a boolean, with
178
- `INVALID_ARGUMENT` before it reads the screen. Types catch these in
179
- TypeScript, but `e2e run` does not type-check, so the check runs for a
180
- JavaScript test or a computed option object too. Playwright options e2e does
181
- not implement, such as `indeterminate` on `toBeChecked` or `useInnerText` on
182
- `toHaveText`, fail this way instead of passing on the default.
183
-
184
- `toHaveText([...])` and `toContainText([...])` check a list, with no
185
- exactly-one rule; the failure message lists every text observed.
186
- `toHaveText([...])` needs the match count to equal the list length and the
187
- match at each position to satisfy the entry there, a string exactly after
188
- normalization, a `RegExp` by test; an empty list holds when nothing matches.
189
- `toContainText([...])` needs each entry contained by a distinct match, in
190
- list order, as Playwright's does: extra matches before, between, or after
191
- are fine, a repeated entry needs a match of its own, and an empty list always
192
- holds. Negated, it fails as soon as that ordered subsequence exists, so
193
- `not.toContainText(['Deleted'])` fails while any match contains `Deleted`.
194
-
195
- ```ts
196
- await expect(screen.getByRole('status')).toHaveText('Saved');
197
- await expect(screen.getByRole('alert')).not.toContainText('error', { ignoreCase: true });
198
- await expect(screen.getByLabel('Subscribe')).toBeChecked({ checked: false });
199
- await expect(screen.getByTestId('todo')).toHaveCount(3);
200
- await expect(screen.getByTestId('todo')).toHaveText(['Write spec', /^Ship/, 'Release']);
201
- await expect(screen.getByTestId('todo')).toContainText(['spec', 'runner', 'Release']);
202
- await expect(screen.getByTestId('todo')).toContainText(['spec', 'Release']);
203
- await expect(screen.getByText('Loaded')).toBeAttached();
204
- await expect(screen.getByRole('dialog')).not.toBeVisible({ timeout: 10_000 });
205
- ```
206
-
207
- ## Screenshot matchers
208
-
209
- `expect(screen)` and `expect(locator)` compare against a PNG stored beside
210
- the test file, on every engine that captures pixels. The
211
- [Visual comparison](/visual-testing) guide shows the workflow.
847
+ ### `ScreenExpectation` and `ScreenshotOptions`
212
848
 
213
849
  ```ts
214
850
  export interface ScreenExpectation {
@@ -235,33 +871,7 @@ export interface ScreenshotOptions {
235
871
  }
236
872
  ```
237
873
 
238
- | Property | Value |
239
- | --- | --- |
240
- | Stored at | `<test file>-snapshots/<name>-<target>-<os>.png`; `.png` is added to a name without it |
241
- | Unnamed | The test's title path, lowercase, and a count: `checkout-pays-by-card-1` |
242
- | Missing | Taken from two screenshots in a row that are the same within the tolerance, then `ASSERTION_FAILED`. Written beside the test; in CI without `--update-snapshots` only attached to the results, at `snapshots/<path in the project>` |
243
- | Written earlier in the run | No comparison, `.not` included, passes against a screenshot the same run wrote (a retry, a repeat, another test naming it): `ASSERTION_FAILED` until the next run |
244
- | Different | Retried until one matches or the timeout; then `ASSERTION_FAILED` with `-expected`, `-actual`, and `-diff` images on the step |
245
- | `--update-snapshots`, `-u` | A missing or different stored screenshot is written from a screen that holds still, and the matcher passes |
246
- | `.not` | Passes once the screen differs; with nothing stored it is `ASSERTION_FAILED` |
247
- | Tolerance | Both `maxDiffPixels` and `maxDiffPixelRatio` set: the smaller wins |
248
- | After a secret fill | `POLICY_DENIED`, as for `app.screenshot()` |
249
-
250
- A name with a path separator, an option the matcher does not take (Playwright's
251
- `fullPage`, `animations`, `clip`), or a value out of range is
252
- `INVALID_ARGUMENT` before the screen is read, and so is `expect` on a screen
253
- scoped to a frame: compare a locator inside it instead.
254
-
255
- ```ts
256
- await expect(screen).toHaveScreenshot('home.png');
257
- await expect(screen.getByRole('navigation')).toHaveScreenshot();
258
- await expect(screen).toHaveScreenshot('feed.png', { mask: [screen.getByTestId('timestamp')], maxDiffPixels: 50 });
259
- ```
260
-
261
- ## browser matchers
262
-
263
- Contributed by `@e2e-dev/web`; the type is exported from that package.
264
- Every matcher call is a recorded `expect.<matcher>` assertion step.
874
+ ### `BrowserExpectation`
265
875
 
266
876
  ```ts
267
877
  export interface BrowserExpectation {
@@ -276,26 +886,7 @@ export interface BrowserExpectation {
276
886
  }
277
887
  ```
278
888
 
279
- A relative `toHaveURL` string resolves against the base URL and matches
280
- exactly after normalization; a `RegExp` tests the full URL. Anything else,
281
- such as a predicate function, is `INVALID_ARGUMENT`. `toHaveURL` also takes
282
- Playwright's `ignoreCase`, with the locator matchers' semantics. Otherwise each
283
- `browser` matcher takes `timeout` only; any other key, such as `ignoreCase` on
284
- `toHaveTitle`, which Playwright does not take either, is `INVALID_ARGUMENT`.
285
-
286
- `toHaveClass` polls for the node too: a target that matches nothing yet is one
287
- more poll, not a failure, until the assertion timeout runs out. An ambiguous
288
- target still fails at once with `LOCATOR_AMBIGUOUS`. It reads the `class`
289
- attribute, so `''` matches `class=""` and not an element with no `class`
290
- attribute at all; Playwright's `toHaveClass('')` accepts both.
291
-
292
- ```ts
293
- await expect(browser).toHaveURL('/dashboard');
294
- await expect(browser).toHaveTitle(/Dashboard/);
295
- await expect(browser).toHaveClass(browser.locator('.card'), 'card active');
296
- ```
297
-
298
- ## Value matchers
889
+ ### `ValueExpectation`
299
890
 
300
891
  ```ts
301
892
  export interface ValueExpectation<T> {
@@ -364,94 +955,7 @@ export type Class = abstract new (...args: never[]) => unknown;
364
955
  export type PropertyPath = string | readonly (string | number)[];
365
956
  ```
366
957
 
367
- Value matchers run synchronously without retrying. For a value that may still
368
- change, use [Polling a value](#polling-a-value).
369
-
370
- ```ts
371
- const total = await screen.getByTestId('total').textContent();
372
- expect(total).toContain('$');
373
- expect(items).toHaveLength(3);
374
- expect(payload).toEqual({ ok: true });
375
- expect(order).toMatchObject({ customer: { name: 'Ada' }, lines: [{ sku: 'a' }, { sku: 'b' }] });
376
- expect(order).toHaveProperty('customer.email', 'ada@example.test');
377
- expect(totals.lineTotal).toBeCloseTo(60);
378
- ```
379
-
380
- `toEqual` compares structure the way Jest's does: primitives with `Object.is`
381
- (so `NaN` equals `NaN` and `0` does not equal `-0`), arrays and plain objects
382
- by their contents, `Date` by time, `RegExp` by source and flags, `Map` and
383
- `Set` by their entries. A property whose value is `undefined` is treated as
384
- absent, a sparse array equals one holding `undefined`, and a class instance
385
- equals a plain object with the same properties. There is no `toStrictEqual`.
386
-
387
- `toMatchObject` reads `expected` as a subset: every property it names must
388
- match recursively, properties it does not name are ignored. An array in
389
- `expected` matches an array of the same length, element by element, each
390
- element again as a subset. Both sides must be objects; a primitive on either
391
- side is refused.
392
-
393
- `toHaveProperty` walks a dotted path or an array of keys. Use the array form
394
- when a key contains a dot, or to index an array by number
395
- (`['lines', 0, 'sku']`). Inherited properties count; a property holding
396
- `undefined` exists. A string, number, or boolean on the path is read through
397
- its wrapper object, as in Jest, so `toHaveProperty('label.length', 3)` and
398
- `toHaveProperty(['label', 0], 'a')` reach into a string; `null` and
399
- `undefined` have no properties. With a second argument the value at the path
400
- must equal it by `toEqual` rules.
401
-
402
- ### Matching a schema
403
-
404
- `toMatchSchema` validates a value against a
405
- [Standard Schema](https://standardschema.dev): a Zod, Valibot, or ArkType
406
- schema, or any library that implements the spec. Pass the schema your app
407
- already parses its API with, and the test fails when the API drifts from it.
408
- It returns the schema's output, typed, so the rest of the test reads
409
- properties without casting:
410
-
411
- ```ts
412
- import { z } from 'zod';
413
-
414
- const User = z.object({ id: z.number(), name: z.string(), email: z.email() });
415
-
416
- const users = expect(await response.json()).toMatchSchema(z.array(User));
417
- expect(users.map((user) => user.name)).toContain('Ada');
418
- ```
419
-
420
- A failure lists every issue by its path (`1.email: Invalid email address`).
421
- The output is the schema's, so defaults and transforms apply. A matcher
422
- returns before anything could await it, so a schema that validates
423
- asynchronously (an async refinement) is `INVALID_ARGUMENT`. Negated, it
424
- passes when the value does not match and returns nothing.
425
-
426
- A matcher that cannot evaluate its value (`toContain` on a number,
427
- `toHaveLength` on an object without one) fails as `ASSERTION_FAILED` like a
428
- mismatch would, so under `expect.poll` a value that has not taken its final
429
- shape yet is one more failing sample. A `toMatch` pattern that is neither a
430
- string nor a `RegExp` is `INVALID_ARGUMENT`.
431
-
432
- The unit-runner matchers (`toThrow`, `toBeInstanceOf`, `toBeNaN`,
433
- `toStrictEqual`, `resolves`, `rejects`, `expect.extend`) are not part of
434
- `expect`. An end-to-end test checks what the app shows and what its API
435
- returns; the value matchers cover that, see
436
- [API](/api-testing). Code-level
437
- checks belong in the unit runner.
438
-
439
- Pass a second argument to describe the check in its failure message:
440
-
441
- ```ts
442
- expect(created, 'the company exists through the API').toBeTruthy();
443
- // ASSERTION_FAILED: the company exists through the API: expected undefined to be truthy
444
- ```
445
-
446
- Only value expectations take a message; a locator or `browser` expectation
447
- already names its locator and matcher in the failure.
448
-
449
- ## Asymmetric matchers
450
-
451
- An asymmetric matcher stands in for a value inside `toEqual`,
452
- `toMatchObject`, the collection form of `toContain`, and the value of
453
- `toHaveProperty`. Where the comparison would test equality, it asks the
454
- matcher instead. They are Jest's matchers, with Jest's rules.
958
+ ### `AsymmetricMatcher`
455
959
 
456
960
  ```ts
457
961
  /**
@@ -468,83 +972,7 @@ export interface AsymmetricMatcher {
468
972
  }
469
973
  ```
470
974
 
471
- | Matcher | Matches |
472
- | --- | --- |
473
- | `expect.any(Class)` | An instance of the class. For `String`, `Number`, `Boolean`, `BigInt`, `Symbol`, and `Function`, the primitive too; for `Object`, anything `typeof` calls an object, `null` included |
474
- | `expect.anything()` | Anything but `null` and `undefined` |
475
- | `expect.objectContaining(sample)` | An object whose properties include every property of `sample`, each compared by `toEqual` rules |
476
- | `expect.arrayContaining(sample)` | An array holding every element of `sample`, in any order, each compared by `toEqual` rules |
477
- | `expect.stringContaining(text)` | A string with `text` as a substring |
478
- | `expect.stringMatching(text \| RegExp)` | A string the RegExp tests true on; a text sample is compiled to a RegExp, so `'a.b'` matches `'axb'` |
479
-
480
- ```ts
481
- expect(created).toEqual({
482
- id: expect.stringMatching(/^usr_/),
483
- createdAt: expect.any(String),
484
- roles: expect.arrayContaining(['member']),
485
- profile: expect.objectContaining({ name: 'Ada' }),
486
- });
487
- expect(rows).toContain(expect.objectContaining({ status: 'paid' }));
488
- expect(response).toHaveProperty('body.total', expect.any(Number));
489
- ```
490
-
491
- In a failure message a matcher prints as its description: `Any<Number>`,
492
- `ObjectContaining {"name":"Ada"}`. Matchers nest: an `objectContaining`
493
- sample may hold an `expect.any`. The string form of `toContain` takes a
494
- string only, so `expect('abc').toContain(expect.stringContaining('a'))` is
495
- refused.
496
-
497
- ## Soft assertions
498
-
499
- `expect.soft(actual)` takes what `expect(actual)` takes and returns the same
500
- matchers. A soft matcher that fails does not throw: the failure is kept on the
501
- running attempt and the body continues. Once the body has settled, and before
502
- `afterEach`, the attempt fails with one `ASSERTION_FAILED` that lists every
503
- kept failure in order. The error's source line is the first soft matcher that
504
- failed.
505
-
506
- ```ts
507
- test('checkout summary', async ({ agent, screen }) => {
508
- await agent.act('add the "Pro" plan to the cart and open the summary');
509
- expect.soft(await screen.getByTestId('subtotal').textContent()).toBe('$40.00');
510
- expect.soft(await screen.getByTestId('tax').textContent()).toBe('$8.00');
511
- await expect.soft(screen.getByRole('button', 'Pay')).toBeEnabled();
512
- await expect(screen.getByTestId('total')).toHaveText('$48.00');
513
- });
514
- ```
515
-
516
- ```text
517
- ASSERTION_FAILED: 2 soft assertions failed
518
- 1. expected "$40.00" to be "$41.00"
519
- 2. expect.toBeEnabled failed
520
- locator: getByRole('button', 'Pay')
521
- expected: enabled
522
- observed: states: disabled (match count 1)
523
- ```
524
-
525
- A soft locator or `browser` matcher is still a recorded `expect.<matcher>` step,
526
- marked failed, so the report shows where in the flow it landed. A soft value
527
- matcher is not a step, the same as a hard one; its failure appears in the
528
- attempt's error records.
529
-
530
- When the body throws on its own, or times out, that error stays the
531
- attempt's verdict and the soft failures are recorded beside it under
532
- `secondaryErrors`. A body that skips itself with `test.skip(...)` after a
533
- soft failure still fails: the soft failures are the attempt's error, and the
534
- skip reason is kept in the attempt's `skip`.
535
-
536
- A soft `toMatchSchema` returns the schema's output when the value matches
537
- and `undefined` after a kept failure, and its type says so.
538
-
539
- Outside a test body there is nothing to keep a failure for, so `expect.soft`
540
- throws at once like `expect`: in a standalone script, and in an `afterEach`
541
- hook, which runs after the soft failures have been settled. There is no
542
- `expect.soft.poll`; `expect.poll` has its own timeout to fail on.
543
-
544
- ## Polling a value
545
-
546
- `expect.poll(read)` re-reads a value until a matcher passes. Use it to wait for
547
- an API response or saved data after an app action.
975
+ ### `PollOptions` and `PollExpectation`
548
976
 
549
977
  ```ts
550
978
  export interface PollOptions {
@@ -579,51 +1007,3 @@ export type NegatedPollExpectation<T> = {
579
1007
  readonly [K in Exclude<keyof ValueExpectation<T>, 'not'>]: (...args: Parameters<ValueExpectation<T>[K]>) => Promise<void>;
580
1008
  };
581
1009
  ```
582
-
583
- Every value matcher is available, returning a promise. Each sample calls
584
- `read` and runs the matcher. A matcher failure or a `read` that throws is one
585
- failing sample; a `read` that hangs is cut at the deadline.
586
-
587
- The timeout defaults to `config.assertionTimeout` during an attempt and
588
- 5000 ms outside one. It is capped by the test deadline in the body, or the
589
- hook's `cleanupTimeout` during `afterEach`.
590
-
591
- When the attempt is cancelled or times out, the loop and any read in flight
592
- stop with `CANCELLED`. `timeout` must be a finite number 0 or more and
593
- `interval` a finite number above 0; anything else is `INVALID_CONFIG` before
594
- the first read.
595
-
596
- At the deadline it throws `ASSERTION_FAILED` naming the matcher, then
597
- `message` when given, then the last sample: the last matcher failure, the
598
- last `read` error, or `no read completed`.
599
-
600
- ```text
601
- expect.poll(...).toBe(...) timed out after 5000 ms
602
- the batch never finished
603
- last: expected "running" to be "done"
604
- ```
605
-
606
- ```ts
607
- await agent.act('create a test named "AI checkout regression" described as {description}', {
608
- params: { description },
609
- });
610
- await expect
611
- .poll(() => getTest(workspace).then((row) => row?.title), { timeout: 15_000 })
612
- .toBe('AI checkout regression');
613
- await expect.poll(async () => (await listTests(workspace)).length).toBeGreaterThan(1);
614
- await expect.poll(() => readStatus(), { message: 'the batch never finished' }).not.toBe('running');
615
- ```
616
-
617
- `expect.poll` is not recorded as a report step; a timeout surfaces only as
618
- the test's failure.
619
-
620
- Await every poll. A poll belongs to the phase that started it: the test body
621
- (with its fixtures and `beforeEach` hooks), one `afterEach` hook or fixture
622
- teardown, or one `beforeAll` or `afterAll` hook. When that phase returns while
623
- the poll still runs, the poll is cancelled and the phase fails with
624
- `STEP_NOT_AWAITED` at the line of the call, whether or not the poll would
625
- have passed. It never fails a later test: a poll started by work its phase
626
- left running, such as a body past its timeout, is cancelled before its first
627
- read. A body that threw keeps its own
628
- error and records this one under `secondaryErrors`, and a suite hook reports
629
- it as `HOOK_FAILED`.