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.
- package/README.md +51 -24
- package/dist/.build.json +1 -1
- package/dist/agent/act.js +3 -3
- package/dist/agent/act.js.map +1 -1
- package/dist/agent/error.d.ts +16 -3
- package/dist/agent/error.d.ts.map +1 -1
- package/dist/agent/error.js +25 -6
- package/dist/agent/error.js.map +1 -1
- package/dist/agent/model/tool-choice.d.ts +24 -1
- package/dist/agent/model/tool-choice.d.ts.map +1 -1
- package/dist/agent/model/tool-choice.js +27 -1
- package/dist/agent/model/tool-choice.js.map +1 -1
- package/dist/agent/step-cache.d.ts +14 -3
- package/dist/agent/step-cache.d.ts.map +1 -1
- package/dist/agent/step-cache.js +13 -3
- package/dist/agent/step-cache.js.map +1 -1
- package/dist/agent/tool-loop.d.ts.map +1 -1
- package/dist/agent/tool-loop.js +62 -9
- package/dist/agent/tool-loop.js.map +1 -1
- package/dist/cache/context.d.ts +19 -6
- package/dist/cache/context.d.ts.map +1 -1
- package/dist/cache/context.js +15 -21
- package/dist/cache/context.js.map +1 -1
- package/dist/cli/init/scaffold.d.ts +1 -0
- package/dist/cli/init/scaffold.d.ts.map +1 -1
- package/dist/cli/init/scaffold.js +2 -1
- package/dist/cli/init/scaffold.js.map +1 -1
- package/dist/cli/init/sibling-versions.json +1 -1
- package/dist/expect/values.js +1 -1
- package/dist/expect/values.js.map +1 -1
- package/dist/run/execute.d.ts.map +1 -1
- package/dist/run/execute.js +21 -8
- package/dist/run/execute.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/docs/agent-steps.mdx +126 -0
- package/docs/agents.mdx +217 -0
- package/docs/api-testing.mdx +65 -0
- package/docs/authentication.mdx +345 -0
- package/docs/browser.mdx +440 -0
- package/docs/bug-bash.mdx +90 -0
- package/docs/cache.mdx +306 -0
- package/docs/ci.mdx +210 -0
- package/docs/coding-agents.mdx +183 -0
- package/docs/debugging.mdx +291 -0
- package/docs/executors.mdx +243 -0
- package/docs/explore.mdx +174 -0
- package/docs/github.mdx +184 -0
- package/docs/index.mdx +82 -0
- package/docs/integrations/eas.mdx +216 -0
- package/docs/integrations/index.mdx +24 -0
- package/docs/integrations/kernel.mdx +163 -0
- package/docs/migrate/cypress.mdx +124 -0
- package/docs/migrate/detox.mdx +140 -0
- package/docs/migrate/maestro.mdx +229 -0
- package/docs/migrate/playwright.mdx +427 -0
- package/docs/migrate/selenium.mdx +110 -0
- package/docs/mobile-ci.mdx +567 -0
- package/docs/mobile.mdx +555 -0
- package/docs/models.mdx +241 -0
- package/docs/quickstart.mdx +260 -0
- package/docs/reference/agent.mdx +524 -0
- package/docs/reference/app.mdx +72 -0
- package/docs/reference/cli.mdx +817 -0
- package/docs/reference/config.mdx +986 -0
- package/docs/reference/engine.mdx +1362 -0
- package/docs/reference/environment.mdx +113 -0
- package/docs/reference/errors.mdx +143 -0
- package/docs/reference/expect.mdx +513 -0
- package/docs/reference/mcp.mdx +202 -0
- package/docs/reference/mobile.mdx +522 -0
- package/docs/reference/reporters.mdx +216 -0
- package/docs/reference/screen.mdx +688 -0
- package/docs/reference/test.mdx +449 -0
- package/docs/reference/web.mdx +611 -0
- package/docs/security.mdx +292 -0
- package/docs/starting-your-app.mdx +231 -0
- package/docs/subscriptions.mdx +157 -0
- package/docs/telemetry.mdx +185 -0
- package/docs/tools.mdx +150 -0
- package/docs/viewports.mdx +146 -0
- package/docs/writing-an-engine.mdx +316 -0
- package/docs/writing-tests.mdx +256 -0
- package/package.json +2 -1
- package/skills/e2e/SKILL.md +4 -1
- package/skills/e2e/references/agent.md +3 -1
- 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.
|