diff --git a/.npmignore b/.npmignore index e4d6a736f..3e5c84af7 100644 --- a/.npmignore +++ b/.npmignore @@ -3,6 +3,7 @@ !lib/**/*.js !types/**/*.d.ts !jasmine.d.ts +!jasmine-wdio-expect-async.d.ts !jest.d.ts !LICENSE !package.json diff --git a/README.md b/README.md index d686b272d..d9ee97d35 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # expect-webdriverio [![Test](https://github.com/webdriverio/expect-webdriverio/actions/workflows/test.yml/badge.svg)](https://github.com/webdriverio/expect-webdriverio/actions/workflows/test.yml) -###### [API](docs/API.md) | [TypeScript / JS Autocomplete](/docs/Types.md) | [Examples](docs/Examples.md) | [Extending Matchers](/docs/Extend.md) +###### [API](docs/API.md) | [TypeScript / JS Autocomplete](docs/Types.md) | [Examples](docs/Examples.md) | [Extending Matchers](docs/CustomMatchers.md) > [WebdriverIO](https://webdriver.io/) Assertion library inspired by [expect](https://www.npmjs.com/package/expect) diff --git a/docs/API.md b/docs/API.md index 1df796e15..76309a988 100644 --- a/docs/API.md +++ b/docs/API.md @@ -111,6 +111,11 @@ When set to `true` (default), the service will automatically assert all soft ass This is useful if you want full control over when soft assertions are verified or if you want to handle soft assertion failures in a custom way. +### Known limitations + +For Jasmine, using `wdio-jasmine-framework` will give a better plug-and-play experiences, else without it, the soft assertion service and custom matchers might not work/be registered correctly. +Moreover, if Jasmine augmentation is used, the soft assertion function are not exposed in the typing, but could still work depending of your configuration. See [this issue](https://github.com/webdriverio/expect-webdriverio/issues/1893) for more details. + ## Default Options These default options below are connected to the [`waitforTimeout`](https://webdriver.io/docs/options#waitfortimeout) and [`waitforInterval`](https://webdriver.io/docs/options#waitforinterval) options set in the config. @@ -667,7 +672,7 @@ await expect(mock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at Checks that mock was called according to the expected options. -Most of the options supports expect/jasmine partial matchers like [expect.objectContaining](https://jestjs.io/docs/en/expect#expectobjectcontainingobject) +Most of the options supports expect/jasmine partial matchers like [expect.objectContaining](https://jestjs.io/docs/expect#expectobjectcontainingobject) ##### Usage @@ -858,7 +863,7 @@ await expect(elem).toHaveElementClass(/Container/i) ## Default Matchers -In addition to the `expect-webdriverio` matchers you can use builtin Jest's [expect](https://jestjs.io/docs/en/expect) assertions or [expect/expectAsync](https://jasmine.github.io/api/3.5/global.html#expect) for Jasmine. +In addition to the `expect-webdriverio` matchers you can use builtin Jest's [expect](https://jestjs.io/docs/expect) assertions or [expect/expectAsync](https://jasmine.github.io/api/edge/global.html#expect) for Jasmine. ## Asymmetric Matchers diff --git a/docs/Extend.md b/docs/CustomMatchers.md similarity index 78% rename from docs/Extend.md rename to docs/CustomMatchers.md index 6975ed5a5..8fa9b00ef 100644 --- a/docs/Extend.md +++ b/docs/CustomMatchers.md @@ -2,8 +2,8 @@ Similar to how `expect-webdriverio` extends Jasmine/Jest matchers it's possible to add custom matchers. -- Jasmine see [custom matchers](https://jasmine.github.io/2.5/custom_matcher.html) doc -- Everyone else see Jest's [expect.extend](https://jestjs.io/docs/en/expect#expectextendmatchers) +- [Jasmine](https://jasmine.github.io/) see [custom matchers](https://jasmine.github.io/tutorials/custom_matchers) doc +- Everyone else see [Jest's expect.extend](https://jestjs.io/docs/expect#expectextendmatchers) Custom matchers should be added in wdio `before` hook diff --git a/docs/Examples.md b/docs/Examples.md index a5afc7d0b..0febe90d8 100644 --- a/docs/Examples.md +++ b/docs/Examples.md @@ -50,7 +50,7 @@ describe('suite', () => { WebdriverIO test runner - Mocha https://github.com/mgrybyk/webdriverio-devtools - Cucumber https://gitlab.com/bar_foo/wdio-cucumber-typescript -- Jasmine https://github.com/mgrybyk/wdio-jasmine-boilerplate +- Jasmine https://github.com/webdriverio/jasmine-boilerplate Standalone - Jest https://github.com/erwinheitzman/jest-webdriverio-standalone-boilerplate diff --git a/docs/Framework.md b/docs/Framework.md new file mode 100644 index 000000000..17f075826 --- /dev/null +++ b/docs/Framework.md @@ -0,0 +1,252 @@ +# Expect-WebDriverIO Framework + +Expect-WebDriverIO is inspired by [`expect`](https://www.npmjs.com/package/expect) but also extends it. Therefore, we can use everything provided by the expect API with some WebDriverIO enhancements. Yes, this is a package of Jest but it is usable without Jest. + +## Compatibility + +We can pair `expect-webdriverio` with [Jest](https://jestjs.io/), [Mocha](https://mochajs.org/), and [Jasmine](https://jasmine.github.io/) and even [Cucumber](https://www.npmjs.com/package/@cucumber/cucumber) + +It is highly recommended to use it with a [WDIO Testrunner](https://webdriver.io/docs/clioptions) which provides additional auto-configuration for a plug-and-play experience. + +When used **outside of [WDIO Testrunner](https://webdriver.io/docs/clioptions)**, types need to be added to your [`tsconfig.json`](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html), and some additional configuration for WDIO matchers, soft assertions, and snapshot service is required. + +### Jest +We can use `expect-webdriverio` with [Jest](https://jestjs.io/) using [`@jest/globals`](https://www.npmjs.com/package/@jest/globals) alone (preferred) and optionally [`@types/jest`](https://www.npmjs.com/package/@types/jest) (which has global ambient support). + - Note: Jest maintainers do not support [`@types/jest`](https://www.npmjs.com/package/@types/jest). If this library gets out of date or has problems, support might be dropped. + - Note: With Jest, the matchers `toMatchSnapshot` and `toMatchInlineSnapshot` are overloaded. To resolve the types correctly, `expect-webdriverio/jest` must be last. + +#### With `@jest/globals` +When paired only with [`@jest/globals`](https://www.npmjs.com/package/@jest/globals), we should `import` the `expect` function from `expect-webdriverio`. + +```ts +import { expect } from 'expect-webdriverio' +import { describe, it, expect as jestExpect } from '@jest/globals' + +describe('My tests', async () => { + it('should verify my browser to have the expected url', async () => { + await expect(browser).toHaveUrl('https://example.com') + }) +}) +``` + +No `types` are expected in `tsconfig.json`. +Optionally, to avoid needing `import { expect } from 'expect-webdriverio'`, you can use the following: + + +```json +{ + "compilerOptions": { + "types": ["expect-webdriverio/expect-global"] + } +} +``` +##### Augmenting `@jest/globals` JestMatchers +Multiple attempts were made to augment `@jest/globals` to support `expect-webdriverio` matchers directly on JestMatchers. However, no namespace is available to augment it; therefore, only module augmentation can be used. This method does not allow adding matchers with the `extends` keyword; instead, they need to be added directly in the interface of the module declaration augmentation, which would create a lot of code duplication. + +This [Jest issue](https://github.com/jestjs/jest/issues/12424) seems to target this problem, but it is still in progress. + +#### With `@types/jest` +When also paired with [`@types/jest`](https://www.npmjs.com/package/@types/jest), no imports are required. Global ambient types are already defined correctly and you can simply use Jest's `expect` directly. + +If you are NOT using WDIO Testrunner, some prerequisite configuration is required. + +Option 1: Replace the expect globally with the `expect-webdriverio` one: +```ts +import { expect } from "expect-webdriverio"; +(globalThis as any).expect = expect; +``` + +Option 2: Reconfigure Jest's expect with the custom matchers and the soft assertion: +```ts +// Configure the custom matchers: +import { expect } from "@jest/globals"; +import { matchers } from "expect-webdriverio"; + +beforeAll(async () => { + expect.extend(matchers as Record); +}); +``` + +[Optional] For the soft assertion, the `createSoftExpect` is currently not correctly exposed but the below works: +```ts +// @ts-ignore +import * as createSoftExpect from "expect-webdriverio/lib/softExpect"; + +beforeAll(async () => { + Object.defineProperty(expect, "soft", { + value: (actual: T) => createSoftExpect.default(actual), + }); + + // Add soft assertions utility methods + Object.defineProperty(expect, "getSoftFailures", { + value: (testId?: string) => SoftAssertService.getInstance().getFailures(testId), + }); + + Object.defineProperty(expect, "assertSoftFailures", { + value: (testId?: string) => SoftAssertService.getInstance().assertNoFailures(testId), + }); + + Object.defineProperty(expect, "clearSoftFailures", { + value: (testId?: string) => SoftAssertService.getInstance().clearFailures(testId), + }); +}); +``` + +Then as shown below, no imports are required and we can use WDIO matchers directly on Jest's `expect`: +```ts +describe('My tests', async () => { + it('should verify my browser to have the expected url', async () => { + await expect(browser).toHaveUrl('https://example.com') + }) +}) +``` + +Expected in `tsconfig.json`: +```json +{ + "compilerOptions": { + "types": [ + "@types/jest", + "expect-webdriverio/jest" // Must be last for overloaded matchers `toMatchSnapshot` and `toMatchInlineSnapshot` + ] + } +} +``` + +### Mocha +When paired with [Mocha](https://mochajs.org/), it can be used without (standalone) or with [`chai`](https://www.chaijs.com/) (or any other assertion library). + +#### Standalone +No import is required; everything is set globally. + +```ts +describe('My tests', async () => { + it('should verify my browser to have the expected url', async () => { + await expect(browser).toHaveUrl('https://example.com') + }) +}) +``` + +Expected in `tsconfig.json`: +```json +{ + "compilerOptions": { + "types": [ + "@types/mocha", + "expect-webdriverio/expect-global" + ] + } +} +``` + +#### Chai +`expect-webdriverio` can coexist with the [Chai](https://www.chaijs.com/) assertion library by importing both libraries explicitly. +See also this [documentation](https://webdriver.io/docs/assertion/#migrating-from-chai). + +### Jasmine +When paired with [Jasmine](https://jasmine.github.io/), [`@wdio/jasmine-framework`](https://www.npmjs.com/package/@wdio/jasmine-framework) is also required to configure it correctly, as it needs to force `expect` to be `expectAsync` and also register the WDIO matchers with `addAsyncMatcher` since `expect-webdriverio` only supports the Jest-style `expect.extend` version. + +The types `expect-webdriverio/jasmine` are still offered but are subject to removal or being moved into `@wdio/jasmine-framework`. The usage of `expectAsync` is also subject to future removal. + +#### Jasmine `expectAsync` +When not using `@wdio/globals/types` or having `@types/jasmine` before it, the Jasmine expect is shown as the global ambient type. Therefore, when also defining `expect-webdriverio/jasmine`, we can use WDIO custom matchers on the `expectAsync`. Without `@wdio/jasmine-framework`, matchers will need to be registered manually. + +```ts +describe('My tests', async () => { + it('should verify my browser to have the expected url', async () => { + await expectAsync(browser).toHaveUrl('https://example.com') + await expectAsync(true).toBe(true) + }) +}) +``` + +Expected in `tsconfig.json`: +```json +{ + "compilerOptions": { + "types": [ + "@types/jasmine", + "expect-webdriverio/jasmine" + ] + } +} +``` + +#### Global `expectAsync` force as `expect` +When the global ambiant is the `expect` of wdio but forced to be `expectAsync` under the hood, like when using `@wdio/jasmine-framework`, then even the basic matchers need to be awaited + +```ts +describe('My tests', async () => { + it('should verify my browser to have the expected url', async () => { + await expect(browser).toHaveUrl('https://example.com') + + // Even basic matchers requires expect since they are promises underneath + await expect(true).toBe(true) + }) +}) +``` + +Expected in `tsconfig.json`: +```json +{ + "compilerOptions": { + "types": [ + "@wdio/globals/types", + "@wdio/jasmine-framework", + "@types/jasmine", + "expect-webdriverio/jasmine-wdio-expect-async", // Force expect to return Promises + ] + } +} +``` + +#### `expect` of `expect-webdriverio` +It is preferable to use the `expect` from `expect-webdriverio` to guarantee future compatibility. + +```ts +// Required if we do not force the 'expect-webdriverio' expect globally with `"expect-webdriverio/expect-global"` +import { expect as wdioExpect } from 'expect-webdriverio' + +describe('My tests', async () => { + it('should verify my browser to have the expected url', async () => { + await wdioExpect(browser).toHaveUrl('https://example.com') + + // No required await + wdioExpect(true).toBe(true) + }) +}) + + +Expected in `tsconfig.json`: +```json +{ + "compilerOptions": { + "types": [ + "@types/jasmine", + // "expect-webdriverio/expect-global", // Optional to have the global ambient expect the one of wdio + ] + } +} +``` + + +#### Asymmetric matchers +Asymmetric matchers have limited support. Even though `jasmine.stringContaining` does not produce a typing error, it may not work even with `@wdio/jasmine-framework`. However, the example below should work: + +```ts +describe('My tests', async () => { + it('should verify my browser to have the expected url', async () => { + await expectAsync(browser).toHaveUrl(wdioExpect.stringContaining('WebdriverIO')) + }) +}) +``` + + +### Jest & Jasmine Augmentation Notes + +If you are already using Jest or Jasmine globally, using `import { expect } from 'expect-webdriverio'` is the most compatible approach, even though augmentation exists. +It is recommended to build your project using this approach instead of relying on augmentation, to ensure future compatibility and avoid augmentation limitations. See [this issue](https://github.com/webdriverio/expect-webdriverio/issues/1893) for more information. + +### Cucumber + +More details to come. In short, when paired with `@wdio/cucumber-framework`, you can use WDIO's expect with Cucumber and even [Gherkin](https://www.npmjs.com/package/@cucumber/gherkin). \ No newline at end of file diff --git a/docs/Types.md b/docs/Types.md index ffc12287f..de1f486ab 100644 --- a/docs/Types.md +++ b/docs/Types.md @@ -1,14 +1,17 @@ +# Types Definition ## TypeScript If you are using the [WDIO Testrunner](https://webdriver.io/docs/clioptions) everything will be automatically setup. Just follow the [setup guide](https://webdriver.io/docs/typescript#framework-setup) from the docs. However if you run WebdriverIO with a different testrunner or in a simple Node.js script you will need to add `expect-webdriverio` to `types` in the `tsconfig.json`. -- `"expect-webdriverio"` for everyone except of Jasmine/Jest users. -- `"expect-webdriverio/jasmine"` Jasmine -- `"expect-webdriverio/jest"` Jest +- `"expect-webdriverio"` for everyone except Jasmine/Jest users. +- `"expect-webdriverio/jasmine"` for [Jasmine](https://jasmine.github.io/) +- `"expect-webdriverio/jest"` for [Jest](https://jestjs.io/) +- `"expect-webdriverio/expect-global"` // Optional, if you wish to use expect of `expect-webdriverio` globally without explicit import + - Note: Same as the former `"expect-webdriverio/types"`, now deprecated! ## JavaScript (VSCode) -It's required to create `jsconfig.json` in project root and refer to the type definitions to make autocompletion work in vanilla js. +It's required to create [`jsconfig.json`](https://code.visualstudio.com/docs/languages/jsconfig) in project root and refer to the type definitions to make autocompletion work in vanilla js. ```json { @@ -19,3 +22,15 @@ It's required to create `jsconfig.json` in project root and refer to the type de ] } ``` + +## Jasmine special case +[Jasmine](https://jasmine.github.io/) is different from [Jest](https://jestjs.io/) or the standard `expect` definition since it supports promises using `expectAsync` which makes it quite challenging. + +Even though this library by itself is not fully Jasmine-ready, it offers the types of the matcher only on the `AsyncMatcher` since using `jasmine.expect` does not work out-of-the-box. However, if you are pulling on the `expect` of `expect-webdriverio`, you will be able to have the WebDriverIO matcher types on `expect`. + +Support of `expectAsync` keyword is subject to change and may be dropped in the future! + +### Dependency on `@wdio/jasmine-framework` +As mentioned above, this library alone is not working with Jasmine. It is required to manually do some tweaks, or it is strongly recommended to also pair it with `@wdio/jasmine-framework`. See [Framework.md](Framework.md) for more information. + +When using [`@wdio/jasmine-framework`](https://www.npmjs.com/package/@wdio/jasmine-framework), since it replaces `jasmine.expect` with `jasmine.expectAsync`, then matchers are usable on the keyword `expect`, but still typing on `expect` directly from [Jasmine](https://jasmine.github.io/) namespace is not supported as of today! \ No newline at end of file diff --git a/jasmine-wdio-expect-async.d.ts b/jasmine-wdio-expect-async.d.ts new file mode 100644 index 000000000..b9caa3d58 --- /dev/null +++ b/jasmine-wdio-expect-async.d.ts @@ -0,0 +1,22 @@ +/// + +/** + * Overrides the default wdio `expect` for Jasmine case specifically since the `expect` is now completely asynchronous which is not the case under Jest or standalone. + */ +declare namespace ExpectWebdriverIO { + interface Expect extends ExpectWebdriverIO.AsymmetricMatchers, ExpectLibInverse, WdioExpect { + /** + * The `expect` function is used every time you want to test a value. + * You will rarely call `expect` by itself. + * + * expect function declaration contains two generics: + * - T: the type of the actual value, e.g. any type, not just WebdriverIO.Browser or WebdriverIO.Element + * - R: the type of the return value, e.g. Promise or void + * + * Note: The function must stay here in the namespace to overwrite correctly the expect function from the expect library. + * + * @param actual The value to apply matchers against. + */ + (actual: T): ExpectWebdriverIO.MatchersAndInverse, T> + } +} \ No newline at end of file diff --git a/jasmine.d.ts b/jasmine.d.ts index 0d9ddcddd..d13a1f2f2 100644 --- a/jasmine.d.ts +++ b/jasmine.d.ts @@ -1,6 +1,27 @@ /// declare namespace jasmine { + + /** + * Async matchers for Jasmine to allow the typing of `expectAsync` with WebDriverIO matchers. + * T is the type of the actual value + * U is the type of the expected value + * Both T,U must stay named as they are to override the default `AsyncMatchers` type from Jasmine. + * + * We force Matchers to return a `Promise` since Jasmine's `expectAsync` expects a promise in all cases (different from Jest) + */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - interface AsyncMatchers extends ExpectWebdriverIO.Matchers, T> {} -} + interface AsyncMatchers extends Omit, T>, 'toMatchSnapshot' | 'toMatchInlineSnapshot'> { + /** + * snapshot matcher + * @param label optional snapshot label + */ + toMatchSnapshot(label?: string): Promise; + /** + * inline snapshot matcher + * @param snapshot snapshot string (autogenerated if not specified) + * @param label optional snapshot label + */ + toMatchInlineSnapshot(snapshot?: string, label?: string): Promise; + } +} \ No newline at end of file diff --git a/jest.d.ts b/jest.d.ts index 3d0182db7..cf96fe737 100644 --- a/jest.d.ts +++ b/jest.d.ts @@ -1,5 +1,37 @@ /// +/** + * Augment the Jest namespace to include the matchers from expect-webdriverio. + * When Jest Library is used, it specifies `expect-webdriverio/jest` for this file in the tsconfig.json's types. + */ + declare namespace jest { - interface Matchers extends ExpectWebdriverIO.Matchers { } -} + + interface Matchers, T> extends ExpectWebdriverIO.Matchers { + + /** + * Below are overloaded Jest's matchers not part of `expect` but of `jest-snapshot`. + * @see https://github.com/jestjs/jest/blob/73dbef5d2d3195a1e55fb254c54cce70d3036252/packages/jest-snapshot/src/types.ts#L37 + * + * Note: We need to define them below so that they are correctly typed overloaded. + * Else even when extending `WdioJestOverloadedMatchers` we have typing errors. + */ + + /** + * snapshot matcher + * @param label optional snapshot label + */ + toMatchSnapshot(label?: string): T extends WdioPromiseLike ? Promise : void; + + /** + * inline snapshot matcher + * @param snapshot snapshot string (autogenerated if not specified) + * @param label optional snapshot label + */ + toMatchInlineSnapshot(snapshot?: string, label?: string): T extends WdioPromiseLike ? Promise : void; + } + + interface Expect extends ExpectWebdriverIO.Expect {} + + interface InverseAsymmetricMatchers extends ExpectWebdriverIO.AsymmetricMatchers {} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fa6a01d08..251d73a5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,12 +11,15 @@ "dependencies": { "@vitest/snapshot": "^3.2.4", "deep-eql": "^5.0.2", - "expect": "^30.0.0", - "jest-matcher-utils": "^30.0.0" + "expect": "^30.0.4", + "jest-matcher-utils": "^30.0.4" }, "devDependencies": { + "@jest/globals": "^30.0.4", "@types/debug": "^4.1.12", + "@types/jasmine": "^5.1.8", "@types/jest": "^30.0.0", + "@types/mocha": "^10.0.10", "@types/node": "^24.0.3", "@vitest/coverage-v8": "^3.2.4", "@wdio/eslint": "^0.1.1", @@ -33,7 +36,7 @@ "webdriverio": "^9.15.0" }, "engines": { - "node": ">=18 || >=20 || >=22" + "node": ">=20" }, "peerDependencies": { "@wdio/globals": "^9.0.0", @@ -80,16 +83,172 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", @@ -99,14 +258,38 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/parser": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", - "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.26.0" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -115,15 +298,288 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", - "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz", + "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1195,6 +1651,120 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -1214,10 +1784,40 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@jest/environment": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.4.tgz", + "integrity": "sha512-5NT+sr7ZOb8wW7C4r7wOKnRQ8zmRWQT2gW4j73IXAKp5/PX1Z8MCStBLQDYfIG3n1Sw0NRfYGdp0iIPVooBAFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.0.4", + "@jest/types": "30.0.1", + "@types/node": "*", + "jest-mock": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.4.tgz", + "integrity": "sha512-Z/DL7t67LBHSX4UzDyeYKqOxE/n7lbrrgEwWM3dGiH5Dgn35nk+YtgzKudmfIrBI8DRRrKYY5BCo3317HZV1Fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.0.4", + "jest-snapshot": "30.0.4" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/expect-utils": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.2.tgz", - "integrity": "sha512-FHF2YdtFBUQOo0/qdgt+6UdBFcNPF/TkVzcc+4vvf8uaBzUlONytGBeeudufIHHW1khRfM1sBbRT1VCK7n/0dQ==", + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.4.tgz", + "integrity": "sha512-EgXecHDNfANeqOkcak0DxsoVI4qkDUsR7n/Lr2vtmTBjwLPBnnPOF71S11Q8IObWzxm2QgQoY6f9hzrRD3gHRA==", "license": "MIT", "dependencies": { "@jest/get-type": "30.0.1" @@ -1226,6 +1826,24 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@jest/fake-timers": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.4.tgz", + "integrity": "sha512-qZ7nxOcL5+gwBO6LErvwVy5k06VsX/deqo2XnVUSTV0TNC9lrg8FC3dARbi+5lmrr5VyX5drragK+xLcOjvjYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.1", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.0.2", + "jest-mock": "30.0.2", + "jest-util": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/get-type": { "version": "30.0.1", "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz", @@ -1235,11 +1853,26 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@jest/globals": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.0.4.tgz", + "integrity": "sha512-avyZuxEHF2EUhFF6NEWVdxkRRV6iXXcIES66DLhuLlU7lXhtFG/ySq/a8SRZmEJSsLkNAFX6z6mm8KWyXe9OEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.0.4", + "@jest/expect": "30.0.4", + "@jest/types": "30.0.1", + "jest-mock": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/pattern": { "version": "30.0.1", "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", - "license": "MIT", "dependencies": { "@types/node": "*", "jest-regex-util": "30.0.1" @@ -1262,11 +1895,119 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/snapshot-utils": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.4.tgz", + "integrity": "sha512-BEpX8M/Y5lG7MI3fmiO+xCnacOrVsnbqVrcDZIT8aSGkKV1w2WwvRQxSWw5SIS8ozg7+h8tSj5EO1Riqqxcdag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/snapshot-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/transform": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.4.tgz", + "integrity": "sha512-atvy4hRph/UxdCIBp+UB2jhEA/jJiUeGZ7QPgBi9jUUKNgi3WEoMXGNG7zbbELG2+88PMabUNCDchmqgJy3ELg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.0.1", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.0", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.2", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.2", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/@jest/types": { "version": "30.0.1", "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.1.tgz", "integrity": "sha512-HGwoYRVF0QSKJu1ZQX0o5ZrUrrhj0aOOFA8hXrumD7SIzjouevhawbTjmXdwOmURdGluU9DM/XvGm3NyFoiQjw==", - "license": "MIT", "dependencies": { "@jest/pattern": "30.0.1", "@jest/schemas": "30.0.1", @@ -1284,7 +2025,6 @@ "version": "30.0.1", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.1.tgz", "integrity": "sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==", - "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.34.0" }, @@ -1293,16 +2033,14 @@ } }, "node_modules/@jest/types/node_modules/@sinclair/typebox": { - "version": "0.34.36", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.36.tgz", - "integrity": "sha512-JFHFhF6MqqRE49JDAGX/EPlHwxIukrKMhNwlMoB/wIJBkvu3+ciO335yDYPP3soI01FkhVXWnyNPKEl+EsC4Zw==", - "license": "MIT" + "version": "0.34.35", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.35.tgz", + "integrity": "sha512-C6ypdODf2VZkgRT6sFM8E1F8vR+HcffniX0Kp8MsU8PIfrlXbNCBz0jzj17GjdmjTx1OtZzdH8+iALL21UjF5A==" }, "node_modules/@jest/types/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -1317,7 +2055,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1330,18 +2067,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -1354,16 +2087,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -1371,9 +2094,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1653,6 +2376,19 @@ "node": ">=14" } }, + "node_modules/@pkgr/core": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", + "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, "node_modules/@promptbook/utils": { "version": "0.69.5", "resolved": "https://registry.npmjs.org/@promptbook/utils/-/utils-0.69.5.tgz", @@ -1982,6 +2718,26 @@ "optional": true, "peer": true }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, "node_modules/@stylistic/eslint-plugin": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-4.2.0.tgz", @@ -2092,6 +2848,12 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jasmine": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.8.tgz", + "integrity": "sha512-u7/CnvRdh6AaaIzYjCgUuVbREFgulhX05Qtf6ZtW+aOcjCKKVvKgpkPYJBFTZSHtFBYimzU4zP0V2vrEsq9Wcg==", + "dev": true + }, "node_modules/@types/jest": { "version": "30.0.0", "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", @@ -2158,6 +2920,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ms": { "version": "0.7.34", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", @@ -2469,6 +3238,13 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, "node_modules/@vitest/coverage-v8": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", @@ -3275,6 +4051,20 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/archiver": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", @@ -3456,6 +4246,111 @@ "devOptional": true, "license": "Apache-2.0" }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz", + "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3639,6 +4534,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -3785,6 +4690,16 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001707", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", @@ -5107,14 +6022,14 @@ "license": "ISC" }, "node_modules/expect": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.2.tgz", - "integrity": "sha512-YN9Mgv2mtTWXVmifQq3QT+ixCL/uLuLJw+fdp8MOjKqu8K3XQh3o5aulMM1tn+O2DdrWNxLZTeJsCY/VofUA0A==", + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.4.tgz", + "integrity": "sha512-dDLGjnP2cKbEppxVICxI/Uf4YemmGMPNy0QytCbfafbpYk9AFQsxb8Uyrxii0RPK7FWgLGlSem+07WirwS3cFQ==", "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.0.2", + "@jest/expect-utils": "30.0.4", "@jest/get-type": "30.0.1", - "jest-matcher-utils": "30.0.2", + "jest-matcher-utils": "30.0.4", "jest-message-util": "30.0.2", "jest-mock": "30.0.2", "jest-util": "30.0.2" @@ -5557,6 +6472,16 @@ "reusify": "^1.0.4" } }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -5691,6 +6616,13 @@ "node": ">=12.20.0" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -5756,6 +6688,16 @@ "node": "^16.13.0 || >=18.0.0" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -5779,6 +6721,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/get-port": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", @@ -6174,6 +7126,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -6433,6 +7397,23 @@ "node": ">=8" } }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/istanbul-lib-report": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", @@ -6494,9 +7475,9 @@ } }, "node_modules/jest-diff": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.2.tgz", - "integrity": "sha512-2UjrNvDJDn/oHFpPrUTVmvYYDNeNtw2DlY3er8bI6vJJb9Fb35ycp/jFLd5RdV59tJ8ekVXX3o/nwPcscgXZJQ==", + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.4.tgz", + "integrity": "sha512-TSjceIf6797jyd+R64NXqicttROD+Qf98fex7CowmlSn7f8+En0da1Dglwr1AXxDtVizoxXYZBlUQwNhoOXkNw==", "license": "MIT", "dependencies": { "@jest/diff-sequences": "30.0.1", @@ -6521,9 +7502,9 @@ } }, "node_modules/jest-diff/node_modules/@sinclair/typebox": { - "version": "0.34.36", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.36.tgz", - "integrity": "sha512-JFHFhF6MqqRE49JDAGX/EPlHwxIukrKMhNwlMoB/wIJBkvu3+ciO335yDYPP3soI01FkhVXWnyNPKEl+EsC4Zw==", + "version": "0.34.37", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.37.tgz", + "integrity": "sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==", "license": "MIT" }, "node_modules/jest-diff/node_modules/ansi-styles": { @@ -6594,15 +7575,40 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-matcher-utils": { + "node_modules/jest-haste-map": { "version": "30.0.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.2.tgz", - "integrity": "sha512-1FKwgJYECR8IT93KMKmjKHSLyru0DqguThov/aWpFccC0wbiXGOxYEu7SScderBD7ruDOpl7lc5NG6w3oxKfaA==", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.2.tgz", + "integrity": "sha512-telJBKpNLeCb4MaX+I5k496556Y2FiKR/QLZc0+MGBYl4k3OO0472drlV2LUe7c1Glng5HuAu+5GLYp//GpdOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.1", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.2", + "jest-worker": "30.0.2", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.4.tgz", + "integrity": "sha512-ubCewJ54YzeAZ2JeHHGVoU+eDIpQFsfPQs0xURPWoNiO42LGJ+QGgfSf+hFIRplkZDkhH5MOvuxHKXRTUU3dUQ==", "license": "MIT", "dependencies": { "@jest/get-type": "30.0.1", "chalk": "^4.1.2", - "jest-diff": "30.0.2", + "jest-diff": "30.0.4", "pretty-format": "30.0.2" }, "engines": { @@ -6622,9 +7628,9 @@ } }, "node_modules/jest-matcher-utils/node_modules/@sinclair/typebox": { - "version": "0.34.36", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.36.tgz", - "integrity": "sha512-JFHFhF6MqqRE49JDAGX/EPlHwxIukrKMhNwlMoB/wIJBkvu3+ciO335yDYPP3soI01FkhVXWnyNPKEl+EsC4Zw==", + "version": "0.34.37", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.37.tgz", + "integrity": "sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==", "license": "MIT" }, "node_modules/jest-matcher-utils/node_modules/ansi-styles": { @@ -6688,7 +7694,6 @@ "version": "30.0.2", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.2.tgz", "integrity": "sha512-vXywcxmr0SsKXF/bAD7t7nMamRvPuJkras00gqYeB1V0WllxZrbZ0paRr3XqpFU2sYYjD0qAaG2fRyn/CGZ0aw==", - "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", "@jest/types": "30.0.1", @@ -6708,7 +7713,6 @@ "version": "30.0.1", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.1.tgz", "integrity": "sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==", - "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.34.0" }, @@ -6717,16 +7721,14 @@ } }, "node_modules/jest-message-util/node_modules/@sinclair/typebox": { - "version": "0.34.36", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.36.tgz", - "integrity": "sha512-JFHFhF6MqqRE49JDAGX/EPlHwxIukrKMhNwlMoB/wIJBkvu3+ciO335yDYPP3soI01FkhVXWnyNPKEl+EsC4Zw==", - "license": "MIT" + "version": "0.34.35", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.35.tgz", + "integrity": "sha512-C6ypdODf2VZkgRT6sFM8E1F8vR+HcffniX0Kp8MsU8PIfrlXbNCBz0jzj17GjdmjTx1OtZzdH8+iALL21UjF5A==" }, "node_modules/jest-message-util/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -6741,7 +7743,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -6757,7 +7758,6 @@ "version": "30.0.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz", "integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==", - "license": "MIT", "dependencies": { "@jest/schemas": "30.0.1", "ansi-styles": "^5.2.0", @@ -6771,7 +7771,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "license": "MIT", "engines": { "node": ">=10" }, @@ -6783,7 +7782,6 @@ "version": "30.0.2", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.2.tgz", "integrity": "sha512-PnZOHmqup/9cT/y+pXIVbbi8ID6U1XHRmbvR7MvUy4SLqhCbwpkmXhLbsWbGewHrV5x/1bF7YDjs+x24/QSvFA==", - "license": "MIT", "dependencies": { "@jest/types": "30.0.1", "@types/node": "*", @@ -6797,16 +7795,128 @@ "version": "30.0.1", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.4.tgz", + "integrity": "sha512-S/8hmSkeUib8WRUq9pWEb5zMfsOjiYWDWzFzKnjX7eDyKKgimsu9hcmsUEg8a7dPAw8s/FacxsXquq71pDgPjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.0.4", + "@jest/get-type": "30.0.1", + "@jest/snapshot-utils": "30.0.4", + "@jest/transform": "30.0.4", + "@jest/types": "30.0.1", + "babel-preset-current-node-syntax": "^1.1.0", + "chalk": "^4.1.2", + "expect": "30.0.4", + "graceful-fs": "^4.2.11", + "jest-diff": "30.0.4", + "jest-matcher-utils": "30.0.4", + "jest-message-util": "30.0.2", + "jest-util": "30.0.2", + "pretty-format": "30.0.2", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/@jest/schemas": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.1.tgz", + "integrity": "sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/@sinclair/typebox": { + "version": "0.34.37", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.37.tgz", + "integrity": "sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz", + "integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.1", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-snapshot/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/jest-util": { "version": "30.0.2", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.2.tgz", "integrity": "sha512-8IyqfKS4MqprBuUpZNlFB5l+WFehc8bfCe1HSZFHzft2mOuND8Cvi9r1musli+u6F3TqanCZ/Ik4H4pXUolZIg==", - "license": "MIT", "dependencies": { "@jest/types": "30.0.1", "@types/node": "*", @@ -6823,7 +7933,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -6838,7 +7947,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -6854,7 +7962,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "license": "MIT", "engines": { "node": ">=12" }, @@ -6862,6 +7969,39 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/jest-worker": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.2.tgz", + "integrity": "sha512-RN1eQmx7qSLFA+o9pfJKlqViwL5wt+OL3Vff/A+/cPsmuw7NPwfgl33AP+/agRmHzPOFgXviRycR9kYwlcRQXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.0.2", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/jiti": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", @@ -6940,6 +8080,19 @@ "dev": true, "license": "MIT" }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -7309,6 +8462,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", @@ -7564,6 +8727,13 @@ "node": ">= 12" } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -7896,6 +9066,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pac-proxy-agent": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.1.0.tgz", @@ -8029,6 +9209,16 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -8126,6 +9316,16 @@ "node": ">=0.10" } }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/pkg-types": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz", @@ -9315,6 +10515,22 @@ "node": ">=8" } }, + "node_modules/synckit": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", + "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.4" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, "node_modules/tar-fs": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz", @@ -9532,6 +10748,13 @@ "node": ">=0.6.0" } }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -9577,6 +10800,16 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "4.38.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.38.0.tgz", @@ -10035,6 +11268,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -10924,6 +12167,20 @@ "devOptional": true, "license": "ISC" }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/ws": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", @@ -10955,6 +12212,13 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 41c8ffd53..474969087 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "exports": { ".": [ { - "types": "./types/standalone.d.ts", + "types": "./types/expect-webdriverio.d.ts", "import": "./lib/index.js" }, "./lib/index.js" @@ -33,42 +33,52 @@ "types": "./jest.d.ts" } ], - "./types": "./types/jest-global.d.ts" + "./jasmine": [ + { + "types": "./jasmine.d.ts" + } + ], + "./types": "./types/expect-global.d.ts", + "./expect-global": "./types/expect-global.d.ts" }, - "types": "./types/standalone.d.ts", + "types": "./types/expect-webdriverio.d.ts", "typeScriptVersion": "3.8.3", "engines": { - "node": ">=18 || >=20 || >=22" + "node": ">=20" }, "scripts": { "build": "run-s clean compile", "clean": "run-p clean:*", "clean:build": "rimraf ./lib", - "clean:tests": "rimraf test-types/**/node_modules && rimraf test-types/**/dist", "compile": "tsc --build tsconfig.build.json", - "tsc:root-types": "tsc jasmine.d.ts jest.d.ts", + "tsc:root-types": "node types-checks-filter-out-node_modules.js", "test": "run-s test:*", "test:tsc": "tsc --project tsconfig.json --noEmit", "test:lint": "eslint .", "test:unit": "vitest --run", - "test:types": "node test-types/copy && npm run ts && npm run clean:tests && npm run tsc:root-types", - "ts": "run-s ts:*", - "ts:default": "cd test-types/default && tsc -p ./tsconfig.json --incremental", - "ts:jest": "cd test-types/jest && tsc -p ./tsconfig.json --incremental", - "ts:jasmine": "cd test-types/jasmine && tsc -p ./tsconfig.json --incremental", + "test:types": "npm run ts && npm run tsc:root-types", + "ts": "run-s ts:* ts:*:*", + "ts:jest:@jest/global": "cd test-types/jest-@jest_global && tsc --project ./tsconfig.json", + "ts:jest:@types-jest": "cd test-types/jest-@types_jest && tsc --project ./tsconfig.json", + "ts:mocha": "cd test-types/mocha && tsc --project ./tsconfig.json", + "ts:jasmine": "cd test-types/jasmine && tsc --project ./tsconfig.json", + "ts:jasmine-async": "cd test-types/jasmine-async && tsc --project ./tsconfig.json", "watch": "npm run compile -- --watch", "prepare": "husky install" }, "dependencies": { "@vitest/snapshot": "^3.2.4", "deep-eql": "^5.0.2", - "expect": "^30.0.0", - "jest-matcher-utils": "^30.0.0" + "expect": "^30.0.4", + "jest-matcher-utils": "^30.0.4" }, "devDependencies": { "@types/debug": "^4.1.12", + "@types/jasmine": "^5.1.8", "@types/jest": "^30.0.0", "@types/node": "^24.0.3", + "@types/mocha": "^10.0.10", + "@jest/globals": "^30.0.4", "@vitest/coverage-v8": "^3.2.4", "@wdio/eslint": "^0.1.1", "@wdio/types": "^9.15.0", diff --git a/src/index.ts b/src/index.ts index b79759e75..c88bd5c10 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,12 @@ -/// +/// import { expect as expectLib } from 'expect' -import type { RawMatcherFn } from './types.js' - +import type { WdioMatchersObject } from './types.js' import * as wdioMatchers from './matchers.js' import { DEFAULT_OPTIONS } from './constants.js' import createSoftExpect from './softExpect.js' import { SoftAssertService } from './softAssert.js' -export const matchers = new Map() +export const matchers: WdioMatchersObject = new Map() const filteredMatchers = {} const extend = expectLib.extend diff --git a/src/jasmineUtils.ts b/src/jasmineUtils.ts index 82d315dae..346fb2698 100644 --- a/src/jasmineUtils.ts +++ b/src/jasmineUtils.ts @@ -59,7 +59,7 @@ function asymmetricMatch(a: any, b: any) { } // Equality function lovingly adapted from isEqual in -// [Underscore](http://underscorejs.org) +// [Underscore](https://underscorejs.org) function eq( a: any, b: any, diff --git a/src/matchers/browser/toHaveClipboardText.ts b/src/matchers/browser/toHaveClipboardText.ts index 98e52424d..00b023408 100644 --- a/src/matchers/browser/toHaveClipboardText.ts +++ b/src/matchers/browser/toHaveClipboardText.ts @@ -7,7 +7,7 @@ const log = logger('expect-webdriverio') export async function toHaveClipboardText( browser: WebdriverIO.Browser, - expectedValue: string | RegExp | ExpectWebdriverIO.PartialMatcher, + expectedValue: string | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { const isNot = this.isNot diff --git a/src/matchers/browser/toHaveTitle.ts b/src/matchers/browser/toHaveTitle.ts index b7287d9f8..4c18dd7f8 100644 --- a/src/matchers/browser/toHaveTitle.ts +++ b/src/matchers/browser/toHaveTitle.ts @@ -3,7 +3,7 @@ import { DEFAULT_OPTIONS } from '../../constants.js' export async function toHaveTitle( browser: WebdriverIO.Browser, - expectedValue: string | RegExp | ExpectWebdriverIO.PartialMatcher, + expectedValue: string | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { const isNot = this.isNot diff --git a/src/matchers/browser/toHaveUrl.ts b/src/matchers/browser/toHaveUrl.ts index a88d25c17..06719ac5d 100644 --- a/src/matchers/browser/toHaveUrl.ts +++ b/src/matchers/browser/toHaveUrl.ts @@ -3,7 +3,7 @@ import { DEFAULT_OPTIONS } from '../../constants.js' export async function toHaveUrl( browser: WebdriverIO.Browser, - expectedValue: string | RegExp | ExpectWebdriverIO.PartialMatcher, + expectedValue: string | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { const isNot = this.isNot diff --git a/src/matchers/element/toHaveAttribute.ts b/src/matchers/element/toHaveAttribute.ts index cd0a2b141..615dab685 100644 --- a/src/matchers/element/toHaveAttribute.ts +++ b/src/matchers/element/toHaveAttribute.ts @@ -17,7 +17,7 @@ async function conditionAttr(el: WebdriverIO.Element, attribute: string) { } -async function conditionAttrAndValue(el: WebdriverIO.Element, attribute: string, value: string | RegExp | ExpectWebdriverIO.PartialMatcher, options: ExpectWebdriverIO.StringOptions) { +async function conditionAttrAndValue(el: WebdriverIO.Element, attribute: string, value: string | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions) { const attr = await el.getAttribute(attribute) if (typeof attr !== 'string') { return { result: false, value: attr } @@ -26,7 +26,7 @@ async function conditionAttrAndValue(el: WebdriverIO.Element, attribute: string, return compareText(attr, value, options) } -export async function toHaveAttributeAndValue(received: WdioElementMaybePromise, attribute: string, value: string | RegExp | ExpectWebdriverIO.PartialMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS) { +export async function toHaveAttributeAndValue(received: WdioElementMaybePromise, attribute: string, value: string | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS) { const isNot = this.isNot const { expectation = 'attribute', verb = 'have' } = this @@ -73,7 +73,7 @@ async function toHaveAttributeFn(received: WdioElementMaybePromise, attribute: s export async function toHaveAttribute( received: WdioElementMaybePromise, attribute: string, - value?: string | RegExp | ExpectWebdriverIO.PartialMatcher, + value?: string | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { await options.beforeAssertion?.({ diff --git a/src/matchers/element/toHaveClass.ts b/src/matchers/element/toHaveClass.ts index da20e3110..864d4ad04 100644 --- a/src/matchers/element/toHaveClass.ts +++ b/src/matchers/element/toHaveClass.ts @@ -1,9 +1,9 @@ import { DEFAULT_OPTIONS } from '../../constants.js' import type { WdioElementMaybePromise } from '../../types.js' -import { compareText, compareTextWithArray, enhanceError, executeCommand, isAsymmeyricMatcher, waitUntil, wrapExpectedWithArray } from '../../utils.js' +import { compareText, compareTextWithArray, enhanceError, executeCommand, isAsymmetricMatcher, waitUntil, wrapExpectedWithArray } from '../../utils.js' import { toHaveAttributeAndValue } from './toHaveAttribute.js' -async function condition(el: WebdriverIO.Element, attribute: string, value: string | RegExp | Array | ExpectWebdriverIO.PartialMatcher, options: ExpectWebdriverIO.StringOptions) { +async function condition(el: WebdriverIO.Element, attribute: string, value: string | RegExp | Array | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions) { const actualClass = await el.getAttribute(attribute) if (typeof actualClass !== 'string') { return { result: false } @@ -13,7 +13,7 @@ async function condition(el: WebdriverIO.Element, attribute: string, value: stri * if value is an asymmetric matcher, no need to split class names * into an array and compare each of them */ - if (isAsymmeyricMatcher(value)) { + if (isAsymmetricMatcher(value)) { return compareText(actualClass, value, options) } @@ -39,7 +39,7 @@ export function toHaveClass(...args: unknown[]) { export async function toHaveElementClass( received: WdioElementMaybePromise, - expectedValue: string | RegExp | Array | ExpectWebdriverIO.PartialMatcher, + expectedValue: string | RegExp | Array | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { const isNot = this.isNot @@ -83,7 +83,7 @@ export async function toHaveElementClass( /** * @deprecated */ -export function toHaveClassContaining(el: WebdriverIO.Element, className: string | RegExp | ExpectWebdriverIO.PartialMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS) { +export function toHaveClassContaining(el: WebdriverIO.Element, className: string | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS) { return toHaveAttributeAndValue.call(this, el, 'class', className, { ...options, containing: true diff --git a/src/matchers/element/toHaveComputedLabel.ts b/src/matchers/element/toHaveComputedLabel.ts index 0e316d061..50e2a9324 100644 --- a/src/matchers/element/toHaveComputedLabel.ts +++ b/src/matchers/element/toHaveComputedLabel.ts @@ -11,7 +11,7 @@ import { async function condition( el: WebdriverIO.Element, - label: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, + label: string | RegExp | WdioAsymmetricMatcher | Array, options: ExpectWebdriverIO.HTMLOptions ) { const actualLabel = await el.getComputedLabel() @@ -23,7 +23,7 @@ async function condition( export async function toHaveComputedLabel( received: WdioElementMaybePromise, - expectedValue: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, + expectedValue: string | RegExp | WdioAsymmetricMatcher | Array, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { const isNot = this.isNot diff --git a/src/matchers/element/toHaveComputedRole.ts b/src/matchers/element/toHaveComputedRole.ts index 22e139ee5..916506b97 100644 --- a/src/matchers/element/toHaveComputedRole.ts +++ b/src/matchers/element/toHaveComputedRole.ts @@ -11,7 +11,7 @@ import { async function condition( el: WebdriverIO.Element, - role: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, + role: string | RegExp | WdioAsymmetricMatcher | Array, options: ExpectWebdriverIO.HTMLOptions ) { const actualRole = await el.getComputedRole() @@ -23,7 +23,7 @@ async function condition( export async function toHaveComputedRole( received: WdioElementMaybePromise, - expectedValue: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, + expectedValue: string | RegExp | WdioAsymmetricMatcher | Array, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { const isNot = this.isNot diff --git a/src/matchers/element/toHaveElementProperty.ts b/src/matchers/element/toHaveElementProperty.ts index 0c7bdfb79..cdf4146b0 100644 --- a/src/matchers/element/toHaveElementProperty.ts +++ b/src/matchers/element/toHaveElementProperty.ts @@ -30,13 +30,13 @@ async function condition( } prop = prop.toString() - return compareText(prop as string, value as string | RegExp | ExpectWebdriverIO.PartialMatcher, options) + return compareText(prop as string, value as string | RegExp | WdioAsymmetricMatcher, options) } export async function toHaveElementProperty( received: WdioElementMaybePromise, property: string, - value?: string | RegExp | ExpectWebdriverIO.PartialMatcher, + value?: string | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { const isNot = this.isNot diff --git a/src/matchers/element/toHaveHTML.ts b/src/matchers/element/toHaveHTML.ts index ac1e45e19..1f7b976e2 100644 --- a/src/matchers/element/toHaveHTML.ts +++ b/src/matchers/element/toHaveHTML.ts @@ -9,7 +9,7 @@ import { wrapExpectedWithArray } from '../../utils.js' -async function condition(el: WebdriverIO.Element, html: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, options: ExpectWebdriverIO.HTMLOptions) { +async function condition(el: WebdriverIO.Element, html: string | RegExp | WdioAsymmetricMatcher | Array, options: ExpectWebdriverIO.HTMLOptions) { const actualHTML = await el.getHTML(options) if (Array.isArray(html)) { return compareTextWithArray(actualHTML, html, options) @@ -19,7 +19,7 @@ async function condition(el: WebdriverIO.Element, html: string | RegExp | Expect export async function toHaveHTML( received: ChainablePromiseArray | ChainablePromiseElement, - expectedValue: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, + expectedValue: string | RegExp | WdioAsymmetricMatcher | Array, options: ExpectWebdriverIO.HTMLOptions = DEFAULT_OPTIONS ) { const isNot = this.isNot diff --git a/src/matchers/element/toHaveId.ts b/src/matchers/element/toHaveId.ts index 99be37be3..6bc3d4ae2 100644 --- a/src/matchers/element/toHaveId.ts +++ b/src/matchers/element/toHaveId.ts @@ -4,7 +4,7 @@ import type { WdioElementMaybePromise } from '../../types.js' export async function toHaveId( el: WdioElementMaybePromise, - expectedValue: string | RegExp | ExpectWebdriverIO.PartialMatcher, + expectedValue: string | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { await options.beforeAssertion?.({ diff --git a/src/matchers/element/toHaveText.ts b/src/matchers/element/toHaveText.ts index 1233733e6..37dbc4d21 100644 --- a/src/matchers/element/toHaveText.ts +++ b/src/matchers/element/toHaveText.ts @@ -8,7 +8,7 @@ import { wrapExpectedWithArray } from '../../utils.js' -async function condition(el: WebdriverIO.Element | WebdriverIO.ElementArray, text: string | RegExp | Array | ExpectWebdriverIO.PartialMatcher | Array, options: ExpectWebdriverIO.StringOptions) { +async function condition(el: WebdriverIO.Element | WebdriverIO.ElementArray, text: string | RegExp | Array | WdioAsymmetricMatcher | Array, options: ExpectWebdriverIO.StringOptions) { const actualTextArray: string[] = [] const resultArray: boolean[] = [] let checkAllValuesMatchCondition: boolean @@ -39,7 +39,7 @@ async function condition(el: WebdriverIO.Element | WebdriverIO.ElementArray, tex export async function toHaveText( received: ChainablePromiseElement | ChainablePromiseArray, - expectedValue: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, + expectedValue: string | RegExp | WdioAsymmetricMatcher | Array, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { const isNot = this.isNot diff --git a/src/matchers/element/toHaveValue.ts b/src/matchers/element/toHaveValue.ts index e11664bdd..6c032b2c8 100644 --- a/src/matchers/element/toHaveValue.ts +++ b/src/matchers/element/toHaveValue.ts @@ -4,7 +4,7 @@ import type { WdioElementMaybePromise } from '../../types.js' export function toHaveValue( el: WdioElementMaybePromise, - value: string | RegExp | ExpectWebdriverIO.PartialMatcher, + value: string | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { return toHaveElementProperty.call(this, el, 'value', value, options) diff --git a/src/matchers/mock/toBeRequestedWith.ts b/src/matchers/mock/toBeRequestedWith.ts index ad55a5eb4..98f2b3267 100644 --- a/src/matchers/mock/toBeRequestedWith.ts +++ b/src/matchers/mock/toBeRequestedWith.ts @@ -122,7 +122,7 @@ const statusCodeMatcher = (statusCode: number, expected?: number | Array */ const urlMatcher = ( url: string, - expected?: string | ExpectWebdriverIO.PartialMatcher | ((url: string) => boolean) + expected?: string | ExpectWebdriverIO.PartialMatcher | ((url: string) => boolean) ) => { if (typeof expected === 'undefined') { return true @@ -140,7 +140,7 @@ const headersMatcher = ( headers: Record, expected?: | Record - | ExpectWebdriverIO.PartialMatcher + | ExpectWebdriverIO.PartialMatcher> | ((headers: Record) => boolean) ) => { /** @@ -215,7 +215,7 @@ const headersMatcher = ( // // get matcher sample if expected value is a special matcher like `expect.objectContaining({ foo: 'bar })` // const actualSample = isMatcher(expected) -// ? (expected as ExpectWebdriverIO.PartialMatcher).sample +// ? (expected as WdioAsymmetricMatcher).sample // : expected // return ( @@ -315,7 +315,7 @@ const requestedWithParamToString = ( param: | string | ExpectWebdriverIO.JsonCompatible - | ExpectWebdriverIO.PartialMatcher + | ExpectWebdriverIO.PartialMatcher | Function | undefined, transformFn?: (param: ExpectWebdriverIO.JsonCompatible) => ExpectWebdriverIO.JsonCompatible | string @@ -330,7 +330,7 @@ const requestedWithParamToString = ( return ( param.constructor.name + ' ' + - (JSON.stringify((param as ExpectWebdriverIO.PartialMatcher).sample) || '') + (JSON.stringify((param as WdioAsymmetricMatcher).sample) || '') ) } else if (transformFn && typeof param === 'object' && param !== null) { param = transformFn(param as ExpectWebdriverIO.JsonCompatible) diff --git a/src/matchers/snapshot.ts b/src/matchers/snapshot.ts index 5a1e6273f..daf36868a 100644 --- a/src/matchers/snapshot.ts +++ b/src/matchers/snapshot.ts @@ -3,7 +3,7 @@ import type { AssertionError } from 'node:assert' import { expect } from 'expect' import { stripSnapshotIndentation } from '@vitest/snapshot' -import { SnapshotService } from '../snapshot.js' +import { SnapshotService } from '../snapshot' interface InlineSnapshotOptions { inlineSnapshot: string diff --git a/src/softExpect.ts b/src/softExpect.ts index 4aa2dfbb9..31edd5402 100644 --- a/src/softExpect.ts +++ b/src/softExpect.ts @@ -76,7 +76,8 @@ const createSoftMatcher = ( return async (...args: unknown[]) => { try { // Build the expectation chain - let expectChain = expect(actual) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let expectChain: any = expect(actual) if (prefix === 'not') { expectChain = expectChain.not @@ -86,7 +87,7 @@ const createSoftMatcher = ( expectChain = expectChain.rejects } - return await ((expectChain as unknown) as Record Promise>)[matcherName](...args) + return await ((expectChain as unknown) as Record ExpectWebdriverIO.AsyncAssertionResult>)[matcherName](...args) } catch (error) { // Record the failure diff --git a/src/types.ts b/src/types.ts index ecda2c93c..d74569d50 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,3 +12,5 @@ export type WdioElementsMaybePromise = export type RawMatcherFn = { (this: Context, actual: unknown, ...expected: unknown[]): ExpectationResult; } + +export type WdioMatchersObject = Map \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index ad2d8bb19..66c946d31 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -16,7 +16,7 @@ const asymmetricMatcher = ? Symbol.for('jest.asymmetricMatcher') : 0x13_57_a5 -export function isAsymmeyricMatcher(expected: unknown): expected is ExpectWebdriverIO.PartialMatcher { +export function isAsymmetricMatcher(expected: unknown): expected is WdioAsymmetricMatcher { return ( typeof expected === 'object' && typeof expected === 'object' && @@ -28,14 +28,14 @@ export function isAsymmeyricMatcher(expected: unknown): expected is ExpectWebdri ) as boolean } -function isStringContainingMatcher(expected: unknown): expected is ExpectWebdriverIO.PartialMatcher { - return isAsymmeyricMatcher(expected) && ['StringContaining', 'StringNotContaining'].includes(expected.toString()) +function isStringContainingMatcher(expected: unknown): expected is WdioAsymmetricMatcher { + return isAsymmetricMatcher(expected) && ['StringContaining', 'StringNotContaining'].includes(expected.toString()) } /** * wait for expectation to succeed * @param condition function - * @param isNot https://jestjs.io/docs/en/expect#thisisnot + * @param isNot https://jestjs.io/docs/expect#thisisnot * @param options wait, interval, etc */ const waitUntil = async ( @@ -90,7 +90,7 @@ async function executeCommandBe( received: WdioElementMaybePromise, command: (el: WebdriverIO.Element) => Promise, options: ExpectWebdriverIO.CommandOptions -): Promise { +): ExpectWebdriverIO.AsyncAssertionResult { const { isNot, expectation, verb = 'be' } = this let el = await received?.getElement() @@ -143,7 +143,7 @@ const compareNumbers = (actual: number, options: ExpectWebdriverIO.NumberOptions export const compareText = ( actual: string, - expected: string | RegExp | ExpectWebdriverIO.PartialMatcher, + expected: string | RegExp | WdioAsymmetricMatcher, { ignoreCase = false, trim = true, @@ -174,11 +174,11 @@ export const compareText = ( } else if (isStringContainingMatcher(expected)) { expected = (expected.toString() === 'StringContaining' ? expect.stringContaining(expected.sample?.toString().toLowerCase()) - : expect.not.stringContaining(expected.sample?.toString().toLowerCase())) as ExpectWebdriverIO.PartialMatcher + : expect.not.stringContaining(expected.sample?.toString().toLowerCase())) as WdioAsymmetricMatcher } } - if (isAsymmeyricMatcher(expected)) { + if (isAsymmetricMatcher(expected)) { const result = expected.asymmetricMatch(actual) return { value: actual, @@ -229,7 +229,7 @@ export const compareText = ( export const compareTextWithArray = ( actual: string, - expectedArray: Array, + expectedArray: Array>, { ignoreCase = false, trim = false, @@ -262,7 +262,7 @@ export const compareTextWithArray = ( if (isStringContainingMatcher(item)) { return (item.toString() === 'StringContaining' ? expect.stringContaining(item.sample?.toString().toLowerCase()) - : expect.not.stringContaining(item.sample?.toString().toLowerCase())) as ExpectWebdriverIO.PartialMatcher + : expect.not.stringContaining(item.sample?.toString().toLowerCase())) as WdioAsymmetricMatcher } return item }) @@ -272,7 +272,7 @@ export const compareTextWithArray = ( if (expected instanceof RegExp) { return !!actual.match(expected) } - if (isAsymmeyricMatcher(expected)) { + if (isAsymmetricMatcher(expected)) { return expected.asymmetricMatch(actual) } if (containing) { diff --git a/test-types/copy.js b/test-types/copy.js deleted file mode 100644 index 39a9dcc93..000000000 --- a/test-types/copy.js +++ /dev/null @@ -1,69 +0,0 @@ -import path from 'node:path' -import url from 'node:url' - -import { rimraf } from 'rimraf' - -import shelljs from 'shelljs' - -const __dirname = path.dirname(url.fileURLToPath(import.meta.url)) -const ROOT = path.resolve(__dirname, '..') - -// TypeScript project root for testing particular typings -const outDirs = ['default', 'jest', 'jasmine'] - -const defaultPackages = ['expect', 'jest-matcher-utils'] -const jestPackages = ['@types/jest'] -const jasminePackages = ['@types/jasmine'] - -const testFile = 'types.ts' - -const artifactDirs = ['node_modules', 'dist', testFile] - -/** - * copy package.json and typings from package to type-generation/test/.../node_modules - */ -async function copy() { - for (const outDir of outDirs) { - const packages = [...defaultPackages] - if (outDir === 'jest') { - packages.push(...jestPackages) - } - if (outDir === 'jasmine') { - packages.push(...jasminePackages) - } - - // link node_modules - for (const packageName of packages) { - const destination = path.join(__dirname, outDir, 'node_modules', packageName) - - const destDir = destination.split(path.sep).slice(0, -1).join(path.sep) - shelljs.mkdir('-p', destDir) - shelljs.ln('-s', path.join(ROOT, 'node_modules', packageName), destination) - } - - // link test file - shelljs.ln('-s', path.join(__dirname, testFile), path.join(__dirname, outDir, testFile)) - - // copy expect-webdriverio - const destDir = path.join(__dirname, outDir, 'node_modules', 'expect-webdriverio') - - shelljs.mkdir('-p', destDir) - shelljs.cp('*.d.ts', destDir) - shelljs.cp('package.json', destDir) - shelljs.cp('-r', 'types', destDir) - } -} - -/** - * delete eventual artifacts from test folders - */ -await Promise.all( - artifactDirs.map((dir) => - Promise.all(outDirs.map((testDir) => rimraf(path.join(__dirname, testDir, dir)))) - ) -) - -/** - * if successful, start test - */ -await copy() diff --git a/test-types/default/tsconfig.json b/test-types/default/tsconfig.json deleted file mode 100644 index 0f5a1fc58..000000000 --- a/test-types/default/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "outDir": "dist", - "noImplicitAny": true, - "target": "ES2020", - "esModuleInterop": true, - "module": "Node16", - "skipLibCheck": true, - "types": [ - "expect-webdriverio" - ] - } -} diff --git a/test-types/jasmine-async/customMatchers/customMatchers-module-expect.d.ts b/test-types/jasmine-async/customMatchers/customMatchers-module-expect.d.ts new file mode 100644 index 000000000..1111096ec --- /dev/null +++ b/test-types/jasmine-async/customMatchers/customMatchers-module-expect.d.ts @@ -0,0 +1,29 @@ +import 'expect' + +/** + * Custom matchers under the `expect` module. + * @see {@link https://jestjs.io/docs/expect#expectextendmatchers} + */ +declare module 'expect' { + interface AsymmetricMatchers { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + toBeWithinRange(floor: number, ceiling: number): any + toHaveSimpleCustomProperty(string: string): string + toHaveCustomProperty(element: ChainablePromiseElement | WebdriverIO.Element): Promise> + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface Matchers { + // Custom matchers in Jasmine need to return a Promise, potential breaking change to document + toBeWithinRange(floor: number, ceiling: number): Promise + toHaveSimpleCustomProperty(string: string | ExpectWebdriverIO.PartialMatcher): Promise + toHaveCustomProperty: + // Useful to typecheck the custom matcher so it is only used on elements + T extends ChainablePromiseElement | WebdriverIO.Element ? + (test: string | ExpectWebdriverIO.PartialMatcher | + // Needed for the custom asymmetric matcher defined above to be typed correctly + Promise>) + // Using `never` blocks the call on non-element types + => Promise : never; + } +} \ No newline at end of file diff --git a/test-types/jasmine-async/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts b/test-types/jasmine-async/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts new file mode 100644 index 000000000..00153b892 --- /dev/null +++ b/test-types/jasmine-async/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts @@ -0,0 +1,14 @@ +/** + * Custom matchers under the `ExpectWebdriverIO` namespace. + * @see {@link https://webdriver.io/docs/custommatchers/#typescript-support} + */ +declare namespace ExpectWebdriverIO { + interface AsymmetricMatchers { + toBeCustom(): ExpectWebdriverIO.PartialMatcher; + toBeCustomPromise(chainableElement: ChainablePromiseElement): Promise>; + } + interface Matchers { + toBeCustom(): R; + toBeCustomPromise: T extends ChainablePromiseElement ? (expected?: string | ExpectWebdriverIO.PartialMatcher | Promise>) => Promise : never; + } +} \ No newline at end of file diff --git a/test-types/jasmine-async/tsconfig.json b/test-types/jasmine-async/tsconfig.json new file mode 100644 index 000000000..90080327e --- /dev/null +++ b/test-types/jasmine-async/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "noEmit": true, + "noImplicitAny": true, + "target": "es2022", + "module": "node18", + "skipLibCheck": true, + "types": [ + "@types/jasmine", + "../../jasmine.d.ts" + ] + } +} \ No newline at end of file diff --git a/test-types/jasmine-async/types-jasmine_async.test.ts b/test-types/jasmine-async/types-jasmine_async.test.ts new file mode 100644 index 000000000..01f9611e4 --- /dev/null +++ b/test-types/jasmine-async/types-jasmine_async.test.ts @@ -0,0 +1,811 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { expect as wdioExpect } from 'expect-webdriverio' +describe('type assertions', () => { + const chainableElement = {} as unknown as ChainablePromiseElement + const chainableArray = {} as ChainablePromiseArray + + const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element + const elementArray: WebdriverIO.ElementArray = [] as unknown as WebdriverIO.ElementArray + + const networkMock: WebdriverIO.Mock = {} as unknown as WebdriverIO.Mock + + // Type assertions + let expectPromiseVoid: Promise + let expectVoid: void + let expectPromiseUnknown: Promise + + describe('Browser', () => { + const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser + + describe('toHaveUrl', () => { + it('should be supported correctly', async () => { + expectPromiseVoid = expectAsync(browser).toHaveUrl('https://example.com') + expectPromiseVoid = expectAsync(browser).not.toHaveUrl('https://example.com') + + // Asymmetric matchers + expectPromiseVoid = expectAsync(browser).toHaveUrl(wdioExpect.stringContaining('WebdriverIO')) + expectPromiseVoid = expectAsync(browser).toHaveUrl(wdioExpect.not.stringContaining('WebdriverIO')) + expectPromiseVoid = expectAsync(browser).toHaveUrl(wdioExpect.any(String)) + expectPromiseVoid = expectAsync(browser).toHaveUrl(wdioExpect.anything()) + + // @ts-expect-error + expectVoid = expectAsync(browser).toHaveUrl('https://example.com') + // @ts-expect-error + expectVoid = expectAsync(browser).not.toHaveUrl('https://example.com') + // @ts-expect-error + expectVoid = expectAsync(browser).toHaveUrl(wdioExpect.stringContaining('WebdriverIO')) + + // @ts-expect-error + await expectAsync(browser).toHaveUrl(6) + }) + + it('should have ts errors when actual is not a Browser element', async () => { + // @ts-expect-error + await expectAsync(element).toHaveUrl('https://example.com') + // @ts-expect-error + await expectAsync(element).not.toHaveUrl('https://example.com') + // @ts-expect-error + await expectAsync(true).toHaveUrl('https://example.com') + // @ts-expect-error + await expectAsync(true).not.toHaveUrl('https://example.com') + }) + }) + + describe('toHaveTitle', () => { + it('should be supported correctly', async () => { + const test = expectAsync('text') + expectPromiseVoid = expectAsync(browser).toHaveTitle('https://example.com') + expectPromiseVoid = expectAsync(browser).not.toHaveTitle('https://example.com') + + // Asymmetric matchers + expectPromiseVoid = expectAsync(browser).toHaveTitle(wdioExpect.stringContaining('WebdriverIO')) + expectPromiseVoid = expectAsync(browser).toHaveTitle(wdioExpect.any(String)) + expectPromiseVoid = expectAsync(browser).toHaveTitle(wdioExpect.anything()) + + // @ts-expect-error + expectVoid = expectAsync(browser).toHaveTitle('https://example.com') + // @ts-expect-error + expectVoid = expectAsync(browser).not.toHaveTitle('https://example.com') + // @ts-expect-error + expectVoid = expectAsync(browser).toHaveTitle(wdioExpect.stringContaining('WebdriverIO')) + }) + + it('should have ts errors when actual is not a Browser element', async () => { + // @ts-expect-error + await expectAsync(element).toHaveTitle('https://example.com') + // @ts-expect-error + await expectAsync(element).not.toHaveTitle('https://example.com') + // @ts-expect-error + await expectAsync(true).toHaveTitle('https://example.com') + // @ts-expect-error + await expectAsync(true).not.toHaveTitle('https://example.com') + }) + }) + }) + + describe('element', () => { + + describe('toBeDisabled', () => { + it('should be supported correctly', async () => { + // Element + expectPromiseVoid = expectAsync(element).toBeDisabled() + expectPromiseVoid = expectAsync(element).not.toBeDisabled() + + // Element array + expectPromiseVoid = expectAsync(elementArray).toBeDisabled() + expectPromiseVoid = expectAsync(elementArray).not.toBeDisabled() + + // Chainable element + expectPromiseVoid = expectAsync(chainableElement).toBeDisabled() + expectPromiseVoid = expectAsync(chainableElement).not.toBeDisabled() + + // Chainable element array + expectPromiseVoid = expectAsync(chainableArray).toBeDisabled() + expectPromiseVoid = expectAsync(chainableArray).not.toBeDisabled() + + // @ts-expect-error + expectVoid = expectAsync(element).toBeDisabled() + // @ts-expect-error + expectVoid = expectAsync(element).not.toBeDisabled() + }) + + it('should have ts errors when actual is not an element', async () => { + // @ts-expect-error + await expectAsync(browser).toBeDisabled() + // @ts-expect-error + await expectAsync(browser).not.toBeDisabled() + // @ts-expect-error + await expectAsync(true).toBeDisabled() + // @ts-expect-error + await expectAsync(true).not.toBeDisabled() + }) + }) + + describe('toHaveText', () => { + it('should be supported correctly', async () => { + expectPromiseVoid = expectAsync(element).toHaveText('text') + expectPromiseVoid = expectAsync(element).toHaveText(/text/) + expectPromiseVoid = expectAsync(element).toHaveText(['text1', 'text2']) + expectPromiseVoid = expectAsync(element).toHaveText([wdioExpect.stringContaining('text1'), wdioExpect.stringContaining('text2')]) + expectPromiseVoid = expectAsync(element).toHaveText([/text1/, /text2/]) + expectPromiseVoid = expectAsync(element).toHaveText(['text1', /text1/, wdioExpect.stringContaining('text3')]) + await expectAsync(element).toHaveText( + 'My-Ex-Am-Ple', + { + replace: [[/-/g, ' '], [/[A-Z]+/g, (match: string) => match.toLowerCase()]] + } + ) + + expectPromiseVoid = expectAsync(element).not.toHaveText('text') + + // @ts-expect-error + expectVoid = expectAsync(element).toHaveText('text') + // @ts-expect-error + await expectAsync(element).toHaveText(6) + + expectPromiseVoid = expectAsync(chainableElement).toHaveText('text') + expectPromiseVoid = expectAsync(chainableElement).toHaveText(/text/) + expectPromiseVoid = expectAsync(chainableElement).toHaveText(['text1', 'text2']) + expectPromiseVoid = expectAsync(chainableElement).toHaveText([wdioExpect.stringContaining('text1'), wdioExpect.stringContaining('text2')]) + expectPromiseVoid = expectAsync(chainableElement).toHaveText([/text1/, /text2/]) + expectPromiseVoid = expectAsync(chainableElement).toHaveText(['text1', /text1/, wdioExpect.stringContaining('text3')]) + + expectPromiseVoid = expectAsync(chainableElement).not.toHaveText('text') + + // @ts-expect-error + expectVoid = expectAsync(chainableElement).toHaveText('text') + // @ts-expect-error + await expectAsync(chainableElement).toHaveText(6) + + expectPromiseVoid = expectAsync(elementArray).toHaveText('text') + expectPromiseVoid = expectAsync(elementArray).toHaveText(/text/) + expectPromiseVoid = expectAsync(elementArray).toHaveText(['text1', 'text2']) + expectPromiseVoid = expectAsync(elementArray).toHaveText([wdioExpect.stringContaining('text1'), wdioExpect.stringContaining('text2')]) + expectPromiseVoid = expectAsync(elementArray).toHaveText([/text1/, /text2/]) + expectPromiseVoid = expectAsync(elementArray).toHaveText(['text1', /text1/, wdioExpect.stringContaining('text3')]) + + expectPromiseVoid = expectAsync(elementArray).not.toHaveText('text') + + // @ts-expect-error + expectVoid = expectAsync(elementArray).toHaveText('text') + // @ts-expect-error + await expectAsync(elementArray).toHaveText(6) + + expectPromiseVoid = expectAsync(chainableArray).toHaveText('text') + expectPromiseVoid = expectAsync(chainableArray).toHaveText(/text/) + expectPromiseVoid = expectAsync(chainableArray).toHaveText(['text1', 'text2']) + expectPromiseVoid = expectAsync(chainableArray).toHaveText([wdioExpect.stringContaining('text1'), wdioExpect.stringContaining('text2')]) + expectPromiseVoid = expectAsync(chainableArray).toHaveText([/text1/, /text2/]) + expectPromiseVoid = expectAsync(chainableArray).toHaveText(['text1', /text1/, wdioExpect.stringContaining('text3')]) + + expectPromiseVoid = expectAsync(chainableArray).not.toHaveText('text') + + // @ts-expect-error + expectVoid = expectAsync(chainableArray).toHaveText('text') + // @ts-expect-error + await expectAsync(chainableArray).toHaveText(6) + + // @ts-expect-error + await expectAsync(browser).toHaveText('text') + }) + + it('should have ts errors when actual is not an element', async () => { + // @ts-expect-error + await expectAsync(browser).toHaveText('text') + // @ts-expect-error + await expectAsync(browser).not.toHaveText('text') + // @ts-expect-error + await expectAsync(true).toHaveText('text') + // @ts-expect-error + await expectAsync(true).toHaveText('text') + }) + + it('should have ts errors when actual is string or Promise', async () => { + // @ts-expect-error + await expectAsync('text').toHaveText('text') + // @ts-expect-error + await expectAsync('text').not.toHaveText('text') + // @ts-expect-error + await expectAsync(Promise.resolve('text')).toHaveText('text') + // @ts-expect-error + await expectAsync(Promise.resolve('text')).toHaveText('text') + }) + }) + + describe('toHaveHeight', () => { + it('should be supported correctly', async () => { + expectPromiseVoid = expectAsync(element).toHaveHeight(100) + expectPromiseVoid = expectAsync(element).toHaveHeight(100, { message: 'Custom error message' }) + expectPromiseVoid = expectAsync(element).not.toHaveHeight(100) + expectPromiseVoid = expectAsync(element).not.toHaveHeight(100, { message: 'Custom error message' }) + + expectPromiseVoid = expectAsync(element).toHaveHeight({ width: 100, height: 200 }) + expectPromiseVoid = expectAsync(element).toHaveHeight({ width: 100, height: 200 }, { message: 'Custom error message' }) + expectPromiseVoid = expectAsync(element).not.toHaveHeight({ width: 100, height: 200 }) + expectPromiseVoid = expectAsync(element).not.toHaveHeight({ width: 100, height: 200 }, { message: 'Custom error message' }) + + // @ts-expect-error + expectVoid = expectAsync(element).toHaveHeight(100) + // @ts-expect-error + expectVoid = expectAsync(element).not.toHaveHeight(100) + + // @ts-expect-error + expectVoid = expectAsync(element).toHaveHeight({ width: 100, height: 200 }) + // @ts-expect-error + expectVoid = expectAsync(element).not.toHaveHeight({ width: 100, height: 200 }) + + // @ts-expect-error + await expectAsync(browser).toHaveHeight(100) + }) + + it('should have ts errors when actual is string or Promise', async () => { + // @ts-expect-error + await expectAsync('text').toHaveText('text') + // @ts-expect-error + await expectAsync('text').not.toHaveText('text') + // @ts-expect-error + await expectAsync(Promise.resolve('text')).toHaveText('text') + // @ts-expect-error + await expectAsync(Promise.resolve('text')).toHaveText('text') + }) + }) + + describe('toMatchSnapshot', () => { + + it('should be supported correctly', async () => { + expectPromiseVoid = expectAsync(element).toMatchSnapshot() + expectPromiseVoid = expectAsync(element).toMatchSnapshot('test label') + expectPromiseVoid = expectAsync(element).not.toMatchSnapshot('test label') + + expectPromiseVoid = expectAsync(chainableElement).toMatchSnapshot() + expectPromiseVoid = expectAsync(chainableElement).toMatchSnapshot('test label') + expectPromiseVoid = expectAsync(chainableElement).not.toMatchSnapshot('test label') + }) + }) + + describe('toMatchInlineSnapshot', () => { + + it('should be correctly supported', async () => { + expectPromiseVoid = expectAsync(element).toMatchInlineSnapshot() + expectPromiseVoid = expectAsync(element).toMatchInlineSnapshot('test snapshot') + expectPromiseVoid = expectAsync(element).toMatchInlineSnapshot('test snapshot', 'test label') + + expectPromiseVoid = expectAsync(chainableElement).toMatchInlineSnapshot() + expectPromiseVoid = expectAsync(chainableElement).toMatchInlineSnapshot('test snapshot') + expectPromiseVoid = expectAsync(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + + //@ts-expect-error + expectVoid = expectAsync(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + }) + + it('should be correctly supported with getCSSProperty()', async () => { + expectPromiseVoid = expectAsync(element.getCSSProperty('test')).toMatchInlineSnapshot() + expectPromiseVoid = expectAsync(element.getCSSProperty('test')).toMatchInlineSnapshot('test snapshot') + expectPromiseVoid = expectAsync(element.getCSSProperty('test')).toMatchInlineSnapshot('test snapshot', 'test label') + + expectPromiseVoid = expectAsync(chainableElement.getCSSProperty('test')).toMatchInlineSnapshot() + expectPromiseVoid = expectAsync(chainableElement.getCSSProperty('test')).toMatchInlineSnapshot('test snapshot') + expectPromiseVoid = expectAsync(chainableElement.getCSSProperty('test')).toMatchInlineSnapshot('test snapshot', 'test label') + + //@ts-expect-error + expectVoid = expectAsync(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + }) + }) + + describe('toBeElementsArrayOfSize', async () => { + + it('should work correctly when actual is chainableArray', async () => { + expectPromiseVoid = expectAsync(chainableArray).toBeElementsArrayOfSize(5) + expectPromiseVoid = expectAsync(chainableArray).toBeElementsArrayOfSize({ lte: 10 }) + + // @ts-expect-error + expectVoid = expectAsync(chainableArray).toBeElementsArrayOfSize(5) + // @ts-expect-error + expectVoid = expectAsync(chainableArray).toBeElementsArrayOfSize({ lte: 10 }) + }) + + it('should not work when actual is not chainableArray', async () => { + // @ts-expect-error + await expectAsync(chainableElement).toBeElementsArrayOfSize(5) + // @ts-expect-error + await expectAsync(chainableElement).toBeElementsArrayOfSize({ lte: 10 }) + // @ts-expect-error + await expectAsync(true).toBeElementsArrayOfSize(5) + // @ts-expect-error + await expectAsync(true).toBeElementsArrayOfSize({ lte: 10 }) + }) + }) + }) + + describe('Custom matchers', () => { + describe('using `ExpectWebdriverIO` namespace augmentation', () => { + + it('should supported correctly a promise custom matcher with only chainableElement as actual', async () => { + expectPromiseVoid = expectAsync(chainableElement).toBeCustomPromise() + expectPromiseVoid = expectAsync(chainableElement).toBeCustomPromise(wdioExpect.stringContaining('test')) + expectPromiseVoid = expectAsync(chainableElement).not.toBeCustomPromise(wdioExpect.not.stringContaining('test')) + + // @ts-expect-error + expectAsync('test').toBeCustomPromise() + // @ts-expect-error + expectVoid = expectAsync(chainableElement).toBeCustomPromise() + // @ts-expect-error + expectVoid = expectAsync(chainableElement).toBeCustomPromise(wdioExpect.stringContaining('test')) + // @ts-expect-error + expectVoid = expectAsync(chainableElement).not.toBeCustomPromise(wdioExpect.stringContaining('test')) + // @ts-expect-error + expectAsync(chainableElement).toBeCustomPromise(wdioExpect.stringContaining(6)) + }) + + it('should support custom asymmetric matcher', async () => { + const expectString1 : ExpectWebdriverIO.PartialMatcher = wdioExpect.toBeCustom() + const expectString2 : ExpectWebdriverIO.PartialMatcher = wdioExpect.not.toBeCustom() + + expectPromiseVoid = expectAsync(chainableElement).toBeCustomPromise(wdioExpect.toBeCustom()) + + // @ts-expect-error + expectPromiseVoid = wdioExpect.toBeCustom() + // @ts-expect-error + expectPromiseVoid = wdioExpect.not.toBeCustom() + + //@ts-expect-error + expectVoid = expectAsync(chainableElement).toBeCustomPromise(wdioExpect.toBeCustom()) + }) + }) + + describe('using `expect` module declaration', () => { + + it('should support a simple matcher', async () => { + expectPromiseVoid = expectAsync(5).toBeWithinRange(1, 10) + + // Or as an asymmetric matcher: + expectPromiseVoid = expectAsync({ value: 5 }).toEqual({ + value: wdioExpect.toBeWithinRange(1, 10) + }) + + // @ts-expect-error + expectVoid = expectAsync(5).toBeWithinRange(1, '10') + // @ts-expect-error + expectAsync(5).toBeWithinRange('1') + }) + + it('should support a simple custom matcher with a chainable element matcher with promise', async () => { + expectPromiseVoid = expectAsync(chainableElement).toHaveSimpleCustomProperty('text') + expectPromiseVoid = expectAsync(chainableElement).toHaveSimpleCustomProperty(wdioExpect.stringContaining('text')) + expectPromiseVoid = expectAsync(chainableElement).not.toHaveSimpleCustomProperty(wdioExpect.not.stringContaining('text')) + + // Or as a custom asymmetric matcher: + expectPromiseVoid = expectAsync(chainableElement).toHaveSimpleCustomProperty( + wdioExpect.toHaveSimpleCustomProperty('string') + ) + const expectString1:string = wdioExpect.toHaveSimpleCustomProperty('string') + const expectString2:string = wdioExpect.not.toHaveSimpleCustomProperty('string') + + // @ts-expect-error + expectVoid = wdioExpect.toHaveSimpleCustomProperty(chainableElement) + // @ts-expect-error + expectVoid = wdioExpect.not.toHaveSimpleCustomProperty(chainableElement) + + // @ts-expect-error + expectVoid = wdioExpect.toHaveSimpleCustomProperty(chainableElement) + }) + + it('should support a chainable element matcher with promise', async () => { + expectPromiseVoid = expectAsync(chainableElement).toHaveCustomProperty('text') + expectPromiseVoid = expectAsync(chainableElement).toHaveCustomProperty(wdioExpect.stringContaining('text')) + expectPromiseVoid = expectAsync(chainableElement).not.toHaveCustomProperty(wdioExpect.not.stringContaining('text')) + + // Or as a custom asymmetric matcher: + expectPromiseVoid = expectAsync(chainableElement).toHaveCustomProperty( + await wdioExpect.toHaveCustomProperty(chainableElement) + ) + const expectPromiseWdioElement1: Promise> = wdioExpect.toHaveCustomProperty(chainableElement) + const expectPromiseWdioElement2: Promise> = wdioExpect.not.toHaveCustomProperty(chainableElement) + + // @ts-expect-error + expectVoid = wdioExpect.toHaveCustomProperty(chainableElement) + // @ts-expect-error + expectVoid = wdioExpect.not.toHaveCustomProperty(chainableElement) + + // @ts-expect-error + expectVoid = wdioExpect.toHaveCustomProperty(chainableElement) + // @ts-expect-error + wdioExpect.toHaveCustomProperty('test') + + await expectAsync(chainableElement).toHaveCustomProperty( + await wdioExpect.toHaveCustomProperty(chainableElement) + ) + }) + }) + }) + + describe('toBe', () => { + it('should expect void type when actual is a boolean', async () => { + expectVoid = expect(true).toBe(true) + expectVoid = expect(true).not.toBe(true) + + expectPromiseVoid = expectAsync(true).toBe(true) + expectPromiseVoid = expectAsync(true).not.toBe(true) + }) + + it('should expect Promise when actual is a chainable since toBe does not need to be awaited', async () => { + expectPromiseVoid = expectAsync(chainableElement).toBe(true) + expectPromiseVoid = expectAsync(chainableElement).not.toBe(true) + }) + + it('should expect Promise type when actual is a Promise since it is expectAsync', async () => { + const promiseBoolean = Promise.resolve(true) + + expectPromiseUnknown = expectAsync(promiseBoolean).toBe(true) + expectPromiseUnknown = expectAsync(promiseBoolean).not.toBe(true) + + expectPromiseVoid = expectAsync(promiseBoolean).toBe(true) + expectPromiseVoid = expectAsync(promiseBoolean).toBe(true) + }) + + it('should work with string', async () => { + expectPromiseUnknown = expectAsync('text').toBe(true) + expectPromiseUnknown = expectAsync('text').not.toBe(true) + expectPromiseUnknown = expectAsync('text').toBe(wdioExpect.stringContaining('text')) + expectPromiseUnknown = expectAsync('text').not.toBe(wdioExpect.stringContaining('text')) + + expectPromiseVoid = expectAsync('text').toBe(true) + expectPromiseVoid = expectAsync('text').not.toBe(true) + expectPromiseVoid = expectAsync('text').toBe(wdioExpect.stringContaining('text')) + expectPromiseVoid = expectAsync('text').not.toBe(wdioExpect.stringContaining('text')) + }) + }) + + describe('Promise type assertions', () => { + const booleanPromise: Promise = Promise.resolve(true) + + it('should expect a Promise of type', async () => { + const expectPromiseBoolean1: jasmine.AsyncMatchers = expectAsync(booleanPromise) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const expectPromiseBoolean2: jasmine.AsyncMatchers = expectAsync(booleanPromise).not + }) + + it('should work with resolves & rejects correctly', async () => { + //@ts-expect-error + expectAsync(booleanPromise).resolves.toBe(true) + //@ts-expect-error + expectAsync(booleanPromise).rejects.toBe(true) + }) + + it('should not support chainable and expect PromiseVoid with toBe', async () => { + expectPromiseVoid = expectAsync(chainableElement).toBe(true) + expectPromiseVoid = expectAsync(chainableElement).not.toBe(true) + }) + }) + + describe('Network Matchers', () => { + const promiseNetworkMock = Promise.resolve(networkMock) + + it('should not have ts errors when typing to Promise', async () => { + expectPromiseVoid = expectAsync(promiseNetworkMock).toBeRequested() + expectPromiseVoid = expectAsync(promiseNetworkMock).toBeRequestedTimes(2) + expectPromiseVoid = expectAsync(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) + + expectPromiseVoid = expectAsync(promiseNetworkMock).not.toBeRequested() + expectPromiseVoid = expectAsync(promiseNetworkMock).not.toBeRequestedTimes(2) + expectPromiseVoid = expectAsync(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) + + expectPromiseVoid = expectAsync(promiseNetworkMock).toBeRequestedWith({ + url: 'http://localhost:8080/api', + method: 'POST', + statusCode: 200, + requestHeaders: { Authorization: 'foo' }, + responseHeaders: { Authorization: 'bar' }, + postData: { title: 'foo', description: 'bar' }, + response: { success: true }, + }) + + expectPromiseVoid = expectAsync(promiseNetworkMock).toBeRequestedWith({ + url: wdioExpect.stringContaining('test'), + method: 'POST', + statusCode: 200, + requestHeaders: wdioExpect.objectContaining({ Authorization: 'foo' }), + responseHeaders: wdioExpect.objectContaining({ Authorization: 'bar' }), + postData: wdioExpect.objectContaining({ title: 'foo', description: 'bar' }), + response: wdioExpect.objectContaining({ success: true }), + }) + + expectPromiseVoid = expectAsync(promiseNetworkMock).toBeRequestedWith({ + url: wdioExpect.stringMatching(/.*\/api\/.*/i), + method: ['POST', 'PUT'], + statusCode: [401, 403], + requestHeaders: headers => headers.Authorization.startsWith('Bearer '), + postData: wdioExpect.objectContaining({ released: true, title: wdioExpect.stringContaining('foobar') }), + response: (r: { data: { items: unknown[] } }) => Array.isArray(r) && r.data.items.length === 20 + }) + }) + + it('should have ts errors when typing to void', async () => { + // @ts-expect-error + expectVoid = expectAsync(promiseNetworkMock).toBeRequested() + // @ts-expect-error + expectVoid = expectAsync(promiseNetworkMock).toBeRequestedTimes(2) // await expectAsync(mock).toBeRequestedTimes({ eq: 2 }) + // @ts-expect-error + expectVoid = expectAsync(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + + // @ts-expect-error + expectVoid = expectAsync(promiseNetworkMock).not.toBeRequested() + // @ts-expect-error + expectVoid = expectAsync(promiseNetworkMock).not.toBeRequestedTimes(2) // await expectAsync(mock).toBeRequestedTimes({ eq: 2 }) + // @ts-expect-error + expectVoid = expectAsync(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + + // @ts-expect-error + expectVoid = expectAsync(promiseNetworkMock).toBeRequestedWith({ + url: 'http://localhost:8080/api', + method: 'POST', + statusCode: 200, + requestHeaders: { Authorization: 'foo' }, + responseHeaders: { Authorization: 'bar' }, + postData: { title: 'foo', description: 'bar' }, + response: { success: true }, + }) + + // @ts-expect-error + expectVoid = expectAsync(promiseNetworkMock).toBeRequestedWith(wdioExpect.objectContaining({ + response: { success: true }, + })) + }) + }) + + describe('Expect', () => { + it('should have ts errors when using a non existing wdioExpect.function', async () => { + // @ts-expect-error + wdioExpect.unimplementedFunction() + }) + + it('should support stringContaining, anything and more', async () => { + wdioExpect.stringContaining('WebdriverIO') + wdioExpect.stringMatching(/WebdriverIO/) + wdioExpect.arrayContaining(['WebdriverIO', 'Test']) + wdioExpect.objectContaining({ name: 'WebdriverIO' }) + // Was not there but works! + wdioExpect.closeTo(5, 10) + wdioExpect.arrayContaining(['WebdriverIO', 'Test']) + // New from jest 30!! + wdioExpect.arrayOf(wdioExpect.stringContaining('WebdriverIO')) + + wdioExpect.anything() + wdioExpect.any(Function) + wdioExpect.any(Number) + wdioExpect.any(Boolean) + wdioExpect.any(String) + wdioExpect.any(Symbol) + wdioExpect.any(Date) + wdioExpect.any(Error) + + wdioExpect.not.stringContaining('WebdriverIO') + wdioExpect.not.stringMatching(/WebdriverIO/) + wdioExpect.not.arrayContaining(['WebdriverIO', 'Test']) + wdioExpect.not.objectContaining({ name: 'WebdriverIO' }) + wdioExpect.not.closeTo(5, 10) + wdioExpect.not.arrayContaining(['WebdriverIO', 'Test']) + wdioExpect.not.arrayOf(wdioExpect.stringContaining('WebdriverIO')) + }) + + describe('Soft Assertions', async () => { + const actualString: string = 'Test Page' + const actualPromiseString: Promise = Promise.resolve('Test Page') + + describe('wdioExpect.soft', () => { + it('should not need to be awaited/be a promise if actual is non-promise type', async () => { + const expectWdioMatcher1: WdioCustomMatchers = wdioExpect.soft(actualString) + expectVoid = wdioExpect.soft(actualString).toBe('Test Page') + expectVoid = wdioExpect.soft(actualString).not.toBe('Test Page') + expectVoid = wdioExpect.soft(actualString).not.toBe(wdioExpect.stringContaining('Test Page')) + + // @ts-expect-error + expectPromiseVoid = wdioExpect.soft(actualString).toBe('Test Page') + // @ts-expect-error + expectPromiseVoid = wdioExpect.soft(actualString).not.toBe('Test Page') + // @ts-expect-error + expectPromiseVoid = wdioExpect.soft(actualString).not.toBe(wdioExpect.stringContaining('Test Page')) + }) + + it('should need to be awaited/be a promise if actual is promise type', async () => { + const expectWdioMatcher1: ExpectWebdriverIO.MatchersAndInverse, Promise> = wdioExpect.soft(actualPromiseString) + expectPromiseVoid = wdioExpect.soft(actualPromiseString).toBe('Test Page') + expectPromiseVoid = wdioExpect.soft(actualPromiseString).not.toBe('Test Page') + expectPromiseVoid = wdioExpect.soft(actualPromiseString).not.toBe(wdioExpect.stringContaining('Test Page')) + + // @ts-expect-error + expectVoid = wdioExpect.soft(actualPromiseString).toBe('Test Page') + // @ts-expect-error + expectVoid = wdioExpect.soft(actualPromiseString).not.toBe('Test Page') + // @ts-expect-error + expectVoid = wdioExpect.soft(actualPromiseString).not.toBe(wdioExpect.stringContaining('Test Page')) + }) + + it('should support chainable element', async () => { + const expectElement: WdioCustomMatchers = wdioExpect.soft(element) + const expectElementChainable: WdioCustomMatchers = wdioExpect.soft(chainableElement) + + // // @ts-expect-error + // const expectElement2: WdioCustomMatchers, WebdriverIO.Element> = wdioExpect.soft(element) + // // @ts-expect-error + // const expectElementChainable2: WdioCustomMatchers, typeof chainableElement> = wdioExpect.soft(chainableElement) + }) + + it('should support chainable element with wdio Matchers', async () => { + expectPromiseVoid = wdioExpect.soft(element).toBeDisplayed() + expectPromiseVoid = wdioExpect.soft(chainableElement).toBeDisplayed() + expectPromiseVoid = wdioExpect.soft(chainableArray).toBeDisplayed() + await wdioExpect.soft(element).toBeDisplayed() + await wdioExpect.soft(chainableElement).toBeDisplayed() + await wdioExpect.soft(chainableArray).toBeDisplayed() + + expectPromiseVoid = wdioExpect.soft(element).not.toBeDisplayed() + expectPromiseVoid = wdioExpect.soft(chainableElement).not.toBeDisplayed() + expectPromiseVoid = wdioExpect.soft(chainableArray).not.toBeDisplayed() + await wdioExpect.soft(element).not.toBeDisplayed() + await wdioExpect.soft(chainableElement).not.toBeDisplayed() + await wdioExpect.soft(chainableArray).not.toBeDisplayed() + + // @ts-expect-error + expectVoid = wdioExpect.soft(element).toBeDisplayed() + // @ts-expect-error + expectVoid = wdioExpect.soft(chainableElement).toBeDisplayed() + // @ts-expect-error + expectVoid = wdioExpect.soft(chainableArray).toBeDisplayed() + + // @ts-expect-error + expectVoid = wdioExpect.soft(element).not.toBeDisplayed() + // @ts-expect-error + expectVoid = wdioExpect.soft(chainableElement).not.toBeDisplayed() + // @ts-expect-error + expectVoid = wdioExpect.soft(chainableArray).not.toBeDisplayed() + }) + it('should work with custom matcher and custom asymmetric matchers from `expect` module', async () => { + expectPromiseVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty('text') + expectPromiseVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty(wdioExpect.stringContaining('text')) + expectPromiseVoid = wdioExpect.soft(chainableElement).not.toHaveCustomProperty(wdioExpect.not.stringContaining('text')) + expectPromiseVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty( + wdioExpect.toHaveCustomProperty(chainableElement) + ) + + // @ts-expect-error + expectVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty('text') + // @ts-expect-error + expectVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty(wdioExpect.stringContaining('text')) + // @ts-expect-error + expectVoid = wdioExpect.soft(chainableElement).not.toHaveCustomProperty(wdioExpect.not.stringContaining('text')) + // @ts-expect-error + expectVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty( + wdioExpect.toHaveCustomProperty(chainableElement) + ) + + expectPromiseVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty('text') + expectPromiseVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty(wdioExpect.stringContaining('text')) + expectPromiseVoid = wdioExpect.soft(chainableElement).not.toHaveCustomProperty(wdioExpect.not.stringContaining('text')) + expectPromiseVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty( + wdioExpect.toHaveCustomProperty(chainableElement) + ) + + // @ts-expect-error + expectVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty('text') + // @ts-expect-error + expectVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty(wdioExpect.stringContaining('text')) + // @ts-expect-error + expectVoid = wdioExpect.soft(chainableElement).not.toHaveCustomProperty(wdioExpect.not.stringContaining('text')) + // @ts-expect-error + expectVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty( + wdioExpect.toHaveCustomProperty(chainableElement) + ) + }) + + it('should work with custom matcher and custom asymmetric matchers from `ExpectWebDriverIO` namespace', async () => { + expectPromiseVoid = wdioExpect.soft(chainableElement).toBeCustomPromise('text') + expectPromiseVoid = wdioExpect.soft(chainableElement).toBeCustomPromise(wdioExpect.stringContaining('text')) + expectPromiseVoid = wdioExpect.soft(chainableElement).not.toBeCustomPromise(wdioExpect.not.stringContaining('text')) + expectPromiseVoid = wdioExpect.soft(chainableElement).toBeCustomPromise( + wdioExpect.toBeCustomPromise(chainableElement) + ) + + // @ts-expect-error + expectVoid = wdioExpect.soft(chainableElement).toBeCustomPromise('text') + // @ts-expect-error + expectVoid = wdioExpect.soft(chainableElement).toBeCustomPromise(wdioExpect.stringContaining('text')) + // @ts-expect-error + expectVoid = wdioExpect.soft(chainableElement).not.toBeCustomPromise(wdioExpect.not.stringContaining('text')) + // @ts-expect-error + expectVoid = wdioExpect.soft(chainableElement).toBeCustomPromise( + wdioExpect.toBeCustomPromise(chainableElement) + ) + + expectPromiseVoid = wdioExpect.soft(chainableElement).toBeCustomPromise('text') + expectPromiseVoid = wdioExpect.soft(chainableElement).toBeCustomPromise(wdioExpect.stringContaining('text')) + expectPromiseVoid = wdioExpect.soft(chainableElement).not.toBeCustomPromise(wdioExpect.not.stringContaining('text')) + expectPromiseVoid = wdioExpect.soft(chainableElement).toBeCustomPromise( + wdioExpect.toBeCustomPromise(chainableElement) + ) + + // @ts-expect-error + expectVoid = wdioExpect.soft(chainableElement).toBeCustomPromise('text') + // @ts-expect-error + expectVoid = wdioExpect.soft(chainableElement).toBeCustomPromise(wdioExpect.stringContaining('text')) + // @ts-expect-error + expectVoid = wdioExpect.soft(chainableElement).not.toBeCustomPromise(wdioExpect.not.stringContaining('text')) + // @ts-expect-error + expectVoid = wdioExpect.soft(chainableElement).toBeCustomPromise( + wdioExpect.toBeCustomPromise(chainableElement) + ) + }) + }) + + describe('wdioExpect.getSoftFailures', () => { + it('should be of type `SoftFailure`', async () => { + const expectSoftFailure1: ExpectWebdriverIO.SoftFailure[] = wdioExpect.getSoftFailures() + + // @ts-expect-error + expectVoid = wdioExpect.getSoftFailures() + }) + }) + + describe('wdioExpect.assertSoftFailures', () => { + it('should be of type void', async () => { + expectVoid = wdioExpect.assertSoftFailures() + + // @ts-expect-error + expectPromiseVoid = wdioExpect.assertSoftFailures() + }) + }) + + describe('wdioExpect.clearSoftFailures', () => { + it('should be of type void', async () => { + expectVoid = wdioExpect.clearSoftFailures() + + // @ts-expect-error + expectPromiseVoid = wdioExpect.clearSoftFailures() + }) + }) + }) + }) + + describe('Asymmetric matchers', () => { + const string: string = 'WebdriverIO is a test framework' + const array: string[] = ['WebdriverIO', 'Test'] + const object: { name: string } = { name: 'WebdriverIO' } + const number: number = 1 + + it('should have no ts error using asymmetric matchers', async () => { + expectAsync(string).toEqual(wdioExpect.stringContaining('WebdriverIO')) + expectAsync(array).toEqual(wdioExpect.arrayContaining(['WebdriverIO', 'Test'])) + expectAsync(object).toEqual(wdioExpect.objectContaining({ name: 'WebdriverIO' })) + // This one is tested and is working correctly, surprisingly! + expectAsync(number).toEqual(wdioExpect.closeTo(1.0001, 0.0001)) + // New from jest 30, should work! + expectAsync(['apple', 'banana', 'cherry']).toEqual(wdioExpect.arrayOf(wdioExpect.any(String))) + }) + }) + + describe('Jasmine only cases', () => { + let expectPromiseLikeVoid: PromiseLike + it('should support expectAsync correctly for non wdio types', async () => { + expectPromiseLikeVoid = expectAsync(Promise.resolve('test')).toBeResolved() + expectPromiseLikeVoid = expectAsync(Promise.resolve('test')).toBeResolvedTo(wdioExpect.stringContaining('test error')) + expectPromiseLikeVoid = expectAsync(Promise.resolve('test')).not.toBeResolvedTo(wdioExpect.not.stringContaining('test error')) + expectPromiseLikeVoid = expectAsync(Promise.resolve('test')).toBeRejected() + expectPromiseLikeVoid = expectAsync(Promise.resolve('test')).not.toBeResolved() + expectPromiseLikeVoid = expectAsync(Promise.resolve('test')).not.toBeRejected() + + // @ts-expect-error + expectVoid = expectAsync(Promise.resolve('test')).toBeResolved() + // @ts-expect-error + expectVoid = expectAsync(Promise.resolve('test')).toBeRejected() + + // @ts-expect-error + expectVoid = expectAsync(Promise.resolve('test')).toBeResolved() + }) + it('jasmine special asymmetric matcher', async () => { + // Note: Even though the below is valid syntax, jasmine prefix for asymmetric matchers is not supported by wdioExpect. + expectAsync({}).toEqual(jasmine.any(Object)) + expectAsync(12).toEqual(jasmine.any(Number)) + }) + + }) +}) diff --git a/test-types/jasmine/customMatchers/customMatchers-module-expect.d.ts b/test-types/jasmine/customMatchers/customMatchers-module-expect.d.ts new file mode 100644 index 000000000..125fb77c7 --- /dev/null +++ b/test-types/jasmine/customMatchers/customMatchers-module-expect.d.ts @@ -0,0 +1,26 @@ +import 'expect' + +/** + * Custom matchers under the `expect` module. + * @see {@link https://jestjs.io/docs/expect#expectextendmatchers} + */ +declare module 'expect' { + interface AsymmetricMatchers { + toBeWithinRange(floor: number, ceiling: number): void + toHaveSimpleCustomProperty(string: string): string + toHaveCustomProperty(element: ChainablePromiseElement | WebdriverIO.Element): Promise> + } + + interface Matchers { + toBeWithinRange(floor: number, ceiling: number): R + toHaveSimpleCustomProperty(string: string | ExpectWebdriverIO.PartialMatcher): R + toHaveCustomProperty: + // Useful to typecheck the custom matcher so it is only used on elements + T extends ChainablePromiseElement | WebdriverIO.Element ? + (test: string | ExpectWebdriverIO.PartialMatcher | + // Needed for the custom asymmetric matcher defined above to be typed correctly + Promise>) + // Using `never` blocks the call on non-element types + => R : never; + } +} \ No newline at end of file diff --git a/test-types/jasmine/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts b/test-types/jasmine/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts new file mode 100644 index 000000000..89423c9d3 --- /dev/null +++ b/test-types/jasmine/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts @@ -0,0 +1,14 @@ +/** + * Custom matchers under the `ExpectWebdriverIO` namespace. + * @see {@link https://webdriver.io/docs/custommatchers/#typescript-support} + */ +declare namespace ExpectWebdriverIO { + interface AsymmetricMatchers { + toBeCustom(): ExpectWebdriverIO.PartialMatcher; + toBeCustomPromise(chainableElement: ChainablePromiseElement): Promise>; + } + interface Matchers { + toBeCustom(): R; + toBeCustomPromise: T extends ChainablePromiseElement ? (expected?: string | ExpectWebdriverIO.PartialMatcher | Promise>) => R : never; + } +} \ No newline at end of file diff --git a/test-types/jasmine/tsconfig.json b/test-types/jasmine/tsconfig.json index 6407f175a..3b199e0ef 100644 --- a/test-types/jasmine/tsconfig.json +++ b/test-types/jasmine/tsconfig.json @@ -1,14 +1,14 @@ { "compilerOptions": { - "outDir": "dist", + "noEmit": true, "noImplicitAny": true, - "target": "ES2020", - "module": "Node16", + "target": "es2022", + "module": "node18", "skipLibCheck": true, "types": [ - "@types/jest", - "expect-webdriverio/jest", - "@wdio/globals/types" + "@types/jasmine", + "../../jasmine.d.ts", + "../../jasmine-wdio-expect-async.d.ts" ] } -} +} \ No newline at end of file diff --git a/test-types/jasmine/types-jasmine.test.ts b/test-types/jasmine/types-jasmine.test.ts new file mode 100644 index 000000000..7b9749f77 --- /dev/null +++ b/test-types/jasmine/types-jasmine.test.ts @@ -0,0 +1,831 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +// Desired since we do not want to overwrite the global `expect` from Jasmine +import { expect as wdioExpect } from 'expect-webdriverio' +describe('type assertions', () => { + const chainableElement = {} as unknown as ChainablePromiseElement + const chainableArray = {} as ChainablePromiseArray + + const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element + const elementArray: WebdriverIO.ElementArray = [] as unknown as WebdriverIO.ElementArray + + const networkMock: WebdriverIO.Mock = {} as unknown as WebdriverIO.Mock + + // Type assertions + let expectPromiseVoid: Promise + let expectVoid: void + + describe('Browser', () => { + const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser + + describe('toHaveUrl', () => { + it('should be supported correctly', async () => { + expectPromiseVoid = wdioExpect(browser).toHaveUrl('https://example.com') + expectPromiseVoid = wdioExpect(browser).not.toHaveUrl('https://example.com') + + // Asymmetric matchers + expectPromiseVoid = wdioExpect(browser).toHaveUrl(wdioExpect.stringContaining('WebdriverIO')) + expectPromiseVoid = wdioExpect(browser).toHaveUrl(wdioExpect.not.stringContaining('WebdriverIO')) + expectPromiseVoid = wdioExpect(browser).toHaveUrl(wdioExpect.any(String)) + expectPromiseVoid = wdioExpect(browser).toHaveUrl(wdioExpect.anything()) + + // @ts-expect-error + expectVoid = wdioExpect(browser).toHaveUrl('https://example.com') + // @ts-expect-error + expectVoid = wdioExpect(browser).not.toHaveUrl('https://example.com') + // @ts-expect-error + expectVoid = wdioExpect(browser).toHaveUrl(wdioExpect.stringContaining('WebdriverIO')) + + // @ts-expect-error + await wdioExpect(browser).toHaveUrl(6) + }) + + it('should have ts errors when actual is not a Browser element', async () => { + // @ts-expect-error + await wdioExpect(element).toHaveUrl('https://example.com') + // @ts-expect-error + await wdioExpect(element).not.toHaveUrl('https://example.com') + // @ts-expect-error + await wdioExpect(true).toHaveUrl('https://example.com') + // @ts-expect-error + await wdioExpect(true).not.toHaveUrl('https://example.com') + }) + }) + + describe('toHaveTitle', () => { + it('should be supported correctly', async () => { + expectPromiseVoid = wdioExpect(browser).toHaveTitle('https://example.com') + expectPromiseVoid = wdioExpect(browser).not.toHaveTitle('https://example.com') + + // Asymmetric matchers + expectPromiseVoid = wdioExpect(browser).toHaveTitle(wdioExpect.stringContaining('WebdriverIO')) + expectPromiseVoid = wdioExpect(browser).toHaveTitle(wdioExpect.any(String)) + expectPromiseVoid = wdioExpect(browser).toHaveTitle(wdioExpect.anything()) + + // @ts-expect-error + expectVoid = wdioExpect(browser).toHaveTitle('https://example.com') + // @ts-expect-error + expectVoid = wdioExpect(browser).not.toHaveTitle('https://example.com') + // @ts-expect-error + expectVoid = wdioExpect(browser).toHaveTitle(wdioExpect.stringContaining('WebdriverIO')) + }) + + it('should have ts errors when actual is not a Browser element', async () => { + // @ts-expect-error + await wdioExpect(element).toHaveTitle('https://example.com') + // @ts-expect-error + await wdioExpect(element).not.toHaveTitle('https://example.com') + // @ts-expect-error + await wdioExpect(true).toHaveTitle('https://example.com') + // @ts-expect-error + await wdioExpect(true).not.toHaveTitle('https://example.com') + }) + }) + }) + + describe('element', () => { + + describe('toBeDisabled', () => { + it('should be supported correctly', async () => { + // Element + expectPromiseVoid = wdioExpect(element).toBeDisabled() + expectPromiseVoid = wdioExpect(element).not.toBeDisabled() + + // Element array + expectPromiseVoid = wdioExpect(elementArray).toBeDisabled() + expectPromiseVoid = wdioExpect(elementArray).not.toBeDisabled() + + // Chainable element + expectPromiseVoid = wdioExpect(chainableElement).toBeDisabled() + expectPromiseVoid = wdioExpect(chainableElement).not.toBeDisabled() + + // Chainable element array + expectPromiseVoid = wdioExpect(chainableArray).toBeDisabled() + expectPromiseVoid = wdioExpect(chainableArray).not.toBeDisabled() + + // @ts-expect-error + expectVoid = wdioExpect(element).toBeDisabled() + // @ts-expect-error + expectVoid = wdioExpect(element).not.toBeDisabled() + }) + + it('should have ts errors when actual is not an element', async () => { + // @ts-expect-error + await wdioExpect(browser).toBeDisabled() + // @ts-expect-error + await wdioExpect(browser).not.toBeDisabled() + // @ts-expect-error + await wdioExpect(true).toBeDisabled() + // @ts-expect-error + await wdioExpect(true).not.toBeDisabled() + }) + }) + + describe('toHaveText', () => { + it('should be supported correctly', async () => { + expectPromiseVoid = wdioExpect(element).toHaveText('text') + expectPromiseVoid = wdioExpect(element).toHaveText(/text/) + expectPromiseVoid = wdioExpect(element).toHaveText(['text1', 'text2']) + expectPromiseVoid = wdioExpect(element).toHaveText([wdioExpect.stringContaining('text1'), wdioExpect.stringContaining('text2')]) + expectPromiseVoid = wdioExpect(element).toHaveText([/text1/, /text2/]) + expectPromiseVoid = wdioExpect(element).toHaveText(['text1', /text1/, wdioExpect.stringContaining('text3')]) + await wdioExpect(element).toHaveText( + 'My-Ex-Am-Ple', + { + replace: [[/-/g, ' '], [/[A-Z]+/g, (match: string) => match.toLowerCase()]] + } + ) + + expectPromiseVoid = wdioExpect(element).not.toHaveText('text') + + // @ts-expect-error + expectVoid = wdioExpect(element).toHaveText('text') + // @ts-expect-error + await wdioExpect(element).toHaveText(6) + + expectPromiseVoid = wdioExpect(chainableElement).toHaveText('text') + expectPromiseVoid = wdioExpect(chainableElement).toHaveText(/text/) + expectPromiseVoid = wdioExpect(chainableElement).toHaveText(['text1', 'text2']) + expectPromiseVoid = wdioExpect(chainableElement).toHaveText([wdioExpect.stringContaining('text1'), wdioExpect.stringContaining('text2')]) + expectPromiseVoid = wdioExpect(chainableElement).toHaveText([/text1/, /text2/]) + expectPromiseVoid = wdioExpect(chainableElement).toHaveText(['text1', /text1/, wdioExpect.stringContaining('text3')]) + + expectPromiseVoid = wdioExpect(chainableElement).not.toHaveText('text') + + // @ts-expect-error + expectVoid = wdioExpect(chainableElement).toHaveText('text') + // @ts-expect-error + await wdioExpect(chainableElement).toHaveText(6) + + expectPromiseVoid = wdioExpect(elementArray).toHaveText('text') + expectPromiseVoid = wdioExpect(elementArray).toHaveText(/text/) + expectPromiseVoid = wdioExpect(elementArray).toHaveText(['text1', 'text2']) + expectPromiseVoid = wdioExpect(elementArray).toHaveText([wdioExpect.stringContaining('text1'), wdioExpect.stringContaining('text2')]) + expectPromiseVoid = wdioExpect(elementArray).toHaveText([/text1/, /text2/]) + expectPromiseVoid = wdioExpect(elementArray).toHaveText(['text1', /text1/, wdioExpect.stringContaining('text3')]) + + expectPromiseVoid = wdioExpect(elementArray).not.toHaveText('text') + + // @ts-expect-error + expectVoid = wdioExpect(elementArray).toHaveText('text') + // @ts-expect-error + await wdioExpect(elementArray).toHaveText(6) + + expectPromiseVoid = wdioExpect(chainableArray).toHaveText('text') + expectPromiseVoid = wdioExpect(chainableArray).toHaveText(/text/) + expectPromiseVoid = wdioExpect(chainableArray).toHaveText(['text1', 'text2']) + expectPromiseVoid = wdioExpect(chainableArray).toHaveText([wdioExpect.stringContaining('text1'), wdioExpect.stringContaining('text2')]) + expectPromiseVoid = wdioExpect(chainableArray).toHaveText([/text1/, /text2/]) + expectPromiseVoid = wdioExpect(chainableArray).toHaveText(['text1', /text1/, wdioExpect.stringContaining('text3')]) + + expectPromiseVoid = wdioExpect(chainableArray).not.toHaveText('text') + + // @ts-expect-error + expectVoid = wdioExpect(chainableArray).toHaveText('text') + // @ts-expect-error + await wdioExpect(chainableArray).toHaveText(6) + + // @ts-expect-error + await wdioExpect(browser).toHaveText('text') + }) + + it('should have ts errors when actual is not an element', async () => { + // @ts-expect-error + await wdioExpect(browser).toHaveText('text') + // @ts-expect-error + await wdioExpect(browser).not.toHaveText('text') + // @ts-expect-error + await wdioExpect(true).toHaveText('text') + // @ts-expect-error + await wdioExpect(true).toHaveText('text') + }) + + it('should have ts errors when actual is string or Promise', async () => { + // @ts-expect-error + await wdioExpect('text').toHaveText('text') + // @ts-expect-error + await wdioExpect('text').not.toHaveText('text') + // @ts-expect-error + await wdioExpect(Promise.resolve('text')).toHaveText('text') + // @ts-expect-error + await wdioExpect(Promise.resolve('text')).toHaveText('text') + }) + }) + + describe('toHaveHeight', () => { + it('should be supported correctly', async () => { + expectPromiseVoid = wdioExpect(element).toHaveHeight(100) + expectPromiseVoid = wdioExpect(element).toHaveHeight(100, { message: 'Custom error message' }) + expectPromiseVoid = wdioExpect(element).not.toHaveHeight(100) + expectPromiseVoid = wdioExpect(element).not.toHaveHeight(100, { message: 'Custom error message' }) + + expectPromiseVoid = wdioExpect(element).toHaveHeight({ width: 100, height: 200 }) + expectPromiseVoid = wdioExpect(element).toHaveHeight({ width: 100, height: 200 }, { message: 'Custom error message' }) + expectPromiseVoid = wdioExpect(element).not.toHaveHeight({ width: 100, height: 200 }) + expectPromiseVoid = wdioExpect(element).not.toHaveHeight({ width: 100, height: 200 }, { message: 'Custom error message' }) + + // @ts-expect-error + expectVoid = wdioExpect(element).toHaveHeight(100) + // @ts-expect-error + expectVoid = wdioExpect(element).not.toHaveHeight(100) + + // @ts-expect-error + expectVoid = wdioExpect(element).toHaveHeight({ width: 100, height: 200 }) + // @ts-expect-error + expectVoid = wdioExpect(element).not.toHaveHeight({ width: 100, height: 200 }) + + // @ts-expect-error + await wdioExpect(browser).toHaveHeight(100) + }) + + it('should have ts errors when actual is string or Promise', async () => { + // @ts-expect-error + await wdioExpect('text').toHaveText('text') + // @ts-expect-error + await wdioExpect('text').not.toHaveText('text') + // @ts-expect-error + await wdioExpect(Promise.resolve('text')).toHaveText('text') + // @ts-expect-error + await wdioExpect(Promise.resolve('text')).toHaveText('text') + }) + }) + + describe('toMatchSnapshot', () => { + + it('should be supported correctly', async () => { + expectVoid = wdioExpect(element).toMatchSnapshot() + expectVoid = wdioExpect(element).toMatchSnapshot('test label') + expectVoid = wdioExpect(element).not.toMatchSnapshot('test label') + + expectPromiseVoid = wdioExpect(chainableElement).toMatchSnapshot() + expectPromiseVoid = wdioExpect(chainableElement).toMatchSnapshot('test label') + expectPromiseVoid = wdioExpect(chainableElement).not.toMatchSnapshot('test label') + + //@ts-expect-error + expectPromiseVoid = wdioExpect(element).toMatchSnapshot() + //@ts-expect-error + expectPromiseVoid = wdioExpect(element).not.toMatchSnapshot() + //@ts-expect-error + expectVoid = wdioExpect(chainableElement).toMatchSnapshot() + //@ts-expect-error + expectVoid = wdioExpect(chainableElement).not.toMatchSnapshot() + }) + }) + + describe('toMatchInlineSnapshot', () => { + + it('should be correctly supported', async () => { + expectVoid = wdioExpect(element).toMatchInlineSnapshot() + expectVoid = wdioExpect(element).toMatchInlineSnapshot('test snapshot') + expectVoid = wdioExpect(element).toMatchInlineSnapshot('test snapshot', 'test label') + + expectPromiseVoid = wdioExpect(chainableElement).toMatchInlineSnapshot() + expectPromiseVoid = wdioExpect(chainableElement).toMatchInlineSnapshot('test snapshot') + expectPromiseVoid = wdioExpect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + + //@ts-expect-error + expectPromiseVoid = wdioExpect(element).toMatchInlineSnapshot() + //@ts-expect-error + expectVoid = wdioExpect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + }) + + it('should be correctly supported with getCSSProperty()', async () => { + expectPromiseVoid = wdioExpect(element.getCSSProperty('test')).toMatchInlineSnapshot() + expectPromiseVoid = wdioExpect(element.getCSSProperty('test')).toMatchInlineSnapshot('test snapshot') + expectPromiseVoid = wdioExpect(element.getCSSProperty('test')).toMatchInlineSnapshot('test snapshot', 'test label') + + expectPromiseVoid = wdioExpect(chainableElement.getCSSProperty('test')).toMatchInlineSnapshot() + expectPromiseVoid = wdioExpect(chainableElement.getCSSProperty('test')).toMatchInlineSnapshot('test snapshot') + expectPromiseVoid = wdioExpect(chainableElement.getCSSProperty('test')).toMatchInlineSnapshot('test snapshot', 'test label') + + //@ts-expect-error + expectPromiseVoid = wdioExpect(element).toMatchInlineSnapshot() + //@ts-expect-error + expectVoid = wdioExpect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + }) + }) + + describe('toBeElementsArrayOfSize', async () => { + + it('should work correctly when actual is chainableArray', async () => { + expectPromiseVoid = wdioExpect(chainableArray).toBeElementsArrayOfSize(5) + expectPromiseVoid = wdioExpect(chainableArray).toBeElementsArrayOfSize({ lte: 10 }) + + // @ts-expect-error + expectVoid = wdioExpect(chainableArray).toBeElementsArrayOfSize(5) + // @ts-expect-error + expectVoid = wdioExpect(chainableArray).toBeElementsArrayOfSize({ lte: 10 }) + }) + + it('should not work when actual is not chainableArray', async () => { + // @ts-expect-error + await wdioExpect(chainableElement).toBeElementsArrayOfSize(5) + // @ts-expect-error + await wdioExpect(chainableElement).toBeElementsArrayOfSize({ lte: 10 }) + // @ts-expect-error + await wdioExpect(true).toBeElementsArrayOfSize(5) + // @ts-expect-error + await wdioExpect(true).toBeElementsArrayOfSize({ lte: 10 }) + }) + }) + }) + + describe('Custom matchers', () => { + describe('using `ExpectWebdriverIO` namespace augmentation', () => { + it('should supported correctly a non-promise custom matcher', async () => { + expectPromiseVoid = wdioExpect('test').toBeCustom() + expectPromiseVoid = wdioExpect('test').not.toBeCustom() + + // @ts-expect-error + expectVoid = wdioExpect('test').toBeCustom() + // @ts-expect-error + expectVoid = wdioExpect('test').not.toBeCustom() + + expectPromiseVoid = wdioExpect(1).toBeWithinRange(0, 2) + }) + + it('should supported correctly a promise custom matcher with only chainableElement as actual', async () => { + expectPromiseVoid = wdioExpect(chainableElement).toBeCustomPromise() + expectPromiseVoid = wdioExpect(chainableElement).toBeCustomPromise(wdioExpect.stringContaining('test')) + expectPromiseVoid = wdioExpect(chainableElement).not.toBeCustomPromise(wdioExpect.not.stringContaining('test')) + + // @ts-expect-error + wdioExpect('test').toBeCustomPromise() + // @ts-expect-error + expectVoid = wdioExpect(chainableElement).toBeCustomPromise() + // @ts-expect-error + expectVoid = wdioExpect(chainableElement).toBeCustomPromise(wdioExpect.stringContaining('test')) + // @ts-expect-error + expectVoid = wdioExpect(chainableElement).not.toBeCustomPromise(wdioExpect.stringContaining('test')) + // @ts-expect-error + wdioExpect(chainableElement).toBeCustomPromise(wdioExpect.stringContaining(6)) + }) + + it('should support custom asymmetric matcher', async () => { + const expectString1 : ExpectWebdriverIO.PartialMatcher = wdioExpect.toBeCustom() + const expectString2 : ExpectWebdriverIO.PartialMatcher = wdioExpect.not.toBeCustom() + + expectPromiseVoid = wdioExpect(chainableElement).toBeCustomPromise(wdioExpect.toBeCustom()) + + // @ts-expect-error + expectPromiseVoid = wdioExpect.toBeCustom() + // @ts-expect-error + expectPromiseVoid = wdioExpect.not.toBeCustom() + + //@ts-expect-error + expectVoid = wdioExpect(chainableElement).toBeCustomPromise(wdioExpect.toBeCustom()) + }) + }) + + describe('using `expect` module declaration', () => { + + it('should support a simple matcher', async () => { + expectPromiseVoid = wdioExpect(5).toBeWithinRange(1, 10) + + // Or as an asymmetric matcher: + expectPromiseVoid = wdioExpect({ value: 5 }).toEqual({ + value: wdioExpect.toBeWithinRange(1, 10) + }) + + // @ts-expect-error + expectVoid = wdioExpect(5).toBeWithinRange(1, '10') + // @ts-expect-error + expectPromiseVoid = wdioExpect(5).toBeWithinRange('1') + }) + + it('should support a simple custom matcher with a chainable element matcher with promise', async () => { + wdioExpect(chainableElement) + expectPromiseVoid = wdioExpect(chainableElement).toHaveSimpleCustomProperty('text') + expectPromiseVoid = wdioExpect(chainableElement).toHaveSimpleCustomProperty(wdioExpect.stringContaining('text')) + expectPromiseVoid = wdioExpect(chainableElement).not.toHaveSimpleCustomProperty(wdioExpect.not.stringContaining('text')) + + // Or as a custom asymmetric matcher: + expectPromiseVoid = wdioExpect(chainableElement).toHaveSimpleCustomProperty( + wdioExpect.toHaveSimpleCustomProperty('string') + ) + const expectString1:string = wdioExpect.toHaveSimpleCustomProperty('string') + const expectString2:string = wdioExpect.not.toHaveSimpleCustomProperty('string') + + // @ts-expect-error + expectVoid = wdioExpect.toHaveSimpleCustomProperty(chainableElement) + // @ts-expect-error + expectVoid = wdioExpect.not.toHaveSimpleCustomProperty(chainableElement) + + // @ts-expect-error + expectVoid = wdioExpect.toHaveSimpleCustomProperty(chainableElement) + }) + + it('should support a chainable element matcher with promise', async () => { + expectPromiseVoid = wdioExpect(chainableElement).toHaveCustomProperty('text') + expectPromiseVoid = wdioExpect(chainableElement).toHaveCustomProperty(wdioExpect.stringContaining('text')) + expectPromiseVoid = wdioExpect(chainableElement).not.toHaveCustomProperty(wdioExpect.not.stringContaining('text')) + + // Or as a custom asymmetric matcher: + expectPromiseVoid = wdioExpect(chainableElement).toHaveCustomProperty( + await wdioExpect.toHaveCustomProperty(chainableElement) + ) + const expectPromiseWdioElement1: Promise> = wdioExpect.toHaveCustomProperty(chainableElement) + const expectPromiseWdioElement2: Promise> = wdioExpect.not.toHaveCustomProperty(chainableElement) + + // @ts-expect-error + expectVoid = wdioExpect.toHaveCustomProperty(chainableElement) + // @ts-expect-error + expectVoid = wdioExpect.not.toHaveCustomProperty(chainableElement) + + // @ts-expect-error + expectVoid = wdioExpect.toHaveCustomProperty(chainableElement) + // @ts-expect-error + wdioExpect.toHaveCustomProperty('test') + + await wdioExpect(chainableElement).toHaveCustomProperty( + await wdioExpect.toHaveCustomProperty(chainableElement) + ) + }) + }) + }) + + describe('toBe', () => { + + it('should expect void type when actual is a boolean', async () => { + expectPromiseVoid = wdioExpect(true).toBe(true) + expectPromiseVoid = wdioExpect(true).not.toBe(true) + + //@ts-expect-error + expectVoid = wdioExpect(true).toBe(true) + //@ts-expect-error + expectVoid = wdioExpect(true).not.toBe(true) + }) + + it('should not expect Promise when actual is a chainable since toBe does not need to be awaited', async () => { + expectPromiseVoid = wdioExpect(chainableElement).toBe(true) + expectPromiseVoid = wdioExpect(chainableElement).not.toBe(true) + + //@ts-expect-error + expectVoid = wdioExpect(chainableElement).toBe(true) + //@ts-expect-error + expectVoid = wdioExpect(chainableElement).not.toBe(true) + }) + + it('should still expect void type when actual is a Promise since we do not overload them', async () => { + const promiseBoolean = Promise.resolve(true) + + expectPromiseVoid = wdioExpect(promiseBoolean).toBe(true) + expectPromiseVoid = wdioExpect(promiseBoolean).not.toBe(true) + + //@ts-expect-error + expectVoid = wdioExpect(promiseBoolean).toBe(true) + //@ts-expect-error + expectVoid = wdioExpect(promiseBoolean).toBe(true) + }) + + it('should work with string', async () => { + expectPromiseVoid = wdioExpect('text').toBe(true) + expectPromiseVoid = wdioExpect('text').not.toBe(true) + expectPromiseVoid = wdioExpect('text').toBe(wdioExpect.stringContaining('text')) + expectPromiseVoid = wdioExpect('text').not.toBe(wdioExpect.stringContaining('text')) + + //@ts-expect-error + expectVoid = wdioExpect('text').toBe(true) + //@ts-expect-error + expectVoid = wdioExpect('text').not.toBe(true) + //@ts-expect-error + expectVoid = wdioExpect('text').toBe(wdioExpect.stringContaining('text')) + //@ts-expect-error + expectVoid = wdioExpect('text').not.toBe(wdioExpect.stringContaining('text')) + }) + }) + + describe('Promise type assertions', () => { + const booleanPromise: Promise = Promise.resolve(true) + + it('should not compile', async () => { + //@ts-expect-error + wdioExpect(booleanPromise).resolves.toBe(true) + //@ts-expect-error + wdioExpect(booleanPromise).rejects.toBe(true) + }) + + }) + + describe('Network Matchers', () => { + const promiseNetworkMock = Promise.resolve(networkMock) + + it('should not have ts errors when typing to Promise', async () => { + expectPromiseVoid = wdioExpect(promiseNetworkMock).toBeRequested() + expectPromiseVoid = wdioExpect(promiseNetworkMock).toBeRequestedTimes(2) + expectPromiseVoid = wdioExpect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) + + expectPromiseVoid = wdioExpect(promiseNetworkMock).not.toBeRequested() + expectPromiseVoid = wdioExpect(promiseNetworkMock).not.toBeRequestedTimes(2) + expectPromiseVoid = wdioExpect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) + + expectPromiseVoid = wdioExpect(promiseNetworkMock).toBeRequestedWith({ + url: 'http://localhost:8080/api', + method: 'POST', + statusCode: 200, + requestHeaders: { Authorization: 'foo' }, + responseHeaders: { Authorization: 'bar' }, + postData: { title: 'foo', description: 'bar' }, + response: { success: true }, + }) + + expectPromiseVoid = wdioExpect(promiseNetworkMock).toBeRequestedWith({ + url: wdioExpect.stringContaining('test'), + method: 'POST', + statusCode: 200, + requestHeaders: wdioExpect.objectContaining({ Authorization: 'foo' }), + responseHeaders: wdioExpect.objectContaining({ Authorization: 'bar' }), + postData: wdioExpect.objectContaining({ title: 'foo', description: 'bar' }), + response: wdioExpect.objectContaining({ success: true }), + }) + + expectPromiseVoid = wdioExpect(promiseNetworkMock).toBeRequestedWith({ + url: wdioExpect.stringMatching(/.*\/api\/.*/i), + method: ['POST', 'PUT'], + statusCode: [401, 403], + requestHeaders: headers => headers.Authorization.startsWith('Bearer '), + postData: wdioExpect.objectContaining({ released: true, title: wdioExpect.stringContaining('foobar') }), + response: (r: { data: { items: unknown[] } }) => Array.isArray(r) && r.data.items.length === 20 + }) + }) + + it('should have ts errors when typing to void', async () => { + // @ts-expect-error + expectVoid = wdioExpect(promiseNetworkMock).toBeRequested() + // @ts-expect-error + expectVoid = wdioExpect(promiseNetworkMock).toBeRequestedTimes(2) // await wdioExpect(mock).toBeRequestedTimes({ eq: 2 }) + // @ts-expect-error + expectVoid = wdioExpect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + + // @ts-expect-error + expectVoid = wdioExpect(promiseNetworkMock).not.toBeRequested() + // @ts-expect-error + expectVoid = wdioExpect(promiseNetworkMock).not.toBeRequestedTimes(2) // await wdioExpect(mock).toBeRequestedTimes({ eq: 2 }) + // @ts-expect-error + expectVoid = wdioExpect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + + // @ts-expect-error + expectVoid = wdioExpect(promiseNetworkMock).toBeRequestedWith({ + url: 'http://localhost:8080/api', + method: 'POST', + statusCode: 200, + requestHeaders: { Authorization: 'foo' }, + responseHeaders: { Authorization: 'bar' }, + postData: { title: 'foo', description: 'bar' }, + response: { success: true }, + }) + + // @ts-expect-error + expectVoid = wdioExpect(promiseNetworkMock).toBeRequestedWith(wdioExpect.objectContaining({ + response: { success: true }, + })) + }) + }) + + describe('Expect', () => { + it('should have ts errors when using a non existing wdioExpect.function', async () => { + // @ts-expect-error + wdioExpect.unimplementedFunction() + }) + + it('should support stringContaining, anything and more', async () => { + wdioExpect.stringContaining('WebdriverIO') + wdioExpect.stringMatching(/WebdriverIO/) + wdioExpect.arrayContaining(['WebdriverIO', 'Test']) + wdioExpect.objectContaining({ name: 'WebdriverIO' }) + // Was not there but works! + wdioExpect.closeTo(5, 10) + wdioExpect.arrayContaining(['WebdriverIO', 'Test']) + // New from jest 30!! + wdioExpect.arrayOf(wdioExpect.stringContaining('WebdriverIO')) + + wdioExpect.anything() + wdioExpect.any(Function) + wdioExpect.any(Number) + wdioExpect.any(Boolean) + wdioExpect.any(String) + wdioExpect.any(Symbol) + wdioExpect.any(Date) + wdioExpect.any(Error) + + wdioExpect.not.stringContaining('WebdriverIO') + wdioExpect.not.stringMatching(/WebdriverIO/) + wdioExpect.not.arrayContaining(['WebdriverIO', 'Test']) + wdioExpect.not.objectContaining({ name: 'WebdriverIO' }) + wdioExpect.not.closeTo(5, 10) + wdioExpect.not.arrayContaining(['WebdriverIO', 'Test']) + wdioExpect.not.arrayOf(wdioExpect.stringContaining('WebdriverIO')) + }) + + describe('Soft Assertions', async () => { + const actualString: string = 'Test Page' + const actualPromiseString: Promise = Promise.resolve('Test Page') + + describe('wdioExpect.soft', () => { + it('should not need to be awaited/be a promise if actual is non-promise type', async () => { + const expectWdioMatcher1: WdioCustomMatchers = wdioExpect.soft(actualString) + expectVoid = wdioExpect.soft(actualString).toBe('Test Page') + expectVoid = wdioExpect.soft(actualString).not.toBe('Test Page') + expectVoid = wdioExpect.soft(actualString).not.toBe(wdioExpect.stringContaining('Test Page')) + + // @ts-expect-error + expectPromiseVoid = wdioExpect.soft(actualString).toBe('Test Page') + // @ts-expect-error + expectPromiseVoid = wdioExpect.soft(actualString).not.toBe('Test Page') + // @ts-expect-error + expectPromiseVoid = wdioExpect.soft(actualString).not.toBe(wdioExpect.stringContaining('Test Page')) + }) + + it('should need to be awaited/be a promise if actual is promise type', async () => { + const expectWdioMatcher1: ExpectWebdriverIO.MatchersAndInverse, Promise> = wdioExpect.soft(actualPromiseString) + expectPromiseVoid = wdioExpect.soft(actualPromiseString).toBe('Test Page') + expectPromiseVoid = wdioExpect.soft(actualPromiseString).not.toBe('Test Page') + expectPromiseVoid = wdioExpect.soft(actualPromiseString).not.toBe(wdioExpect.stringContaining('Test Page')) + + // @ts-expect-error + expectVoid = wdioExpect.soft(actualPromiseString).toBe('Test Page') + // @ts-expect-error + expectVoid = wdioExpect.soft(actualPromiseString).not.toBe('Test Page') + // @ts-expect-error + expectVoid = wdioExpect.soft(actualPromiseString).not.toBe(wdioExpect.stringContaining('Test Page')) + }) + + it('should support chainable element', async () => { + const expectElement: ExpectWebdriverIO.MatchersAndInverse = wdioExpect.soft(element) + const expectElementChainable: ExpectWebdriverIO.MatchersAndInverse = wdioExpect.soft(chainableElement) + + // @ts-expect-error + const expectElement2: ExpectWebdriverIO.MatchersAndInverse, WebdriverIO.Element> = wdioExpect.soft(element) + // @ts-expect-error + const expectElementChainable2: ExpectWebdriverIO.MatchersAndInverse, typeof chainableElement> = wdioExpect.soft(chainableElement) + }) + + it('should support chainable element with wdio Matchers', async () => { + expectPromiseVoid = wdioExpect.soft(element).toBeDisplayed() + expectPromiseVoid = wdioExpect.soft(chainableElement).toBeDisplayed() + expectPromiseVoid = wdioExpect.soft(chainableArray).toBeDisplayed() + await wdioExpect.soft(element).toBeDisplayed() + await wdioExpect.soft(chainableElement).toBeDisplayed() + await wdioExpect.soft(chainableArray).toBeDisplayed() + + expectPromiseVoid = wdioExpect.soft(element).not.toBeDisplayed() + expectPromiseVoid = wdioExpect.soft(chainableElement).not.toBeDisplayed() + expectPromiseVoid = wdioExpect.soft(chainableArray).not.toBeDisplayed() + await wdioExpect.soft(element).not.toBeDisplayed() + await wdioExpect.soft(chainableElement).not.toBeDisplayed() + await wdioExpect.soft(chainableArray).not.toBeDisplayed() + + // @ts-expect-error + expectVoid = wdioExpect.soft(element).toBeDisplayed() + // @ts-expect-error + expectVoid = wdioExpect.soft(chainableElement).toBeDisplayed() + // @ts-expect-error + expectVoid = wdioExpect.soft(chainableArray).toBeDisplayed() + + // @ts-expect-error + expectVoid = wdioExpect.soft(element).not.toBeDisplayed() + // @ts-expect-error + expectVoid = wdioExpect.soft(chainableElement).not.toBeDisplayed() + // @ts-expect-error + expectVoid = wdioExpect.soft(chainableArray).not.toBeDisplayed() + }) + + // Those should return a Promise but soft assertions is not even working at runtime. + // See Jasmine point 6 in the following issue: https://github.com/webdriverio/expect-webdriverio/issues/1893 + // it('should work with custom matcher and custom asymmetric matchers from `expect` module', async () => { + // expectPromiseVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty('text') + // expectPromiseVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty(wdioExpect.stringContaining('text')) + // expectPromiseVoid = wdioExpect.soft(chainableElement).not.toHaveCustomProperty(wdioExpect.not.stringContaining('text')) + // expectPromiseVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty( + // wdioExpect.toHaveCustomProperty(chainableElement) + // ) + + // // @ts-expect-error + // expectVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty('text') + // // @ts-expect-error + // expectVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty(wdioExpect.stringContaining('text')) + // // @ts-expect-error + // expectVoid = wdioExpect.soft(chainableElement).not.toHaveCustomProperty(wdioExpect.not.stringContaining('text')) + // // @ts-expect-error + // expectVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty( + // wdioExpect.toHaveCustomProperty(chainableElement) + // ) + + // expectPromiseVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty('text') + // expectPromiseVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty(wdioExpect.stringContaining('text')) + // expectPromiseVoid = wdioExpect.soft(chainableElement).not.toHaveCustomProperty(wdioExpect.not.stringContaining('text')) + // expectPromiseVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty( + // wdioExpect.toHaveCustomProperty(chainableElement) + // ) + + // // @ts-expect-error + // expectVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty('text') + // // @ts-expect-error + // expectVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty(wdioExpect.stringContaining('text')) + // // @ts-expect-error + // expectVoid = wdioExpect.soft(chainableElement).not.toHaveCustomProperty(wdioExpect.not.stringContaining('text')) + // // @ts-expect-error + // expectVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty( + // wdioExpect.toHaveCustomProperty(chainableElement) + // ) + // }) + + // it('should work with custom matcher and custom asymmetric matchers from `ExpectWebDriverIO` namespace', async () => { + // expectPromiseVoid = wdioExpect.soft(chainableElement).toBeCustomPromise('text') + // expectPromiseVoid = wdioExpect.soft(chainableElement).toBeCustomPromise(wdioExpect.stringContaining('text')) + // expectPromiseVoid = wdioExpect.soft(chainableElement).not.toBeCustomPromise(wdioExpect.not.stringContaining('text')) + // expectPromiseVoid = wdioExpect.soft(chainableElement).toBeCustomPromise( + // wdioExpect.toBeCustomPromise(chainableElement) + // ) + + // // @ts-expect-error + // expectVoid = wdioExpect.soft(chainableElement).toBeCustomPromise('text') + // // @ts-expect-error + // expectVoid = wdioExpect.soft(chainableElement).toBeCustomPromise(wdioExpect.stringContaining('text')) + // // @ts-expect-error + // expectVoid = wdioExpect.soft(chainableElement).not.toBeCustomPromise(wdioExpect.not.stringContaining('text')) + // // @ts-expect-error + // expectVoid = wdioExpect.soft(chainableElement).toBeCustomPromise( + // wdioExpect.toBeCustomPromise(chainableElement) + // ) + + // expectPromiseVoid = wdioExpect.soft(chainableElement).toBeCustomPromise('text') + // expectPromiseVoid = wdioExpect.soft(chainableElement).toBeCustomPromise(wdioExpect.stringContaining('text')) + // expectPromiseVoid = wdioExpect.soft(chainableElement).not.toBeCustomPromise(wdioExpect.not.stringContaining('text')) + // expectPromiseVoid = wdioExpect.soft(chainableElement).toBeCustomPromise( + // wdioExpect.toBeCustomPromise(chainableElement) + // ) + + // // @ts-expect-error + // expectVoid = wdioExpect.soft(chainableElement).toBeCustomPromise('text') + // // @ts-expect-error + // expectVoid = wdioExpect.soft(chainableElement).toBeCustomPromise(wdioExpect.stringContaining('text')) + // // @ts-expect-error + // expectVoid = wdioExpect.soft(chainableElement).not.toBeCustomPromise(wdioExpect.not.stringContaining('text')) + // // @ts-expect-error + // expectVoid = wdioExpect.soft(chainableElement).toBeCustomPromise( + // wdioExpect.toBeCustomPromise(chainableElement) + // ) + // }) + }) + + describe('wdioExpect.getSoftFailures', () => { + it('should be of type `SoftFailure`', async () => { + const expectSoftFailure1: ExpectWebdriverIO.SoftFailure[] = wdioExpect.getSoftFailures() + + // @ts-expect-error + expectVoid = wdioExpect.getSoftFailures() + }) + }) + + describe('wdioExpect.assertSoftFailures', () => { + it('should be of type void', async () => { + expectVoid = wdioExpect.assertSoftFailures() + + // @ts-expect-error + expectPromiseVoid = wdioExpect.assertSoftFailures() + }) + }) + + describe('wdioExpect.clearSoftFailures', () => { + it('should be of type void', async () => { + expectVoid = wdioExpect.clearSoftFailures() + + // @ts-expect-error + expectPromiseVoid = wdioExpect.clearSoftFailures() + }) + }) + }) + }) + + describe('Asymmetric matchers', () => { + const string: string = 'WebdriverIO is a test framework' + const array: string[] = ['WebdriverIO', 'Test'] + const object: { name: string } = { name: 'WebdriverIO' } + const number: number = 1 + + it('should have no ts error using asymmetric matchers', async () => { + wdioExpect(string).toEqual(wdioExpect.stringContaining('WebdriverIO')) + wdioExpect(array).toEqual(wdioExpect.arrayContaining(['WebdriverIO', 'Test'])) + wdioExpect(object).toEqual(wdioExpect.objectContaining({ name: 'WebdriverIO' })) + wdioExpect(number).toEqual(wdioExpect.closeTo(1.0001, 0.0001)) + wdioExpect(['apple', 'banana', 'cherry']).toEqual(wdioExpect.arrayOf(wdioExpect.any(String))) + }) + }) + + describe('Jasmine only cases', () => { + let expectPromiseLikeVoid: PromiseLike + + it('should not overwrite the jasmine global expect', async () => { + const expectVoid: jasmine.ArrayLikeMatchers = expect('test') + }) + it('should support expectAsync correctly for non wdio types', async () => { + expectPromiseLikeVoid = expectAsync(Promise.resolve('test')).toBeResolved() + expectPromiseLikeVoid = expectAsync(Promise.resolve('test')).toBeResolvedTo(wdioExpect.stringContaining('test error')) + expectPromiseLikeVoid = expectAsync(Promise.resolve('test')).not.toBeResolvedTo(wdioExpect.not.stringContaining('test error')) + expectPromiseLikeVoid = expectAsync(Promise.resolve('test')).toBeRejected() + expectPromiseLikeVoid = expectAsync(Promise.resolve('test')).not.toBeResolved() + expectPromiseLikeVoid = expectAsync(Promise.resolve('test')).not.toBeRejected() + }) + }) +}) diff --git a/test-types/jest-@jest_global/customMatchers/customMatchers-module-expect.d.ts b/test-types/jest-@jest_global/customMatchers/customMatchers-module-expect.d.ts new file mode 100644 index 000000000..750d6e1ff --- /dev/null +++ b/test-types/jest-@jest_global/customMatchers/customMatchers-module-expect.d.ts @@ -0,0 +1,26 @@ +import 'expect' + +/** + * Custom matchers under the `expect` module. + * @see {@link https://jestjs.io/docs/expect#expectextendmatchers} + */ +declare module 'expect' { + interface AsymmetricMatchers { + toBeWithinRange(floor: number, ceiling: number): void + toHaveSimpleCustomProperty(string: string): string + toHaveCustomProperty(element: ChainablePromiseElement | WebdriverIO.Element): Promise> + } + + interface Matchers { + toBeWithinRange(floor: number, ceiling: number): R + toHaveSimpleCustomProperty(string: string | ExpectWebdriverIO.PartialMatcher): Promise + toHaveCustomProperty: + // Useful to typecheck the custom matcher so it is only used on elements + T extends ChainablePromiseElement | WebdriverIO.Element ? + (test: string | ExpectWebdriverIO.PartialMatcher | + // Needed for the custom asymmetric matcher defined above to be typed correctly + Promise>) + // Using `never` blocks the call on non-element types + => Promise : never; + } +} \ No newline at end of file diff --git a/test-types/jest-@jest_global/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts b/test-types/jest-@jest_global/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts new file mode 100644 index 000000000..7a833bd87 --- /dev/null +++ b/test-types/jest-@jest_global/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts @@ -0,0 +1,14 @@ +/** + * Custom matchers under the `ExpectWebdriverIO` namespace. + * @see {@link https://webdriver.io/docs/custommatchers/#typescript-support} + */ +declare namespace ExpectWebdriverIO { + interface AsymmetricMatchers { + toBeCustom(): ExpectWebdriverIO.PartialMatcher; + toBeCustomPromise(chainableElement: ChainablePromiseElement): Promise>; + } + interface Matchers { + toBeCustom(): R; + toBeCustomPromise: T extends ChainablePromiseElement ? (expected?: string | ExpectWebdriverIO.PartialMatcher | Promise>) => Promise : never; + } +} \ No newline at end of file diff --git a/test-types/jest-@jest_global/tsconfig.json b/test-types/jest-@jest_global/tsconfig.json new file mode 100644 index 000000000..06f3ef3c7 --- /dev/null +++ b/test-types/jest-@jest_global/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "noEmit": true, + "noImplicitAny": true, + "target": "es2022", + "module": "node18", + "skipLibCheck": true, + } +} diff --git a/test-types/jest-@jest_global/types-jest.test.ts b/test-types/jest-@jest_global/types-jest.test.ts new file mode 100644 index 000000000..193856f87 --- /dev/null +++ b/test-types/jest-@jest_global/types-jest.test.ts @@ -0,0 +1,925 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import { expect } from 'expect-webdriverio' +import { describe, it, expect as jestExpect } from '@jest/globals' + +describe('type assertions', async () => { + const chainableElement = {} as unknown as ChainablePromiseElement + const chainableArray = {} as ChainablePromiseArray + + const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element + const elementArray: WebdriverIO.ElementArray = [] as unknown as WebdriverIO.ElementArray + + const networkMock: WebdriverIO.Mock = {} as unknown as WebdriverIO.Mock + + // Type assertions + let expectPromiseVoid: Promise + let expectVoid: void + + describe('Browser', () => { + const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser + + describe('toHaveUrl', () => { + it('should be supported correctly', async () => { + expectPromiseVoid = expect(browser).toHaveUrl('https://example.com') + expectPromiseVoid = expect(browser).not.toHaveUrl('https://example.com') + + // Asymmetric matchers + expectPromiseVoid = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) + expectPromiseVoid = expect(browser).toHaveUrl(expect.not.stringContaining('WebdriverIO')) + expectPromiseVoid = expect(browser).toHaveUrl(expect.any(String)) + expectPromiseVoid = expect(browser).toHaveUrl(expect.anything()) + + // @ts-expect-error + expectVoid = expect(browser).toHaveUrl('https://example.com') + // @ts-expect-error + expectVoid = expect(browser).not.toHaveUrl('https://example.com') + // @ts-expect-error + expectVoid = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) + + // @ts-expect-error + await expect(browser).toHaveUrl(6) + }) + + it('should have ts errors when actual is not a Browser element', async () => { + // @ts-expect-error + await expect(element).toHaveUrl('https://example.com') + // @ts-expect-error + await expect(element).not.toHaveUrl('https://example.com') + // @ts-expect-error + await expect(true).toHaveUrl('https://example.com') + // @ts-expect-error + await expect(true).not.toHaveUrl('https://example.com') + }) + }) + + describe('toHaveTitle', () => { + it('should be supported correctly', async () => { + expectPromiseVoid = expect(browser).toHaveTitle('https://example.com') + expectPromiseVoid = expect(browser).not.toHaveTitle('https://example.com') + + // Asymmetric matchers + expectPromiseVoid = expect(browser).toHaveTitle(expect.stringContaining('WebdriverIO')) + expectPromiseVoid = expect(browser).toHaveTitle(expect.any(String)) + expectPromiseVoid = expect(browser).toHaveTitle(expect.anything()) + + // @ts-expect-error + expectVoid = expect(browser).toHaveTitle('https://example.com') + // @ts-expect-error + expectVoid = expect(browser).not.toHaveTitle('https://example.com') + // @ts-expect-error + expectVoid = expect(browser).toHaveTitle(expect.stringContaining('WebdriverIO')) + }) + + it('should have ts errors when actual is not a Browser element', async () => { + // @ts-expect-error + await expect(element).toHaveTitle('https://example.com') + // @ts-expect-error + await expect(element).not.toHaveTitle('https://example.com') + // @ts-expect-error + await expect(true).toHaveTitle('https://example.com') + // @ts-expect-error + await expect(true).not.toHaveTitle('https://example.com') + }) + }) + }) + + describe('element', () => { + + describe('toBeDisabled', () => { + it('should be supported correctly', async () => { + // Element + expectPromiseVoid = expect(element).toBeDisabled() + expectPromiseVoid = expect(element).not.toBeDisabled() + + // Element array + expectPromiseVoid = expect(elementArray).toBeDisabled() + expectPromiseVoid = expect(elementArray).not.toBeDisabled() + + // Chainable element + expectPromiseVoid = expect(chainableElement).toBeDisabled() + expectPromiseVoid = expect(chainableElement).not.toBeDisabled() + + // Chainable element array + expectPromiseVoid = expect(chainableArray).toBeDisabled() + expectPromiseVoid = expect(chainableArray).not.toBeDisabled() + + // @ts-expect-error + expectVoid = expect(element).toBeDisabled() + // @ts-expect-error + expectVoid = expect(element).not.toBeDisabled() + }) + + it('should have ts errors when actual is not an element', async () => { + // @ts-expect-error + await expect(browser).toBeDisabled() + // @ts-expect-error + await expect(browser).not.toBeDisabled() + // @ts-expect-error + await expect(true).toBeDisabled() + // @ts-expect-error + await expect(true).not.toBeDisabled() + }) + }) + + describe('toHaveText', () => { + it('should be supported correctly', async () => { + expectPromiseVoid = expect(element).toHaveText('text') + expectPromiseVoid = expect(element).toHaveText(/text/) + expectPromiseVoid = expect(element).toHaveText(['text1', 'text2']) + expectPromiseVoid = expect(element).toHaveText([expect.stringContaining('text1'), expect.stringContaining('text2')]) + expectPromiseVoid = expect(element).toHaveText([/text1/, /text2/]) + expectPromiseVoid = expect(element).toHaveText(['text1', /text1/, expect.stringContaining('text3')]) + await expect(element).toHaveText( + 'My-Ex-Am-Ple', + { + replace: [[/-/g, ' '], [/[A-Z]+/g, (match: string) => match.toLowerCase()]] + } + ) + + expectPromiseVoid = expect(element).not.toHaveText('text') + + // @ts-expect-error + expectVoid = expect(element).toHaveText('text') + // @ts-expect-error + await expect(element).toHaveText(6) + + expectPromiseVoid = expect(chainableElement).toHaveText('text') + expectPromiseVoid = expect(chainableElement).toHaveText(/text/) + expectPromiseVoid = expect(chainableElement).toHaveText(['text1', 'text2']) + expectPromiseVoid = expect(chainableElement).toHaveText([expect.stringContaining('text1'), expect.stringContaining('text2')]) + expectPromiseVoid = expect(chainableElement).toHaveText([/text1/, /text2/]) + expectPromiseVoid = expect(chainableElement).toHaveText(['text1', /text1/, expect.stringContaining('text3')]) + + expectPromiseVoid = expect(chainableElement).not.toHaveText('text') + + // @ts-expect-error + expectVoid = expect(chainableElement).toHaveText('text') + // @ts-expect-error + await expect(chainableElement).toHaveText(6) + + expectPromiseVoid = expect(elementArray).toHaveText('text') + expectPromiseVoid = expect(elementArray).toHaveText(/text/) + expectPromiseVoid = expect(elementArray).toHaveText(['text1', 'text2']) + expectPromiseVoid = expect(elementArray).toHaveText([expect.stringContaining('text1'), expect.stringContaining('text2')]) + expectPromiseVoid = expect(elementArray).toHaveText([/text1/, /text2/]) + expectPromiseVoid = expect(elementArray).toHaveText(['text1', /text1/, expect.stringContaining('text3')]) + + expectPromiseVoid = expect(elementArray).not.toHaveText('text') + + // @ts-expect-error + expectVoid = expect(elementArray).toHaveText('text') + // @ts-expect-error + await expect(elementArray).toHaveText(6) + + expectPromiseVoid = expect(chainableArray).toHaveText('text') + expectPromiseVoid = expect(chainableArray).toHaveText(/text/) + expectPromiseVoid = expect(chainableArray).toHaveText(['text1', 'text2']) + expectPromiseVoid = expect(chainableArray).toHaveText([expect.stringContaining('text1'), expect.stringContaining('text2')]) + expectPromiseVoid = expect(chainableArray).toHaveText([/text1/, /text2/]) + expectPromiseVoid = expect(chainableArray).toHaveText(['text1', /text1/, expect.stringContaining('text3')]) + + expectPromiseVoid = expect(chainableArray).not.toHaveText('text') + + // @ts-expect-error + expectVoid = expect(chainableArray).toHaveText('text') + // @ts-expect-error + await expect(chainableArray).toHaveText(6) + + // @ts-expect-error + await expect(browser).toHaveText('text') + }) + + it('should have ts errors when actual is not an element', async () => { + // @ts-expect-error + await expect(browser).toHaveText('text') + // @ts-expect-error + await expect(browser).not.toHaveText('text') + // @ts-expect-error + await expect(true).toHaveText('text') + // @ts-expect-error + await expect(true).toHaveText('text') + }) + + it('should have ts errors when actual is string or Promise', async () => { + // @ts-expect-error + await expect('text').toHaveText('text') + // @ts-expect-error + await expect('text').not.toHaveText('text') + // @ts-expect-error + await expect(Promise.resolve('text')).toHaveText('text') + // @ts-expect-error + await expect(Promise.resolve('text')).toHaveText('text') + }) + }) + + describe('toHaveHeight', () => { + it('should be supported correctly', async () => { + expectPromiseVoid = expect(element).toHaveHeight(100) + expectPromiseVoid = expect(element).toHaveHeight(100, { message: 'Custom error message' }) + expectPromiseVoid = expect(element).not.toHaveHeight(100) + expectPromiseVoid = expect(element).not.toHaveHeight(100, { message: 'Custom error message' }) + + expectPromiseVoid = expect(element).toHaveHeight({ width: 100, height: 200 }) + expectPromiseVoid = expect(element).toHaveHeight({ width: 100, height: 200 }, { message: 'Custom error message' }) + expectPromiseVoid = expect(element).not.toHaveHeight({ width: 100, height: 200 }) + expectPromiseVoid = expect(element).not.toHaveHeight({ width: 100, height: 200 }, { message: 'Custom error message' }) + + // @ts-expect-error + expectVoid = expect(element).toHaveHeight(100) + // @ts-expect-error + expectVoid = expect(element).not.toHaveHeight(100) + + // @ts-expect-error + expectVoid = expect(element).toHaveHeight({ width: 100, height: 200 }) + // @ts-expect-error + expectVoid = expect(element).not.toHaveHeight({ width: 100, height: 200 }) + + // @ts-expect-error + await expect(browser).toHaveHeight(100) + }) + + it('should have ts errors when actual is string or Promise', async () => { + // @ts-expect-error + await expect('text').toHaveText('text') + // @ts-expect-error + await expect('text').not.toHaveText('text') + // @ts-expect-error + await expect(Promise.resolve('text')).toHaveText('text') + // @ts-expect-error + await expect(Promise.resolve('text')).toHaveText('text') + }) + }) + + describe('toMatchSnapshot', () => { + + it('should be supported correctly', async () => { + expectVoid = expect(element).toMatchSnapshot() + expectVoid = expect(element).toMatchSnapshot('test label') + expectVoid = expect(element).not.toMatchSnapshot('test label') + + expectPromiseVoid = expect(chainableElement).toMatchSnapshot() + expectPromiseVoid = expect(chainableElement).toMatchSnapshot('test label') + expectPromiseVoid = expect(chainableElement).not.toMatchSnapshot('test label') + + //@ts-expect-error + expectPromiseVoid = expect(element).toMatchSnapshot() + //@ts-expect-error + expectPromiseVoid = expect(element).not.toMatchSnapshot() + //@ts-expect-error + expectVoid = expect(chainableElement).toMatchSnapshot() + //@ts-expect-error + expectVoid = expect(chainableElement).not.toMatchSnapshot() + }) + }) + + describe('toMatchInlineSnapshot', () => { + + it('should be correctly supported', async () => { + expectVoid = expect(element).toMatchInlineSnapshot() + expectVoid = expect(element).toMatchInlineSnapshot('test snapshot') + expectVoid = expect(element).toMatchInlineSnapshot('test snapshot', 'test label') + + expectPromiseVoid = expect(chainableElement).toMatchInlineSnapshot() + expectPromiseVoid = expect(chainableElement).toMatchInlineSnapshot('test snapshot') + expectPromiseVoid = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + + //@ts-expect-error + expectPromiseVoid = expect(element).toMatchInlineSnapshot() + //@ts-expect-error + expectVoid = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + }) + + it('should be correctly supported with getCSSProperty()', async () => { + expectPromiseVoid = expect(element.getCSSProperty('test')).toMatchInlineSnapshot() + expectPromiseVoid = expect(element.getCSSProperty('test')).toMatchInlineSnapshot('test snapshot') + expectPromiseVoid = expect(element.getCSSProperty('test')).toMatchInlineSnapshot('test snapshot', 'test label') + + expectPromiseVoid = expect(chainableElement.getCSSProperty('test')).toMatchInlineSnapshot() + expectPromiseVoid = expect(chainableElement.getCSSProperty('test')).toMatchInlineSnapshot('test snapshot') + expectPromiseVoid = expect(chainableElement.getCSSProperty('test')).toMatchInlineSnapshot('test snapshot', 'test label') + + //@ts-expect-error + expectPromiseVoid = expect(element).toMatchInlineSnapshot() + //@ts-expect-error + expectVoid = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + }) + }) + + describe('toBeElementsArrayOfSize', async () => { + + it('should work correctly when actual is chainableArray', async () => { + expectPromiseVoid = expect(chainableArray).toBeElementsArrayOfSize(5) + expectPromiseVoid = expect(chainableArray).toBeElementsArrayOfSize({ lte: 10 }) + + // @ts-expect-error + expectVoid = expect(chainableArray).toBeElementsArrayOfSize(5) + // @ts-expect-error + expectVoid = expect(chainableArray).toBeElementsArrayOfSize({ lte: 10 }) + }) + + it('should not work when actual is not chainableArray', async () => { + // @ts-expect-error + await expect(chainableElement).toBeElementsArrayOfSize(5) + // @ts-expect-error + await expect(chainableElement).toBeElementsArrayOfSize({ lte: 10 }) + // @ts-expect-error + await expect(true).toBeElementsArrayOfSize(5) + // @ts-expect-error + await expect(true).toBeElementsArrayOfSize({ lte: 10 }) + }) + }) + }) + + describe('Custom matchers', () => { + describe('using `ExpectWebdriverIO` namespace augmentation', () => { + it('should supported correctly a non-promise custom matcher', async () => { + expectVoid = expect('test').toBeCustom() + expectVoid = expect('test').not.toBeCustom() + + // @ts-expect-error + expectPromiseVoid = expect('test').toBeCustom() + // @ts-expect-error + expectPromiseVoid = expect('test').not.toBeCustom() + + expectVoid = expect(1).toBeWithinRange(0, 2) + }) + + it('should supported correctly a promise custom matcher with only chainableElement as actual', async () => { + expectPromiseVoid = expect(chainableElement).toBeCustomPromise() + expectPromiseVoid = expect(chainableElement).toBeCustomPromise(expect.stringContaining('test')) + expectPromiseVoid = expect(chainableElement).not.toBeCustomPromise(expect.not.stringContaining('test')) + + // @ts-expect-error + expect('test').toBeCustomPromise() + // @ts-expect-error + expectVoid = expect(chainableElement).toBeCustomPromise() + // @ts-expect-error + expectVoid = expect(chainableElement).toBeCustomPromise(expect.stringContaining('test')) + // @ts-expect-error + expectVoid = expect(chainableElement).not.toBeCustomPromise(expect.stringContaining('test')) + // @ts-expect-error + expect(chainableElement).toBeCustomPromise(expect.stringContaining(6)) + }) + + it('should support custom asymmetric matcher', async () => { + const expectString1 : ExpectWebdriverIO.PartialMatcher = expect.toBeCustom() + const expectString2 : ExpectWebdriverIO.PartialMatcher = expect.not.toBeCustom() + + expectPromiseVoid = expect(chainableElement).toBeCustomPromise(expect.toBeCustom()) + + // @ts-expect-error + expectPromiseVoid = expect.toBeCustom() + // @ts-expect-error + expectPromiseVoid = expect.not.toBeCustom() + + //@ts-expect-error + expectVoid = expect(chainableElement).toBeCustomPromise(expect.toBeCustom()) + }) + }) + + describe('using `expect` module declaration', () => { + + it('should support a simple matcher', async () => { + expectVoid = expect(5).toBeWithinRange(1, 10) + + // Or as an asymmetric matcher: + expectVoid = expect({ value: 5 }).toEqual({ + value: expect.toBeWithinRange(1, 10) + }) + + // @ts-expect-error + expectVoid = expect(5).toBeWithinRange(1, '10') + // @ts-expect-error + expectPromiseVoid = expect(5).toBeWithinRange('1') + }) + + it('should support a simple custom matcher with a chainable element matcher with promise', async () => { + expectPromiseVoid = expect(chainableElement).toHaveSimpleCustomProperty('text') + expectPromiseVoid = expect(chainableElement).toHaveSimpleCustomProperty(expect.stringContaining('text')) + expectPromiseVoid = expect(chainableElement).not.toHaveSimpleCustomProperty(expect.not.stringContaining('text')) + + // Or as a custom asymmetric matcher: + expectPromiseVoid = expect(chainableElement).toHaveSimpleCustomProperty( + expect.toHaveSimpleCustomProperty('string') + ) + const expectString1:string = expect.toHaveSimpleCustomProperty('string') + const expectString2:string = expect.not.toHaveSimpleCustomProperty('string') + + // @ts-expect-error + expectVoid = expect.toHaveSimpleCustomProperty(chainableElement) + // @ts-expect-error + expectVoid = expect.not.toHaveSimpleCustomProperty(chainableElement) + + // @ts-expect-error + expectVoid = expect.toHaveSimpleCustomProperty(chainableElement) + }) + + it('should support a chainable element matcher with promise', async () => { + expectPromiseVoid = expect(chainableElement).toHaveCustomProperty('text') + expectPromiseVoid = expect(chainableElement).toHaveCustomProperty(expect.stringContaining('text')) + expectPromiseVoid = expect(chainableElement).not.toHaveCustomProperty(expect.not.stringContaining('text')) + + // Or as a custom asymmetric matcher: + expectPromiseVoid = expect(chainableElement).toHaveCustomProperty( + await expect.toHaveCustomProperty(chainableElement) + ) + const expectPromiseWdioElement1: Promise> = expect.toHaveCustomProperty(chainableElement) + const expectPromiseWdioElement2: Promise> = expect.not.toHaveCustomProperty(chainableElement) + + // @ts-expect-error + expectVoid = expect.toHaveCustomProperty(chainableElement) + // @ts-expect-error + expectVoid = expect.not.toHaveCustomProperty(chainableElement) + + // @ts-expect-error + expectVoid = expect.toHaveCustomProperty(chainableElement) + // @ts-expect-error + expect.toHaveCustomProperty('test') + + await expect(chainableElement).toHaveCustomProperty( + await expect.toHaveCustomProperty(chainableElement) + ) + }) + }) + }) + + describe('toBe', () => { + + it('should expect void type when actual is a boolean', async () => { + expectVoid = expect(true).toBe(true) + expectVoid = expect(true).not.toBe(true) + + //@ts-expect-error + expectPromiseVoid = expect(true).toBe(true) + //@ts-expect-error + expectPromiseVoid = expect(true).not.toBe(true) + }) + + it('should not expect Promise when actual is a chainable since toBe does not need to be awaited', async () => { + expectVoid = expect(chainableElement).toBe(true) + expectVoid = expect(chainableElement).not.toBe(true) + + //@ts-expect-error + expectPromiseVoid = expect(chainableElement).toBe(true) + //@ts-expect-error + expectPromiseVoid = expect(chainableElement).not.toBe(true) + }) + + it('should still expect void type when actual is a Promise since we do not overload them', async () => { + const promiseBoolean = Promise.resolve(true) + + expectVoid = expect(promiseBoolean).toBeDefined() + expectVoid = expect(promiseBoolean).not.toBeDefined() + + //@ts-expect-error + expectPromiseVoid = expect(promiseBoolean).toBeDefined() + //@ts-expect-error + expectPromiseVoid = expect(promiseBoolean).toBe(true) + }) + + it('should work with string', async () => { + expectVoid = expect('text').toBe(true) + expectVoid = expect('text').not.toBe(true) + expectVoid = expect('text').toBe(expect.stringContaining('text')) + expectVoid = expect('text').not.toBe(expect.stringContaining('text')) + + //@ts-expect-error + expectPromiseVoid = expect('text').toBe(true) + //@ts-expect-error + expectPromiseVoid = expect('text').not.toBe(true) + //@ts-expect-error + expectPromiseVoid = expect('text').toBe(expect.stringContaining('text')) + //@ts-expect-error + expectPromiseVoid = expect('text').not.toBe(expect.stringContaining('text')) + }) + }) + + describe('Promise type assertions', () => { + const booleanPromise: Promise = Promise.resolve(true) + + it('should work with resolves & rejects correctly', async () => { + expectPromiseVoid = expect(booleanPromise).resolves.toBe(true) + expectPromiseVoid = expect(booleanPromise).rejects.toBe(true) + + //@ts-expect-error + expectVoid = expect(booleanPromise).resolves.toBe(true) + //@ts-expect-error + expectVoid = expect(booleanPromise).rejects.toBe(true) + + }) + + it('should not support chainable and expect PromiseVoid with toBe', async () => { + //@ts-expect-error + expectPromiseVoid = expect(chainableElement).toBe(true) + //@ts-expect-error + expectPromiseVoid = expect(chainableElement).not.toBe(true) + }) + }) + + describe('Network Matchers', () => { + const promiseNetworkMock = Promise.resolve(networkMock) + + it('should not have ts errors when typing to Promise', async () => { + expectPromiseVoid = expect(promiseNetworkMock).toBeRequested() + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedTimes(2) + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) + + expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequested() + expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequestedTimes(2) + expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) + + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith({ + url: 'http://localhost:8080/api', + method: 'POST', + statusCode: 200, + requestHeaders: { Authorization: 'foo' }, + responseHeaders: { Authorization: 'bar' }, + postData: { title: 'foo', description: 'bar' }, + response: { success: true }, + }) + + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith({ + url: expect.stringContaining('test'), + method: 'POST', + statusCode: 200, + requestHeaders: expect.objectContaining({ Authorization: 'foo' }), + responseHeaders: expect.objectContaining({ Authorization: 'bar' }), + postData: expect.objectContaining({ title: 'foo', description: 'bar' }), + response: expect.objectContaining({ success: true }), + }) + + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith({ + url: expect.stringMatching(/.*\/api\/.*/i), + method: ['POST', 'PUT'], + statusCode: [401, 403], + requestHeaders: headers => headers.Authorization.startsWith('Bearer '), + postData: expect.objectContaining({ released: true, title: expect.stringContaining('foobar') }), + response: (r: { data: { items: unknown[] } }) => Array.isArray(r) && r.data.items.length === 20 + }) + }) + + it('should have ts errors when typing to void', async () => { + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).toBeRequested() + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).not.toBeRequested() + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).not.toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).toBeRequestedWith({ + url: 'http://localhost:8080/api', + method: 'POST', + statusCode: 200, + requestHeaders: { Authorization: 'foo' }, + responseHeaders: { Authorization: 'bar' }, + postData: { title: 'foo', description: 'bar' }, + response: { success: true }, + }) + + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).toBeRequestedWith(expect.objectContaining({ + response: { success: true }, + })) + }) + }) + + describe('Expect', () => { + it('should have ts errors when using a non existing expect.function', async () => { + // @ts-expect-error + expect.unimplementedFunction() + }) + + it('should support stringContaining, anything and more', async () => { + expect.stringContaining('WebdriverIO') + expect.stringMatching(/WebdriverIO/) + expect.arrayContaining(['WebdriverIO', 'Test']) + expect.objectContaining({ name: 'WebdriverIO' }) + // Was not there but works! + expect.closeTo(5, 10) + expect.arrayContaining(['WebdriverIO', 'Test']) + // New from jest 30!! + expect.arrayOf(expect.stringContaining('WebdriverIO')) + + expect.anything() + expect.any(Function) + expect.any(Number) + expect.any(Boolean) + expect.any(String) + expect.any(Symbol) + expect.any(Date) + expect.any(Error) + + expect.not.stringContaining('WebdriverIO') + expect.not.stringMatching(/WebdriverIO/) + expect.not.arrayContaining(['WebdriverIO', 'Test']) + expect.not.objectContaining({ name: 'WebdriverIO' }) + expect.not.closeTo(5, 10) + expect.not.arrayContaining(['WebdriverIO', 'Test']) + expect.not.arrayOf(expect.stringContaining('WebdriverIO')) + + // expect.not.anything() + // expect.not.any(Function) + // expect.not.any(Number) + // expect.not.any(Boolean) + // expect.not.any(String) + // expect.not.any(Symbol) + // expect.not.any(Date) + // expect.not.any(Error) + }) + + describe('Soft Assertions', async () => { + const actualString: string = 'test' + const actualPromiseString: Promise = Promise.resolve('test') + + describe('expect.soft', () => { + it('should not need to be awaited/be a promise if actual is non-promise type', async () => { + const expectWdioMatcher1: WdioCustomMatchers = expect.soft(actualString) + expectVoid = expect.soft(actualString).toBe('Test Page') + expectVoid = expect.soft(actualString).not.toBe('Test Page') + expectVoid = expect.soft(actualString).not.toBe(expect.stringContaining('Test Page')) + + // @ts-expect-error + expectPromiseVoid = expect.soft(actualString).toBe('Test Page') + // @ts-expect-error + expectPromiseVoid = expect.soft(actualString).not.toBe('Test Page') + // @ts-expect-error + expectPromiseVoid = expect.soft(actualString).not.toBe(expect.stringContaining('Test Page')) + }) + + it('should need to be awaited/be a promise if actual is promise type', async () => { + const expectWdioMatcher1: ExpectWebdriverIO.MatchersAndInverse, Promise> = expect.soft(actualPromiseString) + expectPromiseVoid = expect.soft(actualPromiseString).toBe('Test Page') + expectPromiseVoid = expect.soft(actualPromiseString).not.toBe('Test Page') + expectPromiseVoid = expect.soft(actualPromiseString).not.toBe(expect.stringContaining('Test Page')) + + // @ts-expect-error + expectVoid = expect.soft(actualPromiseString).toBe('Test Page') + // @ts-expect-error + expectVoid = expect.soft(actualPromiseString).not.toBe('Test Page') + // @ts-expect-error + expectVoid = expect.soft(actualPromiseString).not.toBe(expect.stringContaining('Test Page')) + }) + + it('should support chainable element', async () => { + const expectElement: ExpectWebdriverIO.MatchersAndInverse = expect.soft(element) + const expectElementChainable: ExpectWebdriverIO.MatchersAndInverse = expect.soft(chainableElement) + + // @ts-expect-error + const expectElement2: ExpectWebdriverIO.MatchersAndInverse, WebdriverIO.Element> = expect.soft(element) + // @ts-expect-error + const expectElementChainable2: ExpectWebdriverIO.MatchersAndInverse, typeof chainableElement> = expect.soft(chainableElement) + }) + + it('should support chainable element with wdio Matchers', async () => { + expectPromiseVoid = expect.soft(element).toBeDisplayed() + expectPromiseVoid = expect.soft(chainableElement).toBeDisplayed() + expectPromiseVoid = expect.soft(chainableArray).toBeDisplayed() + await expect.soft(element).toBeDisplayed() + await expect.soft(chainableElement).toBeDisplayed() + await expect.soft(chainableArray).toBeDisplayed() + + expectPromiseVoid = expect.soft(element).not.toBeDisplayed() + expectPromiseVoid = expect.soft(chainableElement).not.toBeDisplayed() + expectPromiseVoid = expect.soft(chainableArray).not.toBeDisplayed() + await expect.soft(element).not.toBeDisplayed() + await expect.soft(chainableElement).not.toBeDisplayed() + await expect.soft(chainableArray).not.toBeDisplayed() + + // @ts-expect-error + expectVoid = expect.soft(element).toBeDisplayed() + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toBeDisplayed() + // @ts-expect-error + expectVoid = expect.soft(chainableArray).toBeDisplayed() + + // @ts-expect-error + expectVoid = expect.soft(element).not.toBeDisplayed() + // @ts-expect-error + expectVoid = expect.soft(chainableElement).not.toBeDisplayed() + // @ts-expect-error + expectVoid = expect.soft(chainableArray).not.toBeDisplayed() + }) + + it('should work with custom matcher and custom asymmetric matchers from `expect` module', async () => { + expectPromiseVoid = expect.soft(chainableElement).toHaveCustomProperty('text') + expectPromiseVoid = expect.soft(chainableElement).toHaveCustomProperty(expect.stringContaining('text')) + expectPromiseVoid = expect.soft(chainableElement).not.toHaveCustomProperty(expect.not.stringContaining('text')) + expectPromiseVoid = expect.soft(chainableElement).toHaveCustomProperty( + expect.toHaveCustomProperty(chainableElement) + ) + + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toHaveCustomProperty('text') + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toHaveCustomProperty(expect.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).not.toHaveCustomProperty(expect.not.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toHaveCustomProperty( + expect.toHaveCustomProperty(chainableElement) + ) + + expectPromiseVoid = expect.soft(chainableElement).toHaveCustomProperty('text') + expectPromiseVoid = expect.soft(chainableElement).toHaveCustomProperty(expect.stringContaining('text')) + expectPromiseVoid = expect.soft(chainableElement).not.toHaveCustomProperty(expect.not.stringContaining('text')) + expectPromiseVoid = expect.soft(chainableElement).toHaveCustomProperty( + expect.toHaveCustomProperty(chainableElement) + ) + + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toHaveCustomProperty('text') + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toHaveCustomProperty(expect.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).not.toHaveCustomProperty(expect.not.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toHaveCustomProperty( + expect.toHaveCustomProperty(chainableElement) + ) + }) + + it('should work with custom matcher and custom asymmetric matchers from `ExpectWebDriverIO` namespace', async () => { + expectPromiseVoid = expect.soft(chainableElement).toBeCustomPromise('text') + expectPromiseVoid = expect.soft(chainableElement).toBeCustomPromise(expect.stringContaining('text')) + expectPromiseVoid = expect.soft(chainableElement).not.toBeCustomPromise(expect.not.stringContaining('text')) + expectPromiseVoid = expect.soft(chainableElement).toBeCustomPromise( + expect.toBeCustomPromise(chainableElement) + ) + + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toBeCustomPromise('text') + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toBeCustomPromise(expect.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).not.toBeCustomPromise(expect.not.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toBeCustomPromise( + expect.toBeCustomPromise(chainableElement) + ) + + expectPromiseVoid = expect.soft(chainableElement).toBeCustomPromise('text') + expectPromiseVoid = expect.soft(chainableElement).toBeCustomPromise(expect.stringContaining('text')) + expectPromiseVoid = expect.soft(chainableElement).not.toBeCustomPromise(expect.not.stringContaining('text')) + expectPromiseVoid = expect.soft(chainableElement).toBeCustomPromise( + expect.toBeCustomPromise(chainableElement) + ) + + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toBeCustomPromise('text') + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toBeCustomPromise(expect.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).not.toBeCustomPromise(expect.not.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toBeCustomPromise( + expect.toBeCustomPromise(chainableElement) + ) + }) + }) + + describe('expect.getSoftFailures', () => { + it('should be of type `SoftFailure`', async () => { + const expectSoftFailure1: ExpectWebdriverIO.SoftFailure[] = expect.getSoftFailures() + + // @ts-expect-error + expectVoid = expect.getSoftFailures() + }) + }) + + describe('expect.assertSoftFailures', () => { + it('should be of type void', async () => { + expectVoid = expect.assertSoftFailures() + + // @ts-expect-error + expectPromiseVoid = expect.assertSoftFailures() + }) + }) + + describe('expect.clearSoftFailures', () => { + it('should be of type void', async () => { + expectVoid = expect.clearSoftFailures() + + // @ts-expect-error + expectPromiseVoid = expect.clearSoftFailures() + }) + }) + }) + }) + + describe('Asymmetric matchers', () => { + const string: string = 'WebdriverIO is a test framework' + const array: string[] = ['WebdriverIO', 'Test'] + const object: { name: string } = { name: 'WebdriverIO' } + const number: number = 1 + + it('should have no ts error using asymmetric matchers', async () => { + expect(string).toEqual(expect.stringContaining('WebdriverIO')) + expect(array).toEqual(expect.arrayContaining(['WebdriverIO', 'Test'])) + expect(object).toEqual(expect.objectContaining({ name: 'WebdriverIO' })) + // This one is tested and is working correctly, surprisingly! + expect(number).toEqual(expect.closeTo(1.0001, 0.0001)) + // New from jest 30, should work! + expect(['apple', 'banana', 'cherry']).toEqual(expect.arrayOf(expect.any(String))) + }) + }) + + describe('@types/jest only - original Matchers', () => { + + it('should support mock matchers existing only on JestExpect', () => { + const mockFn = () => {} + + // Jest-specific mock matchers + expect(mockFn).toHaveBeenCalled() + }) + + describe('Jest-specific Promise matchers', () => { + it('should support resolves and rejects', async () => { + const stringPromise = Promise.resolve('Hello Jest') + const rejectedPromise = Promise.reject(new Error('Failed')) + + expectPromiseVoid = jestExpect(stringPromise).resolves.toBe('Hello Jest') + expectPromiseVoid = jestExpect(rejectedPromise).rejects.toThrow('Failed') + + // @ts-expect-error + expectVoid = jestExpect(stringPromise).resolves.toBe('Hello Jest') + // @ts-expect-error + expectVoid = jestExpect(rejectedPromise).rejects.toThrow('Failed') + }) + }) + + describe('toMatchSnapshot & toMatchInlineSnapshot', () => { + const snapshotName: string = 'test-snapshot' + + it('should work with string', async () => { + const jsonString: string = '{}' + const propertyMatchers = 'test' + expectVoid = jestExpect(jsonString).toMatchSnapshot(propertyMatchers) + expectVoid = jestExpect(jsonString).toMatchSnapshot(propertyMatchers, snapshotName) + expectVoid = jestExpect(jsonString).toMatchInlineSnapshot(propertyMatchers) + expectVoid = jestExpect(jsonString).toMatchInlineSnapshot(propertyMatchers, snapshotName) + + expectVoid = jestExpect(jsonString).not.toMatchSnapshot(propertyMatchers) + expectVoid = jestExpect(jsonString).not.toMatchSnapshot(propertyMatchers, snapshotName) + expectVoid = jestExpect(jsonString).not.toMatchInlineSnapshot(propertyMatchers) + expectVoid = jestExpect(jsonString).not.toMatchInlineSnapshot(propertyMatchers, snapshotName) + + // @ts-expect-error + expectPromiseVoid = jestExpect(jsonString).toMatchSnapshot(propertyMatchers) + // @ts-expect-error + expectPromiseVoid = jestExpect(jsonString).toMatchSnapshot(propertyMatchers, snapshotName) + // @ts-expect-error + expectPromiseVoid = jestExpect(jsonString).toMatchInlineSnapshot(propertyMatchers) + // @ts-expect-error + expectPromiseVoid = jestExpect(jsonString).toMatchInlineSnapshot(propertyMatchers, snapshotName) + // @ts-expect-error + expectPromiseVoid = jestExpect(jsonString).not.toMatchSnapshot(propertyMatchers) + // @ts-expect-error + expectPromiseVoid = jestExpect(jsonString).not.toMatchSnapshot(propertyMatchers, snapshotName) + // @ts-expect-error + expectPromiseVoid = jestExpect(jsonString).not.toMatchInlineSnapshot(propertyMatchers) + // @ts-expect-error + expectPromiseVoid = jestExpect(jsonString).not.toMatchInlineSnapshot(propertyMatchers, snapshotName) + }) + + it('should with object', async () => { + const treeObject = { 1: 'test', 2: 'test2' } + const propertyMatchers = { 1: 'test' } + expectVoid = jestExpect(treeObject).toMatchSnapshot(propertyMatchers) + expectVoid = jestExpect(treeObject).toMatchSnapshot(propertyMatchers, snapshotName) + expectVoid = jestExpect(treeObject).toMatchInlineSnapshot(propertyMatchers) + expectVoid = jestExpect(treeObject).toMatchInlineSnapshot(propertyMatchers, snapshotName) + + expectVoid = jestExpect(treeObject).not.toMatchSnapshot(propertyMatchers) + expectVoid = jestExpect(treeObject).not.toMatchSnapshot(propertyMatchers, snapshotName) + expectVoid = jestExpect(treeObject).not.toMatchInlineSnapshot(propertyMatchers) + expectVoid = jestExpect(treeObject).not.toMatchInlineSnapshot(propertyMatchers, snapshotName) + + // @ts-expect-error + expectPromiseVoid = jestExpect(treeObject).toMatchSnapshot(propertyMatchers) + // @ts-expect-error + expectPromiseVoid = jestExpect(treeObject).toMatchSnapshot(propertyMatchers, snapshotName) + // @ts-expect-error + expectPromiseVoid = jestExpect(treeObject).toMatchInlineSnapshot(propertyMatchers) + // @ts-expect-error + expectPromiseVoid = jestExpect(treeObject).toMatchInlineSnapshot(propertyMatchers, snapshotName) + // @ts-expect-error + expectPromiseVoid = jestExpect(treeObject).not.toMatchSnapshot(propertyMatchers) + // @ts-expect-error + expectPromiseVoid = jestExpect(treeObject).not.toMatchSnapshot(propertyMatchers, snapshotName) + // @ts-expect-error + expectPromiseVoid = jestExpect(treeObject).not.toMatchInlineSnapshot(propertyMatchers) + // @ts-expect-error + expectPromiseVoid = jestExpect(treeObject).not.toMatchInlineSnapshot(propertyMatchers, snapshotName) + }) + }) + }) +}) diff --git a/test-types/jest-@types_jest/customMatchers/customMatchers-module-expect.d.ts b/test-types/jest-@types_jest/customMatchers/customMatchers-module-expect.d.ts new file mode 100644 index 000000000..750d6e1ff --- /dev/null +++ b/test-types/jest-@types_jest/customMatchers/customMatchers-module-expect.d.ts @@ -0,0 +1,26 @@ +import 'expect' + +/** + * Custom matchers under the `expect` module. + * @see {@link https://jestjs.io/docs/expect#expectextendmatchers} + */ +declare module 'expect' { + interface AsymmetricMatchers { + toBeWithinRange(floor: number, ceiling: number): void + toHaveSimpleCustomProperty(string: string): string + toHaveCustomProperty(element: ChainablePromiseElement | WebdriverIO.Element): Promise> + } + + interface Matchers { + toBeWithinRange(floor: number, ceiling: number): R + toHaveSimpleCustomProperty(string: string | ExpectWebdriverIO.PartialMatcher): Promise + toHaveCustomProperty: + // Useful to typecheck the custom matcher so it is only used on elements + T extends ChainablePromiseElement | WebdriverIO.Element ? + (test: string | ExpectWebdriverIO.PartialMatcher | + // Needed for the custom asymmetric matcher defined above to be typed correctly + Promise>) + // Using `never` blocks the call on non-element types + => Promise : never; + } +} \ No newline at end of file diff --git a/test-types/jest-@types_jest/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts b/test-types/jest-@types_jest/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts new file mode 100644 index 000000000..7a833bd87 --- /dev/null +++ b/test-types/jest-@types_jest/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts @@ -0,0 +1,14 @@ +/** + * Custom matchers under the `ExpectWebdriverIO` namespace. + * @see {@link https://webdriver.io/docs/custommatchers/#typescript-support} + */ +declare namespace ExpectWebdriverIO { + interface AsymmetricMatchers { + toBeCustom(): ExpectWebdriverIO.PartialMatcher; + toBeCustomPromise(chainableElement: ChainablePromiseElement): Promise>; + } + interface Matchers { + toBeCustom(): R; + toBeCustomPromise: T extends ChainablePromiseElement ? (expected?: string | ExpectWebdriverIO.PartialMatcher | Promise>) => Promise : never; + } +} \ No newline at end of file diff --git a/test-types/jest-@types_jest/tsconfig.json b/test-types/jest-@types_jest/tsconfig.json new file mode 100644 index 000000000..69ed6c969 --- /dev/null +++ b/test-types/jest-@types_jest/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "noEmit": true, + "noImplicitAny": true, + "target": "es2022", + "module": "node18", + "skipLibCheck": true, + "types": [ + "@types/jest", + "../../jest.d.ts", // Needed to be after @types/jest to override the toMatchSnapshot typing + ], + } +} diff --git a/test-types/jest-@types_jest/types-jest.test.ts b/test-types/jest-@types_jest/types-jest.test.ts new file mode 100644 index 000000000..445b7758a --- /dev/null +++ b/test-types/jest-@types_jest/types-jest.test.ts @@ -0,0 +1,908 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +describe('type assertions', async () => { + const chainableElement = {} as unknown as ChainablePromiseElement + const chainableArray = {} as ChainablePromiseArray + + const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element + const elementArray: WebdriverIO.ElementArray = [] as unknown as WebdriverIO.ElementArray + + const networkMock: WebdriverIO.Mock = {} as unknown as WebdriverIO.Mock + + // Type assertions + let expectPromiseVoid: Promise + let expectVoid: void + + describe('Browser', () => { + const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser + + describe('toHaveUrl', () => { + it('should be supported correctly', async () => { + expectPromiseVoid = expect(browser).toHaveUrl('https://example.com') + expectPromiseVoid = expect(browser).not.toHaveUrl('https://example.com') + + // Asymmetric matchers + expectPromiseVoid = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) + expectPromiseVoid = expect(browser).toHaveUrl(expect.not.stringContaining('WebdriverIO')) + expectPromiseVoid = expect(browser).toHaveUrl(expect.any(String)) + expectPromiseVoid = expect(browser).toHaveUrl(expect.anything()) + + // @ts-expect-error + expectVoid = expect(browser).toHaveUrl('https://example.com') + // @ts-expect-error + expectVoid = expect(browser).not.toHaveUrl('https://example.com') + // @ts-expect-error + expectVoid = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) + + // @ts-expect-error + await expect(browser).toHaveUrl(6) + }) + + it('should have ts errors when actual is not a Browser element', async () => { + // @ts-expect-error + await expect(element).toHaveUrl('https://example.com') + // @ts-expect-error + await expect(element).not.toHaveUrl('https://example.com') + // @ts-expect-error + await expect(true).toHaveUrl('https://example.com') + // @ts-expect-error + await expect(true).not.toHaveUrl('https://example.com') + }) + }) + + describe('toHaveTitle', () => { + it('should be supported correctly', async () => { + expectPromiseVoid = expect(browser).toHaveTitle('https://example.com') + expectPromiseVoid = expect(browser).not.toHaveTitle('https://example.com') + + // Asymmetric matchers + expectPromiseVoid = expect(browser).toHaveTitle(expect.stringContaining('WebdriverIO')) + expectPromiseVoid = expect(browser).toHaveTitle(expect.any(String)) + expectPromiseVoid = expect(browser).toHaveTitle(expect.anything()) + + // @ts-expect-error + expectVoid = expect(browser).toHaveTitle('https://example.com') + // @ts-expect-error + expectVoid = expect(browser).not.toHaveTitle('https://example.com') + // @ts-expect-error + expectVoid = expect(browser).toHaveTitle(expect.stringContaining('WebdriverIO')) + }) + + it('should have ts errors when actual is not a Browser element', async () => { + // @ts-expect-error + await expect(element).toHaveTitle('https://example.com') + // @ts-expect-error + await expect(element).not.toHaveTitle('https://example.com') + // @ts-expect-error + await expect(true).toHaveTitle('https://example.com') + // @ts-expect-error + await expect(true).not.toHaveTitle('https://example.com') + }) + }) + }) + + describe('element', () => { + + describe('toBeDisabled', () => { + it('should be supported correctly', async () => { + // Element + expectPromiseVoid = expect(element).toBeDisabled() + expectPromiseVoid = expect(element).not.toBeDisabled() + + // Element array + expectPromiseVoid = expect(elementArray).toBeDisabled() + expectPromiseVoid = expect(elementArray).not.toBeDisabled() + + // Chainable element + expectPromiseVoid = expect(chainableElement).toBeDisabled() + expectPromiseVoid = expect(chainableElement).not.toBeDisabled() + + // Chainable element array + expectPromiseVoid = expect(chainableArray).toBeDisabled() + expectPromiseVoid = expect(chainableArray).not.toBeDisabled() + + // @ts-expect-error + expectVoid = expect(element).toBeDisabled() + // @ts-expect-error + expectVoid = expect(element).not.toBeDisabled() + }) + + it('should have ts errors when actual is not an element', async () => { + // @ts-expect-error + await expect(browser).toBeDisabled() + // @ts-expect-error + await expect(browser).not.toBeDisabled() + // @ts-expect-error + await expect(true).toBeDisabled() + // @ts-expect-error + await expect(true).not.toBeDisabled() + }) + }) + + describe('toHaveText', () => { + it('should be supported correctly', async () => { + expectPromiseVoid = expect(element).toHaveText('text') + expectPromiseVoid = expect(element).toHaveText(/text/) + expectPromiseVoid = expect(element).toHaveText(['text1', 'text2']) + expectPromiseVoid = expect(element).toHaveText([expect.stringContaining('text1'), expect.stringContaining('text2')]) + expectPromiseVoid = expect(element).toHaveText([/text1/, /text2/]) + expectPromiseVoid = expect(element).toHaveText(['text1', /text1/, expect.stringContaining('text3')]) + await expect(element).toHaveText( + 'My-Ex-Am-Ple', + { + replace: [[/-/g, ' '], [/[A-Z]+/g, (match: string) => match.toLowerCase()]] + } + ) + + expectPromiseVoid = expect(element).not.toHaveText('text') + + // @ts-expect-error + expectVoid = expect(element).toHaveText('text') + // @ts-expect-error + await expect(element).toHaveText(6) + + expectPromiseVoid = expect(chainableElement).toHaveText('text') + expectPromiseVoid = expect(chainableElement).toHaveText(/text/) + expectPromiseVoid = expect(chainableElement).toHaveText(['text1', 'text2']) + expectPromiseVoid = expect(chainableElement).toHaveText([expect.stringContaining('text1'), expect.stringContaining('text2')]) + expectPromiseVoid = expect(chainableElement).toHaveText([/text1/, /text2/]) + expectPromiseVoid = expect(chainableElement).toHaveText(['text1', /text1/, expect.stringContaining('text3')]) + + expectPromiseVoid = expect(chainableElement).not.toHaveText('text') + + // @ts-expect-error + expectVoid = expect(chainableElement).toHaveText('text') + // @ts-expect-error + await expect(chainableElement).toHaveText(6) + + expectPromiseVoid = expect(elementArray).toHaveText('text') + expectPromiseVoid = expect(elementArray).toHaveText(/text/) + expectPromiseVoid = expect(elementArray).toHaveText(['text1', 'text2']) + expectPromiseVoid = expect(elementArray).toHaveText([expect.stringContaining('text1'), expect.stringContaining('text2')]) + expectPromiseVoid = expect(elementArray).toHaveText([/text1/, /text2/]) + expectPromiseVoid = expect(elementArray).toHaveText(['text1', /text1/, expect.stringContaining('text3')]) + + expectPromiseVoid = expect(elementArray).not.toHaveText('text') + + // @ts-expect-error + expectVoid = expect(elementArray).toHaveText('text') + // @ts-expect-error + await expect(elementArray).toHaveText(6) + + expectPromiseVoid = expect(chainableArray).toHaveText('text') + expectPromiseVoid = expect(chainableArray).toHaveText(/text/) + expectPromiseVoid = expect(chainableArray).toHaveText(['text1', 'text2']) + expectPromiseVoid = expect(chainableArray).toHaveText([expect.stringContaining('text1'), expect.stringContaining('text2')]) + expectPromiseVoid = expect(chainableArray).toHaveText([/text1/, /text2/]) + expectPromiseVoid = expect(chainableArray).toHaveText(['text1', /text1/, expect.stringContaining('text3')]) + + expectPromiseVoid = expect(chainableArray).not.toHaveText('text') + + // @ts-expect-error + expectVoid = expect(chainableArray).toHaveText('text') + // @ts-expect-error + await expect(chainableArray).toHaveText(6) + + // @ts-expect-error + await expect(browser).toHaveText('text') + }) + + it('should have ts errors when actual is not an element', async () => { + // @ts-expect-error + await expect(browser).toHaveText('text') + // @ts-expect-error + await expect(browser).not.toHaveText('text') + // @ts-expect-error + await expect(true).toHaveText('text') + // @ts-expect-error + await expect(true).toHaveText('text') + }) + + it('should have ts errors when actual is string or Promise', async () => { + // @ts-expect-error + await expect('text').toHaveText('text') + // @ts-expect-error + await expect('text').not.toHaveText('text') + // @ts-expect-error + await expect(Promise.resolve('text')).toHaveText('text') + // @ts-expect-error + await expect(Promise.resolve('text')).toHaveText('text') + }) + }) + + describe('toHaveHeight', () => { + it('should be supported correctly', async () => { + expectPromiseVoid = expect(element).toHaveHeight(100) + expectPromiseVoid = expect(element).toHaveHeight(100, { message: 'Custom error message' }) + expectPromiseVoid = expect(element).not.toHaveHeight(100) + expectPromiseVoid = expect(element).not.toHaveHeight(100, { message: 'Custom error message' }) + + expectPromiseVoid = expect(element).toHaveHeight({ width: 100, height: 200 }) + expectPromiseVoid = expect(element).toHaveHeight({ width: 100, height: 200 }, { message: 'Custom error message' }) + expectPromiseVoid = expect(element).not.toHaveHeight({ width: 100, height: 200 }) + expectPromiseVoid = expect(element).not.toHaveHeight({ width: 100, height: 200 }, { message: 'Custom error message' }) + + // @ts-expect-error + expectVoid = expect(element).toHaveHeight(100) + // @ts-expect-error + expectVoid = expect(element).not.toHaveHeight(100) + + // @ts-expect-error + expectVoid = expect(element).toHaveHeight({ width: 100, height: 200 }) + // @ts-expect-error + expectVoid = expect(element).not.toHaveHeight({ width: 100, height: 200 }) + + // @ts-expect-error + await expect(browser).toHaveHeight(100) + }) + + it('should have ts errors when actual is string or Promise', async () => { + // @ts-expect-error + await expect('text').toHaveText('text') + // @ts-expect-error + await expect('text').not.toHaveText('text') + // @ts-expect-error + await expect(Promise.resolve('text')).toHaveText('text') + // @ts-expect-error + await expect(Promise.resolve('text')).toHaveText('text') + }) + }) + + describe('toMatchSnapshot', () => { + + it('should be supported correctly', async () => { + expectVoid = expect(element).toMatchSnapshot() + expectVoid = expect(element).toMatchSnapshot('test label') + expectVoid = expect(element).not.toMatchSnapshot('test label') + + expectPromiseVoid = expect(chainableElement).toMatchSnapshot() + expectPromiseVoid = expect(chainableElement).toMatchSnapshot('test label') + expectPromiseVoid = expect(chainableElement).not.toMatchSnapshot('test label') + + //@ts-expect-error + expectPromiseVoid = expect(element).toMatchSnapshot() + //@ts-expect-error + expectPromiseVoid = expect(element).not.toMatchSnapshot() + //@ts-expect-error + expectVoid = expect(chainableElement).toMatchSnapshot() + //@ts-expect-error + expectVoid = expect(chainableElement).not.toMatchSnapshot() + }) + }) + + describe('toMatchInlineSnapshot', () => { + + it('should be correctly supported', async () => { + expectVoid = expect(element).toMatchInlineSnapshot() + expectVoid = expect(element).toMatchInlineSnapshot('test snapshot') + expectVoid = expect(element).toMatchInlineSnapshot('test snapshot', 'test label') + + expectPromiseVoid = expect(chainableElement).toMatchInlineSnapshot() + expectPromiseVoid = expect(chainableElement).toMatchInlineSnapshot('test snapshot') + expectPromiseVoid = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + + //@ts-expect-error + expectPromiseVoid = expect(element).toMatchInlineSnapshot() + //@ts-expect-error + expectVoid = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + }) + + it('should be correctly supported with getCSSProperty()', async () => { + expectPromiseVoid = expect(element.getCSSProperty('test')).toMatchInlineSnapshot() + expectPromiseVoid = expect(element.getCSSProperty('test')).toMatchInlineSnapshot('test snapshot') + expectPromiseVoid = expect(element.getCSSProperty('test')).toMatchInlineSnapshot('test snapshot', 'test label') + + expectPromiseVoid = expect(chainableElement.getCSSProperty('test')).toMatchInlineSnapshot() + expectPromiseVoid = expect(chainableElement.getCSSProperty('test')).toMatchInlineSnapshot('test snapshot') + expectPromiseVoid = expect(chainableElement.getCSSProperty('test')).toMatchInlineSnapshot('test snapshot', 'test label') + + //@ts-expect-error + expectPromiseVoid = expect(element).toMatchInlineSnapshot() + //@ts-expect-error + expectVoid = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + }) + }) + + describe('toBeElementsArrayOfSize', async () => { + + it('should work correctly when actual is chainableArray', async () => { + expectPromiseVoid = expect(chainableArray).toBeElementsArrayOfSize(5) + expectPromiseVoid = expect(chainableArray).toBeElementsArrayOfSize({ lte: 10 }) + + // @ts-expect-error + expectVoid = expect(chainableArray).toBeElementsArrayOfSize(5) + // @ts-expect-error + expectVoid = expect(chainableArray).toBeElementsArrayOfSize({ lte: 10 }) + }) + + it('should not work when actual is not chainableArray', async () => { + // @ts-expect-error + await expect(chainableElement).toBeElementsArrayOfSize(5) + // @ts-expect-error + await expect(chainableElement).toBeElementsArrayOfSize({ lte: 10 }) + // @ts-expect-error + await expect(true).toBeElementsArrayOfSize(5) + // @ts-expect-error + await expect(true).toBeElementsArrayOfSize({ lte: 10 }) + }) + }) + }) + + describe('Custom matchers', () => { + describe('using `ExpectWebdriverIO` namespace augmentation', () => { + it('should supported correctly a non-promise custom matcher', async () => { + expectVoid = expect('test').toBeCustom() + expectVoid = expect('test').not.toBeCustom() + + // @ts-expect-error + expectPromiseVoid = expect('test').toBeCustom() + // @ts-expect-error + expectPromiseVoid = expect('test').not.toBeCustom() + + expectVoid = expect(1).toBeWithinRange(0, 2) + }) + + it('should supported correctly a promise custom matcher with only chainableElement as actual', async () => { + expectPromiseVoid = expect(chainableElement).toBeCustomPromise() + expectPromiseVoid = expect(chainableElement).toBeCustomPromise(expect.stringContaining('test')) + expectPromiseVoid = expect(chainableElement).not.toBeCustomPromise(expect.not.stringContaining('test')) + + // @ts-expect-error + expect('test').toBeCustomPromise() + // @ts-expect-error + expectVoid = expect(chainableElement).toBeCustomPromise() + // @ts-expect-error + expectVoid = expect(chainableElement).toBeCustomPromise(expect.stringContaining('test')) + // @ts-expect-error + expectVoid = expect(chainableElement).not.toBeCustomPromise(expect.stringContaining('test')) + // @ts-expect-error + expect(chainableElement).toBeCustomPromise(expect.stringContaining(6)) + }) + + it('should support custom asymmetric matcher', async () => { + const expectString1 : ExpectWebdriverIO.PartialMatcher = expect.toBeCustom() + const expectString2 : ExpectWebdriverIO.PartialMatcher = expect.not.toBeCustom() + + expectPromiseVoid = expect(chainableElement).toBeCustomPromise(expect.toBeCustom()) + + // @ts-expect-error + expectPromiseVoid = expect.toBeCustom() + // @ts-expect-error + expectPromiseVoid = expect.not.toBeCustom() + + //@ts-expect-error + expectVoid = expect(chainableElement).toBeCustomPromise(expect.toBeCustom()) + }) + }) + + describe('using `expect` module declaration', () => { + + it('should support a simple matcher', async () => { + expectVoid = expect(5).toBeWithinRange(1, 10) + + // Or as an asymmetric matcher: + expectVoid = expect({ value: 5 }).toEqual({ + value: expect.toBeWithinRange(1, 10) + }) + + // @ts-expect-error + expectVoid = expect(5).toBeWithinRange(1, '10') + // @ts-expect-error + expectPromiseVoid = expect(5).toBeWithinRange('1') + }) + + it('should support a simple custom matcher with a chainable element matcher with promise', async () => { + expectPromiseVoid = expect(chainableElement).toHaveSimpleCustomProperty('text') + expectPromiseVoid = expect(chainableElement).toHaveSimpleCustomProperty(expect.stringContaining('text')) + expectPromiseVoid = expect(chainableElement).not.toHaveSimpleCustomProperty(expect.not.stringContaining('text')) + + // Or as a custom asymmetric matcher: + expectPromiseVoid = expect(chainableElement).toHaveSimpleCustomProperty( + expect.toHaveSimpleCustomProperty('string') + ) + const expectString1:string = expect.toHaveSimpleCustomProperty('string') + const expectString2:string = expect.not.toHaveSimpleCustomProperty('string') + + // @ts-expect-error + expectVoid = expect.toHaveSimpleCustomProperty(chainableElement) + // @ts-expect-error + expectVoid = expect.not.toHaveSimpleCustomProperty(chainableElement) + + // @ts-expect-error + expectVoid = expect.toHaveSimpleCustomProperty(chainableElement) + }) + + it('should support a chainable element matcher with promise', async () => { + expectPromiseVoid = expect(chainableElement).toHaveCustomProperty('text') + expectPromiseVoid = expect(chainableElement).toHaveCustomProperty(expect.stringContaining('text')) + expectPromiseVoid = expect(chainableElement).not.toHaveCustomProperty(expect.not.stringContaining('text')) + + // Or as a custom asymmetric matcher: + expectPromiseVoid = expect(chainableElement).toHaveCustomProperty( + await expect.toHaveCustomProperty(chainableElement) + ) + const expectPromiseWdioElement1: Promise> = expect.toHaveCustomProperty(chainableElement) + const expectPromiseWdioElement2: Promise> = expect.not.toHaveCustomProperty(chainableElement) + + // @ts-expect-error + expectVoid = expect.toHaveCustomProperty(chainableElement) + // @ts-expect-error + expectVoid = expect.not.toHaveCustomProperty(chainableElement) + + // @ts-expect-error + expectVoid = expect.toHaveCustomProperty(chainableElement) + // @ts-expect-error + expect.toHaveCustomProperty('test') + + await expect(chainableElement).toHaveCustomProperty( + await expect.toHaveCustomProperty(chainableElement) + ) + }) + }) + }) + + describe('toBe', () => { + + it('should expect void type when actual is a boolean', async () => { + expectVoid = expect(true).toBe(true) + expectVoid = expect(true).not.toBe(true) + + //@ts-expect-error + expectPromiseVoid = expect(true).toBe(true) + //@ts-expect-error + expectPromiseVoid = expect(true).not.toBe(true) + }) + + it('should not expect Promise when actual is a chainable since toBe does not need to be awaited', async () => { + expectVoid = expect(chainableElement).toBe(true) + expectVoid = expect(chainableElement).not.toBe(true) + + //@ts-expect-error + expectPromiseVoid = expect(chainableElement).toBe(true) + //@ts-expect-error + expectPromiseVoid = expect(chainableElement).not.toBe(true) + }) + + it('should still expect void type when actual is a Promise since we do not overload them', async () => { + const promiseBoolean = Promise.resolve(true) + + expectVoid = expect(promiseBoolean).toBe(true) + expectVoid = expect(promiseBoolean).not.toBe(true) + + //@ts-expect-error + expectPromiseVoid = expect(promiseBoolean).toBe(true) + //@ts-expect-error + expectPromiseVoid = expect(promiseBoolean).toBe(true) + }) + + it('should work with string', async () => { + expectVoid = expect('text').toBe(true) + expectVoid = expect('text').not.toBe(true) + expectVoid = expect('text').toBe(expect.stringContaining('text')) + expectVoid = expect('text').not.toBe(expect.stringContaining('text')) + + //@ts-expect-error + expectPromiseVoid = expect('text').toBe(true) + //@ts-expect-error + expectPromiseVoid = expect('text').not.toBe(true) + //@ts-expect-error + expectPromiseVoid = expect('text').toBe(expect.stringContaining('text')) + //@ts-expect-error + expectPromiseVoid = expect('text').not.toBe(expect.stringContaining('text')) + }) + }) + + describe('Promise type assertions', () => { + const booleanPromise: Promise = Promise.resolve(true) + + it('should expect a Promise of type', async () => { + const expectPromiseBoolean1: jest.JestMatchers> = expect(booleanPromise) + const expectPromiseBoolean2: jest.Matchers> = expect(booleanPromise).not + + // @ts-expect-error + const expectPromiseBoolean3: jest.JestMatchers = expect(booleanPromise) + //// @ts-expect-error + // const expectPromiseBoolean4: jest.Matchers = expect(booleanPromise).not + }) + + it('should work with resolves & rejects correctly', async () => { + expectPromiseVoid = expect(booleanPromise).resolves.toBe(true) + expectPromiseVoid = expect(booleanPromise).rejects.toBe(true) + + //@ts-expect-error + expectVoid = expect(booleanPromise).resolves.toBe(true) + //@ts-expect-error + expectVoid = expect(booleanPromise).rejects.toBe(true) + + }) + + it('should not support chainable and expect PromiseVoid with toBe', async () => { + //@ts-expect-error + expectPromiseVoid = expect(chainableElement).toBe(true) + //@ts-expect-error + expectPromiseVoid = expect(chainableElement).not.toBe(true) + }) + }) + + describe('Network Matchers', () => { + const promiseNetworkMock = Promise.resolve(networkMock) + + it('should not have ts errors when typing to Promise', async () => { + expectPromiseVoid = expect(promiseNetworkMock).toBeRequested() + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedTimes(2) + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) + + expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequested() + expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequestedTimes(2) + expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) + + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith({ + url: 'http://localhost:8080/api', + method: 'POST', + statusCode: 200, + requestHeaders: { Authorization: 'foo' }, + responseHeaders: { Authorization: 'bar' }, + postData: { title: 'foo', description: 'bar' }, + response: { success: true }, + }) + + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith({ + url: expect.stringContaining('test'), + method: 'POST', + statusCode: 200, + requestHeaders: expect.objectContaining({ Authorization: 'foo' }), + responseHeaders: expect.objectContaining({ Authorization: 'bar' }), + postData: expect.objectContaining({ title: 'foo', description: 'bar' }), + response: expect.objectContaining({ success: true }), + }) + + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith({ + url: expect.stringMatching(/.*\/api\/.*/i), + method: ['POST', 'PUT'], + statusCode: [401, 403], + requestHeaders: headers => headers.Authorization.startsWith('Bearer '), + postData: expect.objectContaining({ released: true, title: expect.stringContaining('foobar') }), + response: (r: { data: { items: unknown[] } }) => Array.isArray(r) && r.data.items.length === 20 + }) + }) + + it('should have ts errors when typing to void', async () => { + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).toBeRequested() + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).not.toBeRequested() + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).not.toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).toBeRequestedWith({ + url: 'http://localhost:8080/api', + method: 'POST', + statusCode: 200, + requestHeaders: { Authorization: 'foo' }, + responseHeaders: { Authorization: 'bar' }, + postData: { title: 'foo', description: 'bar' }, + response: { success: true }, + }) + + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).toBeRequestedWith(expect.objectContaining({ + response: { success: true }, + })) + }) + }) + + describe('Expect', () => { + it('should have ts errors when using a non existing expect.function', async () => { + // @ts-expect-error + expect.unimplementedFunction() + }) + + it('should support stringContaining, anything and more', async () => { + expect.stringContaining('WebdriverIO') + expect.stringMatching(/WebdriverIO/) + expect.arrayContaining(['WebdriverIO', 'Test']) + expect.objectContaining({ name: 'WebdriverIO' }) + // Was not there but works! + expect.closeTo(5, 10) + expect.arrayContaining(['WebdriverIO', 'Test']) + // New from jest 30!! + expect.arrayOf(expect.stringContaining('WebdriverIO')) + + expect.anything() + expect.any(Function) + expect.any(Number) + expect.any(Boolean) + expect.any(String) + expect.any(Symbol) + expect.any(Date) + expect.any(Error) + + expect.not.stringContaining('WebdriverIO') + expect.not.stringMatching(/WebdriverIO/) + expect.not.arrayContaining(['WebdriverIO', 'Test']) + expect.not.objectContaining({ name: 'WebdriverIO' }) + expect.not.closeTo(5, 10) + expect.not.arrayContaining(['WebdriverIO', 'Test']) + expect.not.arrayOf(expect.stringContaining('WebdriverIO')) + expect.not.anything() + expect.not.any(Function) + expect.not.any(Number) + expect.not.any(Boolean) + expect.not.any(String) + expect.not.any(Symbol) + expect.not.any(Date) + expect.not.any(Error) + }) + + describe('Soft Assertions', async () => { + const actualString: string = 'test' + const actualPromiseString: Promise = Promise.resolve('test') + + describe('expect.soft', () => { + it('should not need to be awaited/be a promise if actual is non-promise type', async () => { + const expectWdioMatcher1: WdioCustomMatchers = expect.soft(actualString) + expectVoid = expect.soft(actualString).toBe('Test Page') + expectVoid = expect.soft(actualString).not.toBe('Test Page') + expectVoid = expect.soft(actualString).not.toBe(expect.stringContaining('Test Page')) + + // @ts-expect-error + expectPromiseVoid = expect.soft(actualString).toBe('Test Page') + // @ts-expect-error + expectPromiseVoid = expect.soft(actualString).not.toBe('Test Page') + // @ts-expect-error + expectPromiseVoid = expect.soft(actualString).not.toBe(expect.stringContaining('Test Page')) + }) + + it('should need to be awaited/be a promise if actual is promise type', async () => { + const expectWdioMatcher1: ExpectWebdriverIO.MatchersAndInverse, Promise> = expect.soft(actualPromiseString) + expectPromiseVoid = expect.soft(actualPromiseString).toBe('Test Page') + expectPromiseVoid = expect.soft(actualPromiseString).not.toBe('Test Page') + expectPromiseVoid = expect.soft(actualPromiseString).not.toBe(expect.stringContaining('Test Page')) + + // @ts-expect-error + expectVoid = expect.soft(actualPromiseString).toBe('Test Page') + // @ts-expect-error + expectVoid = expect.soft(actualPromiseString).not.toBe('Test Page') + // @ts-expect-error + expectVoid = expect.soft(actualPromiseString).not.toBe(expect.stringContaining('Test Page')) + }) + + it('should support chainable element', async () => { + const expectElement: ExpectWebdriverIO.MatchersAndInverse = expect.soft(element) + const expectElementChainable: ExpectWebdriverIO.MatchersAndInverse = expect.soft(chainableElement) + + // @ts-expect-error + const expectElement2: ExpectWebdriverIO.MatchersAndInverse, WebdriverIO.Element> = expect.soft(element) + // @ts-expect-error + const expectElementChainable2: ExpectWebdriverIO.MatchersAndInverse, typeof chainableElement> = expect.soft(chainableElement) + }) + + it('should support chainable element with wdio Matchers', async () => { + expectPromiseVoid = expect.soft(element).toBeDisplayed() + expectPromiseVoid = expect.soft(chainableElement).toBeDisplayed() + expectPromiseVoid = expect.soft(chainableArray).toBeDisplayed() + await expect.soft(element).toBeDisplayed() + await expect.soft(chainableElement).toBeDisplayed() + await expect.soft(chainableArray).toBeDisplayed() + + expectPromiseVoid = expect.soft(element).not.toBeDisplayed() + expectPromiseVoid = expect.soft(chainableElement).not.toBeDisplayed() + expectPromiseVoid = expect.soft(chainableArray).not.toBeDisplayed() + await expect.soft(element).not.toBeDisplayed() + await expect.soft(chainableElement).not.toBeDisplayed() + await expect.soft(chainableArray).not.toBeDisplayed() + + // @ts-expect-error + expectVoid = expect.soft(element).toBeDisplayed() + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toBeDisplayed() + // @ts-expect-error + expectVoid = expect.soft(chainableArray).toBeDisplayed() + + // @ts-expect-error + expectVoid = expect.soft(element).not.toBeDisplayed() + // @ts-expect-error + expectVoid = expect.soft(chainableElement).not.toBeDisplayed() + // @ts-expect-error + expectVoid = expect.soft(chainableArray).not.toBeDisplayed() + }) + + it('should work with custom matcher and custom asymmetric matchers from `expect` module', async () => { + expectPromiseVoid = expect.soft(chainableElement).toHaveCustomProperty('text') + expectPromiseVoid = expect.soft(chainableElement).toHaveCustomProperty(expect.stringContaining('text')) + expectPromiseVoid = expect.soft(chainableElement).not.toHaveCustomProperty(expect.not.stringContaining('text')) + expectPromiseVoid = expect.soft(chainableElement).toHaveCustomProperty( + expect.toHaveCustomProperty(chainableElement) + ) + + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toHaveCustomProperty('text') + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toHaveCustomProperty(expect.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).not.toHaveCustomProperty(expect.not.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toHaveCustomProperty( + expect.toHaveCustomProperty(chainableElement) + ) + + expectPromiseVoid = expect.soft(chainableElement).toHaveCustomProperty('text') + expectPromiseVoid = expect.soft(chainableElement).toHaveCustomProperty(expect.stringContaining('text')) + expectPromiseVoid = expect.soft(chainableElement).not.toHaveCustomProperty(expect.not.stringContaining('text')) + expectPromiseVoid = expect.soft(chainableElement).toHaveCustomProperty( + expect.toHaveCustomProperty(chainableElement) + ) + + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toHaveCustomProperty('text') + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toHaveCustomProperty(expect.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).not.toHaveCustomProperty(expect.not.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toHaveCustomProperty( + expect.toHaveCustomProperty(chainableElement) + ) + }) + + it('should work with custom matcher and custom asymmetric matchers from `ExpectWebDriverIO` namespace', async () => { + expectPromiseVoid = expect.soft(chainableElement).toBeCustomPromise('text') + expectPromiseVoid = expect.soft(chainableElement).toBeCustomPromise(expect.stringContaining('text')) + expectPromiseVoid = expect.soft(chainableElement).not.toBeCustomPromise(expect.not.stringContaining('text')) + expectPromiseVoid = expect.soft(chainableElement).toBeCustomPromise( + expect.toBeCustomPromise(chainableElement) + ) + + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toBeCustomPromise('text') + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toBeCustomPromise(expect.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).not.toBeCustomPromise(expect.not.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toBeCustomPromise( + expect.toBeCustomPromise(chainableElement) + ) + + expectPromiseVoid = expect.soft(chainableElement).toBeCustomPromise('text') + expectPromiseVoid = expect.soft(chainableElement).toBeCustomPromise(expect.stringContaining('text')) + expectPromiseVoid = expect.soft(chainableElement).not.toBeCustomPromise(expect.not.stringContaining('text')) + expectPromiseVoid = expect.soft(chainableElement).toBeCustomPromise( + expect.toBeCustomPromise(chainableElement) + ) + + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toBeCustomPromise('text') + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toBeCustomPromise(expect.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).not.toBeCustomPromise(expect.not.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toBeCustomPromise( + expect.toBeCustomPromise(chainableElement) + ) + }) + }) + + describe('expect.getSoftFailures', () => { + it('should be of type `SoftFailure`', async () => { + const expectSoftFailure1: ExpectWebdriverIO.SoftFailure[] = expect.getSoftFailures() + + // @ts-expect-error + expectVoid = expect.getSoftFailures() + }) + }) + + describe('expect.assertSoftFailures', () => { + it('should be of type void', async () => { + expectVoid = expect.assertSoftFailures() + + // @ts-expect-error + expectPromiseVoid = expect.assertSoftFailures() + }) + }) + + describe('expect.clearSoftFailures', () => { + it('should be of type void', async () => { + expectVoid = expect.clearSoftFailures() + + // @ts-expect-error + expectPromiseVoid = expect.clearSoftFailures() + }) + }) + }) + }) + + describe('Asymmetric matchers', () => { + const string: string = 'WebdriverIO is a test framework' + const array: string[] = ['WebdriverIO', 'Test'] + const object: { name: string } = { name: 'WebdriverIO' } + const number: number = 1 + + it('should have no ts error using asymmetric matchers', async () => { + expect(string).toEqual(expect.stringContaining('WebdriverIO')) + expect(array).toEqual(expect.arrayContaining(['WebdriverIO', 'Test'])) + expect(object).toEqual(expect.objectContaining({ name: 'WebdriverIO' })) + // This one is tested and is working correctly, surprisingly! + expect(number).toEqual(expect.closeTo(1.0001, 0.0001)) + // New from jest 30, should work! + expect(['apple', 'banana', 'cherry']).toEqual(expect.arrayOf(expect.any(String))) + }) + }) + + describe('@types/jest only - original Matchers', () => { + describe('toMatchSnapshot & toMatchInlineSnapshot', () => { + const snapshotName: string = 'test-snapshot' + + it('should work with string', async () => { + const jsonString: string = '{}' + const propertyMatchers = 'test' + expectVoid = expect(jsonString).toMatchSnapshot(propertyMatchers) + expectVoid = expect(jsonString).toMatchSnapshot(propertyMatchers, snapshotName) + expectVoid = expect(jsonString).toMatchInlineSnapshot(propertyMatchers) + expectVoid = expect(jsonString).toMatchInlineSnapshot(propertyMatchers, snapshotName) + + expectVoid = expect(jsonString).not.toMatchSnapshot(propertyMatchers) + expectVoid = expect(jsonString).not.toMatchSnapshot(propertyMatchers, snapshotName) + expectVoid = expect(jsonString).not.toMatchInlineSnapshot(propertyMatchers) + expectVoid = expect(jsonString).not.toMatchInlineSnapshot(propertyMatchers, snapshotName) + + // @ts-expect-error + expectPromiseVoid = expect(jsonString).toMatchSnapshot(propertyMatchers) + // @ts-expect-error + expectPromiseVoid = expect(jsonString).toMatchSnapshot(propertyMatchers, snapshotName) + // @ts-expect-error + expectPromiseVoid = expect(jsonString).toMatchInlineSnapshot(propertyMatchers) + // @ts-expect-error + expectPromiseVoid = expect(jsonString).toMatchInlineSnapshot(propertyMatchers, snapshotName) + // @ts-expect-error + expectPromiseVoid = expect(jsonString).not.toMatchSnapshot(propertyMatchers) + // @ts-expect-error + expectPromiseVoid = expect(jsonString).not.toMatchSnapshot(propertyMatchers, snapshotName) + // @ts-expect-error + expectPromiseVoid = expect(jsonString).not.toMatchInlineSnapshot(propertyMatchers) + // @ts-expect-error + expectPromiseVoid = expect(jsonString).not.toMatchInlineSnapshot(propertyMatchers, snapshotName) + }) + + it('should with object', async () => { + const treeObject = { 1: 'test', 2: 'test2' } + const propertyMatchers = { 1: 'test' } + expectVoid = expect(treeObject).toMatchSnapshot(propertyMatchers) + expectVoid = expect(treeObject).toMatchSnapshot(propertyMatchers, snapshotName) + expectVoid = expect(treeObject).toMatchInlineSnapshot(propertyMatchers) + expectVoid = expect(treeObject).toMatchInlineSnapshot(propertyMatchers, snapshotName) + + expectVoid = expect(treeObject).not.toMatchSnapshot(propertyMatchers) + expectVoid = expect(treeObject).not.toMatchSnapshot(propertyMatchers, snapshotName) + expectVoid = expect(treeObject).not.toMatchInlineSnapshot(propertyMatchers) + expectVoid = expect(treeObject).not.toMatchInlineSnapshot(propertyMatchers, snapshotName) + + // @ts-expect-error + expectPromiseVoid = expect(treeObject).toMatchSnapshot(propertyMatchers) + // @ts-expect-error + expectPromiseVoid = expect(treeObject).toMatchSnapshot(propertyMatchers, snapshotName) + // @ts-expect-error + expectPromiseVoid = expect(treeObject).toMatchInlineSnapshot(propertyMatchers) + // @ts-expect-error + expectPromiseVoid = expect(treeObject).toMatchInlineSnapshot(propertyMatchers, snapshotName) + // @ts-expect-error + expectPromiseVoid = expect(treeObject).not.toMatchSnapshot(propertyMatchers) + // @ts-expect-error + expectPromiseVoid = expect(treeObject).not.toMatchSnapshot(propertyMatchers, snapshotName) + // @ts-expect-error + expectPromiseVoid = expect(treeObject).not.toMatchInlineSnapshot(propertyMatchers) + // @ts-expect-error + expectPromiseVoid = expect(treeObject).not.toMatchInlineSnapshot(propertyMatchers, snapshotName) + }) + }) + }) +}) diff --git a/test-types/jest/tsconfig.json b/test-types/jest/tsconfig.json deleted file mode 100644 index 6407f175a..000000000 --- a/test-types/jest/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "outDir": "dist", - "noImplicitAny": true, - "target": "ES2020", - "module": "Node16", - "skipLibCheck": true, - "types": [ - "@types/jest", - "expect-webdriverio/jest", - "@wdio/globals/types" - ] - } -} diff --git a/test-types/mocha/customMatchers/customMatchers-module-expect.d.ts b/test-types/mocha/customMatchers/customMatchers-module-expect.d.ts new file mode 100644 index 000000000..750d6e1ff --- /dev/null +++ b/test-types/mocha/customMatchers/customMatchers-module-expect.d.ts @@ -0,0 +1,26 @@ +import 'expect' + +/** + * Custom matchers under the `expect` module. + * @see {@link https://jestjs.io/docs/expect#expectextendmatchers} + */ +declare module 'expect' { + interface AsymmetricMatchers { + toBeWithinRange(floor: number, ceiling: number): void + toHaveSimpleCustomProperty(string: string): string + toHaveCustomProperty(element: ChainablePromiseElement | WebdriverIO.Element): Promise> + } + + interface Matchers { + toBeWithinRange(floor: number, ceiling: number): R + toHaveSimpleCustomProperty(string: string | ExpectWebdriverIO.PartialMatcher): Promise + toHaveCustomProperty: + // Useful to typecheck the custom matcher so it is only used on elements + T extends ChainablePromiseElement | WebdriverIO.Element ? + (test: string | ExpectWebdriverIO.PartialMatcher | + // Needed for the custom asymmetric matcher defined above to be typed correctly + Promise>) + // Using `never` blocks the call on non-element types + => Promise : never; + } +} \ No newline at end of file diff --git a/test-types/mocha/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts b/test-types/mocha/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts new file mode 100644 index 000000000..7a833bd87 --- /dev/null +++ b/test-types/mocha/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts @@ -0,0 +1,14 @@ +/** + * Custom matchers under the `ExpectWebdriverIO` namespace. + * @see {@link https://webdriver.io/docs/custommatchers/#typescript-support} + */ +declare namespace ExpectWebdriverIO { + interface AsymmetricMatchers { + toBeCustom(): ExpectWebdriverIO.PartialMatcher; + toBeCustomPromise(chainableElement: ChainablePromiseElement): Promise>; + } + interface Matchers { + toBeCustom(): R; + toBeCustomPromise: T extends ChainablePromiseElement ? (expected?: string | ExpectWebdriverIO.PartialMatcher | Promise>) => Promise : never; + } +} \ No newline at end of file diff --git a/test-types/mocha/tsconfig.json b/test-types/mocha/tsconfig.json new file mode 100644 index 000000000..4a66cfff4 --- /dev/null +++ b/test-types/mocha/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "noEmit": true, + "noImplicitAny": true, + "target": "es2022", + "module": "node18", + "skipLibCheck": true, + "types": [ + "@types/mocha", + "../../types/expect-global.d.ts", + ] + } +} diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts new file mode 100644 index 000000000..4d5ebd886 --- /dev/null +++ b/test-types/mocha/types-mocha.test.ts @@ -0,0 +1,841 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import type { ChainablePromiseElement, ChainablePromiseArray } from 'webdriverio' + +describe('type assertions', () => { + const chainableElement = {} as unknown as ChainablePromiseElement + const chainableArray = {} as ChainablePromiseArray + + const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element + const elementArray: WebdriverIO.ElementArray = [] as unknown as WebdriverIO.ElementArray + + const networkMock: WebdriverIO.Mock = {} as unknown as WebdriverIO.Mock + + // Type assertions + let expectPromiseVoid: Promise + let expectVoid: void + + describe('Browser', () => { + const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser + + describe('toHaveUrl', () => { + it('should be supported correctly', async () => { + expectPromiseVoid = expect(browser).toHaveUrl('https://example.com') + expectPromiseVoid = expect(browser).not.toHaveUrl('https://example.com') + + // Asymmetric matchers + expectPromiseVoid = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) + expectPromiseVoid = expect(browser).toHaveUrl(expect.not.stringContaining('WebdriverIO')) + expectPromiseVoid = expect(browser).toHaveUrl(expect.any(String)) + expectPromiseVoid = expect(browser).toHaveUrl(expect.anything()) + + // @ts-expect-error + expectVoid = expect(browser).toHaveUrl('https://example.com') + // @ts-expect-error + expectVoid = expect(browser).not.toHaveUrl('https://example.com') + // @ts-expect-error + expectVoid = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) + + // @ts-expect-error + await expect(browser).toHaveUrl(6) + }) + + it('should have ts errors when actual is not a Browser element', async () => { + // @ts-expect-error + await expect(element).toHaveUrl('https://example.com') + // @ts-expect-error + await expect(element).not.toHaveUrl('https://example.com') + // @ts-expect-error + await expect(true).toHaveUrl('https://example.com') + // @ts-expect-error + await expect(true).not.toHaveUrl('https://example.com') + }) + }) + + describe('toHaveTitle', () => { + it('should be supported correctly', async () => { + expectPromiseVoid = expect(browser).toHaveTitle('https://example.com') + expectPromiseVoid = expect(browser).not.toHaveTitle('https://example.com') + + // Asymmetric matchers + expectPromiseVoid = expect(browser).toHaveTitle(expect.stringContaining('WebdriverIO')) + expectPromiseVoid = expect(browser).toHaveTitle(expect.any(String)) + expectPromiseVoid = expect(browser).toHaveTitle(expect.anything()) + + // @ts-expect-error + expectVoid = expect(browser).toHaveTitle('https://example.com') + // @ts-expect-error + expectVoid = expect(browser).not.toHaveTitle('https://example.com') + // @ts-expect-error + expectVoid = expect(browser).toHaveTitle(expect.stringContaining('WebdriverIO')) + }) + + it('should have ts errors when actual is not a Browser element', async () => { + // @ts-expect-error + await expect(element).toHaveTitle('https://example.com') + // @ts-expect-error + await expect(element).not.toHaveTitle('https://example.com') + // @ts-expect-error + await expect(true).toHaveTitle('https://example.com') + // @ts-expect-error + await expect(true).not.toHaveTitle('https://example.com') + }) + }) + }) + + describe('element', () => { + + describe('toBeDisabled', () => { + it('should be supported correctly', async () => { + // Element + expectPromiseVoid = expect(element).toBeDisabled() + expectPromiseVoid = expect(element).not.toBeDisabled() + + // Element array + expectPromiseVoid = expect(elementArray).toBeDisabled() + expectPromiseVoid = expect(elementArray).not.toBeDisabled() + + // Chainable element + expectPromiseVoid = expect(chainableElement).toBeDisabled() + expectPromiseVoid = expect(chainableElement).not.toBeDisabled() + + // Chainable element array + expectPromiseVoid = expect(chainableArray).toBeDisabled() + expectPromiseVoid = expect(chainableArray).not.toBeDisabled() + + // @ts-expect-error + expectVoid = expect(element).toBeDisabled() + // @ts-expect-error + expectVoid = expect(element).not.toBeDisabled() + }) + + it('should have ts errors when actual is not an element', async () => { + // @ts-expect-error + await expect(browser).toBeDisabled() + // @ts-expect-error + await expect(browser).not.toBeDisabled() + // @ts-expect-error + await expect(true).toBeDisabled() + // @ts-expect-error + await expect(true).not.toBeDisabled() + }) + }) + + describe('toHaveText', () => { + it('should be supported correctly', async () => { + expectPromiseVoid = expect(element).toHaveText('text') + expectPromiseVoid = expect(element).toHaveText(/text/) + expectPromiseVoid = expect(element).toHaveText(['text1', 'text2']) + expectPromiseVoid = expect(element).toHaveText([expect.stringContaining('text1'), expect.stringContaining('text2')]) + expectPromiseVoid = expect(element).toHaveText([/text1/, /text2/]) + expectPromiseVoid = expect(element).toHaveText(['text1', /text1/, expect.stringContaining('text3')]) + await expect(element).toHaveText( + 'My-Ex-Am-Ple', + { + replace: [[/-/g, ' '], [/[A-Z]+/g, (match: string) => match.toLowerCase()]] + } + ) + + expectPromiseVoid = expect(element).not.toHaveText('text') + + // @ts-expect-error + expectVoid = expect(element).toHaveText('text') + // @ts-expect-error + await expect(element).toHaveText(6) + + expectPromiseVoid = expect(chainableElement).toHaveText('text') + expectPromiseVoid = expect(chainableElement).toHaveText(/text/) + expectPromiseVoid = expect(chainableElement).toHaveText(['text1', 'text2']) + expectPromiseVoid = expect(chainableElement).toHaveText([expect.stringContaining('text1'), expect.stringContaining('text2')]) + expectPromiseVoid = expect(chainableElement).toHaveText([/text1/, /text2/]) + expectPromiseVoid = expect(chainableElement).toHaveText(['text1', /text1/, expect.stringContaining('text3')]) + + expectPromiseVoid = expect(chainableElement).not.toHaveText('text') + + // @ts-expect-error + expectVoid = expect(chainableElement).toHaveText('text') + // @ts-expect-error + await expect(chainableElement).toHaveText(6) + + expectPromiseVoid = expect(elementArray).toHaveText('text') + expectPromiseVoid = expect(elementArray).toHaveText(/text/) + expectPromiseVoid = expect(elementArray).toHaveText(['text1', 'text2']) + expectPromiseVoid = expect(elementArray).toHaveText([expect.stringContaining('text1'), expect.stringContaining('text2')]) + expectPromiseVoid = expect(elementArray).toHaveText([/text1/, /text2/]) + expectPromiseVoid = expect(elementArray).toHaveText(['text1', /text1/, expect.stringContaining('text3')]) + + expectPromiseVoid = expect(elementArray).not.toHaveText('text') + + // @ts-expect-error + expectVoid = expect(elementArray).toHaveText('text') + // @ts-expect-error + await expect(elementArray).toHaveText(6) + + expectPromiseVoid = expect(chainableArray).toHaveText('text') + expectPromiseVoid = expect(chainableArray).toHaveText(/text/) + expectPromiseVoid = expect(chainableArray).toHaveText(['text1', 'text2']) + expectPromiseVoid = expect(chainableArray).toHaveText([expect.stringContaining('text1'), expect.stringContaining('text2')]) + expectPromiseVoid = expect(chainableArray).toHaveText([/text1/, /text2/]) + expectPromiseVoid = expect(chainableArray).toHaveText(['text1', /text1/, expect.stringContaining('text3')]) + + expectPromiseVoid = expect(chainableArray).not.toHaveText('text') + + // @ts-expect-error + expectVoid = expect(chainableArray).toHaveText('text') + // @ts-expect-error + await expect(chainableArray).toHaveText(6) + + // @ts-expect-error + await expect(browser).toHaveText('text') + }) + + it('should have ts errors when actual is not an element', async () => { + // @ts-expect-error + await expect(browser).toHaveText('text') + // @ts-expect-error + await expect(browser).not.toHaveText('text') + // @ts-expect-error + await expect(true).toHaveText('text') + // @ts-expect-error + await expect(true).toHaveText('text') + }) + + it('should have ts errors when actual is string or Promise', async () => { + // @ts-expect-error + await expect('text').toHaveText('text') + // @ts-expect-error + await expect('text').not.toHaveText('text') + // @ts-expect-error + await expect(Promise.resolve('text')).toHaveText('text') + // @ts-expect-error + await expect(Promise.resolve('text')).toHaveText('text') + }) + }) + + describe('toHaveHeight', () => { + it('should be supported correctly', async () => { + expectPromiseVoid = expect(element).toHaveHeight(100) + expectPromiseVoid = expect(element).toHaveHeight(100, { message: 'Custom error message' }) + expectPromiseVoid = expect(element).not.toHaveHeight(100) + expectPromiseVoid = expect(element).not.toHaveHeight(100, { message: 'Custom error message' }) + + expectPromiseVoid = expect(element).toHaveHeight({ width: 100, height: 200 }) + expectPromiseVoid = expect(element).toHaveHeight({ width: 100, height: 200 }, { message: 'Custom error message' }) + expectPromiseVoid = expect(element).not.toHaveHeight({ width: 100, height: 200 }) + expectPromiseVoid = expect(element).not.toHaveHeight({ width: 100, height: 200 }, { message: 'Custom error message' }) + + // @ts-expect-error + expectVoid = expect(element).toHaveHeight(100) + // @ts-expect-error + expectVoid = expect(element).not.toHaveHeight(100) + + // @ts-expect-error + expectVoid = expect(element).toHaveHeight({ width: 100, height: 200 }) + // @ts-expect-error + expectVoid = expect(element).not.toHaveHeight({ width: 100, height: 200 }) + + // @ts-expect-error + await expect(browser).toHaveHeight(100) + }) + + it('should have ts errors when actual is string or Promise', async () => { + // @ts-expect-error + await expect('text').toHaveText('text') + // @ts-expect-error + await expect('text').not.toHaveText('text') + // @ts-expect-error + await expect(Promise.resolve('text')).toHaveText('text') + // @ts-expect-error + await expect(Promise.resolve('text')).toHaveText('text') + }) + }) + + describe('toMatchSnapshot', () => { + + it('should be supported correctly', async () => { + expectVoid = expect(element).toMatchSnapshot() + expectVoid = expect(element).toMatchSnapshot('test label') + expectVoid = expect(element).not.toMatchSnapshot('test label') + + expectPromiseVoid = expect(chainableElement).toMatchSnapshot() + expectPromiseVoid = expect(chainableElement).toMatchSnapshot('test label') + expectPromiseVoid = expect(chainableElement).not.toMatchSnapshot('test label') + + //@ts-expect-error + expectPromiseVoid = expect(element).toMatchSnapshot() + //@ts-expect-error + expectPromiseVoid = expect(element).not.toMatchSnapshot() + //@ts-expect-error + expectVoid = expect(chainableElement).toMatchSnapshot() + //@ts-expect-error + expectVoid = expect(chainableElement).not.toMatchSnapshot() + }) + }) + + describe('toMatchInlineSnapshot', () => { + + it('should be correctly supported', async () => { + expectVoid = expect(element).toMatchInlineSnapshot() + expectVoid = expect(element).toMatchInlineSnapshot('test snapshot') + expectVoid = expect(element).toMatchInlineSnapshot('test snapshot', 'test label') + + expectPromiseVoid = expect(chainableElement).toMatchInlineSnapshot() + expectPromiseVoid = expect(chainableElement).toMatchInlineSnapshot('test snapshot') + expectPromiseVoid = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + + //@ts-expect-error + expectPromiseVoid = expect(element).toMatchInlineSnapshot() + //@ts-expect-error + expectVoid = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + }) + + it('should be correctly supported with getCSSProperty()', async () => { + expectPromiseVoid = expect(element.getCSSProperty('test')).toMatchInlineSnapshot() + expectPromiseVoid = expect(element.getCSSProperty('test')).toMatchInlineSnapshot('test snapshot') + expectPromiseVoid = expect(element.getCSSProperty('test')).toMatchInlineSnapshot('test snapshot', 'test label') + + expectPromiseVoid = expect(chainableElement.getCSSProperty('test')).toMatchInlineSnapshot() + expectPromiseVoid = expect(chainableElement.getCSSProperty('test')).toMatchInlineSnapshot('test snapshot') + expectPromiseVoid = expect(chainableElement.getCSSProperty('test')).toMatchInlineSnapshot('test snapshot', 'test label') + + expectVoid = expect(element).toMatchInlineSnapshot() + + //@ts-expect-error + expectPromiseVoid = expect(element).toMatchInlineSnapshot() + //@ts-expect-error + expectVoid = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + + //@ts-expect-error + expectVoid = expect(element.getCSSProperty('test')).toMatchInlineSnapshot() + //@ts-expect-error + expectVoid = expect(chainableElement.getCSSProperty('test')).toMatchInlineSnapshot() + }) + }) + + describe('toBeElementsArrayOfSize', async () => { + + it('should work correctly when actual is chainableArray', async () => { + expectPromiseVoid = expect(chainableArray).toBeElementsArrayOfSize(5) + expectPromiseVoid = expect(chainableArray).toBeElementsArrayOfSize({ lte: 10 }) + + // @ts-expect-error + expectVoid = expect(chainableArray).toBeElementsArrayOfSize(5) + // @ts-expect-error + expectVoid = expect(chainableArray).toBeElementsArrayOfSize({ lte: 10 }) + }) + + it('should not work when actual is not chainableArray', async () => { + // @ts-expect-error + await expect(chainableElement).toBeElementsArrayOfSize(5) + // @ts-expect-error + await expect(chainableElement).toBeElementsArrayOfSize({ lte: 10 }) + // @ts-expect-error + await expect(true).toBeElementsArrayOfSize(5) + // @ts-expect-error + await expect(true).toBeElementsArrayOfSize({ lte: 10 }) + }) + }) + }) + + describe('Custom matchers', () => { + describe('using `ExpectWebdriverIO` namespace augmentation', () => { + it('should supported correctly a non-promise custom matcher', async () => { + expectVoid = expect('test').toBeCustom() + expectVoid = expect('test').not.toBeCustom() + + // @ts-expect-error + expectPromiseVoid = expect('test').toBeCustom() + // @ts-expect-error + expectPromiseVoid = expect('test').not.toBeCustom() + + expectVoid = expect(1).toBeWithinRange(0, 2) + }) + + it('should supported correctly a promise custom matcher with only chainableElement as actual', async () => { + expectPromiseVoid = expect(chainableElement).toBeCustomPromise() + expectPromiseVoid = expect(chainableElement).toBeCustomPromise(expect.stringContaining('test')) + expectPromiseVoid = expect(chainableElement).not.toBeCustomPromise(expect.not.stringContaining('test')) + + // @ts-expect-error + expect('test').toBeCustomPromise() + // @ts-expect-error + expectVoid = expect(chainableElement).toBeCustomPromise() + // @ts-expect-error + expectVoid = expect(chainableElement).toBeCustomPromise(expect.stringContaining('test')) + // @ts-expect-error + expectVoid = expect(chainableElement).not.toBeCustomPromise(expect.stringContaining('test')) + // @ts-expect-error + expect(chainableElement).toBeCustomPromise(expect.stringContaining(6)) + }) + + it('should support custom asymmetric matcher', async () => { + const expectString1 : ExpectWebdriverIO.PartialMatcher = expect.toBeCustom() + const expectString2 : ExpectWebdriverIO.PartialMatcher = expect.not.toBeCustom() + + expectPromiseVoid = expect(chainableElement).toBeCustomPromise(expect.toBeCustom()) + + // @ts-expect-error + expectPromiseVoid = expect.toBeCustom() + // @ts-expect-error + expectPromiseVoid = expect.not.toBeCustom() + + //@ts-expect-error + expectVoid = expect(chainableElement).toBeCustomPromise(expect.toBeCustom()) + }) + }) + + describe('using `expect` module declaration', () => { + + it('should support a simple matcher', async () => { + expectVoid = expect(5).toBeWithinRange(1, 10) + + // Or as an asymmetric matcher: + expectVoid = expect({ value: 5 }).toEqual({ + value: expect.toBeWithinRange(1, 10) + }) + + // @ts-expect-error + expectVoid = expect(5).toBeWithinRange(1, '10') + // @ts-expect-error + expectPromiseVoid = expect(5).toBeWithinRange('1') + }) + + it('should support a simple custom matcher with a chainable element matcher with promise', async () => { + expectPromiseVoid = expect(chainableElement).toHaveSimpleCustomProperty('text') + expectPromiseVoid = expect(chainableElement).toHaveSimpleCustomProperty(expect.stringContaining('text')) + expectPromiseVoid = expect(chainableElement).not.toHaveSimpleCustomProperty(expect.not.stringContaining('text')) + + // Or as a custom asymmetric matcher: + expectPromiseVoid = expect(chainableElement).toHaveSimpleCustomProperty( + expect.toHaveSimpleCustomProperty('string') + ) + const expectString1:string = expect.toHaveSimpleCustomProperty('string') + const expectString2:string = expect.not.toHaveSimpleCustomProperty('string') + + // @ts-expect-error + expectVoid = expect.toHaveSimpleCustomProperty(chainableElement) + // @ts-expect-error + expectVoid = expect.not.toHaveSimpleCustomProperty(chainableElement) + + // @ts-expect-error + expectVoid = expect.toHaveSimpleCustomProperty(chainableElement) + }) + + it('should support a chainable element matcher with promise', async () => { + expectPromiseVoid = expect(chainableElement).toHaveCustomProperty('text') + expectPromiseVoid = expect(chainableElement).toHaveCustomProperty(expect.stringContaining('text')) + expectPromiseVoid = expect(chainableElement).not.toHaveCustomProperty(expect.not.stringContaining('text')) + + // Or as a custom asymmetric matcher: + expectPromiseVoid = expect(chainableElement).toHaveCustomProperty( + await expect.toHaveCustomProperty(chainableElement) + ) + const expectPromiseWdioElement1: Promise> = expect.toHaveCustomProperty(chainableElement) + const expectPromiseWdioElement2: Promise> = expect.not.toHaveCustomProperty(chainableElement) + + // @ts-expect-error + expectVoid = expect.toHaveCustomProperty(chainableElement) + // @ts-expect-error + expectVoid = expect.not.toHaveCustomProperty(chainableElement) + + // @ts-expect-error + expectVoid = expect.toHaveCustomProperty(chainableElement) + // @ts-expect-error + expect.toHaveCustomProperty('test') + + await expect(chainableElement).toHaveCustomProperty( + await expect.toHaveCustomProperty(chainableElement) + ) + }) + }) + }) + + describe('toBe', () => { + + it('should expect void type when actual is a boolean', async () => { + expectVoid = expect(true).toBe(true) + expectVoid = expect(true).not.toBe(true) + + //@ts-expect-error + expectPromiseVoid = expect(true).toBe(true) + //@ts-expect-error + expectPromiseVoid = expect(true).not.toBe(true) + }) + + it('should not expect Promise when actual is a chainable since toBe does not need to be awaited', async () => { + expectVoid = expect(chainableElement).toBe(true) + expectVoid = expect(chainableElement).not.toBe(true) + + //@ts-expect-error + expectPromiseVoid = expect(chainableElement).toBe(true) + //@ts-expect-error + expectPromiseVoid = expect(chainableElement).not.toBe(true) + }) + + it('should still expect void type when actual is a Promise since we do not overload them', async () => { + const promiseBoolean = Promise.resolve(true) + + expectVoid = expect(promiseBoolean).toBeDefined() + expectVoid = expect(promiseBoolean).not.toBeDefined() + + //@ts-expect-error + expectPromiseVoid = expect(promiseBoolean).toBeDefined() + //@ts-expect-error + expectPromiseVoid = expect(promiseBoolean).toBeDefined() + }) + + it('should work with string', async () => { + expectVoid = expect('text').toBe(true) + expectVoid = expect('text').not.toBe(true) + expectVoid = expect('text').toBe(expect.stringContaining('text')) + expectVoid = expect('text').not.toBe(expect.stringContaining('text')) + + //@ts-expect-error + expectPromiseVoid = expect('text').toBe(true) + //@ts-expect-error + expectPromiseVoid = expect('text').not.toBe(true) + //@ts-expect-error + expectPromiseVoid = expect('text').toBe(expect.stringContaining('text')) + //@ts-expect-error + expectPromiseVoid = expect('text').not.toBe(expect.stringContaining('text')) + }) + }) + + describe('Promise type assertions', () => { + const booleanPromise: Promise = Promise.resolve(true) + + it('should have expect return Matchers with a Promise', async () => { + const expectPromiseBoolean1: ExpectWebdriverIO.Matchers> & ExpectLibInverse>> & ExpectWebdriverIO.PromiseMatchers = expect(booleanPromise) + const expectPromiseBoolean2: ExpectWebdriverIO.Matchers> = expect(booleanPromise).not + }) + + it('should work with resolves & rejects correctly', async () => { + expectPromiseVoid = expect(booleanPromise).resolves.toBe(true) + expectPromiseVoid = expect(booleanPromise).rejects.toBe(true) + expectPromiseVoid = expect(booleanPromise).rejects.not.toBe(true) + expectPromiseVoid = expect(booleanPromise).resolves.not.toBe(true) + + //@ts-expect-error + expectVoid = expect(booleanPromise).resolves.toBe(true) + //@ts-expect-error + expectVoid = expect(booleanPromise).rejects.toBe(true) + + //@ts-expect-error + expect(true).resolves.toBe(true) + //@ts-expect-error + expect(true).rejects.toBe(true) + }) + + it('should not support chainable and expect PromiseVoid with toBe', async () => { + //@ts-expect-error + expectPromiseVoid = expect(chainableElement).toBe(true) + //@ts-expect-error + expectPromiseVoid = expect(chainableElement).not.toBe(true) + }) + }) + + describe('Network Matchers', () => { + const promiseNetworkMock = Promise.resolve(networkMock) + + it('should not have ts errors when typing to Promise', async () => { + expectPromiseVoid = expect(promiseNetworkMock).toBeRequested() + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedTimes(2) + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) + + expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequested() + expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequestedTimes(2) + expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) + + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith({ + url: 'http://localhost:8080/api', + method: 'POST', + statusCode: 200, + requestHeaders: { Authorization: 'foo' }, + responseHeaders: { Authorization: 'bar' }, + postData: { title: 'foo', description: 'bar' }, + response: { success: true }, + }) + + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith({ + url: expect.stringContaining('test'), + method: 'POST', + statusCode: 200, + requestHeaders: expect.objectContaining({ Authorization: 'foo' }), + responseHeaders: expect.objectContaining({ Authorization: 'bar' }), + postData: expect.objectContaining({ title: 'foo', description: 'bar' }), + response: expect.objectContaining({ success: true }), + }) + + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith({ + url: expect.stringMatching(/.*\/api\/.*/i), + method: ['POST', 'PUT'], + statusCode: [401, 403], + requestHeaders: headers => headers.Authorization.startsWith('Bearer '), + postData: expect.objectContaining({ released: true, title: expect.stringContaining('foobar') }), + response: (r: { data: { items: unknown[] } }) => Array.isArray(r) && r.data.items.length === 20 + }) + }) + + it('should have ts errors when typing to void', async () => { + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).toBeRequested() + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).not.toBeRequested() + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).not.toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).toBeRequestedWith({ + url: 'http://localhost:8080/api', + method: 'POST', + statusCode: 200, + requestHeaders: { Authorization: 'foo' }, + responseHeaders: { Authorization: 'bar' }, + postData: { title: 'foo', description: 'bar' }, + response: { success: true }, + }) + + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).toBeRequestedWith(expect.objectContaining({ + response: { success: true }, + })) + }) + }) + + describe('Expect', () => { + it('should have ts errors when using a non existing expect.function', async () => { + // @ts-expect-error + expect.unimplementedFunction() + }) + + it('should support stringContaining, anything and more', async () => { + expect.stringContaining('WebdriverIO') + expect.stringMatching(/WebdriverIO/) + expect.arrayContaining(['WebdriverIO', 'Test']) + expect.objectContaining({ name: 'WebdriverIO' }) + // Was not there but works! + expect.closeTo(5, 10) + expect.arrayContaining(['WebdriverIO', 'Test']) + // New from jest 30!! + expect.arrayOf(expect.stringContaining('WebdriverIO')) + + expect.anything() + expect.any(Function) + expect.any(Number) + expect.any(Boolean) + expect.any(String) + expect.any(Symbol) + expect.any(Date) + expect.any(Error) + + expect.not.stringContaining('WebdriverIO') + expect.not.stringMatching(/WebdriverIO/) + expect.not.arrayContaining(['WebdriverIO', 'Test']) + expect.not.objectContaining({ name: 'WebdriverIO' }) + expect.not.closeTo(5, 10) + expect.not.arrayContaining(['WebdriverIO', 'Test']) + expect.not.arrayOf(expect.stringContaining('WebdriverIO')) + }) + + describe('Soft Assertions', async () => { + const actualString: string = 'Test Page' + const actualPromiseString: Promise = Promise.resolve('Test Page') + + describe('expect.soft', () => { + it('should not need to be awaited/be a promise if actual is non-promise type', async () => { + const expectWdioMatcher1: WdioCustomMatchers = expect.soft(actualString) + expectVoid = expect.soft(actualString).toBe('Test Page') + expectVoid = expect.soft(actualString).not.toBe('Test Page') + expectVoid = expect.soft(actualString).not.toBe(expect.stringContaining('Test Page')) + + // @ts-expect-error + expectPromiseVoid = expect.soft(actualString).toBe('Test Page') + // @ts-expect-error + expectPromiseVoid = expect.soft(actualString).not.toBe('Test Page') + // @ts-expect-error + expectPromiseVoid = expect.soft(actualString).not.toBe(expect.stringContaining('Test Page')) + }) + + it('should need to be awaited/be a promise if actual is promise type', async () => { + const expectWdioMatcher1: ExpectWebdriverIO.MatchersAndInverse, Promise> = expect.soft(actualPromiseString) + expectPromiseVoid = expect.soft(actualPromiseString).toBe('Test Page') + expectPromiseVoid = expect.soft(actualPromiseString).not.toBe('Test Page') + expectPromiseVoid = expect.soft(actualPromiseString).not.toBe(expect.stringContaining('Test Page')) + + // @ts-expect-error + expectVoid = expect.soft(actualPromiseString).toBe('Test Page') + // @ts-expect-error + expectVoid = expect.soft(actualPromiseString).not.toBe('Test Page') + // @ts-expect-error + expectVoid = expect.soft(actualPromiseString).not.toBe(expect.stringContaining('Test Page')) + }) + + it('should support chainable element', async () => { + const expectElement: ExpectWebdriverIO.MatchersAndInverse = expect.soft(element) + const expectElementChainable: ExpectWebdriverIO.MatchersAndInverse = expect.soft(chainableElement) + + // @ts-expect-error + const expectElement2: ExpectWebdriverIO.MatchersAndInverse, WebdriverIO.Element> = expect.soft(element) + // @ts-expect-error + const expectElementChainable2: ExpectWebdriverIO.MatchersAndInverse, typeof chainableElement> = expect.soft(chainableElement) + }) + + it('should support chainable element with wdio Matchers', async () => { + expectPromiseVoid = expect.soft(element).toBeDisplayed() + expectPromiseVoid = expect.soft(chainableElement).toBeDisplayed() + expectPromiseVoid = expect.soft(chainableArray).toBeDisplayed() + await expect.soft(element).toBeDisplayed() + await expect.soft(chainableElement).toBeDisplayed() + await expect.soft(chainableArray).toBeDisplayed() + + expectPromiseVoid = expect.soft(element).not.toBeDisplayed() + expectPromiseVoid = expect.soft(chainableElement).not.toBeDisplayed() + expectPromiseVoid = expect.soft(chainableArray).not.toBeDisplayed() + await expect.soft(element).not.toBeDisplayed() + await expect.soft(chainableElement).not.toBeDisplayed() + await expect.soft(chainableArray).not.toBeDisplayed() + + // @ts-expect-error + expectVoid = expect.soft(element).toBeDisplayed() + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toBeDisplayed() + // @ts-expect-error + expectVoid = expect.soft(chainableArray).toBeDisplayed() + + // @ts-expect-error + expectVoid = expect.soft(element).not.toBeDisplayed() + // @ts-expect-error + expectVoid = expect.soft(chainableElement).not.toBeDisplayed() + // @ts-expect-error + expectVoid = expect.soft(chainableArray).not.toBeDisplayed() + }) + + it('should work with custom matcher and custom asymmetric matchers from `expect` module', async () => { + expectPromiseVoid = expect.soft(chainableElement).toHaveCustomProperty('text') + expectPromiseVoid = expect.soft(chainableElement).toHaveCustomProperty(expect.stringContaining('text')) + expectPromiseVoid = expect.soft(chainableElement).not.toHaveCustomProperty(expect.not.stringContaining('text')) + expectPromiseVoid = expect.soft(chainableElement).toHaveCustomProperty( + expect.toHaveCustomProperty(chainableElement) + ) + + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toHaveCustomProperty('text') + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toHaveCustomProperty(expect.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).not.toHaveCustomProperty(expect.not.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toHaveCustomProperty( + expect.toHaveCustomProperty(chainableElement) + ) + + expectPromiseVoid = expect.soft(chainableElement).toHaveCustomProperty('text') + expectPromiseVoid = expect.soft(chainableElement).toHaveCustomProperty(expect.stringContaining('text')) + expectPromiseVoid = expect.soft(chainableElement).not.toHaveCustomProperty(expect.not.stringContaining('text')) + expectPromiseVoid = expect.soft(chainableElement).toHaveCustomProperty( + expect.toHaveCustomProperty(chainableElement) + ) + + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toHaveCustomProperty('text') + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toHaveCustomProperty(expect.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).not.toHaveCustomProperty(expect.not.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toHaveCustomProperty( + expect.toHaveCustomProperty(chainableElement) + ) + }) + + it('should work with custom matcher and custom asymmetric matchers from `ExpectWebDriverIO` namespace', async () => { + expectPromiseVoid = expect.soft(chainableElement).toBeCustomPromise('text') + expectPromiseVoid = expect.soft(chainableElement).toBeCustomPromise(expect.stringContaining('text')) + expectPromiseVoid = expect.soft(chainableElement).not.toBeCustomPromise(expect.not.stringContaining('text')) + expectPromiseVoid = expect.soft(chainableElement).toBeCustomPromise( + expect.toBeCustomPromise(chainableElement) + ) + + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toBeCustomPromise('text') + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toBeCustomPromise(expect.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).not.toBeCustomPromise(expect.not.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toBeCustomPromise( + expect.toBeCustomPromise(chainableElement) + ) + + expectPromiseVoid = expect.soft(chainableElement).toBeCustomPromise('text') + expectPromiseVoid = expect.soft(chainableElement).toBeCustomPromise(expect.stringContaining('text')) + expectPromiseVoid = expect.soft(chainableElement).not.toBeCustomPromise(expect.not.stringContaining('text')) + expectPromiseVoid = expect.soft(chainableElement).toBeCustomPromise( + expect.toBeCustomPromise(chainableElement) + ) + + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toBeCustomPromise('text') + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toBeCustomPromise(expect.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).not.toBeCustomPromise(expect.not.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toBeCustomPromise( + expect.toBeCustomPromise(chainableElement) + ) + }) + }) + + describe('expect.getSoftFailures', () => { + it('should be of type `SoftFailure`', async () => { + const expectSoftFailure1: ExpectWebdriverIO.SoftFailure[] = expect.getSoftFailures() + + // @ts-expect-error + expectVoid = expect.getSoftFailures() + }) + }) + + describe('expect.assertSoftFailures', () => { + it('should be of type void', async () => { + expectVoid = expect.assertSoftFailures() + + // @ts-expect-error + expectPromiseVoid = expect.assertSoftFailures() + }) + }) + + describe('expect.clearSoftFailures', () => { + it('should be of type void', async () => { + expectVoid = expect.clearSoftFailures() + + // @ts-expect-error + expectPromiseVoid = expect.clearSoftFailures() + }) + }) + }) + }) + + describe('Asymmetric matchers', () => { + const string: string = 'WebdriverIO is a test framework' + const array: string[] = ['WebdriverIO', 'Test'] + const object: { name: string } = { name: 'WebdriverIO' } + const number: number = 1 + + it('should have no ts error using asymmetric matchers', async () => { + expect(string).toEqual(expect.stringContaining('WebdriverIO')) + expect(array).toEqual(expect.arrayContaining(['WebdriverIO', 'Test'])) + expect(object).toEqual(expect.objectContaining({ name: 'WebdriverIO' })) + // This one is tested and is working correctly, surprisingly! + expect(number).toEqual(expect.closeTo(1.0001, 0.0001)) + // New from jest 30, should work! + expect(['apple', 'banana', 'cherry']).toEqual(expect.arrayOf(expect.any(String))) + }) + }) +}) diff --git a/test-types/types.ts b/test-types/types.ts deleted file mode 100644 index fcf5b056e..000000000 --- a/test-types/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -/// -/// -const elem: WebdriverIO.Element = {} as unknown as WebdriverIO.Element -const wdioExpect = ExpectWebdriverIO.expect - -wdioExpect(elem).toBeDisabled() -wdioExpect(elem).toHaveAttr('test') -wdioExpect(elem).not.toHaveAttr('test') -wdioExpect(elem).toBe('bar') - -wdioExpect(elem).toHaveElementClass('bar') -wdioExpect(elem).toHaveElementProperty('n', 'v', {}) -wdioExpect({ foo: 'bar' }).toMatchSnapshot() -wdioExpect({ foo: 'bar' }).toMatchInlineSnapshot() diff --git a/test/matchers.test.ts b/test/matchers.test.ts index 5e5f15ab8..f0d642eb4 100644 --- a/test/matchers.test.ts +++ b/test/matchers.test.ts @@ -61,7 +61,13 @@ test('matchers', () => { test('allows to add matcher', () => { const matcher: any = vi.fn((actual: any, expected: any) => ({ pass: actual === expected })) expectLib.extend({ toBeCustom: matcher }) + // @ts-expect-error not in types expectLib('foo').toBeCustom('foo') expect(matchers.keys()).toContain('toBeCustom') }) + +test('Generic asymmetric matchers from Expect library should work', () => { + expectLib(1).toEqual(expectLib.closeTo(1.0001, 0.0001)) + expectLib(['apple', 'banana', 'cherry']).toEqual(expectLib.arrayOf(expectLib.any(String))) +}) diff --git a/test/snapshot.test.ts.snap b/test/snapshot.test.ts.snap index a7e784bd3..7af66be0a 100644 --- a/test/snapshot.test.ts.snap +++ b/test/snapshot.test.ts.snap @@ -5,13 +5,3 @@ exports[`parent > test 1`] = ` "a": "a", } `; - -exports[`parent > test 2`] = ` -{ - "deep": { - "nested": { - "object": "value", - }, - }, -} -`; diff --git a/test/softAssertions.test.ts b/test/softAssertions.test.ts index bef7e9603..04734de9d 100644 --- a/test/softAssertions.test.ts +++ b/test/softAssertions.test.ts @@ -7,7 +7,7 @@ vi.mock('@wdio/globals') describe('Soft Assertions', () => { // Setup a mock element for testing - let el: any + let el: ChainablePromiseElement beforeEach(async () => { el = $('sel') @@ -106,6 +106,23 @@ describe('Soft Assertions', () => { // Should be no failures now expect(expectWdio.getSoftFailures().length).toBe(0) }) + + /** + * TODO: Skipped since soft assertions are currently not supporting basic matchers like toBe or toEqual. To fix one day! + * @see https://github.com/webdriverio/expect-webdriverio/issues/1887 + */ + it.skip('should support basic text matching', async () => { + const softService = SoftAssertService.getInstance() + softService.setCurrentTest('test-7', 'test name', 'test file') + const text = await el.getText() + + expectWdio.soft(text).toEqual('!Actual Text') + + const failures = expectWdio.getSoftFailures() + expect(failures.length).toBe(1) + expect(failures[0].matcherName).toBe('toHaveText') + }) + }) describe('SoftAssertService hooks', () => { diff --git a/tsconfig.json b/tsconfig.json index bd84d6376..e3870dc8b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,20 @@ { + /** + * Let's aligned with the WebdriverIO tsconfig.json. + * @see https://github.com/webdriverio/webdriverio/blob/main/tsconfig.json#L5 + */ "compilerOptions": { "outDir": "./lib/", - "module": "ESNext", - "target": "ES2020", - "lib": ["ES2020", "DOM"], + "module": "esnext", + "target": "es2022", + "lib": ["ES2022", "DOM"], "strictBindCallApply": true, "removeComments": true, "noImplicitAny": true, "skipLibCheck": true, "strictPropertyInitialization": true, "strictNullChecks": true, - "moduleResolution": "Node", + "moduleResolution": "node", // To review since this is equivalent to node10 "allowSyntheticDefaultImports": true, "types": [ "node", diff --git a/tsconfig.types.json b/tsconfig.types.json new file mode 100644 index 000000000..8ab0f0185 --- /dev/null +++ b/tsconfig.types.json @@ -0,0 +1,30 @@ +{ + /** + * TypeScript configuration for the type files. + * + * Running tsc on types is extremely sensible to the node_modules folder so we wrap the tsc command in a script + * @see types-checks-filter-out-node_modules.js + */ + "compilerOptions": { + "target": "ES2022", + "strictBindCallApply": true, + "noImplicitAny": true, + + // Must stay commented to ensure that the types are actually validated + // "skipLibCheck": true, + + "strictPropertyInitialization": true, + "strictNullChecks": true, + "allowSyntheticDefaultImports": true, + "types": [ + "@wdio/types", + ], + "moduleResolution": "node", + "noEmit": true, + }, + + "include": [ + "*.d.ts", + "./types/*.d.ts" + ], +} \ No newline at end of file diff --git a/types-checks-filter-out-node_modules.js b/types-checks-filter-out-node_modules.js new file mode 100644 index 000000000..9b59fcc4e --- /dev/null +++ b/types-checks-filter-out-node_modules.js @@ -0,0 +1,40 @@ +const node = await import('node:child_process') + +/** + * Running tsc on types is extremely sensible to node_modules types that we import and there is no way to exclude them. + * Note: Yes, we try `--exclude` and it is not excluding them. + * So this script just excludes expected errors but still allows to validate the types we release thoroughly + */ + +// List of paths to exclude (relative or absolute, as needed) +const excludeList = [ + 'node_modules/@types/node/url.d.ts', + 'node_modules/urlpattern-polyfill/dist/index.d.ts', + 'node_modules/webdriverio/build/commands/browser/getPuppeteer.d.ts', + 'node_modules/webdriverio/build/types.d.ts', + // Add more paths or patterns as needed +] + +node.exec('tsc --project tsconfig.types.json --noEmit', (error, stdout, stderr) => { + const output = stdout + stderr + const lines = output.split('\n') + let found = false + + for (const line of lines) { + // Check if the line matches any exclusion pattern + const shouldExclude = excludeList.some(excludePath => + line.trim().startsWith(excludePath) + ) + if (!shouldExclude && line.includes('error TS')) { + found = true + console.log(line) + } + } + + if (!found) { + console.log('SUCCESS: No type errors outside the exclusion list.') + } else { + console.error('\nERROR: Type errors found please fix them as much as possible or exclude them in the script `types-checks-filter-out-node_modules.js`') + process.exit(1) + } +}) \ No newline at end of file diff --git a/types/expect-global.d.ts b/types/expect-global.d.ts new file mode 100644 index 000000000..b546de019 --- /dev/null +++ b/types/expect-global.d.ts @@ -0,0 +1,15 @@ +/// + +/** + * Global declaration file for WebdriverIO's Expect library to force the expect. + * Required when used in standalone mode (mocha) or to override the one of Jasmine + */ + +//// @ts-expect-error: IDE might flags this one but just does be concerned by it. This way the `tsc:root-types` can pass! +declare const expect: ExpectWebdriverIO.Expect + +declare namespace NodeJS { + interface Global { + expect: ExpectWebdriverIO.Expect + } +} \ No newline at end of file diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 40e535524..429c12c32 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -4,15 +4,527 @@ type Test = import('@wdio/types').Frameworks.Test type TestResult = import('@wdio/types').Frameworks.TestResult type PickleStep = import('@wdio/types').Frameworks.PickleStep type Scenario = import('@wdio/types').Frameworks.Scenario + type SnapshotResult = import('@vitest/snapshot').SnapshotResult type SnapshotUpdateState = import('@vitest/snapshot').SnapshotUpdateState +type ChainablePromiseElement = import('webdriverio').ChainablePromiseElement +type ChainablePromiseArray = import('webdriverio').ChainablePromiseArray + +type ExpectLibAsymmetricMatchers = import('expect').AsymmetricMatchers +type ExpectLibAsymmetricMatcher = import('expect').AsymmetricMatcher +type ExpectLibMatchers, T> = import('expect').Matchers +type ExpectLibExpect = import('expect').Expect +type ExpectLibInverse = import('expect').Inverse +type ExpectLibSyncExpectationResult = import('expect').SyncExpectationResult +type ExpectLibAsyncExpectationResult = import('expect').AsyncExpectationResult +type ExpectLibExpectationResult = import('expect').ExpectationResult +type ExpectLibMatcherContext = import('expect').MatcherContext + +// Extracted from the expect library, this is the type of the matcher function used in the expect library. +type RawMatcherFn = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this: Context, actual: any, ...expected: Array): ExpectLibExpectationResult; +} + +/** + * Real Promise and wdio chainable promise types. + */ +type WdioPromiseLike = PromiseLike | ChainablePromiseElement | ChainablePromiseArray +type ElementPromise = Promise +type ElementArrayPromise = Promise + +/** + * Only Wdio real promise + */ +type WdioOnlyPromiseLike = ElementPromise | ElementArrayPromise | ChainablePromiseElement | ChainablePromiseArray + +/** + * Only wdio real promise or potential promise usage on element or element array or browser + */ +type WdioOnlyMaybePromiseLike = ElementPromise | ElementArrayPromise | ChainablePromiseElement | ChainablePromiseArray | WebdriverIO.Browser | WebdriverIO.Element | WebdriverIO.ElementArray + +/** + * Note we are defining Matchers outside of the namespace as done in jest library until we can make every typing work correctly. + * Once we have all types working, we could check to bring those back into the `ExpectWebdriverIO` namespace. + */ + +/** + * Type helpers to be able to targets specific types mostly used in conjunctions with the Type of the `actual` parameter of the `expect` + */ +type ElementOrArrayLike = ElementLike | ElementArrayLike +type ElementLike = WebdriverIO.Element | ChainablePromiseElement +type ElementArrayLike = WebdriverIO.ElementArray | ChainablePromiseArray +type MockPromise = Promise + +/** + * Type helpers allowing to use the function when the expect(actual: T) is of the expected type T. + */ +type FnWhenBrowser = ActualT extends WebdriverIO.Browser ? Fn : never +type FnWhenElementOrArrayLike = ActualT extends ElementOrArrayLike ? Fn : never +type FnWhenElementArrayLike = ActualT extends ElementArrayLike ? Fn : never + +/** + * Same as the other but because of Jasmine and it's expectAsync typing which does not force T to be a promise, then we need to account for `WebdriverIO.Mock + */ +type FnWhenMock = ActualT extends MockPromise | WebdriverIO.Mock ? Fn : never + +/** + * Matchers dedicated to Wdio Browser. + * When asserting on a browser's properties requiring to be awaited, the return type is a Promise. + * When actual is not a browser, the return type is never, so the function cannot be used. + */ +interface WdioBrowserMatchers<_R, ActualT>{ + /** + * `WebdriverIO.Browser` -> `getUrl` + */ + toHaveUrl: FnWhenBrowser, options?: ExpectWebdriverIO.StringOptions) => Promise> + + /** + * `WebdriverIO.Browser` -> `getTitle` + */ + toHaveTitle: FnWhenBrowser, options?: ExpectWebdriverIO.StringOptions) => Promise> + + /** + * `WebdriverIO.Browser` -> `execute` + */ + toHaveClipboardText: FnWhenBrowser, options?: ExpectWebdriverIO.StringOptions) => Promise> +} + +/** + * Matchers dedicated to Network Mocking. + * When asserting we wait for the result with `await waitUntil()`, therefore the return type needs to be a Promise. + * When actual is not a WebdriverIO.Mock, the return type is never, so the function cannot be used. + */ +interface WdioNetworkMatchers<_R, ActualT> { + /** + * Check that `WebdriverIO.Mock` was called + */ + toBeRequested: FnWhenMock Promise> + /** + * Check that `WebdriverIO.Mock` was called N times + */ + toBeRequestedTimes: FnWhenMock Promise> + + /** + * Check that `WebdriverIO.Mock` was called with the specific parameters + */ + toBeRequestedWith: FnWhenMock Promise> +} + +/** + * Matchers dedicated to WebdriverIO Element or ElementArray (or chainable). + * When asserting on an element or element array's properties requiring to be awaited, the return type is a Promise. + * When actual is neither of WebdriverIO.Element, WebdriverIO.ElementArray, ChainableElement, ChainableElementArray, the return type is never, so the function cannot be used. + */ +interface WdioElementOrArrayMatchers<_R, ActualT = unknown> { + // ===== $ or $$ ===== + /** + * `WebdriverIO.Element` -> `isDisplayed` + */ + toBeDisplayed: FnWhenElementOrArrayLike Promise> + + /** + * `WebdriverIO.Element` -> `isExisting` + */ + toExist: FnWhenElementOrArrayLike Promise> + + /** + * `WebdriverIO.Element` -> `isExisting` + */ + toBePresent: FnWhenElementOrArrayLike Promise> + + /** + * `WebdriverIO.Element` -> `isExisting` + */ + toBeExisting: FnWhenElementOrArrayLike Promise> + + /** + * `WebdriverIO.Element` -> `getAttribute` + */ + toHaveAttribute: FnWhenElementOrArrayLike, + options?: ExpectWebdriverIO.StringOptions) + => Promise> + + /** + * `WebdriverIO.Element` -> `getAttribute` + */ + toHaveAttr: FnWhenElementOrArrayLike, + options?: ExpectWebdriverIO.StringOptions + ) => Promise> + + /** + * `WebdriverIO.Element` -> `getAttribute` class + * @deprecated since v1.3.1 - use `toHaveElementClass` instead. + */ + toHaveClass: FnWhenElementOrArrayLike, + options?: ExpectWebdriverIO.StringOptions + ) => Promise> + + /** + * `WebdriverIO.Element` -> `getAttribute` class + * + * Checks if an element has the specified class or matches any of the provided class patterns. + * @param className - The class name(s) or pattern(s) to match against. + * @param options - Optional settings that can be passed to the function. + * + * **Usage** + * ```js + * // Check if an element has the class 'btn' + * await expect(element).toHaveElementClass('btn'); + * + * // Check if an element has any of the specified classes + * await expect(element).toHaveElementClass(['btn', 'btn-large']); + * ``` + */ + toHaveElementClass: FnWhenElementOrArrayLike | ExpectWebdriverIO.PartialMatcher, + options?: ExpectWebdriverIO.StringOptions + ) => Promise> + + /** + * `WebdriverIO.Element` -> `getProperty` + */ + toHaveElementProperty: FnWhenElementOrArrayLike, + value?: unknown, + options?: ExpectWebdriverIO.StringOptions + ) => Promise> + + /** + * `WebdriverIO.Element` -> `getProperty` value + */ + toHaveValue: FnWhenElementOrArrayLike, + options?: ExpectWebdriverIO.StringOptions + ) => Promise> + + /** + * `WebdriverIO.Element` -> `isClickable` + */ + toBeClickable: FnWhenElementOrArrayLike Promise> + + /** + * `WebdriverIO.Element` -> `!isEnabled` + */ + toBeDisabled: FnWhenElementOrArrayLike Promise> + + /** + * `WebdriverIO.Element` -> `isDisplayedInViewport` + */ + toBeDisplayedInViewport: FnWhenElementOrArrayLike Promise> + + /** + * `WebdriverIO.Element` -> `isEnabled` + */ + toBeEnabled: FnWhenElementOrArrayLike Promise> + + /** + * `WebdriverIO.Element` -> `isFocused` + */ + toBeFocused: FnWhenElementOrArrayLike Promise> + + /** + * `WebdriverIO.Element` -> `isSelected` + */ + toBeSelected: FnWhenElementOrArrayLike Promise> + + /** + * `WebdriverIO.Element` -> `isSelected` + */ + toBeChecked: FnWhenElementOrArrayLike Promise> + + /** + * `WebdriverIO.Element` -> `$$('./*').length` + * supports less / greater then or equals to be passed in options + */ + toHaveChildren: FnWhenElementOrArrayLike Promise> + + /** + * `WebdriverIO.Element` -> `getAttribute` href + */ + toHaveHref: FnWhenElementOrArrayLike, + options?: ExpectWebdriverIO.StringOptions + ) => Promise> + + /** + * `WebdriverIO.Element` -> `getAttribute` href + */ + toHaveLink: FnWhenElementOrArrayLike, + options?: ExpectWebdriverIO.StringOptions + ) => Promise> + + /** + * `WebdriverIO.Element` -> `getProperty` value + */ + toHaveId: FnWhenElementOrArrayLike, + options?: ExpectWebdriverIO.StringOptions + ) => Promise> + + /** + * `WebdriverIO.Element` -> `getSize` value + */ + toHaveSize: FnWhenElementOrArrayLike Promise> + + /** + * `WebdriverIO.Element` -> `getText` + * Element's text equals the text provided + * + * @param text - The expected text to match. + * @param options - Optional settings that can be passed to the function. + * + * **Usage** + * + * ```js + * // Check if an element has the text + * const elem = await $('.container') + * await expect(elem).toHaveText('Next-gen browser and mobile automation test framework for Node.js') + * + * // Check if an element array contains the specified text + * const elem = await $$('ul > li') + * await expect(elem).toHaveText(['Coffee', 'Tea', 'Milk']) + * ``` + */ + toHaveText: FnWhenElementOrArrayLike | Array>, + options?: ExpectWebdriverIO.StringOptions + ) => Promise> + + /** + * `WebdriverIO.Element` -> `getHTML` + * Element's html equals the html provided + */ + toHaveHTML: FnWhenElementOrArrayLike | Array, + options?: ExpectWebdriverIO.StringOptions + ) => Promise> + + /** + * `WebdriverIO.Element` -> `getComputedLabel` + * Element's computed label equals the computed label provided + */ + toHaveComputedLabel: FnWhenElementOrArrayLike | Array, + options?: ExpectWebdriverIO.StringOptions + ) => Promise> + + /** + * `WebdriverIO.Element` -> `getComputedRole` + * Element's computed role equals the computed role provided + */ + toHaveComputedRole: FnWhenElementOrArrayLike | Array, + options?: ExpectWebdriverIO.StringOptions + ) => Promise> + + /** + * `WebdriverIO.Element` -> `getSize('width')` + * Element's width equals the width provided + */ + toHaveWidth: FnWhenElementOrArrayLike Promise> + + /** + * `WebdriverIO.Element` -> `getSize('height')` or `getSize()` + * Checks if the element's height equals the given number, or its size equals the given object. + * + * @param heightOrSize - Either a number (height) or an object with height and width. + * @param options - Optional command options. + * + * **Usage Example:** + * ```js + * await expect(element).toHaveHeight(42) + * await expect(element).toHaveHeight({ height: 42, width: 42 }) + * ``` + */ + toHaveHeight: FnWhenElementOrArrayLike Promise> + + /** + * `WebdriverIO.Element` -> `getAttribute("style")` + */ + toHaveStyle: FnWhenElementOrArrayLike Promise> +} + +/** + * Matchers dedicated to WebdriverIO ElementArray (or its chainable). + * When asserting on each element's properties requiring awaiting, then return type is a Promise. + * When actual is not of WebdriverIO.ElementArray nor ChainableElementArray, the return type is never, so the function cannot be used. + */ +interface WdioElementArrayOnlyMatchers<_R, ActualT = unknown> { + // ===== $$ only ===== + /** + * `WebdriverIO.ElementArray` -> `$$('...').length` + * supports less / greater then or equals to be passed in options + */ + toBeElementsArrayOfSize: FnWhenElementArrayLike Promise & Promise> +} + +/** + * Matchers supporting basic snapshot tests as well as DOM snapshot testing. + * When the actual is a WebdriverIO.Element, we need to await the `outerHTML` therefore the return type is a Promise. + * + * ⚠️ these matchers overload the similar matchers from jest-expect library. + * Therefore, they also need to be redefined in the jest.d.ts file so correctly overload the matchers from the Jest namespace. + * @see jest.d.ts + */ +interface WdioJestOverloadedMatchers<_R, ActualT> { + /** + * snapshot matcher + * @param label optional snapshot label + */ + toMatchSnapshot(label?: string): ActualT extends WdioPromiseLike ? Promise : void; + /** + * inline snapshot matcher + * @param snapshot snapshot string (autogenerated if not specified) + * @param label optional snapshot label + */ + toMatchInlineSnapshot(snapshot?: string, label?: string): ActualT extends WdioPromiseLike ? Promise : void; +} + +/** + * All the specific WebDriverIO only matchers, excluding the generic matchers from the expect library. + */ +type WdioCustomMatchers = WdioJestOverloadedMatchers & WdioBrowserMatchers & WdioElementOrArrayMatchers & WdioElementArrayOnlyMatchers & WdioNetworkMatchers + +/** + * All the matchers that WebdriverIO Library supports including the generic matchers from the expect library. + */ +type WdioMatchers, ActualT> = WdioCustomMatchers & ExpectLibMatchers + +/** + * Expects specific to WebdriverIO, excluding the generic expect matchers. + */ +interface WdioCustomExpect { + /** + * Creates a soft assertion wrapper around standard expect + * Soft assertions record failures but don't throw errors immediately + * All failures are collected and reported at the end of the test + * Note: Until fixed, soft only support wdio custom matchers, and not the `expect` library matchers. Moreover, it always returns a Promise. + */ + soft(actual: T): T extends PromiseLike ? ExpectWebdriverIO.MatchersAndInverse, T> & ExpectWebdriverIO.PromiseMatchers : ExpectWebdriverIO.MatchersAndInverse; + + /** + * Get all current soft assertion failures + */ + getSoftFailures(testId?: string): ExpectWebdriverIO.SoftFailure[] + + /** + * Manually assert all soft failures (throws an error if any failures exist) + */ + assertSoftFailures(testId?: string): void + + /** + * Clear all current soft assertion failures + */ + clearSoftFailures(testId?: string): void +} + +/** + * Expects supported by the expect-webdriverio library, including the generic expect matchers. + */ +type WdioExpect = WdioCustomExpect & ExpectLibExpect + +/** + * Asymmetric matchers supported by the expect-webdriverio library. + * The type is the same as the one from the expect library, but we need to redefine it to have it available in the `ExpectWebdriverIO` namespace. + */ +type WdioAsymmetricMatchers = ExpectLibAsymmetricMatchers + +/** + * Implementation of the asymmetric matcher. Equivalent as the PartialMatcher but with sample used by implementations. + * For the runtime but not the typing. + */ +type WdioAsymmetricMatcher = ExpectWebdriverIO.PartialMatcher & { + // Overwrite protected properties of expect.AsymmetricMatcher to access them + sample: R; +} + declare namespace ExpectWebdriverIO { + /** + * When importing expect from 'expect-webdriverio', instead of using globals this is the one used. + * Note: Using a const instead of a function, else we cannot use asymmetric matcher like expect.anything(). + */ const expect: ExpectWebdriverIO.Expect + + /** + * Used by the webdriverio main project to configure the matchers in the runner. + */ function setOptions(options: DefaultOptions): void // eslint-disable-next-line @typescript-eslint/no-explicit-any function getConfig(): any + /** + * The below block are overloaded types from the expect library. + * They are required to show "everything" under the `ExpectWebdriverIO` namespace. + * They are also required to be be able to declare custom asymmetric/normal matchers under the `ExpectWebdriverIO` namespace. + * The type `T` must stay named `T` to correctly overload the expect function from the expect library. + */ + + /** + * Expect defining the custom wdio expect and also pulling on asymmetric matchers. + * `AsymmetricMatchers` and `Inverse` needs to be defined and be before the `expect` library Expect (aka `WdioExpect`). + * The above allows to have custom asymmetric matchers under the `ExpectWebdriverIO` namespace. + */ + interface Expect extends ExpectWebdriverIO.AsymmetricMatchers, ExpectLibInverse, WdioExpect { + /** + * The `expect` function is used every time you want to test a value. + * You will rarely call `expect` by itself. + * + * expect function declaration contains two generics: + * - T: the type of the actual value, e.g. any type, not just WebdriverIO.Browser or WebdriverIO.Element + * - R: the type of the return value, e.g. Promise or void + * + * Note: The function must stay here in the namespace to overwrite correctly the expect function from the expect library. + * + * @param actual The value to apply matchers against. + */ + (actual: T): T extends PromiseLike ? ExpectWebdriverIO.MatchersAndInverse & ExpectWebdriverIO.PromiseMatchers : ExpectWebdriverIO.MatchersAndInverse; + } + + interface Matchers, T> extends WdioMatchers {} + + interface AsymmetricMatchers extends WdioAsymmetricMatchers {} + + interface InverseAsymmetricMatchers extends Omit {} + + /** + * End of block overloading types from the expect library. + */ + + type MatchersAndInverse, ActualT> = ExpectWebdriverIO.Matchers & ExpectLibInverse> + + /** + * Take from expect library + */ + type PromiseMatchers = { + /** + * Unwraps the reason of a rejected promise so any other matcher can be chained. + * If the promise is fulfilled the assertion fails. + */ + rejects: MatchersAndInverse, T>; + /** + * Unwraps the value of a fulfilled promise so any other matcher can be chained. + * If the promise is rejected the assertion fails. + */ + resolves: MatchersAndInverse, T>; + } interface SnapshotServiceArgs { updateState?: SnapshotUpdateState resolveSnapshotPath?: (path: string, extension: string) => string @@ -47,7 +559,7 @@ declare namespace ExpectWebdriverIO { class SoftAssertionService implements ServiceInstance { // eslint-disable-next-line @typescript-eslint/no-explicit-any - constructor(serviceOptions?: SoftAssertionServiceOptions, capabilities?: any, config?: any) + constructor(serviceOptions?: SoftAssertionServiceOptions, capabilities?: unknown, config?: any) beforeTest(test: Test): void beforeStep(step: PickleStep, scenario: Scenario): void // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -55,20 +567,15 @@ declare namespace ExpectWebdriverIO { afterStep(step: PickleStep, scenario: Scenario, result: { passed: boolean, error?: Error }): void } - interface AssertionResult { - pass: boolean - message(): string - } + interface AssertionResult extends ExpectLibSyncExpectationResult {} + type AsyncAssertionResult = ExpectLibAsyncExpectationResult - const matchers: Map< - string, - ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - actual: any, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ...expected: any[] - ) => Promise - > + /** + * Used by the wdio main project to configure the matchers in the runner when using Jasmine or Jest. + * Equivalent as `MatchersObject` from the expect library. + * @see https://github.com/jestjs/jest/blob/fd3d6cf9fe416b549a74b6577e5e1ea1130e3659/packages/expect/src/types.ts#L43C13-L43C27 + */ + const matchers: Map interface AssertionHookParams { /** @@ -195,290 +702,28 @@ declare namespace ExpectWebdriverIO { gte?: number } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - interface Matchers { - // ===== $ or $$ ===== - /** - * `WebdriverIO.Element` -> `isDisplayed` - */ - toBeDisplayed(options?: ExpectWebdriverIO.CommandOptions): R - - /** - * `WebdriverIO.Element` -> `isExisting` - */ - toExist(options?: ExpectWebdriverIO.CommandOptions): R - /** - * `WebdriverIO.Element` -> `isExisting` - */ - toBePresent(options?: ExpectWebdriverIO.CommandOptions): R - /** - * `WebdriverIO.Element` -> `isExisting` - */ - toBeExisting(options?: ExpectWebdriverIO.CommandOptions): R - - /** - * `WebdriverIO.Element` -> `getAttribute` - */ - toHaveAttribute( - attribute: string, - value?: string | RegExp | ExpectWebdriverIO.PartialMatcher, - options?: ExpectWebdriverIO.StringOptions - ): R - /** - * `WebdriverIO.Element` -> `getAttribute` - */ - toHaveAttr(attribute: string, value?: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): R - - /** - * `WebdriverIO.Element` -> `getAttribute` class - * @deprecated since v1.3.1 - use `toHaveElementClass` instead. - */ - toHaveClass(className: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): R - - /** - * `WebdriverIO.Element` -> `getAttribute` class - * - * Checks if an element has the specified class or matches any of the provided class patterns. - * @param className - The class name(s) or pattern(s) to match against. - * @param options - Optional settings that can be passed to the function. - * - * **Usage** - * ```js - * // Check if an element has the class 'btn' - * await expect(element).toHaveElementClass('btn') - * - * // Check if an element has any of the specified classes - * await expect(element).toHaveElementClass(['btn', 'btn-large']) - * ``` - */ - toHaveElementClass(className: string | RegExp | Array | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): R - - /** - * `WebdriverIO.Element` -> `getProperty` - */ - toHaveElementProperty( - property: string | RegExp | ExpectWebdriverIO.PartialMatcher, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value?: any, - options?: ExpectWebdriverIO.StringOptions - ): R - - /** - * `WebdriverIO.Element` -> `getProperty` value - */ - toHaveValue(value: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): R - - /** - * `WebdriverIO.Element` -> `isClickable` - */ - toBeClickable(options?: ExpectWebdriverIO.StringOptions): R - - /** - * `WebdriverIO.Element` -> `!isEnabled` - */ - toBeDisabled(options?: ExpectWebdriverIO.StringOptions): R - - /** - * `WebdriverIO.Element` -> `isDisplayedInViewport` - */ - toBeDisplayedInViewport(options?: ExpectWebdriverIO.StringOptions): R - - /** - * `WebdriverIO.Element` -> `isEnabled` - */ - toBeEnabled(options?: ExpectWebdriverIO.StringOptions): R - - /** - * `WebdriverIO.Element` -> `isFocused` - */ - toBeFocused(options?: ExpectWebdriverIO.StringOptions): R - - /** - * `WebdriverIO.Element` -> `isSelected` - */ - toBeSelected(options?: ExpectWebdriverIO.StringOptions): R - - /** - * `WebdriverIO.Element` -> `isSelected` - */ - toBeChecked(options?: ExpectWebdriverIO.StringOptions): R - - /** - * `WebdriverIO.Element` -> `$$('./*').length` - * supports less / greater then or equals to be passed in options - */ - toHaveChildren( - size?: number | ExpectWebdriverIO.NumberOptions, - options?: ExpectWebdriverIO.NumberOptions - ): R - - /** - * `WebdriverIO.Element` -> `getAttribute` href - */ - toHaveHref(href: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): R - /** - * `WebdriverIO.Element` -> `getAttribute` href - */ - toHaveLink(href: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): R - - /** - * `WebdriverIO.Element` -> `getProperty` value - */ - toHaveId(id: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): R - - /** - * `WebdriverIO.Element` -> `getSize` value - */ - toHaveSize(size: { height: number; width: number }, options?: ExpectWebdriverIO.StringOptions): R - - /** - * `WebdriverIO.Element` -> `getText` - * Element's text equals the text provided - * - * @param text - The expected text to match. - * @param options - Optional settings that can be passed to the function. - * - * **Usage** - * - * ```js - * // Check if an element has the text - * const elem = await $('.container') - * await expect(elem).toHaveText('Next-gen browser and mobile automation test framework for Node.js') - * - * // Check if an element array contains the specified text - * const elem = await $$('ul > li') - * await expect(elem).toHaveText(['Coffee', 'Tea', 'Milk']) - * ``` - */ - toHaveText( - text: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, - options?: ExpectWebdriverIO.StringOptions - ): R - - /** - * `WebdriverIO.Element` -> `getHTML` - * Element's html equals the html provided - */ - toHaveHTML(html: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, options?: ExpectWebdriverIO.HTMLOptions): R - - /** - * `WebdriverIO.Element` -> `getComputedLabel` - * Element's computed label equals the computed label provided - */ - toHaveComputedLabel( - computedLabel: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, - options?: ExpectWebdriverIO.StringOptions - ): R - - /** - * `WebdriverIO.Element` -> `getComputedRole` - * Element's computed role equals the computed role provided - */ - toHaveComputedRole( - computedRole: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, - options?: ExpectWebdriverIO.StringOptions - ): R - - /** - * `WebdriverIO.Element` -> `getSize('width')` - * Element's width equals the width provided - */ - toHaveWidth(width: number, options?: ExpectWebdriverIO.CommandOptions): R - - /** - * `WebdriverIO.Element` -> `getSize('height')` - * Element's height equals the height provided - */ - toHaveHeight(height: number, options?: ExpectWebdriverIO.CommandOptions): R - - /** - * `WebdriverIO.Element` -> `getSize()` - * Element's size equals the size provided - */ - toHaveHeight(size: { height: number; width: number }, options?: ExpectWebdriverIO.CommandOptions): R - - /** - * `WebdriverIO.Element` -> `getAttribute("style")` - */ - toHaveStyle(style: { [key: string]: string }, options?: ExpectWebdriverIO.StringOptions): R - - // ===== browser only ===== - /** - * `WebdriverIO.Browser` -> `getUrl` - */ - toHaveUrl(url: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): R - - /** - * `WebdriverIO.Browser` -> `getTitle` - */ - toHaveTitle(title: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): R - - /** - * `WebdriverIO.Browser` -> `execute` - */ - toHaveClipboardText(clipboardText: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): R - - // ===== $$ only ===== - /** - * `WebdriverIO.ElementArray` -> `$$('...').length` - * supports less / greater then or equals to be passed in options - */ - toBeElementsArrayOfSize( - size: number | ExpectWebdriverIO.NumberOptions, - options?: ExpectWebdriverIO.NumberOptions - ): R & Promise - - // ==== network mock ==== - /** - * Check that `WebdriverIO.Mock` was called - */ - toBeRequested(options?: ExpectWebdriverIO.CommandOptions): R - /** - * Check that `WebdriverIO.Mock` was called N times - */ - toBeRequestedTimes( - times: number | ExpectWebdriverIO.NumberOptions, - options?: ExpectWebdriverIO.NumberOptions - ): R - /** - * Check that `WebdriverIO.Mock` was called with the specific parameters - */ - toBeRequestedWith(requestedWith: RequestedWith, options?: ExpectWebdriverIO.CommandOptions): R - /** - * snapshot matcher - * @param label optional snapshot label - */ - toMatchSnapshot(label?: string): R - /** - * inline snapshot matcher - * @param snapshot snapshot string (autogenerated if not specified) - * @param label optional snapshot label - */ - toMatchInlineSnapshot(snapshot?: string, label?: string): R - } - type RequestedWith = { - url?: string | ExpectWebdriverIO.PartialMatcher | ((url: string) => boolean) + url?: string | ExpectWebdriverIO.PartialMatcher| ((url: string) => boolean) method?: string | Array statusCode?: number | Array requestHeaders?: | Record - | ExpectWebdriverIO.PartialMatcher + | ExpectWebdriverIO.PartialMatcher> | ((headers: Record) => boolean) responseHeaders?: | Record - | ExpectWebdriverIO.PartialMatcher + | ExpectWebdriverIO.PartialMatcher> | ((headers: Record) => boolean) postData?: | string | ExpectWebdriverIO.JsonCompatible - | ExpectWebdriverIO.PartialMatcher - | ((r: string | undefined) => boolean) + | ExpectWebdriverIO.PartialMatcher + | ((postData: string | undefined) => boolean) response?: | string | ExpectWebdriverIO.JsonCompatible - | ExpectWebdriverIO.PartialMatcher - | ((r: string) => boolean) + | ExpectWebdriverIO.PartialMatcher + | ((response: unknown) => boolean) } type jsonPrimitive = string | number | boolean | null @@ -486,66 +731,11 @@ declare namespace ExpectWebdriverIO { type jsonArray = Array type JsonCompatible = jsonObject | jsonArray - interface PartialMatcher { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - sample?: any - $$typeof: symbol - // eslint-disable-next-line @typescript-eslint/no-explicit-any - asymmetricMatch(...args: any[]): boolean - toString(): string - } - /** - * expect function declaration, containing two generics: - * - T: the type of the actual value, e.g. WebdriverIO.Browser or WebdriverIO.Element - * - R: the type of the return value, e.g. Promise or void + * Allow to partially matches value. Same as asymmetric matcher in jest. + * Some properties are omitted for the type check to work correctly. */ - interface Expect { - = void | Promise>(actual: T): Matchers - - /** - * Creates a soft assertion wrapper around standard expect - * Soft assertions record failures but don't throw errors immediately - * All failures are collected and reported at the end of the test - */ - soft(actual: T): Matchers, T> - - /** - * Get all current soft assertion failures - */ - getSoftFailures(testId?: string): SoftFailure[] - - /** - * Manually assert all soft failures (throws an error if any failures exist) - */ - assertSoftFailures(testId?: string): void - - /** - * Clear all current soft assertion failures - */ - clearSoftFailures(testId?: string): void - - // Standard asymmetric matchers from Jest - extend(map: Record): void - anything(): PartialMatcher - any(sample: unknown): PartialMatcher - stringContaining(expected: string): PartialMatcher - objectContaining(sample: Record): PartialMatcher - arrayContaining(sample: Array): PartialMatcher - stringMatching(expected: string | RegExp): PartialMatcher - not: AsymmetricMatchers - } - - interface AsymmetricMatchers { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - any(expectedObject: any): PartialMatcher - anything(): PartialMatcher - arrayContaining(sample: Array): PartialMatcher - objectContaining(sample: Record): PartialMatcher - stringContaining(expected: string): PartialMatcher - stringMatching(expected: string | RegExp): PartialMatcher - not: AsymmetricMatchers - } + type PartialMatcher = Omit, 'sample' | 'inverse' | '$$typeof'> } declare module 'expect-webdriverio' { diff --git a/types/jest-global.d.ts b/types/jest-global.d.ts deleted file mode 100644 index 527176723..000000000 --- a/types/jest-global.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// - -// @ts-expect-error -declare const expect: ExpectWebdriverIO.Expect - -declare namespace NodeJS { - interface Global { - expect: ExpectWebdriverIO.Expect; - } -} diff --git a/types/standalone.d.ts b/types/standalone.d.ts deleted file mode 100644 index 54fc1406e..000000000 --- a/types/standalone.d.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* eslint-disable @typescript-eslint/consistent-type-imports*/ -/// - -type ChainablePromiseElement = import('webdriverio').ChainablePromiseElement -type ChainablePromiseArray = import('webdriverio').ChainablePromiseArray - -declare namespace ExpectWebdriverIO { - interface Matchers, T> extends Readonly> { - not: Matchers - resolves: Matchers - rejects: Matchers - } - - /** - * expect function declaration, containing two generics: - * - T: the type of the actual value, e.g. WebdriverIO.Browser or WebdriverIO.Element - * - R: the type of the return value, e.g. Promise or void - */ - type Expect = { - = void | Promise>(actual: T): Matchers - extend(map: Record): void - } & AsymmetricMatchers - - interface AsymmetricMatchers { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - any(expectedObject: any): PartialMatcher - anything(): PartialMatcher - arrayContaining(sample: Array): PartialMatcher - objectContaining(sample: Record): PartialMatcher - stringContaining(expected: string): PartialMatcher - stringMatching(expected: string | RegExp | ExpectWebdriverIO.PartialMatcher): PartialMatcher - not: AsymmetricMatchers - } -} diff --git a/vitest.config.ts b/vitest.config.ts index 21328d163..a3828e52b 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -23,13 +23,16 @@ export default defineConfig({ '.eslintrc.cjs', 'jasmine.d.ts', 'jest.d.ts', - 'types' + 'types', + 'eslint.config.mjs', + 'vitest.config.ts', + 'types-checks-filter-out-node_modules.js', ], thresholds: { - lines: 87, - functions: 86, - statements: 87, - branches: 88 + lines: 89.8, + functions: 86.7, + statements: 89.8, + branches: 88.7, } } }