From a74101d98f6f5632b47285730b9425dedeb02770 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 28 Jul 2023 14:04:01 -0700 Subject: [PATCH] docs: document expect's asymmetric matchers (#24498) References #24460, #24417. --- docs/src/api/class-genericassertions.md | 196 +++++++++++++++++++++++ docs/src/test-assertions-js.md | 37 ++++- packages/playwright-test/types/test.d.ts | 176 +++++++++++++++++++- utils/generate_types/index.js | 10 ++ utils/generate_types/overrides-test.d.ts | 2 +- 5 files changed, 418 insertions(+), 3 deletions(-) diff --git a/docs/src/api/class-genericassertions.md b/docs/src/api/class-genericassertions.md index 0fe46efd31..e9b1d0a71a 100644 --- a/docs/src/api/class-genericassertions.md +++ b/docs/src/api/class-genericassertions.md @@ -341,6 +341,36 @@ const value = { prop: 1 }; expect(value).toEqual({ prop: 1 }); ``` +**Non-strict equality** + +[`method: GenericAssertions.toEqual`] performs deep equality check that compares contents of the received and expected values. To ensure two objects reference the same instance, use [`method: GenericAssertions.toBe`] instead. + +[`method: GenericAssertions.toEqual`] ignores `undefined` properties and array items, and does not insist on object types being equal. For stricter matching, use [`method: GenericAssertions.toStrictEqual`]. + +**Pattern matching** + +[`method: GenericAssertions.toEqual`] can be also used to perform pattern matching on objects, arrays and primitive types, with the help of the following matchers: + +* [`method: GenericAssertions.any`] +* [`method: GenericAssertions.anything`] +* [`method: GenericAssertions.arrayContaining`] +* [`method: GenericAssertions.closeTo`] +* [`method: GenericAssertions.objectContaining`] +* [`method: GenericAssertions.stringContaining`] +* [`method: GenericAssertions.stringMatching`] + +Here is an example that asserts some of the values inside a complex object: +```js +expect({ + list: [1, 2, 3], + obj: { prop: 'Hello world!', another: 'some other value' }, + extra: 'extra', +}).toEqual(expect.objectContaining({ + list: expect.arrayContaining([2, 3]), + obj: expect.objectContaining({ prop: expect.stringContaining('Hello') }), +})); +``` + ### param: GenericAssertions.toEqual.expected * since: v1.9 - `expected` <[any]> @@ -532,3 +562,169 @@ expect(() => { - `expected` ?<[any]> Expected error message or error object. + + +## method: GenericAssertions.any +* since: v1.9 + +`expect.any()` matches any object instance created from the [`param: constructor`] or a corresponding primitive type. Use it inside [`method: GenericAssertions.toEqual`] to perform pattern matching. + +**Usage** + +```js +// Match instance of a class. +class Example {} +expect(new Example()).toEqual(expect.any(Example)); + +// Match any number. +expect({ prop: 1 }).toEqual({ prop: expect.any(Number) }); + +// Match any string. +expect('abc').toEqual(expect.any(String)); +``` + +### param: GenericAssertions.any.constructor +* since: v1.9 +- `constructor` <[Function]> + +Constructor of the expected object like `ExampleClass`, or a primitive boxed type like `Number`. + + +## method: GenericAssertions.anything +* since: v1.9 + +`expect.anything()` matches everything except `null` and `undefined`. Use it inside [`method: GenericAssertions.toEqual`] to perform pattern matching. + +**Usage** + +```js +const value = { prop: 1 }; +expect(value).toEqual({ prop: expect.anything() }); +expect(value).not.toEqual({ otherProp: expect.anything() }); +``` + + +## method: GenericAssertions.arrayContaining +* since: v1.9 + +`expect.arrayContaining()` matches an array that contains all of the elements in the expected array, in any order. Note that received array may be a superset of the expected array and contain some extra elements. + +Use this method inside [`method: GenericAssertions.toEqual`] to perform pattern matching. + +**Usage** + +```js +expect([1, 2, 3]).toEqual(expect.arrayContaining([3, 1])); +expect([1, 2, 3]).not.toEqual(expect.arrayContaining([1, 4])); +``` + +### param: GenericAssertions.arrayContaining.expected +* since: v1.9 +- `expected` <[Array]<[any]>> + +Expected array that is a subset of the received value. + + + +## method: GenericAssertions.closeTo +* since: v1.9 + +Compares floating point numbers for approximate equality. Use this method inside [`method: GenericAssertions.toEqual`] to perform pattern matching. When just comparing two numbers, prefer [`method: GenericAssertions.toBeCloseTo`]. + +**Usage** + +```js +expect({ prop: 0.1 + 0.2 }).not.toEqual({ prop: 0.3 }); +expect({ prop: 0.1 + 0.2 }).toEqual({ prop: expect.closeTo(0.3, 5) }); +``` + +### param: GenericAssertions.closeTo.expected +* since: v1.9 +- `expected` <[float]> + +Expected value. + +### param: GenericAssertions.closeTo.numDigits +* since: v1.9 +- `numDigits` ?<[int]> + +The number of decimal digits after the decimal point that must be equal. + + +## method: GenericAssertions.objectContaining +* since: v1.9 + +`expect.objectContaining()` matches an object that contains and matches all of the properties in the expected object. Note that received object may be a superset of the expected object and contain some extra properties. + +Use this method inside [`method: GenericAssertions.toEqual`] to perform pattern matching. Object properties can be matchers to further relax the expectation. See examples. + +**Usage** + +```js +// Assert some of the properties. +expect({ foo: 1, bar: 2 }).toEqual(expect.objectContaining({ foo: 1 })); + +// Matchers can be used on the properties as well. +expect({ foo: 1, bar: 2 }).toEqual(expect.objectContaining({ bar: expect.any(Number) })); + +// Complex matching of sub-properties. +expect({ + list: [1, 2, 3], + obj: { prop: 'Hello world!', another: 'some other value' }, + extra: 'extra', +}).toEqual(expect.objectContaining({ + list: expect.arrayContaining([2, 3]), + obj: expect.objectContaining({ prop: expect.stringContaining('Hello') }), +})); +``` + +### param: GenericAssertions.objectContaining.expected +* since: v1.9 +- `expected` <[Object]> + +Expected object pattern that contains a subset of the properties. + + +## method: GenericAssertions.stringContaining +* since: v1.9 + +`expect.stringContaining()` matches a string that contains the expected substring. Use this method inside [`method: GenericAssertions.toEqual`] to perform pattern matching. + +**Usage** + +```js +expect('Hello world!').toEqual(expect.stringContaining('Hello')); +``` + +### param: GenericAssertions.stringContaining.expected +* since: v1.9 +- `expected` <[string]> + +Expected substring. + + +## method: GenericAssertions.stringMatching +* since: v1.9 + +`expect.stringMatching()` matches a received string that in turn matches the expected pattern. Use this method inside [`method: GenericAssertions.toEqual`] to perform pattern matching. + +**Usage** + +```js +expect('123ms').toEqual(expect.stringMatching(/\d+m?s/)); + +// Inside another matcher. +expect({ + status: 'passed', + time: '123ms', +}).toEqual({ + status: expect.stringMatching(/passed|failed/), + time: expect.stringMatching(/\d+m?s/), +}); +``` + +### param: GenericAssertions.stringMatching.expected +* since: v1.9 +- `expected` <[string]|[RegExp]> + +Pattern that expected string should match. diff --git a/docs/src/test-assertions-js.md b/docs/src/test-assertions-js.md index 2d50bffa48..d201973b31 100644 --- a/docs/src/test-assertions-js.md +++ b/docs/src/test-assertions-js.md @@ -20,7 +20,7 @@ Playwright will be re-testing the element with the test id of `status` until the By default, the timeout for assertions is set to 5 seconds. Learn more about [various timeouts](./test-timeouts.md). -## List of assertions +## Popular assertions | Assertion | Description | | :- | :- | @@ -50,6 +50,41 @@ By default, the timeout for assertions is set to 5 seconds. Learn more about [va | [`method: PageAssertions.toHaveURL`] | Page has a URL | | [`method: APIResponseAssertions.toBeOK`] | Response has an OK status | +## Generic assertions + +| Assertion | Description | +| :- | :- | +| [`method: GenericAssertions.toBe`] | Value is the same | +| [`method: GenericAssertions.toBeCloseTo`] | Number is approximately equal | +| [`method: GenericAssertions.toBeDefined`] | Value is not `undefined` | +| [`method: GenericAssertions.toBeFalsy`] | Value is falsy, e.g. `false`, `0`, `null`, etc. | +| [`method: GenericAssertions.toBeGreaterThan`] | Number is more than | +| [`method: GenericAssertions.toBeGreaterThanOrEqual`] | Number is more than or equal | +| [`method: GenericAssertions.toBeInstanceOf`] | Object is an instance of a class | +| [`method: GenericAssertions.toBeLessThan`] | Number is less than | +| [`method: GenericAssertions.toBeLessThanOrEqual`] | Number is less than or equal | +| [`method: GenericAssertions.toBeNaN`] | Value is `NaN` | +| [`method: GenericAssertions.toBeNull`] | Value is `null` | +| [`method: GenericAssertions.toBeTruthy`] | Value is truthy, i.e. not `false`, `0`, `null`, etc. | +| [`method: GenericAssertions.toBeUndefined`] | Value is `undefined` | +| [`method: GenericAssertions.toContain#1`] | String contains a substring | +| [`method: GenericAssertions.toContain#2`] | Array or set contains an element | +| [`method: GenericAssertions.toContainEqual`] | Array or set contains a similar element | +| [`method: GenericAssertions.toEqual`] | Value is similar - deep equality and pattern matching | +| [`method: GenericAssertions.toHaveLength`] | Array or string has length | +| [`method: GenericAssertions.toHaveProperty`] | Object has a property | +| [`method: GenericAssertions.toMatch`] | String matches a regular expression | +| [`method: GenericAssertions.toMatchObject`] | Object contains specified properties | +| [`method: GenericAssertions.toStrictEqual`] | Value is similar, including property types | +| [`method: GenericAssertions.toThrow`] | Function throws an error | +| [`method: GenericAssertions.any`] | Matches any instance of a class/primitive | +| [`method: GenericAssertions.anything`] | Matches antyhing | +| [`method: GenericAssertions.arrayContaining`] | Array contains specific elements | +| [`method: GenericAssertions.closeTo`] | Number is approximately equal | +| [`method: GenericAssertions.objectContaining`] | Object contains specific properties | +| [`method: GenericAssertions.stringContaining`] | String contains a substring | +| [`method: GenericAssertions.stringMatching`] | String matches a regular expression | + ## Negating Matchers In general, we can expect the opposite to be true by adding a `.not` to the front diff --git a/packages/playwright-test/types/test.d.ts b/packages/playwright-test/types/test.d.ts index 7eb840b2d3..caa14b73e5 100644 --- a/packages/playwright-test/types/test.d.ts +++ b/packages/playwright-test/types/test.d.ts @@ -4334,13 +4334,148 @@ export type PlaywrightTestConfig = Config; -type AsymmetricMatchers = { +interface AsymmetricMatchers { + /** + * `expect.any()` matches any object instance created from the `constructor` or a corresponding primitive type. Use it + * inside + * [genericAssertions.toEqual(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-to-equal) + * to perform pattern matching. + * + * **Usage** + * + * ```js + * // Match instance of a class. + * class Example {} + * expect(new Example()).toEqual(expect.any(Example)); + * + * // Match any number. + * expect({ prop: 1 }).toEqual({ prop: expect.any(Number) }); + * + * // Match any string. + * expect('abc').toEqual(expect.any(String)); + * ``` + * + * @param constructor Constructor of the expected object like `ExampleClass`, or a primitive boxed type like `Number`. + */ any(sample: unknown): AsymmetricMatcher; + /** + * `expect.anything()` matches everything except `null` and `undefined`. Use it inside + * [genericAssertions.toEqual(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-to-equal) + * to perform pattern matching. + * + * **Usage** + * + * ```js + * const value = { prop: 1 }; + * expect(value).toEqual({ prop: expect.anything() }); + * expect(value).not.toEqual({ otherProp: expect.anything() }); + * ``` + * + */ anything(): AsymmetricMatcher; + /** + * `expect.arrayContaining()` matches an array that contains all of the elements in the expected array, in any order. + * Note that received array may be a superset of the expected array and contain some extra elements. + * + * Use this method inside + * [genericAssertions.toEqual(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-to-equal) + * to perform pattern matching. + * + * **Usage** + * + * ```js + * expect([1, 2, 3]).toEqual(expect.arrayContaining([3, 1])); + * expect([1, 2, 3]).not.toEqual(expect.arrayContaining([1, 4])); + * ``` + * + * @param expected Expected array that is a subset of the received value. + */ arrayContaining(sample: Array): AsymmetricMatcher; + /** + * Compares floating point numbers for approximate equality. Use this method inside + * [genericAssertions.toEqual(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-to-equal) + * to perform pattern matching. When just comparing two numbers, prefer + * [genericAssertions.toBeCloseTo(expected[, numDigits])](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-to-be-close-to). + * + * **Usage** + * + * ```js + * expect({ prop: 0.1 + 0.2 }).not.toEqual({ prop: 0.3 }); + * expect({ prop: 0.1 + 0.2 }).toEqual({ prop: expect.closeTo(0.3, 5) }); + * ``` + * + * @param expected Expected value. + * @param numDigits The number of decimal digits after the decimal point that must be equal. + */ closeTo(sample: number, precision?: number): AsymmetricMatcher; + /** + * `expect.objectContaining()` matches an object that contains and matches all of the properties in the expected + * object. Note that received object may be a superset of the expected object and contain some extra properties. + * + * Use this method inside + * [genericAssertions.toEqual(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-to-equal) + * to perform pattern matching. Object properties can be matchers to further relax the expectation. See examples. + * + * **Usage** + * + * ```js + * // Assert some of the properties. + * expect({ foo: 1, bar: 2 }).toEqual(expect.objectContaining({ foo: 1 })); + * + * // Matchers can be used on the properties as well. + * expect({ foo: 1, bar: 2 }).toEqual(expect.objectContaining({ bar: expect.any(Number) })); + * + * // Complex matching of sub-properties. + * expect({ + * list: [1, 2, 3], + * obj: { prop: 'Hello world!', another: 'some other value' }, + * extra: 'extra', + * }).toEqual(expect.objectContaining({ + * list: expect.arrayContaining([2, 3]), + * obj: expect.objectContaining({ prop: expect.stringContaining('Hello') }), + * })); + * ``` + * + * @param expected Expected object pattern that contains a subset of the properties. + */ objectContaining(sample: Record): AsymmetricMatcher; + /** + * `expect.stringContaining()` matches a string that contains the expected substring. Use this method inside + * [genericAssertions.toEqual(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-to-equal) + * to perform pattern matching. + * + * **Usage** + * + * ```js + * expect('Hello world!').toEqual(expect.stringContaining('Hello')); + * ``` + * + * @param expected Expected substring. + */ stringContaining(sample: string): AsymmetricMatcher; + /** + * `expect.stringMatching()` matches a received string that in turn matches the expected pattern. Use this method + * inside + * [genericAssertions.toEqual(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-to-equal) + * to perform pattern matching. + * + * **Usage** + * + * ```js + * expect('123ms').toEqual(expect.stringMatching(/\d+m?s/)); + * + * // Inside another matcher. + * expect({ + * status: 'passed', + * time: '123ms', + * }).toEqual({ + * status: expect.stringMatching(/passed|failed/), + * time: expect.stringMatching(/\d+m?s/), + * }); + * ``` + * + * @param expected Pattern that expected string should match. + */ stringMatching(sample: string | RegExp): AsymmetricMatcher; } @@ -4616,6 +4751,45 @@ interface GenericAssertions { * expect(value).toEqual({ prop: 1 }); * ``` * + * **Non-strict equality** + * + * [genericAssertions.toEqual(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-to-equal) + * performs deep equality check that compares contents of the received and expected values. To ensure two objects + * reference the same instance, use + * [genericAssertions.toBe(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-to-be) + * instead. + * + * [genericAssertions.toEqual(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-to-equal) + * ignores `undefined` properties and array items, and does not insist on object types being equal. For stricter + * matching, use + * [genericAssertions.toStrictEqual(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-to-strict-equal). + * + * **Pattern matching** + * + * [genericAssertions.toEqual(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-to-equal) + * can be also used to perform pattern matching on objects, arrays and primitive types, with the help of the following + * matchers: + * - [genericAssertions.any(constructor)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-any) + * - [genericAssertions.anything()](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-anything) + * - [genericAssertions.arrayContaining(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-array-containing) + * - [genericAssertions.closeTo(expected[, numDigits])](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-close-to) + * - [genericAssertions.objectContaining(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-object-containing) + * - [genericAssertions.stringContaining(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-string-containing) + * - [genericAssertions.stringMatching(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-string-matching) + * + * Here is an example that asserts some of the values inside a complex object: + * + * ```js + * expect({ + * list: [1, 2, 3], + * obj: { prop: 'Hello world!', another: 'some other value' }, + * extra: 'extra', + * }).toEqual(expect.objectContaining({ + * list: expect.arrayContaining([2, 3]), + * obj: expect.objectContaining({ prop: expect.stringContaining('Hello') }), + * })); + * ``` + * * @param expected Expected value. */ toEqual(expected: unknown): R; diff --git a/utils/generate_types/index.js b/utils/generate_types/index.js index cf68dfd32f..2925174000 100644 --- a/utils/generate_types/index.js +++ b/utils/generate_types/index.js @@ -95,6 +95,8 @@ class TypesGenerator { const handledClasses = new Set(); let overrides = await parseOverrides(overridesFile, className => { + if (className === 'AsymmetricMatchers') + return ''; const docClass = this.docClassForName(className); if (!docClass) return ''; @@ -569,6 +571,13 @@ class TypesGenerator { 'TestOptions', 'TestConfig.use', 'TestProject.use', + 'GenericAssertions.any', + 'GenericAssertions.anything', + 'GenericAssertions.arrayContaining', + 'GenericAssertions.closeTo', + 'GenericAssertions.objectContaining', + 'GenericAssertions.stringContaining', + 'GenericAssertions.stringMatching', ]), overridesToDocsClassMapping: new Map([ ['TestType', 'Test'], @@ -580,6 +589,7 @@ class TypesGenerator { ['PlaywrightTestOptions', 'TestOptions'], ['PlaywrightWorkerArgs', 'Fixtures'], ['PlaywrightTestArgs', 'Fixtures'], + ['AsymmetricMatchers', 'GenericAssertions'], ]), ignoreMissing: new Set([ 'FullConfig.configFile', diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 28d0a8ff88..014453e0ae 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -280,7 +280,7 @@ export type PlaywrightTestConfig = Config; -type AsymmetricMatchers = { +interface AsymmetricMatchers { any(sample: unknown): AsymmetricMatcher; anything(): AsymmetricMatcher; arrayContaining(sample: Array): AsymmetricMatcher;