e2e 0.15.1 → 0.15.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 (87) hide show
  1. package/README.md +51 -24
  2. package/dist/.build.json +1 -1
  3. package/dist/agent/act.js +3 -3
  4. package/dist/agent/act.js.map +1 -1
  5. package/dist/agent/error.d.ts +16 -3
  6. package/dist/agent/error.d.ts.map +1 -1
  7. package/dist/agent/error.js +25 -6
  8. package/dist/agent/error.js.map +1 -1
  9. package/dist/agent/model/tool-choice.d.ts +24 -1
  10. package/dist/agent/model/tool-choice.d.ts.map +1 -1
  11. package/dist/agent/model/tool-choice.js +27 -1
  12. package/dist/agent/model/tool-choice.js.map +1 -1
  13. package/dist/agent/step-cache.d.ts +14 -3
  14. package/dist/agent/step-cache.d.ts.map +1 -1
  15. package/dist/agent/step-cache.js +13 -3
  16. package/dist/agent/step-cache.js.map +1 -1
  17. package/dist/agent/tool-loop.d.ts.map +1 -1
  18. package/dist/agent/tool-loop.js +62 -9
  19. package/dist/agent/tool-loop.js.map +1 -1
  20. package/dist/cache/context.d.ts +19 -6
  21. package/dist/cache/context.d.ts.map +1 -1
  22. package/dist/cache/context.js +15 -21
  23. package/dist/cache/context.js.map +1 -1
  24. package/dist/cli/init/scaffold.d.ts +1 -0
  25. package/dist/cli/init/scaffold.d.ts.map +1 -1
  26. package/dist/cli/init/scaffold.js +2 -1
  27. package/dist/cli/init/scaffold.js.map +1 -1
  28. package/dist/cli/init/sibling-versions.json +1 -1
  29. package/dist/expect/values.js +1 -1
  30. package/dist/expect/values.js.map +1 -1
  31. package/dist/run/execute.d.ts.map +1 -1
  32. package/dist/run/execute.js +21 -8
  33. package/dist/run/execute.js.map +1 -1
  34. package/dist/types.d.ts +1 -1
  35. package/dist/types.d.ts.map +1 -1
  36. package/docs/agent-steps.mdx +126 -0
  37. package/docs/agents.mdx +217 -0
  38. package/docs/api-testing.mdx +65 -0
  39. package/docs/authentication.mdx +345 -0
  40. package/docs/browser.mdx +440 -0
  41. package/docs/bug-bash.mdx +90 -0
  42. package/docs/cache.mdx +306 -0
  43. package/docs/ci.mdx +210 -0
  44. package/docs/coding-agents.mdx +183 -0
  45. package/docs/debugging.mdx +291 -0
  46. package/docs/executors.mdx +243 -0
  47. package/docs/explore.mdx +174 -0
  48. package/docs/github.mdx +184 -0
  49. package/docs/index.mdx +82 -0
  50. package/docs/integrations/eas.mdx +216 -0
  51. package/docs/integrations/index.mdx +24 -0
  52. package/docs/integrations/kernel.mdx +163 -0
  53. package/docs/migrate/cypress.mdx +124 -0
  54. package/docs/migrate/detox.mdx +140 -0
  55. package/docs/migrate/maestro.mdx +229 -0
  56. package/docs/migrate/playwright.mdx +427 -0
  57. package/docs/migrate/selenium.mdx +110 -0
  58. package/docs/mobile-ci.mdx +567 -0
  59. package/docs/mobile.mdx +555 -0
  60. package/docs/models.mdx +241 -0
  61. package/docs/quickstart.mdx +260 -0
  62. package/docs/reference/agent.mdx +524 -0
  63. package/docs/reference/app.mdx +72 -0
  64. package/docs/reference/cli.mdx +817 -0
  65. package/docs/reference/config.mdx +986 -0
  66. package/docs/reference/engine.mdx +1362 -0
  67. package/docs/reference/environment.mdx +113 -0
  68. package/docs/reference/errors.mdx +143 -0
  69. package/docs/reference/expect.mdx +513 -0
  70. package/docs/reference/mcp.mdx +202 -0
  71. package/docs/reference/mobile.mdx +522 -0
  72. package/docs/reference/reporters.mdx +216 -0
  73. package/docs/reference/screen.mdx +688 -0
  74. package/docs/reference/test.mdx +449 -0
  75. package/docs/reference/web.mdx +611 -0
  76. package/docs/security.mdx +292 -0
  77. package/docs/starting-your-app.mdx +231 -0
  78. package/docs/subscriptions.mdx +157 -0
  79. package/docs/telemetry.mdx +185 -0
  80. package/docs/tools.mdx +150 -0
  81. package/docs/viewports.mdx +146 -0
  82. package/docs/writing-an-engine.mdx +316 -0
  83. package/docs/writing-tests.mdx +256 -0
  84. package/package.json +2 -1
  85. package/skills/e2e/SKILL.md +4 -1
  86. package/skills/e2e/references/agent.md +3 -1
  87. package/skills/e2e/references/setup.md +13 -4
@@ -0,0 +1,513 @@
1
+ ---
2
+ title: expect
3
+ description: "Locator assertions, fixture assertions, and plain value matchers."
4
+ ---
5
+
6
+ Use `expect(locator)` to wait for an app condition and `expect(value)` to
7
+ check a value immediately. `expect.poll` repeatedly reads a value until its
8
+ matcher passes. `expect.soft` keeps a failure for the end of the test body
9
+ instead of throwing it, and `expect.any` and the other
10
+ [asymmetric matchers](#asymmetric-matchers) stand in for values inside a
11
+ structural comparison.
12
+
13
+ ```ts
14
+ /** The `expect(actual)` call: a locator, an engine fixture, or a value, told apart by the argument. */
15
+ export interface ExpectCall {
16
+ (actual: Locator): AsyncExpectation;
17
+ <E extends object>(actual: Expectable<E>): E;
18
+ <T>(actual: T, message?: string): ValueExpectation<T>;
19
+ }
20
+
21
+ /** The `expect.soft(actual)` call: `expect(actual)` whose failures are kept instead of thrown. */
22
+ export interface SoftExpectCall {
23
+ (actual: Locator): AsyncExpectation;
24
+ <E extends object>(actual: Expectable<E>): E;
25
+ <T>(actual: T, message?: string): SoftValueExpectation<T>;
26
+ }
27
+
28
+ /** The `expect` entry: the call, `expect.poll`, `expect.soft`, and the asymmetric matchers. */
29
+ export interface Expect extends ExpectCall {
30
+ /** Re-reads a value until the chosen matcher holds or `timeout` passes. */
31
+ poll<T>(read: () => T | Promise<T>, options?: PollOptions): PollExpectation<T>;
32
+ /** The same matchers, a failure kept on the attempt instead of thrown; the attempt fails once the body has settled. */
33
+ readonly soft: SoftExpectCall;
34
+ any(sample: Class | typeof BigInt | typeof Symbol): AsymmetricMatcher;
35
+ anything(): AsymmetricMatcher;
36
+ objectContaining(sample: object): AsymmetricMatcher;
37
+ arrayContaining(sample: readonly unknown[]): AsymmetricMatcher;
38
+ stringContaining(sample: string): AsymmetricMatcher;
39
+ stringMatching(sample: string | RegExp): AsymmetricMatcher;
40
+ }
41
+ ```
42
+
43
+ Engine fixtures can provide matchers too, such as `expect(browser).toHaveURL()`.
44
+ Such a fixture is an `Expectable<E>`: its engine attached the matcher set `E`
45
+ through [`expectable`](/reference/engine#contributed-fixtures), and
46
+ `expect(fixture)` returns it.
47
+ Other arguments use value matchers, including promises, so await a promise
48
+ before passing its result to `expect`.
49
+
50
+ | Property | Value |
51
+ | --- | --- |
52
+ | Default timeout | `config.assertionTimeout`, 5000 ms |
53
+ | Override | `{ timeout }` in milliseconds, on every locator and `browser` matcher |
54
+ | Negation grace window | A negated matcher passes after 1000 ms of continuous truth, or after the whole budget when `{ timeout }` is shorter than that |
55
+ | Failure code | `ASSERTION_FAILED`, exit code 1 |
56
+
57
+ Locator strictness: except for `toHaveCount` and the list form of
58
+ `toHaveText` and `toContainText`, a positive matcher requires exactly one
59
+ match; zero matches keep polling until the timeout, and multiple matches fail
60
+ immediately with `LOCATOR_AMBIGUOUS`. `toBeHidden`, negated visibility, and
61
+ negated `toBeAttached` accept zero matches.
62
+
63
+ Text comparison is normalized: leading and trailing whitespace is trimmed and
64
+ each run of whitespace collapses to one space. `toHaveText` and
65
+ `toContainText` read the same rendered text as
66
+ [`textContent()`](/reference/screen#locator-reads), `innerText` on the web.
67
+ A `TextMatch` string matches exactly; a `RegExp` uses its own source and
68
+ flags; anything else, such as a predicate function, is `INVALID_ARGUMENT`.
69
+ `toHaveValue` is the exception: a form control's value is compared as it is,
70
+ newlines and trailing spaces included, and the failure message prints it that
71
+ way; a `contenteditable` editor's value is its rendered text, kept the same
72
+ way, and an empty editor reads as `''`. A control the
73
+ platform reports without a value, such as a cleared field on a device, reads
74
+ as `''`. A node whose role carries no value, such as a heading or a link,
75
+ never satisfies `toHaveValue`, negated or not; the message reads
76
+ `no value (not a form control)`. A secure field, such as a password input,
77
+ is never judged: the engine withholds its value and text, so `toHaveValue`,
78
+ `toHaveText`, and `toContainText` throw `POLICY_DENIED` there like
79
+ `inputValue()` and `textContent()`, negated or in list form too, instead of
80
+ reading the withheld value as `''` and passing a filled password field as
81
+ cleared. `toHaveAccessibleName` and the state matchers still answer there.
82
+ Assert the outcome of the fill, not the field.
83
+
84
+ ## Locator matchers
85
+
86
+ ```ts
87
+ export interface AsyncExpectation {
88
+ /** Inverts the matcher. A negated matcher passes after 1000 ms of continuous truth. */
89
+ readonly not: AsyncExpectation;
90
+ /** Waits for visibility. */
91
+ toBeVisible(options?: { timeout?: number }): Promise<void>;
92
+ /** Waits for hidden or absent state. */
93
+ toBeHidden(options?: { timeout?: number }): Promise<void>;
94
+ /** Waits for one match to exist, visible or not. */
95
+ toBeAttached(options?: { timeout?: number }): Promise<void>;
96
+ /** Waits for enabled state. */
97
+ toBeEnabled(options?: { timeout?: number }): Promise<void>;
98
+ /** Waits for disabled state. */
99
+ toBeDisabled(options?: { timeout?: number }): Promise<void>;
100
+ /** Waits for checked state. */
101
+ toBeChecked(options?: { timeout?: number }): Promise<void>;
102
+ /** Waits for selected state. */
103
+ toBeSelected(options?: { timeout?: number }): Promise<void>;
104
+ /** Waits for expanded state. */
105
+ toBeExpanded(options?: { timeout?: number }): Promise<void>;
106
+ /** Waits for focused state. */
107
+ toBeFocused(options?: { timeout?: number }): Promise<void>;
108
+ /** Waits for exact normalized text; a list waits for exactly that many matches, each with its entry's text, in order. */
109
+ toHaveText(expected: TextMatch, options?: { timeout?: number }): Promise<void>;
110
+ toHaveText(expected: readonly TextMatch[], options?: { timeout?: number }): Promise<void>;
111
+ /** Waits for contained normalized text; a list waits for each entry to be contained by a distinct match, in order, extra matches allowed. */
112
+ toContainText(expected: TextMatch, options?: { timeout?: number }): Promise<void>;
113
+ toContainText(expected: readonly TextMatch[], options?: { timeout?: number }): Promise<void>;
114
+ /** Waits for a form control's value, compared as it is. */
115
+ toHaveValue(expected: TextMatch, options?: { timeout?: number }): Promise<void>;
116
+ /** Waits for the attribute to be present; with `value`, for it to match. */
117
+ toHaveAttribute(name: string, options?: { timeout?: number }): Promise<void>;
118
+ toHaveAttribute(name: string, value: TextMatch, options?: { timeout?: number }): Promise<void>;
119
+ /** Waits for an exact match count. */
120
+ toHaveCount(expected: number, options?: { timeout?: number }): Promise<void>;
121
+ /** Waits for an accessible name. */
122
+ toHaveAccessibleName(expected: TextMatch, options?: { timeout?: number }): Promise<void>;
123
+ }
124
+ ```
125
+
126
+ A missing `checked`, `selected`, or `expanded` state counts as `false`, so
127
+ `.not.toBeChecked()` passes on a button or a heading; a node is enabled
128
+ unless it reports `disabled`. On the web, `disabled` is the state the
129
+ browser applies, not the attribute alone: a control inside a
130
+ `<fieldset disabled>` is disabled unless it sits in that fieldset's first
131
+ `<legend>`, and `aria-disabled="true"` on an ancestor disables the widgets
132
+ below it until an `aria-disabled="false"` cuts the chain, the same reading
133
+ Playwright's `toBeDisabled` gives. `toHaveAttribute(name, value)`
134
+ compares like `toHaveText`, after normalization. On a secure field, such as a
135
+ password input, `toHaveAttribute` throws `POLICY_DENIED` like
136
+ `getAttribute()`, for every attribute name and negated too: the field's
137
+ `value` attribute is withheld, so reading it as absent would pass
138
+ `not.toHaveAttribute('value')` on a password field that has one.
139
+
140
+ `toBeAttached` passes once one match exists, hidden or shown, where
141
+ `toBeVisible` needs a shown one. Negated, it passes once nothing matches.
142
+
143
+ `toHaveText([...])` and `toContainText([...])` check a list, with no
144
+ exactly-one rule; the failure message lists every text observed.
145
+ `toHaveText([...])` needs the match count to equal the list length and the
146
+ match at each position to satisfy the entry there, a string exactly after
147
+ normalization, a `RegExp` by test; an empty list holds when nothing matches.
148
+ `toContainText([...])` needs each entry contained by a distinct match, in
149
+ list order, as Playwright's does: extra matches before, between, or after
150
+ are fine, a repeated entry needs a match of its own, and an empty list always
151
+ holds. Negated, it fails as soon as that ordered subsequence exists, so
152
+ `not.toContainText(['Deleted'])` fails while any match contains `Deleted`.
153
+
154
+ ```ts
155
+ await expect(screen.getByRole('status')).toHaveText('Saved');
156
+ await expect(screen.getByTestId('todo')).toHaveCount(3);
157
+ await expect(screen.getByTestId('todo')).toHaveText(['Write spec', /^Ship/, 'Release']);
158
+ await expect(screen.getByTestId('todo')).toContainText(['spec', 'runner', 'Release']);
159
+ await expect(screen.getByTestId('todo')).toContainText(['spec', 'Release']);
160
+ await expect(screen.getByText('Loaded')).toBeAttached();
161
+ await expect(screen.getByRole('dialog')).not.toBeVisible({ timeout: 10_000 });
162
+ ```
163
+
164
+ ## browser matchers
165
+
166
+ Contributed by `@e2e-dev/web`; the type is exported from that package.
167
+ Every matcher call is a recorded `expect.<matcher>` assertion step.
168
+
169
+ ```ts
170
+ export interface BrowserExpectation {
171
+ /** Inverts the matcher. */
172
+ readonly not: BrowserExpectation;
173
+ /** Waits for the current URL to match. */
174
+ toHaveURL(expected: string | RegExp, options?: { timeout?: number }): Promise<void>;
175
+ /** Waits for the current title to match. */
176
+ toHaveTitle(expected: TextMatch, options?: { timeout?: number }): Promise<void>;
177
+ /** Waits for the target's `class` attribute to match: a string is the whole normalized class list, a RegExp is tested against it. */
178
+ toHaveClass(target: Locator, expected: TextMatch, options?: { timeout?: number }): Promise<void>;
179
+ }
180
+ ```
181
+
182
+ A relative `toHaveURL` string resolves against the base URL and matches
183
+ exactly after normalization; a `RegExp` tests the full URL. Anything else,
184
+ such as a predicate function, is `INVALID_ARGUMENT`.
185
+
186
+ `toHaveClass` polls for the node too: a target that matches nothing yet is one
187
+ more poll, not a failure, until the assertion timeout runs out. An ambiguous
188
+ target still fails at once with `LOCATOR_AMBIGUOUS`. It reads the `class`
189
+ attribute, so `''` matches `class=""` and not an element with no `class`
190
+ attribute at all; Playwright's `toHaveClass('')` accepts both.
191
+
192
+ ```ts
193
+ await expect(browser).toHaveURL('/dashboard');
194
+ await expect(browser).toHaveTitle(/Dashboard/);
195
+ await expect(browser).toHaveClass(browser.locator('.card'), 'card active');
196
+ ```
197
+
198
+ ## Value matchers
199
+
200
+ ```ts
201
+ export interface ValueExpectation<T> {
202
+ /** Inverts the matcher. */
203
+ readonly not: NegatedValueExpectation<T>;
204
+ /** Compares with Object.is. */
205
+ toBe(expected: T): void;
206
+ /** Performs recursive structural equality; `undefined` properties are ignored and class types are not compared. */
207
+ toEqual(expected: unknown): void;
208
+ /** Requires every property of `expected` to match recursively; extra properties on the value are fine, arrays must match element-wise with equal length. */
209
+ toMatchObject(expected: object): void;
210
+ /** Requires a truthy value. */
211
+ toBeTruthy(): void;
212
+ /** Requires a falsy value. */
213
+ toBeFalsy(): void;
214
+ /** Requires null. */
215
+ toBeNull(): void;
216
+ /** Requires undefined. */
217
+ toBeUndefined(): void;
218
+ /** Requires any value but undefined; null passes. */
219
+ toBeDefined(): void;
220
+ /** Requires a `length` property equal to `expected`: a string, an array, or anything array-like. */
221
+ toHaveLength(expected: number): void;
222
+ /** Requires the property at `path` to exist; with `expected`, to equal it by `toEqual` rules. */
223
+ toHaveProperty(path: PropertyPath, ...expected: [] | [expected: unknown]): void;
224
+ /** Requires string or collection containment. */
225
+ toContain(expected: unknown): void;
226
+ /** Requires a string or regexp match. */
227
+ toMatch(expected: string | RegExp): void;
228
+ /** Requires a numeric lower bound. */
229
+ toBeGreaterThan(expected: number): void;
230
+ /** Requires a numeric lower bound, inclusive. */
231
+ toBeGreaterThanOrEqual(expected: number): void;
232
+ /** Requires a numeric upper bound. */
233
+ toBeLessThan(expected: number): void;
234
+ /** Requires a numeric upper bound, inclusive. */
235
+ toBeLessThanOrEqual(expected: number): void;
236
+ /** Requires a number within `10 ** -digits / 2` of `expected`; `digits` defaults to 2. */
237
+ toBeCloseTo(expected: number, digits?: number): void;
238
+ /**
239
+ * Requires the value to pass a synchronous Standard Schema (Zod, Valibot,
240
+ * ArkType, ...) and returns the schema's output, typed:
241
+ * `const users = expect(await response.json()).toMatchSchema(Users)`.
242
+ * A schema that validates asynchronously is `INVALID_ARGUMENT`.
243
+ */
244
+ toMatchSchema<Schema extends StandardSchemaV1>(schema: Schema): StandardSchemaV1.InferOutput<Schema>;
245
+ }
246
+
247
+ /** `expect(value).not`: every value matcher inverted, none returning a value; `Back` is what `.not` returns to. */
248
+ export type NegatedValueExpectation<T, Back = ValueExpectation<T>> = {
249
+ readonly not: Back;
250
+ } & {
251
+ readonly [K in Exclude<keyof ValueExpectation<T>, 'not'>]: (...args: Parameters<ValueExpectation<T>[K]>) => void;
252
+ };
253
+
254
+ /** `expect.soft(value)`: the value matchers, with `toMatchSchema` returning `undefined` after a kept failure. */
255
+ export type SoftValueExpectation<T> = Omit<ValueExpectation<T>, 'toMatchSchema' | 'not'> & {
256
+ readonly not: NegatedValueExpectation<T, SoftValueExpectation<T>>;
257
+ toMatchSchema<Schema extends StandardSchemaV1>(schema: Schema): StandardSchemaV1.InferOutput<Schema> | undefined;
258
+ };
259
+
260
+ /** A class, abstract or not: what `expect.any` takes. */
261
+ export type Class = abstract new (...args: never[]) => unknown;
262
+
263
+ /** A property path for `toHaveProperty`: dotted (`'user.address.city'`) or one key per element (`['user', 'address', 'city']`). */
264
+ export type PropertyPath = string | readonly (string | number)[];
265
+ ```
266
+
267
+ Value matchers run synchronously without retrying. For a value that may still
268
+ change, use [Polling a value](#polling-a-value).
269
+
270
+ ```ts
271
+ const total = await screen.getByTestId('total').textContent();
272
+ expect(total).toContain('$');
273
+ expect(items).toHaveLength(3);
274
+ expect(payload).toEqual({ ok: true });
275
+ expect(order).toMatchObject({ customer: { name: 'Ada' }, lines: [{ sku: 'a' }, { sku: 'b' }] });
276
+ expect(order).toHaveProperty('customer.email', 'ada@example.test');
277
+ expect(totals.lineTotal).toBeCloseTo(60);
278
+ ```
279
+
280
+ `toEqual` compares structure the way Jest's does: primitives with `Object.is`
281
+ (so `NaN` equals `NaN` and `0` does not equal `-0`), arrays and plain objects
282
+ by their contents, `Date` by time, `RegExp` by source and flags, `Map` and
283
+ `Set` by their entries. A property whose value is `undefined` is treated as
284
+ absent, a sparse array equals one holding `undefined`, and a class instance
285
+ equals a plain object with the same properties. There is no `toStrictEqual`.
286
+
287
+ `toMatchObject` reads `expected` as a subset: every property it names must
288
+ match recursively, properties it does not name are ignored. An array in
289
+ `expected` matches an array of the same length, element by element, each
290
+ element again as a subset. Both sides must be objects; a primitive on either
291
+ side is refused.
292
+
293
+ `toHaveProperty` walks a dotted path or an array of keys. Use the array form
294
+ when a key contains a dot, or to index an array by number
295
+ (`['lines', 0, 'sku']`). Inherited properties count; a property holding
296
+ `undefined` exists. With a second argument the value at the path must equal it
297
+ by `toEqual` rules.
298
+
299
+ ### Matching a schema
300
+
301
+ `toMatchSchema` validates a value against a
302
+ [Standard Schema](https://standardschema.dev): a Zod, Valibot, or ArkType
303
+ schema, or any library that implements the spec. Pass the schema your app
304
+ already parses its API with, and the test fails when the API drifts from it.
305
+ It returns the schema's output, typed, so the rest of the test reads
306
+ properties without casting:
307
+
308
+ ```ts
309
+ import { z } from 'zod';
310
+
311
+ const User = z.object({ id: z.number(), name: z.string(), email: z.email() });
312
+
313
+ const users = expect(await response.json()).toMatchSchema(z.array(User));
314
+ expect(users.map((user) => user.name)).toContain('Ada');
315
+ ```
316
+
317
+ A failure lists every issue by its path (`1.email: Invalid email address`).
318
+ The output is the schema's, so defaults and transforms apply. A matcher
319
+ returns before anything could await it, so a schema that validates
320
+ asynchronously (an async refinement) is `INVALID_ARGUMENT`. Negated, it
321
+ passes when the value does not match and returns nothing.
322
+
323
+ A matcher that cannot evaluate its value (`toContain` on a number,
324
+ `toHaveLength` on an object without one) fails as `ASSERTION_FAILED` like a
325
+ mismatch would, so under `expect.poll` a value that has not taken its final
326
+ shape yet is one more failing sample. A `toMatch` pattern that is neither a
327
+ string nor a `RegExp` is `INVALID_ARGUMENT`.
328
+
329
+ The unit-runner matchers (`toThrow`, `toBeInstanceOf`, `toBeNaN`,
330
+ `toStrictEqual`, `resolves`, `rejects`, `expect.extend`) are not part of
331
+ `expect`. An end-to-end test checks what the app shows and what its API
332
+ returns; the value matchers cover that, see
333
+ [Testing APIs](/api-testing). Code-level
334
+ checks belong in the unit runner.
335
+
336
+ Pass a second argument to describe the check in its failure message:
337
+
338
+ ```ts
339
+ expect(created, 'the company exists through the API').toBeTruthy();
340
+ // ASSERTION_FAILED: the company exists through the API: expected undefined to be truthy
341
+ ```
342
+
343
+ Only value expectations take a message; a locator or `browser` expectation
344
+ already names its locator and matcher in the failure.
345
+
346
+ ## Asymmetric matchers
347
+
348
+ An asymmetric matcher stands in for a value inside `toEqual`,
349
+ `toMatchObject`, the collection form of `toContain`, and the value of
350
+ `toHaveProperty`. Where the comparison would test equality, it asks the
351
+ matcher instead. They are Jest's matchers, with Jest's rules.
352
+
353
+ ```ts
354
+ /**
355
+ * A stand-in for a value inside `toEqual`, `toMatchObject`, `toContain`, and
356
+ * `toHaveProperty`: `expect.any(Number)` in place of a number,
357
+ * `expect.objectContaining({ id: 1 })` in place of a record. The structural
358
+ * matchers ask it whether a value matches instead of comparing.
359
+ */
360
+ export interface AsymmetricMatcher {
361
+ /** Whether `other` satisfies the matcher. */
362
+ asymmetricMatch(other: unknown): boolean;
363
+ /** The matcher's name, `ObjectContaining`; a failure message adds the sample. */
364
+ toString(): string;
365
+ }
366
+ ```
367
+
368
+ | Matcher | Matches |
369
+ | --- | --- |
370
+ | `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 |
371
+ | `expect.anything()` | Anything but `null` and `undefined` |
372
+ | `expect.objectContaining(sample)` | An object whose properties include every property of `sample`, each compared by `toEqual` rules |
373
+ | `expect.arrayContaining(sample)` | An array holding every element of `sample`, in any order, each compared by `toEqual` rules |
374
+ | `expect.stringContaining(text)` | A string with `text` as a substring |
375
+ | `expect.stringMatching(text \| RegExp)` | A string the RegExp tests true on; a text sample is compiled to a RegExp, so `'a.b'` matches `'axb'` |
376
+
377
+ ```ts
378
+ expect(created).toEqual({
379
+ id: expect.stringMatching(/^usr_/),
380
+ createdAt: expect.any(String),
381
+ roles: expect.arrayContaining(['member']),
382
+ profile: expect.objectContaining({ name: 'Ada' }),
383
+ });
384
+ expect(rows).toContain(expect.objectContaining({ status: 'paid' }));
385
+ expect(response).toHaveProperty('body.total', expect.any(Number));
386
+ ```
387
+
388
+ In a failure message a matcher prints as its description: `Any<Number>`,
389
+ `ObjectContaining {"name":"Ada"}`. Matchers nest: an `objectContaining`
390
+ sample may hold an `expect.any`. The string form of `toContain` takes a
391
+ string only, so `expect('abc').toContain(expect.stringContaining('a'))` is
392
+ refused.
393
+
394
+ ## Soft assertions
395
+
396
+ `expect.soft(actual)` takes what `expect(actual)` takes and returns the same
397
+ matchers. A soft matcher that fails does not throw: the failure is kept on the
398
+ running attempt and the body continues. Once the body has settled, and before
399
+ `afterEach`, the attempt fails with one `ASSERTION_FAILED` that lists every
400
+ kept failure in order. The error's source line is the first soft matcher that
401
+ failed.
402
+
403
+ ```ts
404
+ test('checkout summary', async ({ agent, screen }) => {
405
+ await agent.act('add the "Pro" plan to the cart and open the summary');
406
+ expect.soft(await screen.getByTestId('subtotal').textContent()).toBe('$40.00');
407
+ expect.soft(await screen.getByTestId('tax').textContent()).toBe('$8.00');
408
+ await expect.soft(screen.getByRole('button', 'Pay')).toBeEnabled();
409
+ await expect(screen.getByTestId('total')).toHaveText('$48.00');
410
+ });
411
+ ```
412
+
413
+ ```text
414
+ ASSERTION_FAILED: 2 soft assertions failed
415
+ 1. expected "$40.00" to be "$41.00"
416
+ 2. expect.toBeEnabled failed
417
+ locator: getByRole('button', 'Pay')
418
+ expected: enabled
419
+ observed: states: disabled (match count 1)
420
+ ```
421
+
422
+ A soft locator or `browser` matcher is still a recorded `expect.<matcher>` step,
423
+ marked failed, so the report shows where in the flow it landed. A soft value
424
+ matcher is not a step, the same as a hard one; its failure appears in the
425
+ final error only.
426
+
427
+ When the body throws on its own, or times out, that error stays the
428
+ attempt's verdict and the soft failures are recorded beside it under
429
+ `secondaryErrors`. A body that skips itself with `test.skip(...)` drops them.
430
+
431
+ A soft `toMatchSchema` returns the schema's output when the value matches
432
+ and `undefined` after a kept failure, and its type says so.
433
+
434
+ Outside a test body there is nothing to keep a failure for, so `expect.soft`
435
+ throws at once like `expect`: in a standalone script, and in an `afterEach`
436
+ hook, which runs after the soft failures have been settled. There is no
437
+ `expect.soft.poll`; `expect.poll` has its own timeout to fail on.
438
+
439
+ ## Polling a value
440
+
441
+ `expect.poll(read)` re-reads a value until a matcher passes. Use it to wait for
442
+ an API response or saved data after an app action.
443
+
444
+ ```ts
445
+ export interface PollOptions {
446
+ /** Deadline in milliseconds. Default 5000. */
447
+ timeout?: number;
448
+ /** Pause between reads in milliseconds. Default 100. */
449
+ interval?: number;
450
+ /** Extra line in the timeout error, after the matcher line. */
451
+ message?: string;
452
+ }
453
+
454
+ /**
455
+ * The asynchronous form of every `ValueExpectation<T>` matcher: the same
456
+ * names and parameters, each resolving once the re-read value passes;
457
+ * `toMatchSchema` resolves to the schema's output for the passing read.
458
+ * Derived from `ValueExpectation<T>` so the two cannot drift.
459
+ */
460
+ export type PollExpectation<T> = {
461
+ readonly not: NegatedPollExpectation<T>;
462
+ } & {
463
+ readonly [K in Exclude<keyof ValueExpectation<T>, 'not' | 'toMatchSchema'>]: (
464
+ ...args: Parameters<ValueExpectation<T>[K]>
465
+ ) => Promise<void>;
466
+ } & {
467
+ toMatchSchema<Schema extends StandardSchemaV1>(schema: Schema): Promise<StandardSchemaV1.InferOutput<Schema>>;
468
+ };
469
+
470
+ /** `expect.poll(read).not`: every poll matcher inverted, none resolving to a value. */
471
+ export type NegatedPollExpectation<T> = {
472
+ readonly not: PollExpectation<T>;
473
+ } & {
474
+ readonly [K in Exclude<keyof ValueExpectation<T>, 'not'>]: (...args: Parameters<ValueExpectation<T>[K]>) => Promise<void>;
475
+ };
476
+ ```
477
+
478
+ Every value matcher is available, returning a promise. Each sample calls
479
+ `read` and runs the matcher. A matcher failure or a `read` that throws is one
480
+ failing sample; a `read` that hangs is cut at the deadline.
481
+
482
+ The timeout defaults to `config.assertionTimeout` during an attempt and
483
+ 5000 ms outside one. It is capped by the test deadline in the body, or the
484
+ hook's `cleanupTimeout` during `afterEach`.
485
+
486
+ When the attempt is cancelled or times out, the loop and any read in flight
487
+ stop with `CANCELLED`. `timeout` must be a finite number 0 or more and
488
+ `interval` a finite number above 0; anything else is `INVALID_CONFIG` before
489
+ the first read.
490
+
491
+ At the deadline it throws `ASSERTION_FAILED` naming the matcher, then
492
+ `message` when given, then the last sample: the last matcher failure, the
493
+ last `read` error, or `no read completed`.
494
+
495
+ ```text
496
+ expect.poll(...).toBe(...) timed out after 5000 ms
497
+ the batch never finished
498
+ last: expected "running" to be "done"
499
+ ```
500
+
501
+ ```ts
502
+ await agent.act('create a test named "AI checkout regression" described as {description}', {
503
+ params: { description },
504
+ });
505
+ await expect
506
+ .poll(() => getTest(workspace).then((row) => row?.title), { timeout: 15_000 })
507
+ .toBe('AI checkout regression');
508
+ await expect.poll(async () => (await listTests(workspace)).length).toBeGreaterThan(1);
509
+ await expect.poll(() => readStatus(), { message: 'the batch never finished' }).not.toBe('running');
510
+ ```
511
+
512
+ `expect.poll` is not recorded as a report step; a timeout surfaces only as
513
+ the test's failure.