From 9683aac364992e486bce4af0682423574ce5229e Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sat, 21 Jun 2025 21:58:19 -0400 Subject: [PATCH 01/99] Working case with only matchers --- .DS_Store | Bin 0 -> 6148 bytes jest.d.ts | 13 +- package-lock.json | 1203 +++++++++++++++++- package.json | 3 +- src/index.ts | 34 - src/matchers.ts | 4 +- src/matchers/element/toHaveClass.ts | 4 +- src/matchers/mock/toBeRequestedWith.ts | 405 ------ src/matchers/snapshot.ts | 182 --- src/snapshot.ts | 133 -- src/softAssert.ts | 139 -- src/softAssertService.ts | 86 -- src/softExpect.ts | 105 -- src/utils.ts | 8 +- test-types/copy.js | 69 - test-types/default/tsconfig.json | 13 - test-types/jasmine/tsconfig.json | 14 - test-types/jest/tsconfig.json | 1 - test-types/jest/types.test.ts | 99 ++ test-types/types.ts | 2 +- test/index.test.ts | 5 +- test/matchers.test.ts | 20 +- test/matchers/mock/toBeRequestedWith.test.ts | 487 ------- test/snapshot.test.ts | 64 - test/snapshot.test.ts.snap | 17 - test/softAssertions.test.ts | 484 ------- types/expect-webdriverio.d.ts | 636 ++++----- types/jest-global.d.ts | 10 - types/standalone.d.ts | 49 +- 29 files changed, 1544 insertions(+), 2745 deletions(-) create mode 100644 .DS_Store delete mode 100644 src/matchers/mock/toBeRequestedWith.ts delete mode 100644 src/matchers/snapshot.ts delete mode 100644 src/snapshot.ts delete mode 100644 src/softAssert.ts delete mode 100644 src/softAssertService.ts delete mode 100644 src/softExpect.ts delete mode 100644 test-types/copy.js delete mode 100644 test-types/default/tsconfig.json delete mode 100644 test-types/jasmine/tsconfig.json create mode 100644 test-types/jest/types.test.ts delete mode 100644 test/matchers/mock/toBeRequestedWith.test.ts delete mode 100644 test/snapshot.test.ts delete mode 100644 test/snapshot.test.ts.snap delete mode 100644 test/softAssertions.test.ts delete mode 100644 types/jest-global.d.ts diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..ccc58d091d22398ba5eaae9280a0deec5d13e53c GIT binary patch literal 6148 zcmeHKF-`+P474FdM4FV8`vD4mu!@ow declare namespace jest { - interface Matchers extends ExpectWebdriverIO.Matchers { } -} + // noinspection JSUnusedGlobalSymbols + interface Matchers extends CustomMatchers{} + + // noinspection JSUnusedGlobalSymbols + + interface Expect extends CustomMatchers {} + + // noinspection JSUnusedGlobalSymbols + + interface InverseAsymmetricMatchers extends Expect {} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4c7789355..2c1797d73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "lodash.isequal": "^4.5.0" }, "devDependencies": { + "@jest/expect": "^30.0.0", "@types/debug": "^4.1.12", "@types/jest": "^30.0.0", "@types/lodash.isequal": "^4.5.8", @@ -81,12 +82,148 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/compat-data": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz", + "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", + "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.4", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.4", + "@babel/types": "^7.27.3", + "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, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "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, + "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, + "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, + "bin": { + "semver": "bin/semver.js" + } + }, + "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, + "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, + "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, + "engines": { + "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==", + "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, - "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -100,14 +237,35 @@ "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, + "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, + "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.27.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", + "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/types": "^7.26.0" + "@babel/types": "^7.27.3" }, "bin": { "parser": "bin/babel-parser.js" @@ -116,15 +274,277 @@ "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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.27.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", + "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "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.27.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", + "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", "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" @@ -1196,6 +1616,111 @@ "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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 + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -1210,7 +1735,19 @@ "version": "30.0.1", "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", - "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.2.tgz", + "integrity": "sha512-blWRFPjv2cVfh42nLG6L3xIEbw+bnuiZYZDl/BZlsNG/i3wKV6FpPZ2EPHguk7t5QpLaouIu+7JmYO4uBR6AOg==", + "dev": true, + "dependencies": { + "expect": "30.0.2", + "jest-snapshot": "30.0.2" + }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } @@ -1219,7 +1756,6 @@ "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==", - "license": "MIT", "dependencies": { "@jest/get-type": "30.0.1" }, @@ -1231,7 +1767,6 @@ "version": "30.0.1", "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz", "integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==", - "license": "MIT", "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } @@ -1240,7 +1775,6 @@ "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" @@ -1263,11 +1797,113 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/snapshot-utils": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.1.tgz", + "integrity": "sha512-6Dpv7vdtoRiISEFwYF8/c7LIvqXD7xDXtLPNzC2xqAfBznKip0MQM+rkseKwUPUpv2PJ7KW/YsnwWXrIL2xF+A==", + "dev": true, + "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, + "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, + "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.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.2.tgz", + "integrity": "sha512-kJIuhLMTxRF7sc0gPzPtCDib/V9KwW3I2U25b+lYCYMVqHHSrcZopS8J8H+znx9yixuFv+Iozl8raLt/4MoxrA==", + "dev": true, + "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, + "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, + "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", @@ -1285,7 +1921,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" }, @@ -1294,16 +1929,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" }, @@ -1318,7 +1951,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" @@ -1654,6 +2286,18 @@ "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, + "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", @@ -2487,6 +3131,12 @@ "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 + }, "node_modules/@vitest/coverage-v8": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", @@ -3293,6 +3943,19 @@ "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, + "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", @@ -3474,6 +4137,105 @@ "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, + "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, + "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, + "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, + "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, + "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, + "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", @@ -3657,6 +4419,15 @@ "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, + "dependencies": { + "node-int64": "^0.4.0" + } + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -3803,6 +4574,15 @@ "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, + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001707", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", @@ -5129,7 +5909,6 @@ "version": "30.0.2", "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.2.tgz", "integrity": "sha512-YN9Mgv2mtTWXVmifQq3QT+ixCL/uLuLJw+fdp8MOjKqu8K3XQh3o5aulMM1tn+O2DdrWNxLZTeJsCY/VofUA0A==", - "license": "MIT", "dependencies": { "@jest/expect-utils": "30.0.2", "@jest/get-type": "30.0.1", @@ -5576,6 +6355,15 @@ "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, + "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", @@ -5710,6 +6498,12 @@ "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 + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -5775,6 +6569,15 @@ "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, + "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", @@ -5798,6 +6601,15 @@ "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, + "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", @@ -6193,6 +7005,17 @@ "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, + "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", @@ -6452,6 +7275,22 @@ "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, + "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", @@ -6516,7 +7355,6 @@ "version": "30.0.2", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.2.tgz", "integrity": "sha512-2UjrNvDJDn/oHFpPrUTVmvYYDNeNtw2DlY3er8bI6vJJb9Fb35ycp/jFLd5RdV59tJ8ekVXX3o/nwPcscgXZJQ==", - "license": "MIT", "dependencies": { "@jest/diff-sequences": "30.0.1", "@jest/get-type": "30.0.1", @@ -6531,7 +7369,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" }, @@ -6540,16 +7377,14 @@ } }, "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==", - "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-diff/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" }, @@ -6564,7 +7399,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" @@ -6580,7 +7414,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", @@ -6594,7 +7427,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" }, @@ -6613,11 +7445,34 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-haste-map": { + "version": "30.0.2", + "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, + "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.2", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.2.tgz", "integrity": "sha512-1FKwgJYECR8IT93KMKmjKHSLyru0DqguThov/aWpFccC0wbiXGOxYEu7SScderBD7ruDOpl7lc5NG6w3oxKfaA==", - "license": "MIT", "dependencies": { "@jest/get-type": "30.0.1", "chalk": "^4.1.2", @@ -6632,7 +7487,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" }, @@ -6641,10 +7495,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==", - "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-matcher-utils/node_modules/ansi-styles": { "version": "4.3.0", @@ -6681,7 +7534,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", @@ -6695,7 +7547,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" }, @@ -6707,7 +7558,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", @@ -6727,7 +7577,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" }, @@ -6736,16 +7585,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" }, @@ -6760,7 +7607,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" @@ -6776,7 +7622,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", @@ -6790,7 +7635,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" }, @@ -6802,7 +7646,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": "*", @@ -6816,16 +7659,121 @@ "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==", - "license": "MIT", "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-snapshot": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.2.tgz", + "integrity": "sha512-KeoHikoKGln3OlN7NS7raJ244nIVr2K46fBTNdfuxqYv2/g4TVyWDSO4fmk08YBJQMjs3HNfG1rlLfL/KA+nUw==", + "dev": true, + "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.2", + "@jest/get-type": "30.0.1", + "@jest/snapshot-utils": "30.0.1", + "@jest/transform": "30.0.2", + "@jest/types": "30.0.1", + "babel-preset-current-node-syntax": "^1.1.0", + "chalk": "^4.1.2", + "expect": "30.0.2", + "graceful-fs": "^4.2.11", + "jest-diff": "30.0.2", + "jest-matcher-utils": "30.0.2", + "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, + "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.35", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.35.tgz", + "integrity": "sha512-C6ypdODf2VZkgRT6sFM8E1F8vR+HcffniX0Kp8MsU8PIfrlXbNCBz0jzj17GjdmjTx1OtZzdH8+iALL21UjF5A==", + "dev": true + }, + "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, + "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, + "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, + "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, + "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": "*", @@ -6842,7 +7790,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" }, @@ -6857,7 +7804,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" @@ -6873,7 +7819,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" }, @@ -6881,6 +7826,37 @@ "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, + "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, + "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", @@ -6959,6 +7935,18 @@ "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, + "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", @@ -7326,6 +8314,15 @@ "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, + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", @@ -7581,6 +8578,12 @@ "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 + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -7913,6 +8916,15 @@ "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, + "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", @@ -8046,6 +9058,15 @@ "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, + "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", @@ -8143,6 +9164,15 @@ "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, + "engines": { + "node": ">= 6" + } + }, "node_modules/pkg-types": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz", @@ -9332,6 +10362,21 @@ "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, + "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", @@ -9549,6 +10594,12 @@ "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 + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -10052,6 +11103,15 @@ "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, + "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", @@ -10941,6 +12001,19 @@ "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, + "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", @@ -10972,6 +12045,12 @@ "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 + }, "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 0d9a5219c..d0acc33d8 100644 --- a/package.json +++ b/package.json @@ -53,9 +53,7 @@ "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", "watch": "npm run compile -- --watch", "prepare": "husky install" }, @@ -67,6 +65,7 @@ }, "devDependencies": { "@types/debug": "^4.1.12", + "@jest/expect": "^30.0.0", "@types/jest": "^30.0.0", "@types/lodash.isequal": "^4.5.8", "@types/node": "^24.0.3", diff --git a/src/index.ts b/src/index.ts index b79759e75..9c8689db4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,8 +4,6 @@ import type { RawMatcherFn } 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() const filteredMatchers = {} @@ -31,27 +29,6 @@ type MatchersObject = Parameters[0] expectLib.extend(filteredMatchers as MatchersObject) -// Extend the expect object with soft assertions -const expectWithSoft = expectLib as unknown as ExpectWebdriverIO.Expect -Object.defineProperty(expectWithSoft, 'soft', { - value: (actual: T) => createSoftExpect(actual) -}) - -// Add soft assertions utility methods -Object.defineProperty(expectWithSoft, 'getSoftFailures', { - value: (testId?: string) => SoftAssertService.getInstance().getFailures(testId) -}) - -Object.defineProperty(expectWithSoft, 'assertSoftFailures', { - value: (testId?: string) => SoftAssertService.getInstance().assertNoFailures(testId) -}) - -Object.defineProperty(expectWithSoft, 'clearSoftFailures', { - value: (testId?: string) => SoftAssertService.getInstance().clearFailures(testId) -}) - -export const expect = expectWithSoft - export const getConfig = (): ExpectWebdriverIO.DefaultOptions => DEFAULT_OPTIONS export const setDefaultOptions = (options = {}): void => { Object.entries(options).forEach(([key, value]) => { @@ -63,17 +40,6 @@ export const setDefaultOptions = (options = {}): void => { } export const setOptions = setDefaultOptions -/** - * export snapshot utilities - */ -export { SnapshotService } from './snapshot.js' - -/** - * export soft assertion utilities - */ -export { SoftAssertService } from './softAssert.js' -export { SoftAssertionService, type SoftAssertionServiceOptions } from './softAssertService.js' - /** * export utils */ diff --git a/src/matchers.ts b/src/matchers.ts index 323fafc05..9bd6d4f4c 100644 --- a/src/matchers.ts +++ b/src/matchers.ts @@ -26,6 +26,4 @@ export * from './matchers/element/toHaveValue.js' export * from './matchers/element/toHaveWidth.js' export * from './matchers/elements/toBeElementsArrayOfSize.js' export * from './matchers/mock/toBeRequested.js' -export * from './matchers/mock/toBeRequestedTimes.js' -export * from './matchers/mock/toBeRequestedWith.js' -export * from './matchers/snapshot.js' +export * from './matchers/mock/toBeRequestedTimes.js' \ No newline at end of file diff --git a/src/matchers/element/toHaveClass.ts b/src/matchers/element/toHaveClass.ts index da20e3110..5cb5d4791 100644 --- a/src/matchers/element/toHaveClass.ts +++ b/src/matchers/element/toHaveClass.ts @@ -1,6 +1,6 @@ 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) { @@ -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) } diff --git a/src/matchers/mock/toBeRequestedWith.ts b/src/matchers/mock/toBeRequestedWith.ts deleted file mode 100644 index ad55a5eb4..000000000 --- a/src/matchers/mock/toBeRequestedWith.ts +++ /dev/null @@ -1,405 +0,0 @@ -import type { local } from 'webdriver' - -import { waitUntil, enhanceError } from '../../utils.js' -import { equals } from '../../jasmineUtils.js' -import { DEFAULT_OPTIONS } from '../../constants.js' - -const STR_LIMIT = 80 -const KEY_LIMIT = 12 - -interface RequestMock { - request: local.NetworkRequestData, - response: local.NetworkResponseData -} - -function reduceHeaders(headers: local.NetworkHeader[]) { - return Object.entries(headers).reduce((acc, [, value]: [string, local.NetworkHeader]) => { - acc[value.name] = value.value.value - return acc - }, {} as Record) -} - -export async function toBeRequestedWith( - received: WebdriverIO.Mock, - expectedValue: ExpectWebdriverIO.RequestedWith = {}, - options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS -) { - const isNot = this.isNot || false - const { expectation = 'called with', verb = 'be' } = this - - await options.beforeAssertion?.({ - matcherName: 'toBeRequestedWith', - expectedValue, - options, - }) - - let actual: RequestMock | undefined - const pass = await waitUntil( - async () => { - for (const call of received.calls) { - actual = call - if ( - methodMatcher(call.request.method, expectedValue.method) && - statusCodeMatcher(call.response.status, expectedValue.statusCode) && - urlMatcher(call.request.url, expectedValue.url) && - headersMatcher(reduceHeaders(call.request.headers), expectedValue.requestHeaders) && - headersMatcher(reduceHeaders(call.response.headers), expectedValue.responseHeaders) - // && - // bodyMatcher(call.postData, expectedValue.postData) && - // bodyMatcher(call.body, expectedValue.response) - ) { - return true - } - } - - return false - }, - isNot, - { ...options, wait: isNot ? 0 : options.wait } - ) - - const message = enhanceError( - 'mock', - minifyRequestedWith(expectedValue), - minifyRequestMock(actual, expectedValue) || 'was not called', - this, - verb, - expectation, - '', - options - ) - - const result: ExpectWebdriverIO.AssertionResult = { - pass, - message: (): string => message - } - - await options.afterAssertion?.({ - matcherName: 'toBeRequestedWith', - expectedValue, - options, - result - }) - - return result -} - -/** - * is actual method matching an expected method or methods - */ -const methodMatcher = (method: string, expected?: string | Array) => { - if (typeof expected === 'undefined') { - return true - } - if (!Array.isArray(expected)) { - expected = [expected] - } - return expected - .map((m) => { - if (typeof m !== 'string') { - return console.error('expect.toBeRequestedWith: unsupported value passed to method ' + m) - } - return m.toUpperCase() - }) - .includes(method) -} - -/** - * is actual statusCode matching an expected statusCode or statusCodes - */ -const statusCodeMatcher = (statusCode: number, expected?: number | Array) => { - if (typeof expected === 'undefined') { - return true - } - if (!Array.isArray(expected)) { - expected = [expected] - } - return expected.includes(statusCode) -} - -/** - * is actual url matching an expected condition - */ -const urlMatcher = ( - url: string, - expected?: string | ExpectWebdriverIO.PartialMatcher | ((url: string) => boolean) -) => { - if (typeof expected === 'undefined') { - return true - } - if (typeof expected === 'function') { - return expected(url) - } - return equals(url, expected) -} - -/** - * is headers url matching an expected condition - */ -const headersMatcher = ( - headers: Record, - expected?: - | Record - | ExpectWebdriverIO.PartialMatcher - | ((headers: Record) => boolean) -) => { - /** - * match with no headers were passed in as filter or - * if header matcher is an empty object, match with no headers - */ - if ( - typeof expected === 'undefined' || - typeof expected === 'object' && Object.keys(expected).length === 0 - ) { - return true - } - /** - * call function of provided - */ - if (typeof expected === 'function') { - return expected(headers) - } - return equals(headers, expected) -} - -/** - * is postData/response matching an expected condition - */ -// const bodyMatcher = ( -// body: string | Buffer | ExpectWebdriverIO.JsonCompatible | undefined, -// expected?: -// | string -// | ExpectWebdriverIO.JsonCompatible -// | ExpectWebdriverIO.PartialMatcher -// | ((r: string | Buffer | ExpectWebdriverIO.JsonCompatible | undefined) => boolean) -// ) => { -// if (typeof expected === 'undefined') { -// return true -// } -// if (typeof expected === 'function') { -// return expected(body) -// } -// if (typeof body === 'undefined') { -// return false -// } - -// let parsedBody = body -// if (body instanceof Buffer) { -// parsedBody = body.toString() -// } - -// // convert postData/body from string to JSON if expected value is JSON-like -// if (typeof(body) === 'string' && isExpectedJsonLike(expected)) { -// parsedBody = tryParseBody(body) - -// // failed to parse string as JSON -// if (parsedBody === null) { -// return false -// } -// } - -// return equals(parsedBody, expected) -// } - -// const isExpectedJsonLike = ( -// expected: -// | string -// | ExpectWebdriverIO.JsonCompatible -// | ExpectWebdriverIO.PartialMatcher -// | undefined -// | Function -// ) => { -// if (typeof expected === 'undefined') { -// return false -// } - -// // 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 - -// return ( -// Array.isArray(actualSample) || -// (typeof actualSample === 'object' && -// actualSample !== null && -// actualSample instanceof RegExp === false) -// ) -// } - -/** - * is jasmine/jest special matcher - * - * Jest and Jasmine support special matchers like `jasmine.objectContaining`, `expect.arrayContaining`, etc. - * - * All these kind of objects have `sample` and `asymmetricMatch` function in __proto__ - * `expect.objectContaining({ foo: 'bar })` -> `{ sample: { foo: 'bar' }, __proto__: asymmetricMatch() {} }` - * - * jasmine.any and jasmine.anything don't have `sample` property - * @param filter - */ -const isMatcher = (filter: unknown) => { - return ( - typeof filter === 'object' && - filter !== null && - '__proto__' in filter && - typeof filter.__proto__ === 'object' && - filter.__proto__ && - 'asymmetricMatch' in filter.__proto__ && - typeof filter.__proto__.asymmetricMatch === 'function' - ) -} - -// const tryParseBody = (jsonString: string | undefined, fallback: any = null) => { -// try { -// return typeof jsonString === 'undefined' ? fallback : JSON.parse(jsonString) -// } catch { -// return fallback -// } -// } - -/** - * shorten long url, headers, postData, body - */ -const minifyRequestMock = ( - requestMock?: { - request: local.NetworkRequestData, - response: local.NetworkResponseData - }, - requestedWith?: ExpectWebdriverIO.RequestedWith -) => { - if (typeof requestMock === 'undefined') { - return requestMock - } - - const r: Record = { - url: requestMock.request.url, - method: requestMock.request.method, - requestHeaders: requestMock.request.headers, - responseHeaders: requestMock.response.headers, - // postData: typeof requestMock.postData === 'string' && isExpectedJsonLike(requestedWith.postData) - // ? tryParseBody(requestMock.postData, requestMock.postData) - // : requestMock.postData, - // response: typeof requestMock.body === 'string' && isExpectedJsonLike(requestedWith.response) - // ? tryParseBody(requestMock.body, requestMock.body) - // : requestMock.body, - } - - deleteUndefinedValues(r, requestedWith) - - return minifyRequestedWith(r) -} - -/** - * shorten long url, headers, postData, response - * and transform Function/Matcher to string - */ -const minifyRequestedWith = (r: ExpectWebdriverIO.RequestedWith) => { - const result = { - url: requestedWithParamToString(r.url), - method: r.method, - requestHeaders: requestedWithParamToString(r.requestHeaders, shortenJson), - responseHeaders: requestedWithParamToString(r.responseHeaders, shortenJson), - postData: requestedWithParamToString(r.postData, shortenJson), - response: requestedWithParamToString(r.response, shortenJson), - } - - deleteUndefinedValues(result) - - return result -} - -/** - * transform Function/Matcher/JSON to string if needed - */ -const requestedWithParamToString = ( - param: - | string - | ExpectWebdriverIO.JsonCompatible - | ExpectWebdriverIO.PartialMatcher - | Function - | undefined, - transformFn?: (param: ExpectWebdriverIO.JsonCompatible) => ExpectWebdriverIO.JsonCompatible | string -) => { - if (typeof param === 'undefined') { - return - } - - if (typeof param === 'function') { - param = param.toString() - } else if (isMatcher(param)) { - return ( - param.constructor.name + - ' ' + - (JSON.stringify((param as ExpectWebdriverIO.PartialMatcher).sample) || '') - ) - } else if (transformFn && typeof param === 'object' && param !== null) { - param = transformFn(param as ExpectWebdriverIO.JsonCompatible) - } - - if (typeof param === 'string') { - param = shortenString(param) - } - - return param -} - -/** - * shorten object key/values and decrease array size - * ex: `{ someVeryLongKey: 'someVeryLongValue' }` -> `{ som..Key: 'som..lue' }` - */ -const shortenJson = ( - obj: ExpectWebdriverIO.JsonCompatible, - lengthLimit = STR_LIMIT * 2, - keyLimit = KEY_LIMIT -): ExpectWebdriverIO.JsonCompatible => { - if (JSON.stringify(obj).length < lengthLimit) { - return obj as ExpectWebdriverIO.JsonCompatible - } - - if (Array.isArray(obj)) { - const firstItem: object | string = - typeof obj[0] === 'object' && obj[0] !== null - ? shortenJson(obj[0], lengthLimit / 2, keyLimit / 4) - : shortenString(JSON.stringify(obj[0])) - return [firstItem, `... ${obj.length - 1} more items`] as string[] - } - - const minifiedObject: Record = {} - const entries = Object.entries(obj) - - if (keyLimit >= 4) { - entries.slice(0, keyLimit).forEach(([k, v]) => { - if (typeof v === 'object' && v !== null) { - v = shortenJson(v, lengthLimit / 2, keyLimit / 4) - } else if (typeof v === 'string') { - v = shortenString(v, 16) - } - minifiedObject[shortenString(k, 24)] = v - }) - } - if (entries.length > keyLimit) { - minifiedObject['...'] = `${entries.length} items in total` - } - - return minifiedObject as ExpectWebdriverIO.JsonCompatible -} - -/** - * shorten string - * ex: '1234567890' -> '12..90' - */ -const shortenString = (str: string, limit = STR_LIMIT) => { - return str.length > limit ? str.substring(0, limit / 2 - 1) + '..' + str.substr(1 - limit / 2) : str -} - -const deleteUndefinedValues = (obj: Record, baseline = obj) => { - Object.keys(obj).forEach((k) => { - if (typeof baseline[k] === 'undefined') { - delete obj[k] - } - }) -} - -export function toBeRequestedWithResponse(...args: unknown[]) { - return toBeRequestedWith.call(this, ...args) -} diff --git a/src/matchers/snapshot.ts b/src/matchers/snapshot.ts deleted file mode 100644 index 5a1e6273f..000000000 --- a/src/matchers/snapshot.ts +++ /dev/null @@ -1,182 +0,0 @@ -import path from 'node:path' -import type { AssertionError } from 'node:assert' - -import { expect } from 'expect' -import { stripSnapshotIndentation } from '@vitest/snapshot' -import { SnapshotService } from '../snapshot.js' - -interface InlineSnapshotOptions { - inlineSnapshot: string - error: Error -} - -/** - * Vitest snapshot client returns a snapshot error with an `actual` and `expected` - * property containing strings of the compared snapshots. In case these don't match - * we use this helper method to return a proper assertion message that contains - * nice color highlighting etc. For that we just re-assert the two strings. - * @param snapshotError error message from snapshot client - * @returns matcher result - */ -function returnSnapshotError (snapshotError: AssertionError) { - /** - * wrap into another try catch block so we can get a better - * assertion message - */ - try { - expect(snapshotError.actual).toBe(snapshotError.expected) - } catch (e) { - return { - pass: false, - message: () => (e as Error).message - } - } - - /** - * this should never happen but in case it does we want to - */ - throw snapshotError -} - -/** - * Helper method to assert snapshots - * @param received element to snapshot - * @param message optional message on failure - * @returns matcher results - */ -function toMatchSnapshotAssert (received: unknown, message: string, inlineOptions?: InlineSnapshotOptions) { - const snapshotService = SnapshotService.initiate() - try { - snapshotService.client.assert({ - received, - message, - filepath: snapshotService.currentFilePath as string, - name: snapshotService.currentTestName as string, - /** - * apply inline options if needed - */ - ...(inlineOptions ? { - ...inlineOptions, - isInline: true - } : { - isInline: false - }) - }) - return { - pass: true, - message: () => 'Snapshot matches' - } - } catch (e: unknown) { - return returnSnapshotError(e as AssertionError) - } -} - -/** - * Asynchronous version of `toMatchSnapshot` that works with WebdriverIO elements. - * @param elem a WebdriverIO element - * @param message optional message on failure - * @returns matcher results - */ -async function toMatchSnapshotAsync (asyncReceived: unknown, message: string, inlineOptions?: InlineSnapshotOptions) { - let received: WebdriverIO.Element | unknown = await asyncReceived - - if (received && typeof received === 'object' && 'elementId' in received) { - received = await (received as WebdriverIO.Element).getHTML({ - includeSelectorTag: true - }) - } - return toMatchSnapshotAssert(received, message, inlineOptions) -} - -/** - * We want to keep this method synchronous so that doing snapshots for basic - * elements doesn't require an `await` and matches other framework behavior. - * @param received element to snapshot - * @param message optional message on failure - * @returns matcher results - */ -function toMatchSnapshotHelper(received: unknown, message: string, inlineOptions?: InlineSnapshotOptions) { - const snapshotService = SnapshotService.initiate() - if (!snapshotService.currentFilePath || !snapshotService.currentTestName) { - throw new Error('Snapshot service is not initialized') - } - - /** - * allow to match DOM snapshots - */ - if ( - received && typeof received === 'object' && - ( - 'elementId' in received || - 'then' in received - ) - ) { - return toMatchSnapshotAsync(received, message, inlineOptions) - } - - return toMatchSnapshotAssert(received, message, inlineOptions) -} - -export function toMatchSnapshot(received: unknown, message: string) { - return toMatchSnapshotHelper(received, message) -} - -export function toMatchInlineSnapshot(received: unknown, inlineSnapshot: string, message: string) { - /** - * When running component/unit tests in the browser we receive a stack trace - * through the `this` scope. - */ - const browserErrorLine: string = this.errorStack - - function __INLINE_SNAPSHOT__(inlineSnapshot: string, message: string) { - /** - * create a error object to pass along that helps Vitest's snapshot manager - * to infer the stack trace and locate the inline snapshot - */ - const error = new Error('inline snapshot') - - /** - * merge stack traces from browser and node and push the error of the test - * into the stack trace - */ - if (browserErrorLine && error.stack) { - const stack = error.stack.split('\n') - error.stack = [ - ...stack.slice(0, 4), - browserErrorLine, - ...stack.slice(3) - ].join('\n') - } - const trace = error.stack?.split('\n').filter((line) => ( - line.includes('__INLINE_SNAPSHOT__') || - !( - line.includes('__EXTERNAL_MATCHER_TRAP__') || - line.includes(`expect-webdriverio${path.sep}lib${path.sep}matchers${path.sep}snapshot.js:`) - ) - )).filter((line) => ( - /** - * remove jasmine-core stack trace to make it work with jasmine - */ - !line.includes('node_modules/jasmine-core/') - )) || [] - - /** - * tweak the stack trace to enable inline snapshot testing within this projects - * unit tests - */ - if (process.env.WDIO_INTERNAL_TEST) { - trace.splice(2, 1) - } - - if (inlineSnapshot) { - inlineSnapshot = stripSnapshotIndentation(inlineSnapshot) - } - - error.stack = trace.join('\n') - return toMatchSnapshotHelper(received, message, { - inlineSnapshot, - error - }) - } - return __INLINE_SNAPSHOT__(inlineSnapshot, message) -} diff --git a/src/snapshot.ts b/src/snapshot.ts deleted file mode 100644 index 91b7b99e9..000000000 --- a/src/snapshot.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { expect } from '@wdio/globals' -import { SnapshotClient, type SnapshotResult, type SnapshotStateOptions, type SnapshotUpdateState } from '@vitest/snapshot' -import { NodeSnapshotEnvironment } from '@vitest/snapshot/environment' - -import type { Services, Frameworks } from '@wdio/types' - -/** - * only create instance once to avoid memory leak - */ -let service: SnapshotService - -export type SnapshotFormat = SnapshotStateOptions['snapshotFormat'] -type ResolveSnapshotPathFunction = (path: string, extension: string) => string -interface SnapshotServiceArgs { - updateState?: SnapshotUpdateState - resolveSnapshotPath?: ResolveSnapshotPathFunction - snapshotFormat?: SnapshotFormat -} - -class WebdriverIOSnapshotEnvironment extends NodeSnapshotEnvironment { - #resolveSnapshotPath?: (path: string, extension: string) => string - - constructor (resolveSnapshotPath?: ResolveSnapshotPathFunction) { - super({}) - this.#resolveSnapshotPath = resolveSnapshotPath - } - - async resolvePath (filepath: string): Promise { - if (this.#resolveSnapshotPath) { - return this.#resolveSnapshotPath(filepath, '.snap') - } - return super.resolvePath(filepath) - } -} - -/** - * Snapshot service to take snapshots of elements. - * The `@wdio/runner` module will attach this service to the test environment - * so it can track the current test file and test name. - * - * @param {string} updateState update state - * @return {SnapshotService} - */ -export class SnapshotService implements Services.ServiceInstance { - #currentFilePath?: string - #currentTestName?: string - #options: SnapshotStateOptions - #snapshotResults: SnapshotResult[] = [] - #snapshotClient = new SnapshotClient({ - isEqual: this.#isEqual.bind(this), - }) - - constructor (options?: SnapshotServiceArgs) { - const updateSnapshot = (Boolean(process.env.CI) && !options?.updateState) - ? 'none' - : options?.updateState - ? options.updateState - : 'new' - - // Only set snapshotFormat if user provides explicit options - const snapshotFormatConfig = options?.snapshotFormat ? { - printBasicPrototype: false, - escapeString: false, - ...options.snapshotFormat - } : undefined - - this.#options = { - updateSnapshot, - snapshotEnvironment: new WebdriverIOSnapshotEnvironment(options?.resolveSnapshotPath), - ...(snapshotFormatConfig && { snapshotFormat: snapshotFormatConfig }) - } as const - } - - get currentFilePath () { - return this.#currentFilePath - } - - get currentTestName () { - return this.#currentTestName - } - - get client () { - return this.#snapshotClient - } - - get results () { - return this.#snapshotResults - } - - async beforeTest(test: Frameworks.Test) { - this.#currentFilePath = test.file - this.#currentTestName = `${test.parent} > ${test.title}` - await this.#snapshotClient.setup(test.file, this.#options) - } - - async beforeStep(step: Frameworks.PickleStep, scenario: Frameworks.Scenario) { - const file = scenario.uri - const testName = `${scenario.name} > ${step.text}` - - this.#currentFilePath = file - this.#currentTestName = testName - await this.#snapshotClient.setup(file, this.#options) - } - - async after() { - if (!this.#currentFilePath) { - return - } - - const result = await this.#snapshotClient.finish(this.#currentFilePath) - if (!result) { - return - } - this.#snapshotResults.push(result) - } - - #isEqual (received: unknown, expected: unknown) { - try { - expect(received).toBe(expected) - return true - } catch { - return false - } - } - - static initiate (options?: SnapshotServiceArgs) { - if (!service) { - service = new SnapshotService(options) - } - return service - } -} - diff --git a/src/softAssert.ts b/src/softAssert.ts deleted file mode 100644 index 73f1a9903..000000000 --- a/src/softAssert.ts +++ /dev/null @@ -1,139 +0,0 @@ -import type { AssertionError } from 'node:assert' - -interface SoftFailure { - error: AssertionError | Error; - matcherName: string; - location?: string; -} - -interface TestIdentifier { - id: string; - name?: string; - file?: string; -} - -/** - * Soft assertion service to collect failures without stopping test execution - */ -export class SoftAssertService { - private static instance: SoftAssertService - private failureMap: Map = new Map() - private currentTest: TestIdentifier | null = null - - private constructor() { } - - /** - * Get singleton instance - */ - public static getInstance(): SoftAssertService { - if (!SoftAssertService.instance) { - SoftAssertService.instance = new SoftAssertService() - } - return SoftAssertService.instance - } - - /** - * Set the current test context - */ - public setCurrentTest(testId: string, testName?: string, testFile?: string): void { - this.currentTest = { id: testId, name: testName, file: testFile } - if (!this.failureMap.has(testId)) { - this.failureMap.set(testId, []) - } - } - - /** - * Clear the current test context - */ - public clearCurrentTest(): void { - this.currentTest = null - } - - /** - * Get current test ID - */ - public getCurrentTestId(): string | null { - return this.currentTest?.id || null - } - - /** - * Add a soft failure for the current test - */ - public addFailure(error: Error, matcherName: string): void { - const testId = this.getCurrentTestId() - if (!testId) { - throw error // If no test context, throw the error immediately - } - - // Extract stack information to get file and line number - const stackLines = error.stack?.split('\n') || [] - let location = '' - - // Find the first non-expect-webdriverio line in the stack - for (const line of stackLines) { - if (line && !line.includes('expect-webdriverio') && !line.includes('node_modules')) { - location = line.trim() - break - } - } - - const failures = this.failureMap.get(testId) || [] - failures.push({ error, matcherName, location }) - this.failureMap.set(testId, failures) - } - - /** - * Get all failures for a specific test - */ - public getFailures(testId?: string): SoftFailure[] { - const id = testId || this.getCurrentTestId() - if (!id) { - return [] - } - return this.failureMap.get(id) || [] - } - - /** - * Clear failures for a specific test - */ - public clearFailures(testId?: string): void { - const id = testId || this.getCurrentTestId() - if (id) { - this.failureMap.delete(id) - } - } - - /** - * Throw an aggregated error if there are failures for the current test - */ - public assertNoFailures(testId?: string): void { - const id = testId || this.getCurrentTestId() - if (!id) { - return - } - - const failures = this.getFailures(id) - if (failures.length === 0) { - return - } - - // Create a formatted error message with all failures - let message = `${failures.length} soft assertion failure${failures.length > 1 ? 's' : ''}:\n\n` - - failures.forEach((failure, index) => { - message += `${index + 1}) ${failure.matcherName}: ${failure.error.message}\n` - if (failure.location) { - message += ` at ${failure.location}\n` - } - message += '\n' - }) - - // Clear failures for this test to prevent duplicate reporting - this.clearFailures(id) - - // Throw an aggregated error - const error = new Error(message) - error.name = 'SoftAssertionsError' - throw error - } -} diff --git a/src/softAssertService.ts b/src/softAssertService.ts deleted file mode 100644 index eb9507348..000000000 --- a/src/softAssertService.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { Services } from '@wdio/types' -import type { Frameworks } from '@wdio/types' -import { SoftAssertService } from './softAssert.js' - -export interface SoftAssertionServiceOptions { - autoAssertOnTestEnd?: boolean; -} - -/** - * WebdriverIO service to integrate soft assertions into the test lifecycle - */ -export class SoftAssertionService implements Services.ServiceInstance { - private softAssertService: SoftAssertService - public options: SoftAssertionServiceOptions - - constructor( - serviceOptions?: SoftAssertionServiceOptions, - ) { - this.softAssertService = SoftAssertService.getInstance() - this.options = { - autoAssertOnTestEnd: true, - ...serviceOptions - } - } - - /** - * Hook before a test starts - */ - beforeTest(test: Frameworks.Test) { - const testId = this.getTestId(test) - this.softAssertService.setCurrentTest(testId, test.title, test.file) - } - - /** - * Hook before a Cucumber step starts - */ - beforeStep(step: Frameworks.PickleStep, scenario: Frameworks.Scenario) { - const stepId = `${scenario.uri || ''}:${scenario.name || ''}:${step.text || ''}` - this.softAssertService.setCurrentTest(stepId, step.text, scenario.uri) - } - - /** - * Hook after a test completes - */ - afterTest(test: Frameworks.Test, _: unknown, result: Frameworks.TestResult) { - // Only assert failures if: - // 1. The test hasn't yet failed for another reason - // 2. Auto-assertion is enabled in the configuration - if (!result.error && this.options.autoAssertOnTestEnd) { - try { - const testId = this.getTestId(test) - this.softAssertService.assertNoFailures(testId) - } catch (error) { - // Update the test result with our aggregated error - result.error = error - result.passed = false - } - } - this.softAssertService.clearCurrentTest() - } - - /** - * Hook after a Cucumber step completes - */ - afterStep(step: Frameworks.PickleStep, scenario: Frameworks.Scenario, result: { passed: boolean, error?: Error }) { - // Only assert failures if the step hasn't already failed for another reason - if (result.passed) { - try { - const stepId = `${scenario.uri || ''}:${scenario.name || ''}:${step.text || ''}` - this.softAssertService.assertNoFailures(stepId) - } catch (error) { - // Update the step result with our aggregated error - result.error = error as Error - result.passed = false - } - } - this.softAssertService.clearCurrentTest() - } - - /** - * Generate a unique test ID from a test object - */ - private getTestId(test: Frameworks.Test): string { - return `${test.file || ''}:${test.parent || ''}:${test.title || ''}` - } -} diff --git a/src/softExpect.ts b/src/softExpect.ts deleted file mode 100644 index 4aa2dfbb9..000000000 --- a/src/softExpect.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { expect, matchers } from './index.js' -import { SoftAssertService } from './softAssert.js' - -/** - * Creates a soft assertion wrapper using lazy evaluation - * Only creates matchers when they're actually accessed - */ -const createSoftExpect = (actual: T): ExpectWebdriverIO.Matchers, T> => { - const softService = SoftAssertService.getInstance() - - // Use a simple proxy that creates matchers on-demand - return new Proxy({} as ExpectWebdriverIO.Matchers, T>, { - get(target, prop) { - const propName = String(prop) - - // Handle .not specially - if (propName === 'not') { - return createSoftNotProxy(actual, softService) - } - - // Handle resolves/rejects (rarely used in WebdriverIO) - if (propName === 'resolves' || propName === 'rejects') { - return createSoftChainProxy(actual, propName, softService) - } - - // Handle matchers - if (matchers.has(propName)) { - return createSoftMatcher(actual, propName, softService) - } - - // For any other properties, return undefined - return undefined - } - }) -} - -/** - * Creates a soft .not proxy - */ -const createSoftNotProxy = (actual: T, softService: SoftAssertService) => { - return new Proxy({} as ExpectWebdriverIO.Matchers, T>, { - get(target, prop) { - const propName = String(prop) - if (matchers.has(propName)) { - return createSoftMatcher(actual, propName, softService, 'not') - } - return undefined - } - }) -} - -/** - * Creates a soft chain proxy (resolves/rejects) - */ -const createSoftChainProxy = (actual: T, chainType: string, softService: SoftAssertService) => { - return new Proxy({} as ExpectWebdriverIO.Matchers, T>, { - get(target, prop) { - const propName = String(prop) - if (matchers.has(propName)) { - return createSoftMatcher(actual, propName, softService, chainType) - } - return undefined - } - }) -} - -/** - * Creates a single soft matcher function - */ -const createSoftMatcher = ( - actual: T, - matcherName: string, - softService: SoftAssertService, - prefix?: string -) => { - return async (...args: unknown[]) => { - try { - // Build the expectation chain - let expectChain = expect(actual) - - if (prefix === 'not') { - expectChain = expectChain.not - } else if (prefix === 'resolves') { - expectChain = expectChain.resolves - } else if (prefix === 'rejects') { - expectChain = expectChain.rejects - } - - return await ((expectChain as unknown) as Record Promise>)[matcherName](...args) - - } catch (error) { - // Record the failure - const fullMatcherName = prefix ? `${prefix}.${matcherName}` : matcherName - softService.addFailure(error as Error, fullMatcherName) - - // Return a passing result to continue execution - return { - pass: true, - message: () => `Soft assertion failed: ${fullMatcherName}` - } - } - } -} - -export default createSoftExpect diff --git a/src/utils.ts b/src/utils.ts index 94013f390..f5ecb0f23 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 ExpectWebdriverIO.PartialMatcher { return ( typeof expected === 'object' && typeof expected === 'object' && @@ -29,7 +29,7 @@ export function isAsymmeyricMatcher(expected: unknown): expected is ExpectWebdri } function isStringContainingMatcher(expected: unknown): expected is ExpectWebdriverIO.PartialMatcher { - return isAsymmeyricMatcher(expected) && ['StringContaining', 'StringNotContaining'].includes(expected.toString()) + return isAsymmetricMatcher(expected) && ['StringContaining', 'StringNotContaining'].includes(expected.toString()) } /** @@ -178,7 +178,7 @@ export const compareText = ( } } - if (isAsymmeyricMatcher(expected)) { + if (isAsymmetricMatcher(expected)) { const result = expected.asymmetricMatch(actual) return { value: actual, @@ -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/tsconfig.json b/test-types/jasmine/tsconfig.json deleted file mode 100644 index 6407f175a..000000000 --- a/test-types/jasmine/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/jest/tsconfig.json b/test-types/jest/tsconfig.json index 6407f175a..5a6adaf9a 100644 --- a/test-types/jest/tsconfig.json +++ b/test-types/jest/tsconfig.json @@ -7,7 +7,6 @@ "skipLibCheck": true, "types": [ "@types/jest", - "expect-webdriverio/jest", "@wdio/globals/types" ] } diff --git a/test-types/jest/types.test.ts b/test-types/jest/types.test.ts new file mode 100644 index 000000000..0a0edba96 --- /dev/null +++ b/test-types/jest/types.test.ts @@ -0,0 +1,99 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/// + +describe('type assertions', () => { + + describe('browser type assertions', () => { + const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser + it('should not have ts errors and be able to await the promise', async () => { + const browserExpectHaveUrlIsPromiseVoid: Promise = expect(browser).toHaveUrl('https://example.com') + await browserExpectHaveUrlIsPromiseVoid + + const browserExpectNotHaveUrlIsPromiseVoid: Promise = expect(browser).not.toHaveUrl('https://example.com') + await browserExpectNotHaveUrlIsPromiseVoid + }) + + it('should have ts errors and not need to await the promise', async () => { + // @ts-expect-error + const browserExpectHaveUrlIsVoid: void = expect(browser).toHaveUrl('https://example.com') + // @ts-expect-error + const browserExpectNotHaveUrlIsVoid: void = expect(browser).not.toHaveUrl('https://example.com') + }) + }) + + describe('element type assertions', () => { + const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element + it('should not have ts errors and be able to await the promise', async () => { + // expect no ts errors + const expectIsPromiseVoid: Promise = expect(element).toBeDisabled() + await expectIsPromiseVoid + + const expectNotIsPromiseVoid: Promise = expect(element).not.toBeDisabled() + await expectNotIsPromiseVoid + }) + + it('should have ts errors when typing to void', async () => { + // @ts-expect-error + const expectToBeIsVoid: void = expect(element).toBeDisabled() + // @ts-expect-error + const expectNotToBeIsVoid: void = expect(element).not.toBeDisabled() + }) + }) + + describe('boolean type assertions', () => { + it('should not have ts errors when typing to void', async () => { + // Expect no ts errors + const expectToBeIsVoid: void = expect(true).toBe(true) + const expectNotToBeIsVoid: void = expect(true).not.toBe(true) + }) + + it('should have ts errors when typing to Promise', async () => { + //@ts-expect-error + const expectToBeIsNotPromiseVoid: Promise = expect(true).toBe(true) + //@ts-expect-error + const expectToBeIsNotPromiseVoid: Promise = expect(true).not.toBe(true) + }) + }) + + describe('string type assertions', () => { + it('should not have ts errors when typing to void', async () => { + // Expect no ts errors + const expectToBeIsVoid: void = expect('test').toBe('test') + }) + + it('should have ts errors when typing to Promise', async () => { + //@ts-expect-error + const expectToBeIsNotPromiseVoid: Promise = expect('test').toBe('test') + }) + }) + + describe('Promise type assertions', () => { + const booleanPromise: Promise = Promise.resolve(true) + + it('should not have ts errors when typing to void', async () => { + const expectToBeIsVoid: void = expect(booleanPromise).toBe(true) + const expectAwaitToBeIsVoid: void = expect(await booleanPromise).toBe(true) + }) + + it('should not have ts errors when resolves and rejects is typed to Promise', async () => { + // TODO the below needs to return Promise but currently returns void + const expectResolvesToBeIsVoid: Promise = expect(booleanPromise).resolves.toBe(true) + const expectRejectsToBeIsVoid: Promise = expect(booleanPromise).rejects.toBe(true) + }) + + it('should have ts errors when typing to Promise', async () => { + //@ts-expect-error + const expectToBeIsNotPromiseVoid: Promise = expect(booleanPromise).toBe(true) + //@ts-expect-error + const expectToBeIsNotPromiseVoid: Promise = expect(await booleanPromise).toBe(true) + }) + + it('should have ts errors when typing resolves and reject is typed to void', async () => { + // TODO the below needs to return Promise but currently returns void + //@ts-expect-error + const expectResolvesToBeIsVoid: void = expect(booleanPromise).resolves.toBe(true) + //@ts-expect-error + const expectRejectsToBeIsVoid: void = expect(booleanPromise).rejects.toBe(true) + }) + }) +}) diff --git a/test-types/types.ts b/test-types/types.ts index fcf5b056e..d9848ef9b 100644 --- a/test-types/types.ts +++ b/test-types/types.ts @@ -1,5 +1,5 @@ /// -/// +/// const elem: WebdriverIO.Element = {} as unknown as WebdriverIO.Element const wdioExpect = ExpectWebdriverIO.expect diff --git a/test/index.test.ts b/test/index.test.ts index 25c6c95d7..6607fccf1 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,9 +1,8 @@ import { test, expect } from 'vitest' -import { setOptions, expect as expectExport, matchers, utils } from '../src/index.js' +import { setOptions, matchers, utils } from '../src/index.js' test('index', () => { expect(setOptions.name).toBe('setDefaultOptions') - expect(expectExport).toBeDefined() expect(utils.compareText).toBeDefined() - expect(matchers.size).toEqual(41) + expect(matchers.size).toEqual(37) }) diff --git a/test/matchers.test.ts b/test/matchers.test.ts index 5e5f15ab8..4469213ff 100644 --- a/test/matchers.test.ts +++ b/test/matchers.test.ts @@ -1,5 +1,5 @@ -import { test, expect, vi } from 'vitest' -import { matchers, expect as expectLib } from '../src/index.js' +import { test, expect } from 'vitest' +import { matchers } from '../src/index.js' const ALL_MATCHERS = [ // browser @@ -45,23 +45,9 @@ const ALL_MATCHERS = [ // mock 'toBeRequested', - 'toBeRequestedTimes', - 'toBeRequestedWith', - 'toBeRequestedWithResponse', - - // snapshot - 'toMatchSnapshot', - 'toMatchInlineSnapshot' + 'toBeRequestedTimes' ] test('matchers', () => { expect([...matchers.keys()]).toEqual(ALL_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') -}) diff --git a/test/matchers/mock/toBeRequestedWith.test.ts b/test/matchers/mock/toBeRequestedWith.test.ts deleted file mode 100644 index 0fd51ff70..000000000 --- a/test/matchers/mock/toBeRequestedWith.test.ts +++ /dev/null @@ -1,487 +0,0 @@ -import { vi, test, describe, expect, beforeEach, afterEach } from 'vitest' - -import { toBeRequestedWith } from '../../../src/matchers/mock/toBeRequestedWith.js' -import type { local } from 'webdriver' -import { removeColors, getExpectMessage, getExpected, getReceived } from '../../__fixtures__/utils.js' - -vi.mock('@wdio/globals') - -interface Scenario { - name: string - mocks: local.NetworkBaseParameters[] - pass: boolean - params: ExpectWebdriverIO.RequestedWith -} - -class TestMock { - _calls: local.NetworkBaseParameters[] - - constructor() { - this._calls = [] - } - get calls() { - return this._calls - } -} - -function reduceHeaders(headers: local.NetworkHeader[]) { - return Object.entries(headers).reduce((acc, [, value]: [string, local.NetworkHeader]) => { - acc[value.name] = value.value.value - return acc - }, {} as Record) -} - -const authKey = 'Bearer ' + '2'.repeat(128) - -const mockGet: local.NetworkAuthRequiredParameters = { - request: { - url: 'http://localhost:8080/api/search?pages=20', - method: 'GET', - request: '123', - headersSize: 123, - bodySize: 123, - timings: {} as any, - cookies: [], - headers: [{ - name: 'Authorization', - value: { type: 'string', value: authKey } - }, { - name: 'foo', - value: { type: 'string', value: 'bar' } - }] - }, - response: { - headers: {}, - status: 200, - } as any, - // body: JSON.stringify({ - // total: 100, - // page: 1, - // data: { - // aLongValue1: { - // k1: { value1: 'bar1' }, - // k2: { value2: 'bar2' }, - // }, - // foo: { id: 1 }, - // bar: { id: 2 }, - // longValue2: { value: 'foo2' }, - // longValue3: { value: 'foo3' }, - // }, - // }), - // initialPriority: 'Low', - // referrerPolicy: 'origin', -} as any - -const mockPost: local.NetworkAuthRequiredParameters = { - request: { - url: 'https://my-app/api/add-tags', - method: 'POST', - request: '123', - headersSize: 123, - bodySize: 123, - timings: {} as any, - cookies: [], - headers: [{ - name: 'Authorization', - value: { type: 'string', value: authKey } - }, { - name: 'foo', - value: { type: 'string', value: 'bar' } - }, { - name: 'Accept', - value: { type: 'string', value: '*' } - }], - }, - response: { - status: 201, - headers: [] - } as any, - // body: JSON.stringify([ - // { id: 1, name: 'foo' }, - // { id: 2, name: 'bar' }, - // ]), - // postData: JSON.stringify([{ id: 1 }, { search: { name: 'bar' } }]), - // initialPriority: 'Low', - // referrerPolicy: 'origin', -} as any - -describe('toBeRequestedWith', () => { - test('wait for success, exact match', async () => { - const mock: any = new TestMock() - - setTimeout(() => { - mock.calls.push({ ...mockGet }) - }, 5) - setTimeout(() => { - mock.calls.push({ ...mockGet }, { ...mockPost }) - }, 15) - - const params = { - url: mockPost.request.url, - method: mockPost.request.method, - requestHeaders: {}, - statusCode: mockPost.response.status, - responseHeaders: {}, - // postData: mockPost.postData, - // response: JSON.parse(mockPost.body as string), - } - - const beforeAssertion = vi.fn() - const afterAssertion = vi.fn() - const result = await toBeRequestedWith.call({}, mock, params, { beforeAssertion, afterAssertion }) - expect(result.pass).toBe(true) - expect(beforeAssertion).toBeCalledWith({ - matcherName: 'toBeRequestedWith', - expectedValue: params, - options: { beforeAssertion, afterAssertion }, - }) - expect(afterAssertion).toBeCalledWith({ - matcherName: 'toBeRequestedWith', - expectedValue: params, - options: { beforeAssertion, afterAssertion }, - result - }) - }) - - test('wait for failure', async () => { - const mock: any = new TestMock() - - setTimeout(() => { - mock.calls.push({ ...mockGet }, { ...mockPost }) - }, 15) - - const params = { - url: 'post.url', - method: 'post.method', - requestHeaders: {}, - responseHeaders: {} - // postData: {}, - // response: 'post.body', - } - - const result = await toBeRequestedWith.call({}, mock, params) - expect(result.pass).toBe(false) - }) - - test('wait for NOT failure, empty params', async () => { - const mock: any = new TestMock() - mock.calls.push({ ...mockGet }, { ...mockPost }) - setTimeout(() => { - mock.calls.push({ ...mockGet }, { ...mockPost }) - }, 10) - - const result = await toBeRequestedWith.call({ isNot: true }, mock, {}) - expect(result.pass).toBe(true) - }) - - test('wait for NOT success', async () => { - const mock: any = new TestMock() - - setTimeout(() => { - mock.calls.push({ ...mockGet }, { ...mockPost }) - }, 10) - - const result = await toBeRequestedWith.call({ isNot: true }, mock, { method: 'DELETE' }) - expect(result.pass).toBe(false) - }) - - const scenarios: Scenario[] = [ - // success - { - name: 'success, url only', - mocks: [{ ...mockPost }], - pass: true, - params: { - url: mockPost.request.url, - }, - }, - { - name: 'success, method only', - mocks: [{ ...mockPost }], - pass: true, - params: { - method: ['DELETE', 'PUT', mockPost.request.method, 'GET'], - }, - }, - { - name: 'success, statusCode only', - mocks: [{ ...mockPost }], - pass: true, - params: { - statusCode: [203, 200, 201], - }, - }, - { - name: 'success, requestHeaders only', - mocks: [{ ...mockPost }], - pass: true, - params: { - requestHeaders: { - Authorization: authKey, - foo: 'bar', - Accept: '*' - }, - }, - }, - { - name: 'success, responseHeaders only', - mocks: [{ ...mockPost }], - pass: true, - params: { - responseHeaders: {}, - }, - }, - // { - // name: 'success, postData only', - // mocks: [{ ...mockPost }], - // pass: true, - // params: { - // postData: JSON.parse(mockPost.postData as string), - // }, - // }, - // { - // name: 'success, response only', - // mocks: [{ ...mockPost }], - // pass: true, - // params: { - // response: mockPost.body, - // }, - // }, - // failure - { - name: 'failure, url only', - mocks: [{ ...mockPost }], - pass: false, - params: { - url: '/api/api', - }, - }, - { - name: 'failure, method only', - mocks: [{ ...mockPost }], - pass: false, - params: { - method: ['DELETE', 'PUT'], - }, - }, - { - name: 'failure, statusCode only', - mocks: [{ ...mockPost }], - pass: false, - params: { - statusCode: [400, 401], - }, - }, - { - name: 'failure, requestHeaders only', - mocks: [{ ...mockPost }], - pass: false, - params: { - requestHeaders: { Cache: 'false' }, - }, - }, - { - name: 'failure, responseHeaders only', - mocks: [{ ...mockPost }], - pass: false, - params: { - responseHeaders: { Cache: 'false' }, - }, - }, - // { - // name: 'failure, postData only', - // mocks: [{ ...mockPost }], - // pass: false, - // params: { - // postData: 'foobar', - // }, - // }, - // { - // name: 'failure, response only', - // mocks: [{ ...mockGet }], - // pass: false, - // params: { - // response: { foobar: true }, - // }, - // }, - // special matcher - { - name: 'special matcher, url', - mocks: [{ ...mockPost }], - pass: true, - params: { - url: expect.stringMatching(/.*\/API\/.*/i), - }, - }, - { - name: 'special matcher, headers', - mocks: [{ ...mockPost }], - pass: true, - params: { - requestHeaders: expect.objectContaining({ - Authorization: expect.stringContaining('Bearer '), - }), - }, - }, - { - name: 'special matcher, postData', - mocks: [{ ...mockPost }], - pass: true, - params: { - postData: expect.stringMatching('"search"'), - }, - }, - { - name: 'special matcher, response', - mocks: [{ ...mockPost }], - pass: true, - params: { - response: expect.arrayContaining([expect.objectContaining({ id: 2 })]), - }, - }, - // function - { - name: 'function, url', - mocks: [{ ...mockPost }], - pass: true, - params: { - url: (url: string) => url.startsWith('https'), - }, - }, - { - name: 'function, headers', - mocks: [{ ...mockPost }], - pass: true, - params: { - requestHeaders: (headers: Record) => headers.foo === 'bar', - }, - }, - { - name: 'function, postData', - mocks: [{ ...mockPost }], - pass: true, - params: { - postData: (r: string) => (JSON.parse(r) as Array>).length === 2, - }, - }, - { - name: 'function, response', - mocks: [{ ...mockPost }], - pass: true, - params: { - response: (r: string) => r.includes('id') && r.includes('name'), - }, - }, - // no postData - // { - // name: 'no postData', - // mocks: [{ ...mockGet }], - // pass: false, - // params: { - // postData: 'something', - // }, - // }, - // body is not a JSON - // { - // name: 'body as string', - // mocks: [{ ...mockGet, body: 'asd' }], - // pass: true, - // params: { - // response: 'asd', - // }, - // }, - // { - // name: 'body as Buffer', - // mocks: [{ ...mockGet, body: Buffer.from('asd') }], - // pass: true, - // params: { - // response: 'asd', - // }, - // }, - // { - // name: 'body as JSON', - // mocks: [{ ...mockGet, body: 'asd' }], - // pass: false, - // params: { - // response: { foo: 'bar' }, - // }, - // }, - ] - - scenarios.forEach((scenario) => { - test(scenario.name, async () => { - const mock: any = new TestMock() - mock.calls.push(...scenario.mocks) - - const result = await toBeRequestedWith.call({}, mock, scenario.params as any) - expect(result.pass).toBe(scenario.pass) - }) - }) - - describe('error messages', () => { - const consoleError = global.console.error - beforeEach(() => { - global.console.error = vi.fn() - }) - - test('unsupported method', async () => { - const mock: any = new TestMock() - mock.calls.push({ ...mockGet }) - - const result = await toBeRequestedWith.call({}, mock, { method: 1234 } as any) - expect(result.pass).toBe(false) - expect(global.console.error).toBeCalledWith( - 'expect.toBeRequestedWith: unsupported value passed to method 1234' - ) - }) - - afterEach(() => { - global.console.error = consoleError - }) - }) - - test('message', async () => { - const mock: any = new TestMock() - - const requested = await toBeRequestedWith.call({}, mock, { - url: () => false, - method: ['DELETE', 'PUT'], - requestHeaders: reduceHeaders(mockPost.request.headers), - responseHeaders: reduceHeaders(mockPost.response.headers), - postData: expect.anything(), - response: [...Array(50).keys()].map((_, id) => ({ id, name: `name_${id}` })), - }) - const wasNotCalled = removeColors(requested.message()) - expect(getExpectMessage(wasNotCalled)).toBe('Expect mock to be called with') - expect(getExpected(wasNotCalled)).toBe( - 'Expected: {' + - '"method": ["DELETE", "PUT"], ' + - '"postData": "Anything ", ' + - '"requestHeaders": {"Accept": "*", "Authorization": "Bearer ..2222222", "foo": "bar"}, ' + - '"response": [{"id": 0, "name": "name_0"}, "... 49 more items"], ' + - '"responseHeaders": {}, ' + - '"url": "() => false"}' - ) - expect(getReceived(wasNotCalled)).toBe('Received: "was not called"') - - mock.calls.push(mockPost) - - const notRequested = await toBeRequestedWith.call({ isNot: true }, mock, { - url: () => true, - method: mockPost.request.method, - }) - const wasCalled = removeColors(notRequested.message()) - expect(wasCalled).toBe( - `Expect mock not to be called with - -- Expected [not] - 1 -+ Received + 1 - - Object { - "method": "POST", -- "url": "() => true", -+ "url": "https://my-app/api/add-tags", - }` - ) - }) -}) diff --git a/test/snapshot.test.ts b/test/snapshot.test.ts deleted file mode 100644 index bfe61a4c5..000000000 --- a/test/snapshot.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import fs from 'node:fs/promises' -import path from 'node:path' -import { fileURLToPath } from 'node:url' -import { test, expect } from 'vitest' -import type { Frameworks } from '@wdio/types' - -import { expect as expectExport, SnapshotService } from '../src/index.js' - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const __filename = path.basename(fileURLToPath(import.meta.url)) - -const service = SnapshotService.initiate({ - resolveSnapshotPath: (path, extension) => path + extension -}) - -test('supports snapshot testing', async () => { - await service.beforeTest({ - title: 'test', - parent: 'parent', - file: path.join(__dirname, __filename), - } as Frameworks.Test) - - process.env.WDIO_INTERNAL_TEST = 'true' - - const exp = expectExport - expect(exp).toBeDefined() - expect(exp({}).toMatchSnapshot).toBeDefined() - expect(exp({}).toMatchInlineSnapshot).toBeDefined() - await exp({ a: 'a' }).toMatchSnapshot() - await exp({ deep: { nested: { object: 'value' } } }).toMatchInlineSnapshot(` - { - "deep": { - "nested": { - "object": "value", - }, - }, - } - `) - await service.after() - - const expectedSnapfileExist = await fs.access(path.resolve(__dirname, 'snapshot.test.ts.snap')) - .then(() => true, () => false) - expect(expectedSnapfileExist).toBe(true) -}) - -test('supports cucumber snapshot testing', async () => { - await service.beforeStep({ - text: 'Fake step', - } as Frameworks.PickleStep, { - name: 'Fake scenario', - uri: `${__dirname}/file.feature`, - } as Frameworks.Scenario) - - const exp = expectExport - expect(exp).toBeDefined() - expect(exp({}).toMatchSnapshot).toBeDefined() - expect(exp({}).toMatchInlineSnapshot).toBeDefined() - await exp({ cucum: 'ber' }).toMatchSnapshot() - await service.after() - - const expectedSnapfileExist = await fs.access(path.resolve(__dirname, 'file.feature.snap')) - .then(() => true, () => false) - expect(expectedSnapfileExist).toBe(true) -}) diff --git a/test/snapshot.test.ts.snap b/test/snapshot.test.ts.snap deleted file mode 100644 index a7e784bd3..000000000 --- a/test/snapshot.test.ts.snap +++ /dev/null @@ -1,17 +0,0 @@ -// Snapshot v1 - -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 deleted file mode 100644 index bef7e9603..000000000 --- a/test/softAssertions.test.ts +++ /dev/null @@ -1,484 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest' -import { $ } from '@wdio/globals' -import { expect as expectWdio, SoftAssertionService, SoftAssertService } from '../src/index.js' -import type { TestResult } from '@wdio/types/build/Frameworks' - -vi.mock('@wdio/globals') - -describe('Soft Assertions', () => { - // Setup a mock element for testing - let el: any - - beforeEach(async () => { - el = $('sel') - // We need to mock getText() which is what the toHaveText matcher actually calls - el.getText = vi.fn().mockImplementation(() => 'Actual Text') - // Clear any soft assertion failures before each test - expectWdio.clearSoftFailures() - }) - - describe('expect.soft', () => { - it('should not throw immediately on failure', async () => { - const softService = SoftAssertService.getInstance() - softService.setCurrentTest('test-1', 'test name', 'test file') - - await expectWdio.soft(el).toHaveText('Expected Text') - - // Verify the failure was recorded - const failures = expectWdio.getSoftFailures() - expect(failures.length).toBe(1) - expect(failures[0].matcherName).toBe('toHaveText') - expect(failures[0].error.message).toContain('text') - }) - - it('should support chained assertions with .not', async () => { - // Setup a test ID for this test - const softService = SoftAssertService.getInstance() - softService.setCurrentTest('test-2', 'test name', 'test file') - - // This should not throw even though it fails - await expectWdio.soft(el).not.toHaveText('Actual Text') - - // Verify the failure was recorded - const failures = expectWdio.getSoftFailures() - expect(failures.length).toBe(1) - expect(failures[0].matcherName).toBe('not.toHaveText') - }) - - it('should support multiple soft failures in the same test', async () => { - // Setup a test ID for this test - const softService = SoftAssertService.getInstance() - softService.setCurrentTest('test-3', 'test name', 'test file') - - // These should not throw even though they fail - await expectWdio.soft(el).toHaveText('First Expected') - await expectWdio.soft(el).toHaveText('Second Expected') - await expectWdio.soft(el).toHaveText('Third Expected') - - // Verify all failures were recorded - const failures = expectWdio.getSoftFailures() - expect(failures.length).toBe(3) - expect(failures[0].matcherName).toBe('toHaveText') - expect(failures[1].matcherName).toBe('toHaveText') - expect(failures[2].matcherName).toBe('toHaveText') - }) - - it('should allow passing assertions', async () => { - // Set up a test ID for this test - const softService = SoftAssertService.getInstance() - softService.setCurrentTest('test-4', 'test name', 'test file') - - // This should pass normally - await expectWdio.soft(el).toHaveText('Actual Text') - - // Verify no failures were recorded - const failures = expectWdio.getSoftFailures() - expect(failures.length).toBe(0) - }) - - it('assertSoftFailures should throw if failures exist', async () => { - // Setup a test ID for this test - const softService = SoftAssertService.getInstance() - softService.setCurrentTest('test-5', 'test name', 'test file') - - // Record a failure - await expectWdio.soft(el).toHaveText('Expected Text') - - // Should throw when asserting failures - await expect(() => expectWdio.assertSoftFailures()).toThrow(/1 soft assertion failure/) - }) - - it('clearSoftFailures should remove all failures', async () => { - // Setup a test ID for this test - const softService = SoftAssertService.getInstance() - softService.setCurrentTest('test-6', 'test name', 'test file') - - // Record failures - await expectWdio.soft(el).toHaveText('First Expected') - await expectWdio.soft(el).toHaveText('Second Expected') - - // Verify failures were recorded - expect(expectWdio.getSoftFailures().length).toBe(2) - - // Clear failures - expectWdio.clearSoftFailures() - - // Should be no failures now - expect(expectWdio.getSoftFailures().length).toBe(0) - }) - }) - - describe('SoftAssertService hooks', () => { - it('afterTest should throw if soft failures exist', () => { - const softService = SoftAssertService.getInstance() - softService.setCurrentTest('test-hooks-1', 'test hooks', 'test file') - - const error = new Error('Test failure') - softService.addFailure(error, 'toBeDisplayed') - - // Create mock test result object - const testResult = { passed: true, error: 'undefined' } as TestResult - - // Create a mock service - const service = new SoftAssertionService() - - // Call afterTest hook - this should update the result - service.afterTest({ - file: 'test file', parent: '', title: 'test hooks', - fullName: '', - ctx: undefined, - type: '', - fullTitle: '', - pending: false - }, null, testResult) - - // Verify the test result was updated - expect(testResult.passed).toBe(true) - expect(testResult.error).toBeDefined() - }) - }) - - describe('Different Matcher Types', () => { - beforeEach(async () => { - el = $('sel') - // Mock different methods for different matchers - el.getText = vi.fn().mockImplementation(() => 'Actual Text') - el.isDisplayed = vi.fn().mockImplementation(() => false) - el.getAttribute = vi.fn().mockImplementation(() => 'actual-class') - el.isClickable = vi.fn().mockImplementation(() => false) - expectWdio.clearSoftFailures() - }) - - it('should handle boolean matchers', async () => { - const softService = SoftAssertService.getInstance() - softService.setCurrentTest('boolean-test', 'boolean test', 'test file') - - // Test boolean matcher - await expectWdio.soft(el).toBeDisplayed() - await expectWdio.soft(el).toBeClickable() - - const failures = expectWdio.getSoftFailures() - expect(failures.length).toBe(2) - expect(failures[0].matcherName).toBe('toBeDisplayed') - expect(failures[1].matcherName).toBe('toBeClickable') - }) - - it('should handle attribute matchers with multiple parameters', async () => { - const softService = SoftAssertService.getInstance() - softService.setCurrentTest('attribute-test', 'attribute test', 'test file') - - await expectWdio.soft(el).toHaveAttribute('class', 'expected-class') - - const failures = expectWdio.getSoftFailures() - expect(failures.length).toBe(1) - expect(failures[0].matcherName).toBe('toHaveAttribute') - }) - - it('should handle matchers with options', async () => { - const softService = SoftAssertService.getInstance() - softService.setCurrentTest('options-test', 'options test', 'test file') - - await expectWdio.soft(el).toHaveText('Expected', { ignoreCase: true, wait: 1000 }) - - const failures = expectWdio.getSoftFailures() - expect(failures.length).toBe(1) - expect(failures[0].matcherName).toBe('toHaveText') - }) - }) - - describe('Test Isolation', () => { - it('should isolate failures between different test contexts', async () => { - const softService = SoftAssertService.getInstance() - - // Test 1 - softService.setCurrentTest('isolation-test-1', 'test 1', 'file1') - await expectWdio.soft(el).toHaveText('Expected Text 1') - expect(expectWdio.getSoftFailures().length).toBe(1) - - // Test 2 - should have separate failures - softService.setCurrentTest('isolation-test-2', 'test 2', 'file2') - await expectWdio.soft(el).toHaveText('Expected Text 2') - - // Test 2 should only see its own failure - expect(expectWdio.getSoftFailures('isolation-test-2').length).toBe(1) - expect(expectWdio.getSoftFailures('isolation-test-1').length).toBe(1) - - // Clear one test shouldn't affect the other - expectWdio.clearSoftFailures('isolation-test-1') - expect(expectWdio.getSoftFailures('isolation-test-1').length).toBe(0) - expect(expectWdio.getSoftFailures('isolation-test-2').length).toBe(1) - }) - - it('should handle calls without test context gracefully', async () => { - const softService = SoftAssertService.getInstance() - softService.clearCurrentTest() // No test context - - // Should throw immediately when no test context - await expect(async () => { - await expectWdio.soft(el).toHaveText('Expected Text') - }).rejects.toThrow() - }) - - it('should handle rapid concurrent soft assertions', async () => { - const softService = SoftAssertService.getInstance() - softService.setCurrentTest('concurrent-test', 'concurrent', 'test file') - - el.getText = vi.fn().mockImplementation(() => 'Actual Text') - el.isDisplayed = vi.fn().mockImplementation(() => false) - el.isClickable = vi.fn().mockImplementation(() => false) - - // Fire multiple assertions rapidly - const promises = [ - expectWdio.soft(el).toHaveText('Expected 1'), - expectWdio.soft(el).toBeDisplayed(), - expectWdio.soft(el).toBeClickable() - ] - - await Promise.all(promises) - - const failures = expectWdio.getSoftFailures() - - expect(failures.length).toBe(3) - - // Verify all different matchers were recorded - const matcherNames = failures.map(f => f.matcherName) - expect(matcherNames).toContain('toHaveText') - expect(matcherNames).toContain('toBeDisplayed') - expect(matcherNames).toContain('toBeClickable') - }) - }) - - describe('Edge Cases & Error Handling', () => { - it('should handle matcher that throws non-standard errors', async () => { - const softService = SoftAssertService.getInstance() - softService.setCurrentTest('error-test', 'error test', 'test file') - - // Mock a matcher that throws a unique error - const originalMethod = el.getText - el.getText = vi.fn().mockImplementation(() => { - throw new TypeError('Weird browser error') - }) - - await expectWdio.soft(el).toHaveText('Expected Text') - - const failures = expectWdio.getSoftFailures() - expect(failures.length).toBe(1) - expect(failures[0].error).toBeInstanceOf(Error) - expect(failures[0].error.message).toContain('Weird browser error') - - // Restore - el.getText = originalMethod - }) - - it('should handle very long error messages', async () => { - const softService = SoftAssertService.getInstance() - softService.setCurrentTest('long-error-test', 'long error', 'test file') - - const veryLongText = 'A'.repeat(10000) - await expectWdio.soft(el).toHaveText(veryLongText) - - const failures = expectWdio.getSoftFailures() - expect(failures.length).toBe(1) - expect(failures[0].error.message.length).toBeGreaterThan(0) - }) - - it('should handle null/undefined values gracefully', async () => { - const softService = SoftAssertService.getInstance() - softService.setCurrentTest('null-test', 'null test', 'test file') - - // Test with null/undefined values - await expectWdio.soft(el).toHaveText(null as any) - await expectWdio.soft(el).toHaveAttribute('class', undefined as any) - - const failures = expectWdio.getSoftFailures() - expect(failures.length).toBe(2) - }) - - it('should capture error location information', async () => { - const softService = SoftAssertService.getInstance() - softService.setCurrentTest('location-test', 'location test', 'test file') - - await expectWdio.soft(el).toHaveText('Expected Text') - - const failures = expectWdio.getSoftFailures() - expect(failures.length).toBe(1) - - // Should have location info (if implemented) - if (failures[0].location) { - expect(failures[0].location).toBeTruthy() - expect(typeof failures[0].location).toBe('string') - } - }) - - it('should handle maximum failure limits', async () => { - const softService = SoftAssertService.getInstance() - softService.setCurrentTest('limit-test', 'limit test', 'test file') - - // Generate many failures - const promises = [] - for (let i = 0; i < 150; i++) { - promises.push(expectWdio.soft(el).toHaveText(`Expected ${i}`)) - } - - await Promise.all(promises) - - const failures = expectWdio.getSoftFailures() - // Should either limit failures or handle large numbers gracefully - expect(failures.length).toBeGreaterThan(0) - expect(failures.length).toBeLessThanOrEqual(150) - }) - }) - - describe('SoftAssertionService Configuration', () => { - beforeEach(() => { - expectWdio.clearSoftFailures() - }) - - it('should auto-assert failures by default', () => { - const softService = SoftAssertService.getInstance() - - const testId = 'test file::config default' - softService.setCurrentTest(testId, 'config default', 'test file') - - const error = new Error('Test failure') - softService.addFailure(error, 'toBeDisplayed') - - const service = new SoftAssertionService() - - const testResult = { passed: true, error: undefined } as TestResult - - service.afterTest({ - file: 'test file', - parent: '', - title: 'config default', - fullName: '', - ctx: undefined, - type: '', - fullTitle: '', - pending: false - }, null, testResult) - - expect(testResult.passed).toBe(false) - expect(testResult.error).toBeDefined() - }) - - it('should not auto-assert when autoAssertOnTestEnd is false', () => { - const softService = SoftAssertService.getInstance() - - const testId = 'test file::config disabled' - softService.setCurrentTest(testId, 'config disabled', 'test file') - - const error = new Error('Test failure') - softService.addFailure(error, 'toBeDisplayed') - - const service = new SoftAssertionService({ autoAssertOnTestEnd: false }) - - // Create mock test result object - const testResult = { passed: true, error: undefined } as TestResult - - // Call afterTest hook - should NOT update the result because auto-assert is disabled - service.afterTest({ - file: 'test file', - parent: '', - title: 'config disabled', - fullName: '', - ctx: undefined, - type: '', - fullTitle: '', - pending: false - }, null, testResult) - - expect(testResult.passed).toBe(true) - expect(testResult.error).toBeUndefined() - - const failures = expectWdio.getSoftFailures(testId) - expect(failures.length).toBe(1) - }) - - it('should still auto-assert with explicit autoAssertOnTestEnd: true', () => { - const softService = SoftAssertService.getInstance() - - const testId = 'test file::config explicit' - softService.setCurrentTest(testId, 'config explicit', 'test file') - - const error = new Error('Test failure') - softService.addFailure(error, 'toBeDisplayed') - - const service = new SoftAssertionService({ autoAssertOnTestEnd: true }) - - const testResult = { passed: true, error: undefined } as TestResult - - service.afterTest({ - file: 'test file', - parent: '', - title: 'config explicit', - fullName: '', - ctx: undefined, - type: '', - fullTitle: '', - pending: false - }, null, testResult) - - expect(testResult.passed).toBe(false) - expect(testResult.error).toBeDefined() - }) - - it('should skip auto-assert if test already has an error', () => { - const softService = SoftAssertService.getInstance() - softService.setCurrentTest('config-existing-error-test', 'config existing error', 'test file') - - const error = new Error('Soft assertion failure') - softService.addFailure(error, 'toBeDisplayed') - - const service = new SoftAssertionService() - - const existingError = new Error('Pre-existing test error') - const testResult = { passed: false, error: existingError } as TestResult - - service.afterTest({ - file: 'test file', - parent: '', - title: 'config existing error', - fullName: '', - ctx: undefined, - type: '', - fullTitle: '', - pending: false - }, null, testResult) - - expect(testResult.passed).toBe(false) - expect(testResult.error).toBe(existingError) - expect(testResult.error?.message).toBe('Pre-existing test error') - }) - - it('should accept undefined options and use defaults', () => { - const service = new SoftAssertionService(undefined) - expect(service).toBeDefined() - - const softService = SoftAssertService.getInstance() - - const testId = 'test file::config undefined' - softService.setCurrentTest(testId, 'config undefined', 'test file') - - const error = new Error('Test failure') - softService.addFailure(error, 'toBeDisplayed') - - const testResult = { passed: true, error: undefined } as TestResult - - service.afterTest({ - file: 'test file', - parent: '', - title: 'config undefined', - fullName: '', - ctx: undefined, - type: '', - fullTitle: '', - pending: false - }, null, testResult) - - expect(testResult.passed).toBe(false) - expect(testResult.error).toBeDefined() - }) - }) -}) - diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 40e535524..9fe9b7314 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -7,53 +7,265 @@ type Scenario = import('@wdio/types').Frameworks.Scenario type SnapshotResult = import('@vitest/snapshot').SnapshotResult type SnapshotUpdateState = import('@vitest/snapshot').SnapshotUpdateState -declare namespace ExpectWebdriverIO { - const expect: ExpectWebdriverIO.Expect - function setOptions(options: DefaultOptions): void - // eslint-disable-next-line @typescript-eslint/no-explicit-any - function getConfig(): any +interface CustomMatchers extends Record{ + // ===== $ or $$ ===== + /** + * `WebdriverIO.Element` -> `isDisplayed` + */ + toBeDisplayed(options?: ExpectWebdriverIO.CommandOptions): Promise - interface SnapshotServiceArgs { - updateState?: SnapshotUpdateState - resolveSnapshotPath?: (path: string, extension: string) => string - } + /** + * `WebdriverIO.Element` -> `isExisting` + */ + toExist(options?: ExpectWebdriverIO.CommandOptions): Promise + /** + * `WebdriverIO.Element` -> `isExisting` + */ + toBePresent(options?: ExpectWebdriverIO.CommandOptions): Promise + /** + * `WebdriverIO.Element` -> `isExisting` + */ + toBeExisting(options?: ExpectWebdriverIO.CommandOptions): Promise - class SnapshotService { - static initiate(options: SnapshotServiceArgs): ServiceInstance & { - results: SnapshotResult[] - } - } + /** + * `WebdriverIO.Element` -> `getAttribute` + */ + toHaveAttribute( + attribute: string, + value?: string | RegExp | ExpectWebdriverIO.PartialMatcher, + options?: ExpectWebdriverIO.StringOptions + ): Promise + /** + * `WebdriverIO.Element` -> `getAttribute` + */ + toHaveAttr(attribute: string, value?: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): Promise - interface SoftFailure { - error: Error - matcherName: string - location?: string - } + /** + * `WebdriverIO.Element` -> `getAttribute` class + * @deprecated since v1.3.1 - use `toHaveElementClass` instead. + */ + toHaveClass(className: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): Promise - class SoftAssertService { - static getInstance(): SoftAssertService - setCurrentTest(testId: string, testName?: string, testFile?: string): void - clearCurrentTest(): void - getCurrentTestId(): string | null - addFailure(error: Error, matcherName: string): void - getFailures(testId?: string): SoftFailure[] - clearFailures(testId?: string): void - assertNoFailures(testId?: string): void - } + /** + * `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): Promise - interface SoftAssertionServiceOptions { - autoAssertOnTestEnd?: boolean - } + /** + * `WebdriverIO.Element` -> `getProperty` + */ + toHaveElementProperty( + property: string | RegExp | ExpectWebdriverIO.PartialMatcher, + value?: any, + options?: ExpectWebdriverIO.StringOptions + ): Promise - class SoftAssertionService implements ServiceInstance { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - constructor(serviceOptions?: SoftAssertionServiceOptions, capabilities?: any, config?: any) - beforeTest(test: Test): void - beforeStep(step: PickleStep, scenario: Scenario): void - // eslint-disable-next-line @typescript-eslint/no-explicit-any - afterTest(test: Test, context: any, result: TestResult): void - afterStep(step: PickleStep, scenario: Scenario, result: { passed: boolean, error?: Error }): void - } + /** + * `WebdriverIO.Element` -> `getProperty` value + */ + toHaveValue(value: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): Promise + + /** + * `WebdriverIO.Element` -> `isClickable` + */ + toBeClickable(options?: ExpectWebdriverIO.StringOptions): Promise + + /** + * `WebdriverIO.Element` -> `!isEnabled` + */ + toBeDisabled(options?: ExpectWebdriverIO.StringOptions): Promise + + /** + * `WebdriverIO.Element` -> `isDisplayedInViewport` + */ + toBeDisplayedInViewport(options?: ExpectWebdriverIO.StringOptions): Promise + + /** + * `WebdriverIO.Element` -> `isEnabled` + */ + toBeEnabled(options?: ExpectWebdriverIO.StringOptions): Promise + + /** + * `WebdriverIO.Element` -> `isFocused` + */ + toBeFocused(options?: ExpectWebdriverIO.StringOptions): Promise + + /** + * `WebdriverIO.Element` -> `isSelected` + */ + toBeSelected(options?: ExpectWebdriverIO.StringOptions): Promise + + /** + * `WebdriverIO.Element` -> `isSelected` + */ + toBeChecked(options?: ExpectWebdriverIO.StringOptions): Promise + + /** + * `WebdriverIO.Element` -> `$$('./*').length` + * supports less / greater then or equals to be passed in options + */ + toHaveChildren( + size?: number | ExpectWebdriverIO.NumberOptions, + options?: ExpectWebdriverIO.NumberOptions + ): Promise + + /** + * `WebdriverIO.Element` -> `getAttribute` href + */ + toHaveHref(href: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): Promise + /** + * `WebdriverIO.Element` -> `getAttribute` href + */ + toHaveLink(href: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): Promise + + /** + * `WebdriverIO.Element` -> `getProperty` value + */ + toHaveId(id: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): Promise + + /** + * `WebdriverIO.Element` -> `getSize` value + */ + toHaveSize(size: { height: number; width: number }, options?: ExpectWebdriverIO.StringOptions): 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( + text: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array ExpectWebdriverIO.PartialMatcher | ExpectWebdriverIO.PartialMatcher, + options?: ExpectWebdriverIO.StringOptions + ): Promise + + /** + * `WebdriverIO.Element` -> `getHTML` + * Element's html equals the html provided + */ + toHaveHTML(html: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, options?: ExpectWebdriverIO.HTMLOptions): Promise + + /** + * `WebdriverIO.Element` -> `getComputedLabel` + * Element's computed label equals the computed label provided + */ + toHaveComputedLabel( + computedLabel: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, + options?: ExpectWebdriverIO.StringOptions + ): Promise + + /** + * `WebdriverIO.Element` -> `getComputedRole` + * Element's computed role equals the computed role provided + */ + toHaveComputedRole( + computedRole: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, + options?: ExpectWebdriverIO.StringOptions + ): Promise + + /** + * `WebdriverIO.Element` -> `getSize('width')` + * Element's width equals the width provided + */ + toHaveWidth(width: number, options?: ExpectWebdriverIO.CommandOptions): Promise + + /** + * `WebdriverIO.Element` -> `getSize('height')` + * Element's height equals the height provided + */ + toHaveHeight(height: number, options?: ExpectWebdriverIO.CommandOptions): Promise + + /** + * `WebdriverIO.Element` -> `getSize()` + * Element's size equals the size provided + */ + toHaveHeight(size: { height: number; width: number }, options?: ExpectWebdriverIO.CommandOptions): Promise + + /** + * `WebdriverIO.Element` -> `getAttribute("style")` + */ + toHaveStyle(style: { [key: string]: string }, options?: ExpectWebdriverIO.StringOptions): Promise + + // ===== browser only ===== + /** + * `WebdriverIO.Browser` -> `getUrl` + */ + toHaveUrl(url: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): Promise + + /** + * `WebdriverIO.Browser` -> `getTitle` + */ + toHaveTitle(title: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): Promise + + /** + * `WebdriverIO.Browser` -> `execute` + */ + toHaveClipboardText(clipboardText: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): Promise + + // ===== $$ only ===== + /** + * `WebdriverIO.ElementArray` -> `$$('...').length` + * supports less / greater then or equals to be passed in options + */ + toBeElementsArrayOfSize( + size: number | ExpectWebdriverIO.NumberOptions, + options?: ExpectWebdriverIO.NumberOptions + ): Promise & Promise; + + // ==== network mock ==== + /** + * Check that `WebdriverIO.Mock` was called + */ + toBeRequested(options?: ExpectWebdriverIO.CommandOptions): Promise + /** + * Check that `WebdriverIO.Mock` was called N times + */ + toBeRequestedTimes( + times: number | ExpectWebdriverIO.NumberOptions, + options?: ExpectWebdriverIO.NumberOptions + ): Promise + /** + * 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 +} + +declare namespace ExpectWebdriverIO { + function setOptions(options: DefaultOptions): void + function getConfig(): any interface AssertionResult { pass: boolean @@ -194,293 +406,6 @@ 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) - method?: string | Array - statusCode?: number | Array - requestHeaders?: - | Record - | ExpectWebdriverIO.PartialMatcher - | ((headers: Record) => boolean) - responseHeaders?: - | Record - | ExpectWebdriverIO.PartialMatcher - | ((headers: Record) => boolean) - postData?: - | string - | ExpectWebdriverIO.JsonCompatible - | ExpectWebdriverIO.PartialMatcher - | ((r: string | undefined) => boolean) - response?: - | string - | ExpectWebdriverIO.JsonCompatible - | ExpectWebdriverIO.PartialMatcher - | ((r: string) => boolean) - } - type jsonPrimitive = string | number | boolean | null type jsonObject = { [x: string]: jsonPrimitive | jsonObject | jsonArray } type jsonArray = Array @@ -494,60 +419,9 @@ declare namespace ExpectWebdriverIO { 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 - */ - 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 - } } declare module 'expect-webdriverio' { - export = ExpectWebdriverIO + const matchers: CustomMatchers; + export = matchers; } 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 index 54fc1406e..b319ebe3d 100644 --- a/types/standalone.d.ts +++ b/types/standalone.d.ts @@ -1,34 +1,33 @@ /* 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 - } + // interface Matchers 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 + // /** + // * 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 - } + // interface AsymmetricMatchers { + // 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 + // } } From 7b69c77eb5a96c6c0a7a8a9b54761b041b528237 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sat, 21 Jun 2025 23:35:02 -0400 Subject: [PATCH 02/99] Block toHaveUrl on element --- jest.d.ts | 9 +++------ test-types/jest/types.test.ts | 19 +++++++++++++++++++ types/expect-webdriverio.d.ts | 5 +++-- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/jest.d.ts b/jest.d.ts index 57261aee9..89f3a640d 100644 --- a/jest.d.ts +++ b/jest.d.ts @@ -1,14 +1,11 @@ /// declare namespace jest { - // noinspection JSUnusedGlobalSymbols - interface Matchers extends CustomMatchers{} - // noinspection JSUnusedGlobalSymbols + interface Matchers extends CustomMatchers{} - interface Expect extends CustomMatchers {} - - // noinspection JSUnusedGlobalSymbols + // eslint-disable-next-line @typescript-eslint/no-explicit-any + interface Expect extends CustomMatchers {} interface InverseAsymmetricMatchers extends Expect {} } \ No newline at end of file diff --git a/test-types/jest/types.test.ts b/test-types/jest/types.test.ts index 0a0edba96..b390d7265 100644 --- a/test-types/jest/types.test.ts +++ b/test-types/jest/types.test.ts @@ -38,6 +38,11 @@ describe('type assertions', () => { // @ts-expect-error const expectNotToBeIsVoid: void = expect(element).not.toBeDisabled() }) + + it('toHaveUrl should not work on element', async () => { + // @ts-expect-error + await expect(element).toHaveUrl('https://example.com') + }) }) describe('boolean type assertions', () => { @@ -96,4 +101,18 @@ describe('type assertions', () => { const expectRejectsToBeIsVoid: void = expect(booleanPromise).rejects.toBe(true) }) }) + + describe('Wdio async toMatchSnapshot', () => { + const booleanPromise: Promise = Promise.resolve(true) + + it('should not have ts errors when typing to void', async () => { + const expectToBeIsPromise: Promise = expect($('.findme')).toMatchSnapshot() + }) + + it('should not have ts errors when typing to void', async () => { + //@ts-expect-error + const expectNotToBeVoid: void = expect($('.findme')).toMatchSnapshot() + }) + }) + }) diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 9fe9b7314..20bcfd136 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -7,7 +7,7 @@ type Scenario = import('@wdio/types').Frameworks.Scenario type SnapshotResult = import('@vitest/snapshot').SnapshotResult type SnapshotUpdateState = import('@vitest/snapshot').SnapshotUpdateState -interface CustomMatchers extends Record{ +interface CustomMatchers extends Record{ // ===== $ or $$ ===== /** * `WebdriverIO.Element` -> `isDisplayed` @@ -216,7 +216,7 @@ interface CustomMatchers extends Record{ /** * `WebdriverIO.Browser` -> `getUrl` */ - toHaveUrl(url: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): Promise + toHaveUrl: T extends WebdriverIO.Browser ? (url: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions) => Promise: never; /** * `WebdriverIO.Browser` -> `getTitle` @@ -250,6 +250,7 @@ interface CustomMatchers extends Record{ times: number | ExpectWebdriverIO.NumberOptions, options?: ExpectWebdriverIO.NumberOptions ): Promise + /** * snapshot matcher * @param label optional snapshot label From 7b857311f68b55cb15ac836381bde90a9e0d4cbe Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sun, 22 Jun 2025 10:50:44 -0400 Subject: [PATCH 03/99] Working overloaded snapshot in jest --- jest.d.ts | 35 +- package-lock.json | 1170 +------------------------- package.json | 2 +- test-types/jest/tsconfig.json | 1 + test-types/jest/types-jest.test.ts | 155 ++++ test-types/jest/types.test.ts | 118 --- test-types/mocha/tsconfig.json | 15 + test-types/mocha/types-mocha.test.ts | 155 ++++ types/expect-webdriverio.d.ts | 10 +- types/global.d.ts | 8 + types/standalone.d.ts | 12 +- 11 files changed, 416 insertions(+), 1265 deletions(-) create mode 100644 test-types/jest/types-jest.test.ts delete mode 100644 test-types/jest/types.test.ts create mode 100644 test-types/mocha/tsconfig.json create mode 100644 test-types/mocha/types-mocha.test.ts create mode 100644 types/global.d.ts diff --git a/jest.d.ts b/jest.d.ts index 89f3a640d..749315782 100644 --- a/jest.d.ts +++ b/jest.d.ts @@ -1,11 +1,42 @@ /// +/*/// */ + +type ChainablePromiseElement = ReturnType +type WdioElementLike = WebdriverIO.Element | ChainablePromiseElement declare namespace jest { - interface Matchers extends CustomMatchers{} + interface Matchers extends WdioMatchers{ + + /** + * Below are overloaded Jest's matchers not part of `expect` but of `jest-snapshot`. + * We need to define them below so that they are correctly typed overloaded + * @see https://github.com/jestjs/jest/blob/73dbef5d2d3195a1e55fb254c54cce70d3036252/packages/jest-snapshot/src/types.ts#L37 + */ + + /** + * snapshot matcher + * @param label optional snapshot label + */ + toMatchSnapshot(label?: string): Promise; + + // TODO - this is not working as expected, need to investigate + /** + * snapshot matcher + * @param label optional snapshot label + */ + // toMatchSnapshot: T extends WdioElementLike ? (label: string) => Promise : (hint?: string) => R; + + /** + * inline snapshot matcher + * @param snapshot snapshot string (autogenerated if not specified) + * @param label optional snapshot label + */ + toMatchInlineSnapshot(snapshot?: string, label?: string): Promise + } // eslint-disable-next-line @typescript-eslint/no-explicit-any - interface Expect extends CustomMatchers {} + interface Expect extends WdioMatchers {} interface InverseAsymmetricMatchers extends Expect {} } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2c1797d73..391370a59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,10 +15,10 @@ "lodash.isequal": "^4.5.0" }, "devDependencies": { - "@jest/expect": "^30.0.0", "@types/debug": "^4.1.12", "@types/jest": "^30.0.0", "@types/lodash.isequal": "^4.5.8", + "@types/mocha": "^10.0.10", "@types/node": "^24.0.3", "@vitest/coverage-v8": "^3.2.4", "@wdio/eslint": "^0.1.1", @@ -82,143 +82,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/compat-data": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz", - "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", - "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.4", - "@babel/parser": "^7.27.4", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.27.4", - "@babel/types": "^7.27.3", - "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, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", - "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.27.5", - "@babel/types": "^7.27.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "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, - "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, - "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, - "bin": { - "semver": "bin/semver.js" - } - }, - "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, - "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, - "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, - "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", @@ -237,28 +100,6 @@ "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, - "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, - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/parser": { "version": "7.27.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", @@ -274,269 +115,6 @@ "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, - "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, - "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, - "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, - "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, - "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, - "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, - "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, - "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, - "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, - "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, - "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, - "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, - "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, - "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, - "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, - "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, - "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, - "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.27.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", - "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", - "@babel/parser": "^7.27.4", - "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { "version": "7.27.6", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", @@ -1616,111 +1194,6 @@ "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, - "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, - "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, - "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, - "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, - "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, - "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, - "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, - "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 - }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -1739,19 +1212,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/expect": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.2.tgz", - "integrity": "sha512-blWRFPjv2cVfh42nLG6L3xIEbw+bnuiZYZDl/BZlsNG/i3wKV6FpPZ2EPHguk7t5QpLaouIu+7JmYO4uBR6AOg==", - "dev": true, - "dependencies": { - "expect": "30.0.2", - "jest-snapshot": "30.0.2" - }, - "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", @@ -1797,109 +1257,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/snapshot-utils": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.1.tgz", - "integrity": "sha512-6Dpv7vdtoRiISEFwYF8/c7LIvqXD7xDXtLPNzC2xqAfBznKip0MQM+rkseKwUPUpv2PJ7KW/YsnwWXrIL2xF+A==", - "dev": true, - "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, - "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, - "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.2", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.2.tgz", - "integrity": "sha512-kJIuhLMTxRF7sc0gPzPtCDib/V9KwW3I2U25b+lYCYMVqHHSrcZopS8J8H+znx9yixuFv+Iozl8raLt/4MoxrA==", - "dev": true, - "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, - "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, - "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", @@ -2286,18 +1643,6 @@ "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, - "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", @@ -2820,6 +2165,12 @@ "@types/lodash": "*" } }, + "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 + }, "node_modules/@types/ms": { "version": "0.7.34", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", @@ -3131,12 +2482,6 @@ "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 - }, "node_modules/@vitest/coverage-v8": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", @@ -3943,19 +3288,6 @@ "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, - "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", @@ -4108,133 +3440,34 @@ }, "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/async-retry": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", - "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "retry": "0.13.1" - } - }, - "node_modules/b4a": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", - "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", - "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, - "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, - "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, - "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, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" }, - "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==", + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", "dev": true, + "license": "MIT", "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" + "retry": "0.13.1" } }, - "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, - "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/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "devOptional": true, + "license": "Apache-2.0" }, "node_modules/balanced-match": { "version": "1.0.2", @@ -4419,15 +3652,6 @@ "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, - "dependencies": { - "node-int64": "^0.4.0" - } - }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -4574,15 +3798,6 @@ "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, - "engines": { - "node": ">=6" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001707", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", @@ -6355,15 +5570,6 @@ "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, - "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", @@ -6498,12 +5704,6 @@ "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 - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -6569,15 +5769,6 @@ "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, - "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", @@ -6601,15 +5792,6 @@ "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, - "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", @@ -7005,17 +6187,6 @@ "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, - "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", @@ -7275,22 +6446,6 @@ "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, - "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", @@ -7445,30 +6600,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-haste-map": { - "version": "30.0.2", - "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, - "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.2", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.2.tgz", @@ -7663,113 +6794,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-snapshot": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.2.tgz", - "integrity": "sha512-KeoHikoKGln3OlN7NS7raJ244nIVr2K46fBTNdfuxqYv2/g4TVyWDSO4fmk08YBJQMjs3HNfG1rlLfL/KA+nUw==", - "dev": true, - "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.2", - "@jest/get-type": "30.0.1", - "@jest/snapshot-utils": "30.0.1", - "@jest/transform": "30.0.2", - "@jest/types": "30.0.1", - "babel-preset-current-node-syntax": "^1.1.0", - "chalk": "^4.1.2", - "expect": "30.0.2", - "graceful-fs": "^4.2.11", - "jest-diff": "30.0.2", - "jest-matcher-utils": "30.0.2", - "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, - "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.35", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.35.tgz", - "integrity": "sha512-C6ypdODf2VZkgRT6sFM8E1F8vR+HcffniX0Kp8MsU8PIfrlXbNCBz0jzj17GjdmjTx1OtZzdH8+iALL21UjF5A==", - "dev": true - }, - "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, - "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, - "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, - "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, - "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", @@ -7826,37 +6850,6 @@ "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, - "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, - "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", @@ -7935,18 +6928,6 @@ "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, - "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", @@ -8314,15 +7295,6 @@ "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, - "dependencies": { - "tmpl": "1.0.5" - } - }, "node_modules/memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", @@ -8578,12 +7550,6 @@ "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 - }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -8916,15 +7882,6 @@ "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, - "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", @@ -9058,15 +8015,6 @@ "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, - "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", @@ -9164,15 +8112,6 @@ "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, - "engines": { - "node": ">= 6" - } - }, "node_modules/pkg-types": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz", @@ -10362,21 +9301,6 @@ "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, - "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", @@ -10594,12 +9518,6 @@ "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 - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -11103,15 +10021,6 @@ "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, - "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", @@ -12001,19 +10910,6 @@ "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, - "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", @@ -12045,12 +10941,6 @@ "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 - }, "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 d0acc33d8..315719294 100644 --- a/package.json +++ b/package.json @@ -65,9 +65,9 @@ }, "devDependencies": { "@types/debug": "^4.1.12", - "@jest/expect": "^30.0.0", "@types/jest": "^30.0.0", "@types/lodash.isequal": "^4.5.8", + "@types/mocha": "^10.0.10", "@types/node": "^24.0.3", "@vitest/coverage-v8": "^3.2.4", "@wdio/eslint": "^0.1.1", diff --git a/test-types/jest/tsconfig.json b/test-types/jest/tsconfig.json index 5a6adaf9a..2dc67605a 100644 --- a/test-types/jest/tsconfig.json +++ b/test-types/jest/tsconfig.json @@ -7,6 +7,7 @@ "skipLibCheck": true, "types": [ "@types/jest", + "../../jest.d.ts", // Needed to be after @types/jest to override the toMatchSnapshot typing "@wdio/globals/types" ] } diff --git a/test-types/jest/types-jest.test.ts b/test-types/jest/types-jest.test.ts new file mode 100644 index 000000000..ccfe324b8 --- /dev/null +++ b/test-types/jest/types-jest.test.ts @@ -0,0 +1,155 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +describe('type assertions', () => { + + describe('toHaveUrl', () => { + const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser + it('should not have ts errors and be able to await the promise when actual is browser', async () => { + const expectPromiseVoid: Promise = expect(browser).toHaveUrl('https://example.com') + await expectPromiseVoid + + const expectNotPromiseVoid: Promise = expect(browser).not.toHaveUrl('https://example.com') + await expectNotPromiseVoid + }) + + it('should have ts errors and not need to await the promise when actual is browser', async () => { + // @ts-expect-error + const expectVoid: void = expect(browser).toHaveUrl('https://example.com') + // @ts-expect-error + const expectNotVoid: void = expect(browser).not.toHaveUrl('https://example.com') + }) + + it('should have ts errors when actual is an element', async () => { + const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element + // @ts-expect-error + await expect(element).toHaveUrl('https://example.com') + }) + + it('should have ts errors when actual is an ChainableElement', async () => { + const chainableElement = $('findMe') + // @ts-expect-error + await expect(chainableElement).toHaveUrl('https://example.com') + }) + }) + + describe('element type assertions', () => { + const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element + const chainableElement = $('findMe') + + describe('toBeDisabled', () => { + it('should not have ts errors and be able to await the promise for element', async () => { + // expect no ts errors + const expectIsPromiseVoid: Promise = expect(element).toBeDisabled() + await expectIsPromiseVoid + + const expectNotIsPromiseVoid: Promise = expect(element).not.toBeDisabled() + await expectNotIsPromiseVoid + }) + + it('should not have ts errors and be able to await the promise for chainable', async () => { + // expect no ts errors + const expectIsPromiseVoid: Promise = expect(chainableElement).toBeDisabled() + await expectIsPromiseVoid + + const expectNotIsPromiseVoid: Promise = expect(chainableElement).not.toBeDisabled() + await expectNotIsPromiseVoid + }) + + it('should have ts errors when typing to void for element', async () => { + // @ts-expect-error + const expectToBeIsVoid: void = expect(element).toBeDisabled() + // @ts-expect-error + const expectNotToBeIsVoid: void = expect(element).not.toBeDisabled() + }) + + it('should have ts errors when typing to void for chainable', async () => { + // @ts-expect-error + const expectToBeIsVoid: void = expect(chainableElement).toBeDisabled() + // @ts-expect-error + const expectNotToBeIsVoid: void = expect(chainableElement).not.toBeDisabled() + }) + }) + + describe('toMatchSnapshot', () => { + const booleanPromise: Promise = Promise.resolve(true) + + it('should not have ts errors when typing to Promise for an element', async () => { + const expectPromise1: Promise = expect(element).toMatchSnapshot() + const expectPromise2: Promise = expect(element).toMatchSnapshot('test label') + }) + + it('should not have ts errors when typing to Promise for a chainable', async () => { + const expectPromise1: Promise = expect(chainableElement).toMatchSnapshot() + const expectPromise2: Promise = expect(chainableElement).toMatchSnapshot('test label') + }) + + // We need somehow to exclude the Jest types one for this to success + it('should have ts errors when typing to void for an element like', async () => { + //@ts-expect-error + const expectNotToBeVoid1: void = expect(element).toMatchSnapshot() + //@ts-expect-error + const expectNotToBeVoid2: void = expect(chainableElement).toMatchSnapshot() + }) + + // TODO - conditional types check on T to have the below match void does not work + // it('should not have ts errors when typing to void for a string', async () => { + // const expectNotToBeVoid: void = expect('.findme').toMatchSnapshot() + // }) + }) + }) + + describe('boolean type assertions', () => { + it('should not have ts errors when typing to void', async () => { + // Expect no ts errors + const expectToBeIsVoid: void = expect(true).toBe(true) + const expectNotToBeIsVoid: void = expect(true).not.toBe(true) + }) + + it('should have ts errors when typing to Promise', async () => { + //@ts-expect-error + const expectToBeIsNotPromiseVoid: Promise = expect(true).toBe(true) + //@ts-expect-error + const expectToBeIsNotPromiseVoid: Promise = expect(true).not.toBe(true) + }) + }) + + describe('string type assertions', () => { + it('should not have ts errors when typing to void', async () => { + // Expect no ts errors + const expectToBeIsVoid: void = expect('test').toBe('test') + }) + + it('should have ts errors when typing to Promise', async () => { + //@ts-expect-error + const expectToBeIsNotPromiseVoid: Promise = expect('test').toBe('test') + }) + }) + + describe('Promise<> type assertions', () => { + const booleanPromise: Promise = Promise.resolve(true) + + it('should not have ts errors when typing to void', async () => { + const expectToBeIsVoid: void = expect(booleanPromise).toBe(true) + const expectAwaitToBeIsVoid: void = expect(await booleanPromise).toBe(true) + }) + + it('should not have ts errors when resolves and rejects is typed to Promise', async () => { + // TODO the below needs to return Promise but currently returns void + const expectResolvesToBeIsVoid: Promise = expect(booleanPromise).resolves.toBe(true) + const expectRejectsToBeIsVoid: Promise = expect(booleanPromise).rejects.toBe(true) + }) + + it('should have ts errors when typing to Promise', async () => { + //@ts-expect-error + const expectToBeIsNotPromiseVoid: Promise = expect(booleanPromise).toBe(true) + //@ts-expect-error + const expectToBeIsNotPromiseVoid: Promise = expect(await booleanPromise).toBe(true) + }) + + it('should have ts errors when typing resolves and reject is typed to void', async () => { + //@ts-expect-error + const expectResolvesToBeIsVoid: void = expect(booleanPromise).resolves.toBe(true) + //@ts-expect-error + const expectRejectsToBeIsVoid: void = expect(booleanPromise).rejects.toBe(true) + }) + }) +}) diff --git a/test-types/jest/types.test.ts b/test-types/jest/types.test.ts deleted file mode 100644 index b390d7265..000000000 --- a/test-types/jest/types.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/// - -describe('type assertions', () => { - - describe('browser type assertions', () => { - const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser - it('should not have ts errors and be able to await the promise', async () => { - const browserExpectHaveUrlIsPromiseVoid: Promise = expect(browser).toHaveUrl('https://example.com') - await browserExpectHaveUrlIsPromiseVoid - - const browserExpectNotHaveUrlIsPromiseVoid: Promise = expect(browser).not.toHaveUrl('https://example.com') - await browserExpectNotHaveUrlIsPromiseVoid - }) - - it('should have ts errors and not need to await the promise', async () => { - // @ts-expect-error - const browserExpectHaveUrlIsVoid: void = expect(browser).toHaveUrl('https://example.com') - // @ts-expect-error - const browserExpectNotHaveUrlIsVoid: void = expect(browser).not.toHaveUrl('https://example.com') - }) - }) - - describe('element type assertions', () => { - const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element - it('should not have ts errors and be able to await the promise', async () => { - // expect no ts errors - const expectIsPromiseVoid: Promise = expect(element).toBeDisabled() - await expectIsPromiseVoid - - const expectNotIsPromiseVoid: Promise = expect(element).not.toBeDisabled() - await expectNotIsPromiseVoid - }) - - it('should have ts errors when typing to void', async () => { - // @ts-expect-error - const expectToBeIsVoid: void = expect(element).toBeDisabled() - // @ts-expect-error - const expectNotToBeIsVoid: void = expect(element).not.toBeDisabled() - }) - - it('toHaveUrl should not work on element', async () => { - // @ts-expect-error - await expect(element).toHaveUrl('https://example.com') - }) - }) - - describe('boolean type assertions', () => { - it('should not have ts errors when typing to void', async () => { - // Expect no ts errors - const expectToBeIsVoid: void = expect(true).toBe(true) - const expectNotToBeIsVoid: void = expect(true).not.toBe(true) - }) - - it('should have ts errors when typing to Promise', async () => { - //@ts-expect-error - const expectToBeIsNotPromiseVoid: Promise = expect(true).toBe(true) - //@ts-expect-error - const expectToBeIsNotPromiseVoid: Promise = expect(true).not.toBe(true) - }) - }) - - describe('string type assertions', () => { - it('should not have ts errors when typing to void', async () => { - // Expect no ts errors - const expectToBeIsVoid: void = expect('test').toBe('test') - }) - - it('should have ts errors when typing to Promise', async () => { - //@ts-expect-error - const expectToBeIsNotPromiseVoid: Promise = expect('test').toBe('test') - }) - }) - - describe('Promise type assertions', () => { - const booleanPromise: Promise = Promise.resolve(true) - - it('should not have ts errors when typing to void', async () => { - const expectToBeIsVoid: void = expect(booleanPromise).toBe(true) - const expectAwaitToBeIsVoid: void = expect(await booleanPromise).toBe(true) - }) - - it('should not have ts errors when resolves and rejects is typed to Promise', async () => { - // TODO the below needs to return Promise but currently returns void - const expectResolvesToBeIsVoid: Promise = expect(booleanPromise).resolves.toBe(true) - const expectRejectsToBeIsVoid: Promise = expect(booleanPromise).rejects.toBe(true) - }) - - it('should have ts errors when typing to Promise', async () => { - //@ts-expect-error - const expectToBeIsNotPromiseVoid: Promise = expect(booleanPromise).toBe(true) - //@ts-expect-error - const expectToBeIsNotPromiseVoid: Promise = expect(await booleanPromise).toBe(true) - }) - - it('should have ts errors when typing resolves and reject is typed to void', async () => { - // TODO the below needs to return Promise but currently returns void - //@ts-expect-error - const expectResolvesToBeIsVoid: void = expect(booleanPromise).resolves.toBe(true) - //@ts-expect-error - const expectRejectsToBeIsVoid: void = expect(booleanPromise).rejects.toBe(true) - }) - }) - - describe('Wdio async toMatchSnapshot', () => { - const booleanPromise: Promise = Promise.resolve(true) - - it('should not have ts errors when typing to void', async () => { - const expectToBeIsPromise: Promise = expect($('.findme')).toMatchSnapshot() - }) - - it('should not have ts errors when typing to void', async () => { - //@ts-expect-error - const expectNotToBeVoid: void = expect($('.findme')).toMatchSnapshot() - }) - }) - -}) diff --git a/test-types/mocha/tsconfig.json b/test-types/mocha/tsconfig.json new file mode 100644 index 000000000..665569493 --- /dev/null +++ b/test-types/mocha/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "outDir": "dist", + "noImplicitAny": true, + "target": "ES2020", + "module": "Node16", + "skipLibCheck": true, + "types": [ + "@types/expect", + "@types/mocha", + "../../types/global.d.ts", + "@wdio/globals/types", + ] + } +} diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts new file mode 100644 index 000000000..ccfe324b8 --- /dev/null +++ b/test-types/mocha/types-mocha.test.ts @@ -0,0 +1,155 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +describe('type assertions', () => { + + describe('toHaveUrl', () => { + const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser + it('should not have ts errors and be able to await the promise when actual is browser', async () => { + const expectPromiseVoid: Promise = expect(browser).toHaveUrl('https://example.com') + await expectPromiseVoid + + const expectNotPromiseVoid: Promise = expect(browser).not.toHaveUrl('https://example.com') + await expectNotPromiseVoid + }) + + it('should have ts errors and not need to await the promise when actual is browser', async () => { + // @ts-expect-error + const expectVoid: void = expect(browser).toHaveUrl('https://example.com') + // @ts-expect-error + const expectNotVoid: void = expect(browser).not.toHaveUrl('https://example.com') + }) + + it('should have ts errors when actual is an element', async () => { + const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element + // @ts-expect-error + await expect(element).toHaveUrl('https://example.com') + }) + + it('should have ts errors when actual is an ChainableElement', async () => { + const chainableElement = $('findMe') + // @ts-expect-error + await expect(chainableElement).toHaveUrl('https://example.com') + }) + }) + + describe('element type assertions', () => { + const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element + const chainableElement = $('findMe') + + describe('toBeDisabled', () => { + it('should not have ts errors and be able to await the promise for element', async () => { + // expect no ts errors + const expectIsPromiseVoid: Promise = expect(element).toBeDisabled() + await expectIsPromiseVoid + + const expectNotIsPromiseVoid: Promise = expect(element).not.toBeDisabled() + await expectNotIsPromiseVoid + }) + + it('should not have ts errors and be able to await the promise for chainable', async () => { + // expect no ts errors + const expectIsPromiseVoid: Promise = expect(chainableElement).toBeDisabled() + await expectIsPromiseVoid + + const expectNotIsPromiseVoid: Promise = expect(chainableElement).not.toBeDisabled() + await expectNotIsPromiseVoid + }) + + it('should have ts errors when typing to void for element', async () => { + // @ts-expect-error + const expectToBeIsVoid: void = expect(element).toBeDisabled() + // @ts-expect-error + const expectNotToBeIsVoid: void = expect(element).not.toBeDisabled() + }) + + it('should have ts errors when typing to void for chainable', async () => { + // @ts-expect-error + const expectToBeIsVoid: void = expect(chainableElement).toBeDisabled() + // @ts-expect-error + const expectNotToBeIsVoid: void = expect(chainableElement).not.toBeDisabled() + }) + }) + + describe('toMatchSnapshot', () => { + const booleanPromise: Promise = Promise.resolve(true) + + it('should not have ts errors when typing to Promise for an element', async () => { + const expectPromise1: Promise = expect(element).toMatchSnapshot() + const expectPromise2: Promise = expect(element).toMatchSnapshot('test label') + }) + + it('should not have ts errors when typing to Promise for a chainable', async () => { + const expectPromise1: Promise = expect(chainableElement).toMatchSnapshot() + const expectPromise2: Promise = expect(chainableElement).toMatchSnapshot('test label') + }) + + // We need somehow to exclude the Jest types one for this to success + it('should have ts errors when typing to void for an element like', async () => { + //@ts-expect-error + const expectNotToBeVoid1: void = expect(element).toMatchSnapshot() + //@ts-expect-error + const expectNotToBeVoid2: void = expect(chainableElement).toMatchSnapshot() + }) + + // TODO - conditional types check on T to have the below match void does not work + // it('should not have ts errors when typing to void for a string', async () => { + // const expectNotToBeVoid: void = expect('.findme').toMatchSnapshot() + // }) + }) + }) + + describe('boolean type assertions', () => { + it('should not have ts errors when typing to void', async () => { + // Expect no ts errors + const expectToBeIsVoid: void = expect(true).toBe(true) + const expectNotToBeIsVoid: void = expect(true).not.toBe(true) + }) + + it('should have ts errors when typing to Promise', async () => { + //@ts-expect-error + const expectToBeIsNotPromiseVoid: Promise = expect(true).toBe(true) + //@ts-expect-error + const expectToBeIsNotPromiseVoid: Promise = expect(true).not.toBe(true) + }) + }) + + describe('string type assertions', () => { + it('should not have ts errors when typing to void', async () => { + // Expect no ts errors + const expectToBeIsVoid: void = expect('test').toBe('test') + }) + + it('should have ts errors when typing to Promise', async () => { + //@ts-expect-error + const expectToBeIsNotPromiseVoid: Promise = expect('test').toBe('test') + }) + }) + + describe('Promise<> type assertions', () => { + const booleanPromise: Promise = Promise.resolve(true) + + it('should not have ts errors when typing to void', async () => { + const expectToBeIsVoid: void = expect(booleanPromise).toBe(true) + const expectAwaitToBeIsVoid: void = expect(await booleanPromise).toBe(true) + }) + + it('should not have ts errors when resolves and rejects is typed to Promise', async () => { + // TODO the below needs to return Promise but currently returns void + const expectResolvesToBeIsVoid: Promise = expect(booleanPromise).resolves.toBe(true) + const expectRejectsToBeIsVoid: Promise = expect(booleanPromise).rejects.toBe(true) + }) + + it('should have ts errors when typing to Promise', async () => { + //@ts-expect-error + const expectToBeIsNotPromiseVoid: Promise = expect(booleanPromise).toBe(true) + //@ts-expect-error + const expectToBeIsNotPromiseVoid: Promise = expect(await booleanPromise).toBe(true) + }) + + it('should have ts errors when typing resolves and reject is typed to void', async () => { + //@ts-expect-error + const expectResolvesToBeIsVoid: void = expect(booleanPromise).resolves.toBe(true) + //@ts-expect-error + const expectRejectsToBeIsVoid: void = expect(booleanPromise).rejects.toBe(true) + }) + }) +}) diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 20bcfd136..ec69a6db8 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -250,7 +250,13 @@ interface CustomMatchers extends Record{ times: number | ExpectWebdriverIO.NumberOptions, options?: ExpectWebdriverIO.NumberOptions ): Promise +} +/** + * Those need to be also duplicated in jest.d.ts in order for the typing to correctly overload the matchers (we cannot just extend the Matchers interface) + * @see + */ +interface OverloadedMatchers { /** * snapshot matcher * @param label optional snapshot label @@ -261,9 +267,11 @@ interface CustomMatchers extends Record{ * @param snapshot snapshot string (autogenerated if not specified) * @param label optional snapshot label */ - toMatchInlineSnapshot(snapshot?: string, label?: string): Promise + toMatchInlineSnapshot(snapshot?: string, label?: string): Promise } +interface WdioMatchers extends CustomMatchers, OverloadedMatchers {} + declare namespace ExpectWebdriverIO { function setOptions(options: DefaultOptions): void function getConfig(): any diff --git a/types/global.d.ts b/types/global.d.ts new file mode 100644 index 000000000..811c97a1f --- /dev/null +++ b/types/global.d.ts @@ -0,0 +1,8 @@ +/// +/// + +declare namespace NodeJS { + interface Global { + expect: WdioExpect + } +} \ No newline at end of file diff --git a/types/standalone.d.ts b/types/standalone.d.ts index b319ebe3d..93f3b96bd 100644 --- a/types/standalone.d.ts +++ b/types/standalone.d.ts @@ -1,10 +1,16 @@ /* eslint-disable @typescript-eslint/consistent-type-imports*/ /// -type ChainablePromiseElement = import('webdriverio').ChainablePromiseElement -type ChainablePromiseArray = import('webdriverio').ChainablePromiseArray - declare namespace ExpectWebdriverIO { + + interface Matchers extends WdioMatchers{} + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + interface Expect extends WdioMatchers {} + + interface InverseAsymmetricMatchers extends Expect {} + + // interface Matchers extends Readonly> { // not: Matchers // resolves: Matchers From 07f43ae8de02009540082e4b6c7002563d3acce2 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sun, 22 Jun 2025 13:27:07 -0400 Subject: [PATCH 04/99] A good working case with standalone + add back snapshot --- src/index.ts | 5 + src/matchers.ts | 3 +- src/matchers/snapshot.ts | 182 +++++++++++++++++++++++++++ src/snapshot.ts | 133 ++++++++++++++++++++ test-types/mocha/tsconfig.json | 5 +- test-types/mocha/types-mocha.test.ts | 46 +++---- test/index.test.ts | 2 +- test/matchers.test.ts | 6 +- types/global.d.ts | 6 +- types/standalone.d.ts | 54 ++++---- 10 files changed, 383 insertions(+), 59 deletions(-) create mode 100644 src/matchers/snapshot.ts create mode 100644 src/snapshot.ts diff --git a/src/index.ts b/src/index.ts index 9c8689db4..2c74b7265 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,6 +40,11 @@ export const setDefaultOptions = (options = {}): void => { } export const setOptions = setDefaultOptions +/** + * export snapshot utilities + */ +export { SnapshotService } from './snapshot.js' + /** * export utils */ diff --git a/src/matchers.ts b/src/matchers.ts index 9bd6d4f4c..4f3176730 100644 --- a/src/matchers.ts +++ b/src/matchers.ts @@ -26,4 +26,5 @@ export * from './matchers/element/toHaveValue.js' export * from './matchers/element/toHaveWidth.js' export * from './matchers/elements/toBeElementsArrayOfSize.js' export * from './matchers/mock/toBeRequested.js' -export * from './matchers/mock/toBeRequestedTimes.js' \ No newline at end of file +export * from './matchers/mock/toBeRequestedTimes.js' +export * from './matchers/snapshot.js' diff --git a/src/matchers/snapshot.ts b/src/matchers/snapshot.ts new file mode 100644 index 000000000..daf36868a --- /dev/null +++ b/src/matchers/snapshot.ts @@ -0,0 +1,182 @@ +import path from 'node:path' +import type { AssertionError } from 'node:assert' + +import { expect } from 'expect' +import { stripSnapshotIndentation } from '@vitest/snapshot' +import { SnapshotService } from '../snapshot' + +interface InlineSnapshotOptions { + inlineSnapshot: string + error: Error +} + +/** + * Vitest snapshot client returns a snapshot error with an `actual` and `expected` + * property containing strings of the compared snapshots. In case these don't match + * we use this helper method to return a proper assertion message that contains + * nice color highlighting etc. For that we just re-assert the two strings. + * @param snapshotError error message from snapshot client + * @returns matcher result + */ +function returnSnapshotError (snapshotError: AssertionError) { + /** + * wrap into another try catch block so we can get a better + * assertion message + */ + try { + expect(snapshotError.actual).toBe(snapshotError.expected) + } catch (e) { + return { + pass: false, + message: () => (e as Error).message + } + } + + /** + * this should never happen but in case it does we want to + */ + throw snapshotError +} + +/** + * Helper method to assert snapshots + * @param received element to snapshot + * @param message optional message on failure + * @returns matcher results + */ +function toMatchSnapshotAssert (received: unknown, message: string, inlineOptions?: InlineSnapshotOptions) { + const snapshotService = SnapshotService.initiate() + try { + snapshotService.client.assert({ + received, + message, + filepath: snapshotService.currentFilePath as string, + name: snapshotService.currentTestName as string, + /** + * apply inline options if needed + */ + ...(inlineOptions ? { + ...inlineOptions, + isInline: true + } : { + isInline: false + }) + }) + return { + pass: true, + message: () => 'Snapshot matches' + } + } catch (e: unknown) { + return returnSnapshotError(e as AssertionError) + } +} + +/** + * Asynchronous version of `toMatchSnapshot` that works with WebdriverIO elements. + * @param elem a WebdriverIO element + * @param message optional message on failure + * @returns matcher results + */ +async function toMatchSnapshotAsync (asyncReceived: unknown, message: string, inlineOptions?: InlineSnapshotOptions) { + let received: WebdriverIO.Element | unknown = await asyncReceived + + if (received && typeof received === 'object' && 'elementId' in received) { + received = await (received as WebdriverIO.Element).getHTML({ + includeSelectorTag: true + }) + } + return toMatchSnapshotAssert(received, message, inlineOptions) +} + +/** + * We want to keep this method synchronous so that doing snapshots for basic + * elements doesn't require an `await` and matches other framework behavior. + * @param received element to snapshot + * @param message optional message on failure + * @returns matcher results + */ +function toMatchSnapshotHelper(received: unknown, message: string, inlineOptions?: InlineSnapshotOptions) { + const snapshotService = SnapshotService.initiate() + if (!snapshotService.currentFilePath || !snapshotService.currentTestName) { + throw new Error('Snapshot service is not initialized') + } + + /** + * allow to match DOM snapshots + */ + if ( + received && typeof received === 'object' && + ( + 'elementId' in received || + 'then' in received + ) + ) { + return toMatchSnapshotAsync(received, message, inlineOptions) + } + + return toMatchSnapshotAssert(received, message, inlineOptions) +} + +export function toMatchSnapshot(received: unknown, message: string) { + return toMatchSnapshotHelper(received, message) +} + +export function toMatchInlineSnapshot(received: unknown, inlineSnapshot: string, message: string) { + /** + * When running component/unit tests in the browser we receive a stack trace + * through the `this` scope. + */ + const browserErrorLine: string = this.errorStack + + function __INLINE_SNAPSHOT__(inlineSnapshot: string, message: string) { + /** + * create a error object to pass along that helps Vitest's snapshot manager + * to infer the stack trace and locate the inline snapshot + */ + const error = new Error('inline snapshot') + + /** + * merge stack traces from browser and node and push the error of the test + * into the stack trace + */ + if (browserErrorLine && error.stack) { + const stack = error.stack.split('\n') + error.stack = [ + ...stack.slice(0, 4), + browserErrorLine, + ...stack.slice(3) + ].join('\n') + } + const trace = error.stack?.split('\n').filter((line) => ( + line.includes('__INLINE_SNAPSHOT__') || + !( + line.includes('__EXTERNAL_MATCHER_TRAP__') || + line.includes(`expect-webdriverio${path.sep}lib${path.sep}matchers${path.sep}snapshot.js:`) + ) + )).filter((line) => ( + /** + * remove jasmine-core stack trace to make it work with jasmine + */ + !line.includes('node_modules/jasmine-core/') + )) || [] + + /** + * tweak the stack trace to enable inline snapshot testing within this projects + * unit tests + */ + if (process.env.WDIO_INTERNAL_TEST) { + trace.splice(2, 1) + } + + if (inlineSnapshot) { + inlineSnapshot = stripSnapshotIndentation(inlineSnapshot) + } + + error.stack = trace.join('\n') + return toMatchSnapshotHelper(received, message, { + inlineSnapshot, + error + }) + } + return __INLINE_SNAPSHOT__(inlineSnapshot, message) +} diff --git a/src/snapshot.ts b/src/snapshot.ts new file mode 100644 index 000000000..3c412ff29 --- /dev/null +++ b/src/snapshot.ts @@ -0,0 +1,133 @@ +// import { expect } from '@wdio/globals' +import { SnapshotClient, type SnapshotResult, type SnapshotStateOptions, type SnapshotUpdateState } from '@vitest/snapshot' +import { NodeSnapshotEnvironment } from '@vitest/snapshot/environment' + +import type { Services, Frameworks } from '@wdio/types' + +/** + * only create instance once to avoid memory leak + */ +let service: SnapshotService + +export type SnapshotFormat = SnapshotStateOptions['snapshotFormat'] +type ResolveSnapshotPathFunction = (path: string, extension: string) => string +interface SnapshotServiceArgs { + updateState?: SnapshotUpdateState + resolveSnapshotPath?: ResolveSnapshotPathFunction + snapshotFormat?: SnapshotFormat +} + +class WebdriverIOSnapshotEnvironment extends NodeSnapshotEnvironment { + #resolveSnapshotPath?: (path: string, extension: string) => string + + constructor (resolveSnapshotPath?: ResolveSnapshotPathFunction) { + super({}) + this.#resolveSnapshotPath = resolveSnapshotPath + } + + async resolvePath (filepath: string): Promise { + if (this.#resolveSnapshotPath) { + return this.#resolveSnapshotPath(filepath, '.snap') + } + return super.resolvePath(filepath) + } +} + +/** + * Snapshot service to take snapshots of elements. + * The `@wdio/runner` module will attach this service to the test environment + * so it can track the current test file and test name. + * + * @param {string} updateState update state + * @return {SnapshotService} + */ +export class SnapshotService implements Services.ServiceInstance { + #currentFilePath?: string + #currentTestName?: string + #options: SnapshotStateOptions + #snapshotResults: SnapshotResult[] = [] + #snapshotClient = new SnapshotClient({ + isEqual: this.#isEqual.bind(this), + }) + + constructor (options?: SnapshotServiceArgs) { + const updateSnapshot = (Boolean(process.env.CI) && !options?.updateState) + ? 'none' + : options?.updateState + ? options.updateState + : 'new' + + // Only set snapshotFormat if user provides explicit options + const snapshotFormatConfig = options?.snapshotFormat ? { + printBasicPrototype: false, + escapeString: false, + ...options.snapshotFormat + } : undefined + + this.#options = { + updateSnapshot, + snapshotEnvironment: new WebdriverIOSnapshotEnvironment(options?.resolveSnapshotPath), + ...(snapshotFormatConfig && { snapshotFormat: snapshotFormatConfig }) + } as const + } + + get currentFilePath () { + return this.#currentFilePath + } + + get currentTestName () { + return this.#currentTestName + } + + get client () { + return this.#snapshotClient + } + + get results () { + return this.#snapshotResults + } + + async beforeTest(test: Frameworks.Test) { + this.#currentFilePath = test.file + this.#currentTestName = `${test.parent} > ${test.title}` + await this.#snapshotClient.setup(test.file, this.#options) + } + + async beforeStep(step: Frameworks.PickleStep, scenario: Frameworks.Scenario) { + const file = scenario.uri + const testName = `${scenario.name} > ${step.text}` + + this.#currentFilePath = file + this.#currentTestName = testName + await this.#snapshotClient.setup(file, this.#options) + } + + async after() { + if (!this.#currentFilePath) { + return + } + + const result = await this.#snapshotClient.finish(this.#currentFilePath) + if (!result) { + return + } + this.#snapshotResults.push(result) + } + + #isEqual (_received: unknown, _expected: unknown) { + try { + // TODO dprevost to uncomment when `Type 'Expect' has no call signatures.ts(2349)` is fixed + //expect(received).toBe(expected) + return true + } catch { + return false + } + } + + static initiate (options?: SnapshotServiceArgs) { + if (!service) { + service = new SnapshotService(options) + } + return service + } +} \ No newline at end of file diff --git a/test-types/mocha/tsconfig.json b/test-types/mocha/tsconfig.json index 665569493..12b4aff00 100644 --- a/test-types/mocha/tsconfig.json +++ b/test-types/mocha/tsconfig.json @@ -6,10 +6,11 @@ "module": "Node16", "skipLibCheck": true, "types": [ - "@types/expect", "@types/mocha", + "expect", + // "@wdio/globals/types", // TODO to review adding the global wdio types interfere with local types + "../../types/standalone.d.ts", "../../types/global.d.ts", - "@wdio/globals/types", ] } } diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index ccfe324b8..41bd3de61 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -24,16 +24,16 @@ describe('type assertions', () => { await expect(element).toHaveUrl('https://example.com') }) - it('should have ts errors when actual is an ChainableElement', async () => { - const chainableElement = $('findMe') - // @ts-expect-error - await expect(chainableElement).toHaveUrl('https://example.com') - }) + // it('should have ts errors when actual is an ChainableElement', async () => { + // const chainableElement = $('findMe') + // // @ts-expect-error + // await expect(chainableElement).toHaveUrl('https://example.com') + // }) }) describe('element type assertions', () => { const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element - const chainableElement = $('findMe') + // const chainableElement = $('findMe') describe('toBeDisabled', () => { it('should not have ts errors and be able to await the promise for element', async () => { @@ -45,14 +45,14 @@ describe('type assertions', () => { await expectNotIsPromiseVoid }) - it('should not have ts errors and be able to await the promise for chainable', async () => { - // expect no ts errors - const expectIsPromiseVoid: Promise = expect(chainableElement).toBeDisabled() - await expectIsPromiseVoid + // it('should not have ts errors and be able to await the promise for chainable', async () => { + // // expect no ts errors + // const expectIsPromiseVoid: Promise = expect(chainableElement).toBeDisabled() + // await expectIsPromiseVoid - const expectNotIsPromiseVoid: Promise = expect(chainableElement).not.toBeDisabled() - await expectNotIsPromiseVoid - }) + // const expectNotIsPromiseVoid: Promise = expect(chainableElement).not.toBeDisabled() + // await expectNotIsPromiseVoid + // }) it('should have ts errors when typing to void for element', async () => { // @ts-expect-error @@ -77,10 +77,10 @@ describe('type assertions', () => { const expectPromise2: Promise = expect(element).toMatchSnapshot('test label') }) - it('should not have ts errors when typing to Promise for a chainable', async () => { - const expectPromise1: Promise = expect(chainableElement).toMatchSnapshot() - const expectPromise2: Promise = expect(chainableElement).toMatchSnapshot('test label') - }) + // it('should not have ts errors when typing to Promise for a chainable', async () => { + // const expectPromise1: Promise = expect(chainableElement).toMatchSnapshot() + // const expectPromise2: Promise = expect(chainableElement).toMatchSnapshot('test label') + // }) // We need somehow to exclude the Jest types one for this to success it('should have ts errors when typing to void for an element like', async () => { @@ -145,11 +145,11 @@ describe('type assertions', () => { const expectToBeIsNotPromiseVoid: Promise = expect(await booleanPromise).toBe(true) }) - it('should have ts errors when typing resolves and reject is typed to void', async () => { - //@ts-expect-error - const expectResolvesToBeIsVoid: void = expect(booleanPromise).resolves.toBe(true) - //@ts-expect-error - const expectRejectsToBeIsVoid: void = expect(booleanPromise).rejects.toBe(true) - }) + // it('should have ts errors when typing resolves and reject is typed to void', async () => { + // //@ts-expect-error + // const expectResolvesToBeIsVoid: void = expect(booleanPromise).resolves.toBe(true) + // //@ts-expect-error + // const expectRejectsToBeIsVoid: void = expect(booleanPromise).rejects.toBe(true) + // }) }) }) diff --git a/test/index.test.ts b/test/index.test.ts index 6607fccf1..09120e2f8 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -4,5 +4,5 @@ import { setOptions, matchers, utils } from '../src/index.js' test('index', () => { expect(setOptions.name).toBe('setDefaultOptions') expect(utils.compareText).toBeDefined() - expect(matchers.size).toEqual(37) + expect(matchers.size).toEqual(39) }) diff --git a/test/matchers.test.ts b/test/matchers.test.ts index 4469213ff..ba1842403 100644 --- a/test/matchers.test.ts +++ b/test/matchers.test.ts @@ -45,7 +45,11 @@ const ALL_MATCHERS = [ // mock 'toBeRequested', - 'toBeRequestedTimes' + 'toBeRequestedTimes', + + // snapshot + 'toMatchSnapshot', + 'toMatchInlineSnapshot' ] test('matchers', () => { diff --git a/types/global.d.ts b/types/global.d.ts index 811c97a1f..bffeb52d0 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -1,8 +1,10 @@ /// -/// + +// @ts-expect-error +declare const expect: ExpectWebdriverIO.Expect declare namespace NodeJS { interface Global { - expect: WdioExpect + expect: ExpectWebdriverIO.Expect } } \ No newline at end of file diff --git a/types/standalone.d.ts b/types/standalone.d.ts index 93f3b96bd..4ad5ff9cf 100644 --- a/types/standalone.d.ts +++ b/types/standalone.d.ts @@ -1,39 +1,35 @@ /* eslint-disable @typescript-eslint/consistent-type-imports*/ /// +/// + +type ExpectAsymmetricMatchers = import('expect').AsymmetricMatchers; +type ExpectBaseExpect = import('expect').BaseExpect; + +// Not exportable from 'expect' +type Inverse = { + /** + * Inverse next matcher. If you know how to test something, `.not` lets you test its opposite. + */ + not: Matchers; +}; declare namespace ExpectWebdriverIO { interface Matchers extends WdioMatchers{} - // eslint-disable-next-line @typescript-eslint/no-explicit-any - interface Expect extends WdioMatchers {} + /** + * Mostly derived from the types of `jest-expect` but adapted to work with WebdriverIO. + * @see https://github.com/jestjs/jest/blob/main/packages/jest-expect/src/types.ts + */ + interface Expect extends ExpectBaseExpect, ExpectAsymmetricMatchers, Inverse> { + /** + * The `expect` function is used every time you want to test a value. + * You will rarely call `expect` by itself. + * + * @param actual The value to apply matchers against. + */ + (actual: T): WdioMatchers & Inverse> + } interface InverseAsymmetricMatchers extends Expect {} - - - // interface Matchers 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 { - // 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 - // } } From 9a72d900e8de886f8ab7665cc23ba4e08723915a Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sun, 22 Jun 2025 13:36:43 -0400 Subject: [PATCH 05/99] A good working version of standalone --- types/standalone.d.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/types/standalone.d.ts b/types/standalone.d.ts index 4ad5ff9cf..dc6a97cd1 100644 --- a/types/standalone.d.ts +++ b/types/standalone.d.ts @@ -4,6 +4,7 @@ type ExpectAsymmetricMatchers = import('expect').AsymmetricMatchers; type ExpectBaseExpect = import('expect').BaseExpect; +type ExpectMatchers = import('expect').Matchers; // Not exportable from 'expect' type Inverse = { @@ -15,7 +16,7 @@ type Inverse = { declare namespace ExpectWebdriverIO { - interface Matchers extends WdioMatchers{} + interface Matchers extends WdioMatchers, ExpectMatchers {} /** * Mostly derived from the types of `jest-expect` but adapted to work with WebdriverIO. @@ -28,7 +29,7 @@ declare namespace ExpectWebdriverIO { * * @param actual The value to apply matchers against. */ - (actual: T): WdioMatchers & Inverse> + (actual: T): Matchers & Inverse> } interface InverseAsymmetricMatchers extends Expect {} From 74fc41462e391a5c223c186b8adafa478c83baad Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sun, 22 Jun 2025 14:00:33 -0400 Subject: [PATCH 06/99] Code review + add a toBe with element + chainable ToBe seems fine to not return a promise since I did not find example of forcing to be promises when using element or chainable Code review --- .DS_Store | Bin 6148 -> 6148 bytes test-types/jest/types-jest.test.ts | 17 ++++++++++++++--- test-types/mocha/tsconfig.json | 4 ++-- test-types/mocha/types-mocha.test.ts | 1 + types/expect-webdriverio.d.ts | 16 ++++++++++++---- types/global.d.ts | 3 ++- 6 files changed, 31 insertions(+), 10 deletions(-) diff --git a/.DS_Store b/.DS_Store index ccc58d091d22398ba5eaae9280a0deec5d13e53c..1432c941fcc593c5975dc5d3e9cbe911d25e285e 100644 GIT binary patch delta 57 zcmV-90LK4>FoZCW7XgQnaTbv-ApruBP&<<_6a { }) }) - describe('boolean type assertions', () => { - it('should not have ts errors when typing to void', async () => { + describe('toBe', () => { + const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element + const chainableElement = $('findMe') + + it('should not have ts errors when typing to void when actual is boolean', async () => { // Expect no ts errors const expectToBeIsVoid: void = expect(true).toBe(true) const expectNotToBeIsVoid: void = expect(true).not.toBe(true) }) - it('should have ts errors when typing to Promise', async () => { + it('should have ts errors when typing to Promise when actual is boolean', async () => { //@ts-expect-error const expectToBeIsNotPromiseVoid: Promise = expect(true).toBe(true) //@ts-expect-error const expectToBeIsNotPromiseVoid: Promise = expect(true).not.toBe(true) }) + + it('should not have ts errors when typing to void when actual is an awaited element or chainable', async () => { + const isClickableElement = await element.isClickable() + const expectPromiseVoid1: void = expect(isClickableElement).toBe(true) + + const isClickableChainable: boolean = await chainableElement.isClickable() + const expectPromiseVoid2: void = expect(isClickableChainable).toBe(true) + }) }) describe('string type assertions', () => { diff --git a/test-types/mocha/tsconfig.json b/test-types/mocha/tsconfig.json index 12b4aff00..7a58c97f8 100644 --- a/test-types/mocha/tsconfig.json +++ b/test-types/mocha/tsconfig.json @@ -8,9 +8,9 @@ "types": [ "@types/mocha", "expect", - // "@wdio/globals/types", // TODO to review adding the global wdio types interfere with local types - "../../types/standalone.d.ts", "../../types/global.d.ts", + // "@wdio/types", + // "@wdio/globals/types", // TODO to review adding the global wdio types interfere with local types ] } } diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index 41bd3de61..145459d30 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -24,6 +24,7 @@ describe('type assertions', () => { await expect(element).toHaveUrl('https://example.com') }) + // TODO dprevost find a way to pull @wdio/globals for ChainableElement without affecting local types... // it('should have ts errors when actual is an ChainableElement', async () => { // const chainableElement = $('findMe') // // @ts-expect-error diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index ec69a6db8..2a7e203a8 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -7,7 +7,13 @@ type Scenario = import('@wdio/types').Frameworks.Scenario type SnapshotResult = import('@vitest/snapshot').SnapshotResult type SnapshotUpdateState = import('@vitest/snapshot').SnapshotUpdateState -interface CustomMatchers extends Record{ + +/** + * 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. + */ + +interface WdioCustomMatchers extends Record{ // ===== $ or $$ ===== /** * `WebdriverIO.Element` -> `isDisplayed` @@ -256,7 +262,7 @@ interface CustomMatchers extends Record{ * Those need to be also duplicated in jest.d.ts in order for the typing to correctly overload the matchers (we cannot just extend the Matchers interface) * @see */ -interface OverloadedMatchers { +interface WdioOverloadedMatchers { /** * snapshot matcher * @param label optional snapshot label @@ -270,7 +276,7 @@ interface OverloadedMatchers { toMatchInlineSnapshot(snapshot?: string, label?: string): Promise } -interface WdioMatchers extends CustomMatchers, OverloadedMatchers {} +interface WdioMatchers extends WdioCustomMatchers, WdioOverloadedMatchers {} declare namespace ExpectWebdriverIO { function setOptions(options: DefaultOptions): void @@ -431,6 +437,8 @@ declare namespace ExpectWebdriverIO { } declare module 'expect-webdriverio' { - const matchers: CustomMatchers; + + // TODO dprevost should we also have an expect const here too? + const matchers: WdioCustomMatchers; export = matchers; } diff --git a/types/global.d.ts b/types/global.d.ts index bffeb52d0..20abc4c89 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -1,6 +1,7 @@ /// -// @ts-expect-error +// On IDE restart, it seems to conflict with one defined in `types/jest` +// @ts-ignore declare const expect: ExpectWebdriverIO.Expect declare namespace NodeJS { From ffac6f7573c89de49e416161e5b6170a184ad8dc Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sun, 22 Jun 2025 14:32:21 -0400 Subject: [PATCH 07/99] Bring back `snapshot.test.ts` --- jest.d.ts | 6 ++- src/index.ts | 21 +++++++++++ src/snapshot.ts | 7 ++-- test/matchers.test.ts | 14 ++++++- test/snapshot.test.ts | 75 ++++++++++++++++++++++++++++++++++++++ test/snapshot.test.ts.snap | 7 ++++ 6 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 test/snapshot.test.ts create mode 100644 test/snapshot.test.ts.snap diff --git a/jest.d.ts b/jest.d.ts index 749315782..15fa91e12 100644 --- a/jest.d.ts +++ b/jest.d.ts @@ -1,8 +1,10 @@ /// + +// TODO dprevost try to type conditional the toMatchSnapshot later... /*/// */ -type ChainablePromiseElement = ReturnType -type WdioElementLike = WebdriverIO.Element | ChainablePromiseElement +// type ChainablePromiseElement = ReturnType +// type WdioElementLike = WebdriverIO.Element | ChainablePromiseElement declare namespace jest { diff --git a/src/index.ts b/src/index.ts index 2c74b7265..44fc295f1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,27 @@ type MatchersObject = Parameters[0] expectLib.extend(filteredMatchers as MatchersObject) +// Extend the expect object with soft assertions +const expectWithSoft = expectLib as unknown as ExpectWebdriverIO.Expect +// Object.defineProperty(expectWithSoft, 'soft', { +// value: (actual: T) => createSoftExpect(actual) +// }) + +// // Add soft assertions utility methods +// Object.defineProperty(expectWithSoft, 'getSoftFailures', { +// value: (testId?: string) => SoftAssertService.getInstance().getFailures(testId) +// }) + +// Object.defineProperty(expectWithSoft, 'assertSoftFailures', { +// value: (testId?: string) => SoftAssertService.getInstance().assertNoFailures(testId) +// }) + +// Object.defineProperty(expectWithSoft, 'clearSoftFailures', { +// value: (testId?: string) => SoftAssertService.getInstance().clearFailures(testId) +// }) + +export const expect = expectWithSoft + export const getConfig = (): ExpectWebdriverIO.DefaultOptions => DEFAULT_OPTIONS export const setDefaultOptions = (options = {}): void => { Object.entries(options).forEach(([key, value]) => { diff --git a/src/snapshot.ts b/src/snapshot.ts index 3c412ff29..8223dfafe 100644 --- a/src/snapshot.ts +++ b/src/snapshot.ts @@ -1,4 +1,4 @@ -// import { expect } from '@wdio/globals' +import { expect } from '@wdio/globals' import { SnapshotClient, type SnapshotResult, type SnapshotStateOptions, type SnapshotUpdateState } from '@vitest/snapshot' import { NodeSnapshotEnvironment } from '@vitest/snapshot/environment' @@ -114,10 +114,9 @@ export class SnapshotService implements Services.ServiceInstance { this.#snapshotResults.push(result) } - #isEqual (_received: unknown, _expected: unknown) { + #isEqual (received: unknown, expected: unknown) { try { - // TODO dprevost to uncomment when `Type 'Expect' has no call signatures.ts(2349)` is fixed - //expect(received).toBe(expected) + expect(received).toBe(expected) return true } catch { return false diff --git a/test/matchers.test.ts b/test/matchers.test.ts index ba1842403..852e45e5e 100644 --- a/test/matchers.test.ts +++ b/test/matchers.test.ts @@ -1,5 +1,5 @@ -import { test, expect } from 'vitest' -import { matchers } from '../src/index.js' +import { test, expect, vi } from 'vitest' +import { matchers, expect as expectLib } from '../src/index.js' const ALL_MATCHERS = [ // browser @@ -55,3 +55,13 @@ const ALL_MATCHERS = [ test('matchers', () => { expect([...matchers.keys()]).toEqual(ALL_MATCHERS) }) + +test('allows to add matcher', () => { + const matcher: any = vi.fn((actual: any, expected: any) => ({ pass: actual === expected })) + expectLib.extend({ toBeCustom: matcher }) + + // TODO dprevost see later if we really the below to expect a ts error since it is not working anymore... + //// @ts-expect-error not in types + expectLib('foo').toBeCustom('foo') + expect(matchers.keys()).toContain('toBeCustom') +}) \ No newline at end of file diff --git a/test/snapshot.test.ts b/test/snapshot.test.ts new file mode 100644 index 000000000..8d45559b3 --- /dev/null +++ b/test/snapshot.test.ts @@ -0,0 +1,75 @@ +import fs from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { test, expect } from 'vitest' +import type { Frameworks } from '@wdio/types' + +import { expect as expectExport, SnapshotService } from '../src/index.js' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const __filename = path.basename(fileURLToPath(import.meta.url)) + +const service = SnapshotService.initiate({ + resolveSnapshotPath: (path, extension) => path + extension +}) + +// TODO dprevost the below is missing in the snapshot.test.ts.snap file +// exports[`parent > test 2`] = ` +// { +// "deep": { +// "nested": { +// "object": "value", +// }, +// }, +// } +// `; + +test('supports snapshot testing', async () => { + await service.beforeTest({ + title: 'test', + parent: 'parent', + file: path.join(__dirname, __filename), + } as Frameworks.Test) + + process.env.WDIO_INTERNAL_TEST = 'true' + + const exp = expectExport + expect(exp).toBeDefined() + expect(exp({}).toMatchSnapshot).toBeDefined() + expect(exp({}).toMatchInlineSnapshot).toBeDefined() + await exp({ a: 'a' }).toMatchSnapshot() + await exp({ deep: { nested: { object: 'value' } } }).toMatchInlineSnapshot(` + { + "deep": { + "nested": { + "object": "value", + }, + }, + } + `) + await service.after() + + const expectedSnapfileExist = await fs.access(path.resolve(__dirname, 'snapshot.test.ts.snap')) + .then(() => true, () => false) + expect(expectedSnapfileExist).toBe(true) +}) + +test('supports cucumber snapshot testing', async () => { + await service.beforeStep({ + text: 'Fake step', + } as Frameworks.PickleStep, { + name: 'Fake scenario', + uri: `${__dirname}/file.feature`, + } as Frameworks.Scenario) + + const exp = expectExport + expect(exp).toBeDefined() + expect(exp({}).toMatchSnapshot).toBeDefined() + expect(exp({}).toMatchInlineSnapshot).toBeDefined() + await exp({ cucum: 'ber' }).toMatchSnapshot() + await service.after() + + const expectedSnapfileExist = await fs.access(path.resolve(__dirname, 'file.feature.snap')) + .then(() => true, () => false) + expect(expectedSnapfileExist).toBe(true) +}) \ No newline at end of file diff --git a/test/snapshot.test.ts.snap b/test/snapshot.test.ts.snap new file mode 100644 index 000000000..7af66be0a --- /dev/null +++ b/test/snapshot.test.ts.snap @@ -0,0 +1,7 @@ +// Snapshot v1 + +exports[`parent > test 1`] = ` +{ + "a": "a", +} +`; From b254057918e90460547a27378887ea9fb6a97c05 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sun, 22 Jun 2025 15:10:03 -0400 Subject: [PATCH 08/99] Bring back toBeRequestedWith --- src/matchers.ts | 2 + src/matchers/mock/toBeRequestedWith.ts | 405 +++++++++++++++ test-types/jest/types-jest.test.ts | 21 +- test/index.test.ts | 5 +- test/matchers.test.ts | 2 + test/matchers/mock/toBeRequestedWith.test.ts | 487 +++++++++++++++++++ 6 files changed, 914 insertions(+), 8 deletions(-) create mode 100644 src/matchers/mock/toBeRequestedWith.ts create mode 100644 test/matchers/mock/toBeRequestedWith.test.ts diff --git a/src/matchers.ts b/src/matchers.ts index 4f3176730..eed5a93b0 100644 --- a/src/matchers.ts +++ b/src/matchers.ts @@ -27,4 +27,6 @@ export * from './matchers/element/toHaveWidth.js' export * from './matchers/elements/toBeElementsArrayOfSize.js' export * from './matchers/mock/toBeRequested.js' export * from './matchers/mock/toBeRequestedTimes.js' +export * from './matchers/mock/toBeRequestedWith.js' export * from './matchers/snapshot.js' + diff --git a/src/matchers/mock/toBeRequestedWith.ts b/src/matchers/mock/toBeRequestedWith.ts new file mode 100644 index 000000000..a87dec074 --- /dev/null +++ b/src/matchers/mock/toBeRequestedWith.ts @@ -0,0 +1,405 @@ +import type { local } from 'webdriver' + +import { waitUntil, enhanceError } from '../../utils.js' +import { equals } from '../../jasmineUtils.js' +import { DEFAULT_OPTIONS } from '../../constants.js' + +const STR_LIMIT = 80 +const KEY_LIMIT = 12 + +interface RequestMock { + request: local.NetworkRequestData, + response: local.NetworkResponseData +} + +function reduceHeaders(headers: local.NetworkHeader[]) { + return Object.entries(headers).reduce((acc, [, value]: [string, local.NetworkHeader]) => { + acc[value.name] = value.value.value + return acc + }, {} as Record) +} + +export async function toBeRequestedWith( + received: WebdriverIO.Mock, + expectedValue: ExpectWebdriverIO.RequestedWith = {}, + options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS +) { + const isNot = this.isNot || false + const { expectation = 'called with', verb = 'be' } = this + + await options.beforeAssertion?.({ + matcherName: 'toBeRequestedWith', + expectedValue, + options, + }) + + let actual: RequestMock | undefined + const pass = await waitUntil( + async () => { + for (const call of received.calls) { + actual = call + if ( + methodMatcher(call.request.method, expectedValue.method) && + statusCodeMatcher(call.response.status, expectedValue.statusCode) && + urlMatcher(call.request.url, expectedValue.url) && + headersMatcher(reduceHeaders(call.request.headers), expectedValue.requestHeaders) && + headersMatcher(reduceHeaders(call.response.headers), expectedValue.responseHeaders) + // && + // bodyMatcher(call.postData, expectedValue.postData) && + // bodyMatcher(call.body, expectedValue.response) + ) { + return true + } + } + + return false + }, + isNot, + { ...options, wait: isNot ? 0 : options.wait } + ) + + const message = enhanceError( + 'mock', + minifyRequestedWith(expectedValue), + minifyRequestMock(actual, expectedValue) || 'was not called', + this, + verb, + expectation, + '', + options + ) + + const result: ExpectWebdriverIO.AssertionResult = { + pass, + message: (): string => message + } + + await options.afterAssertion?.({ + matcherName: 'toBeRequestedWith', + expectedValue, + options, + result + }) + + return result +} + +/** + * is actual method matching an expected method or methods + */ +const methodMatcher = (method: string, expected?: string | Array) => { + if (typeof expected === 'undefined') { + return true + } + if (!Array.isArray(expected)) { + expected = [expected] + } + return expected + .map((m) => { + if (typeof m !== 'string') { + return console.error('expect.toBeRequestedWith: unsupported value passed to method ' + m) + } + return m.toUpperCase() + }) + .includes(method) +} + +/** + * is actual statusCode matching an expected statusCode or statusCodes + */ +const statusCodeMatcher = (statusCode: number, expected?: number | Array) => { + if (typeof expected === 'undefined') { + return true + } + if (!Array.isArray(expected)) { + expected = [expected] + } + return expected.includes(statusCode) +} + +/** + * is actual url matching an expected condition + */ +const urlMatcher = ( + url: string, + expected?: string | ExpectWebdriverIO.PartialMatcher | ((url: string) => boolean) +) => { + if (typeof expected === 'undefined') { + return true + } + if (typeof expected === 'function') { + return expected(url) + } + return equals(url, expected) +} + +/** + * is headers url matching an expected condition + */ +const headersMatcher = ( + headers: Record, + expected?: + | Record + | ExpectWebdriverIO.PartialMatcher + | ((headers: Record) => boolean) +) => { + /** + * match with no headers were passed in as filter or + * if header matcher is an empty object, match with no headers + */ + if ( + typeof expected === 'undefined' || + typeof expected === 'object' && Object.keys(expected).length === 0 + ) { + return true + } + /** + * call function of provided + */ + if (typeof expected === 'function') { + return expected(headers) + } + return equals(headers, expected) +} + +/** + * is postData/response matching an expected condition + */ +// const bodyMatcher = ( +// body: string | Buffer | ExpectWebdriverIO.JsonCompatible | undefined, +// expected?: +// | string +// | ExpectWebdriverIO.JsonCompatible +// | ExpectWebdriverIO.PartialMatcher +// | ((r: string | Buffer | ExpectWebdriverIO.JsonCompatible | undefined) => boolean) +// ) => { +// if (typeof expected === 'undefined') { +// return true +// } +// if (typeof expected === 'function') { +// return expected(body) +// } +// if (typeof body === 'undefined') { +// return false +// } + +// let parsedBody = body +// if (body instanceof Buffer) { +// parsedBody = body.toString() +// } + +// // convert postData/body from string to JSON if expected value is JSON-like +// if (typeof(body) === 'string' && isExpectedJsonLike(expected)) { +// parsedBody = tryParseBody(body) + +// // failed to parse string as JSON +// if (parsedBody === null) { +// return false +// } +// } + +// return equals(parsedBody, expected) +// } + +// const isExpectedJsonLike = ( +// expected: +// | string +// | ExpectWebdriverIO.JsonCompatible +// | ExpectWebdriverIO.PartialMatcher +// | undefined +// | Function +// ) => { +// if (typeof expected === 'undefined') { +// return false +// } + +// // 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 + +// return ( +// Array.isArray(actualSample) || +// (typeof actualSample === 'object' && +// actualSample !== null && +// actualSample instanceof RegExp === false) +// ) +// } + +/** + * is jasmine/jest special matcher + * + * Jest and Jasmine support special matchers like `jasmine.objectContaining`, `expect.arrayContaining`, etc. + * + * All these kind of objects have `sample` and `asymmetricMatch` function in __proto__ + * `expect.objectContaining({ foo: 'bar })` -> `{ sample: { foo: 'bar' }, __proto__: asymmetricMatch() {} }` + * + * jasmine.any and jasmine.anything don't have `sample` property + * @param filter + */ +const isMatcher = (filter: unknown) => { + return ( + typeof filter === 'object' && + filter !== null && + '__proto__' in filter && + typeof filter.__proto__ === 'object' && + filter.__proto__ && + 'asymmetricMatch' in filter.__proto__ && + typeof filter.__proto__.asymmetricMatch === 'function' + ) +} + +// const tryParseBody = (jsonString: string | undefined, fallback: any = null) => { +// try { +// return typeof jsonString === 'undefined' ? fallback : JSON.parse(jsonString) +// } catch { +// return fallback +// } +// } + +/** + * shorten long url, headers, postData, body + */ +const minifyRequestMock = ( + requestMock?: { + request: local.NetworkRequestData, + response: local.NetworkResponseData + }, + requestedWith?: ExpectWebdriverIO.RequestedWith +) => { + if (typeof requestMock === 'undefined') { + return requestMock + } + + const r: Record = { + url: requestMock.request.url, + method: requestMock.request.method, + requestHeaders: requestMock.request.headers, + responseHeaders: requestMock.response.headers, + // postData: typeof requestMock.postData === 'string' && isExpectedJsonLike(requestedWith.postData) + // ? tryParseBody(requestMock.postData, requestMock.postData) + // : requestMock.postData, + // response: typeof requestMock.body === 'string' && isExpectedJsonLike(requestedWith.response) + // ? tryParseBody(requestMock.body, requestMock.body) + // : requestMock.body, + } + + deleteUndefinedValues(r, requestedWith) + + return minifyRequestedWith(r) +} + +/** + * shorten long url, headers, postData, response + * and transform Function/Matcher to string + */ +const minifyRequestedWith = (r: ExpectWebdriverIO.RequestedWith) => { + const result = { + url: requestedWithParamToString(r.url), + method: r.method, + requestHeaders: requestedWithParamToString(r.requestHeaders, shortenJson), + responseHeaders: requestedWithParamToString(r.responseHeaders, shortenJson), + postData: requestedWithParamToString(r.postData, shortenJson), + response: requestedWithParamToString(r.response, shortenJson), + } + + deleteUndefinedValues(result) + + return result +} + +/** + * transform Function/Matcher/JSON to string if needed + */ +const requestedWithParamToString = ( + param: + | string + | ExpectWebdriverIO.JsonCompatible + | ExpectWebdriverIO.PartialMatcher + | Function + | undefined, + transformFn?: (param: ExpectWebdriverIO.JsonCompatible) => ExpectWebdriverIO.JsonCompatible | string +) => { + if (typeof param === 'undefined') { + return + } + + if (typeof param === 'function') { + param = param.toString() + } else if (isMatcher(param)) { + return ( + param.constructor.name + + ' ' + + (JSON.stringify((param as ExpectWebdriverIO.PartialMatcher).sample) || '') + ) + } else if (transformFn && typeof param === 'object' && param !== null) { + param = transformFn(param as ExpectWebdriverIO.JsonCompatible) + } + + if (typeof param === 'string') { + param = shortenString(param) + } + + return param +} + +/** + * shorten object key/values and decrease array size + * ex: `{ someVeryLongKey: 'someVeryLongValue' }` -> `{ som..Key: 'som..lue' }` + */ +const shortenJson = ( + obj: ExpectWebdriverIO.JsonCompatible, + lengthLimit = STR_LIMIT * 2, + keyLimit = KEY_LIMIT +): ExpectWebdriverIO.JsonCompatible => { + if (JSON.stringify(obj).length < lengthLimit) { + return obj as ExpectWebdriverIO.JsonCompatible + } + + if (Array.isArray(obj)) { + const firstItem: object | string = + typeof obj[0] === 'object' && obj[0] !== null + ? shortenJson(obj[0], lengthLimit / 2, keyLimit / 4) + : shortenString(JSON.stringify(obj[0])) + return [firstItem, `... ${obj.length - 1} more items`] as string[] + } + + const minifiedObject: Record = {} + const entries = Object.entries(obj) + + if (keyLimit >= 4) { + entries.slice(0, keyLimit).forEach(([k, v]) => { + if (typeof v === 'object' && v !== null) { + v = shortenJson(v, lengthLimit / 2, keyLimit / 4) + } else if (typeof v === 'string') { + v = shortenString(v, 16) + } + minifiedObject[shortenString(k, 24)] = v + }) + } + if (entries.length > keyLimit) { + minifiedObject['...'] = `${entries.length} items in total` + } + + return minifiedObject as ExpectWebdriverIO.JsonCompatible +} + +/** + * shorten string + * ex: '1234567890' -> '12..90' + */ +const shortenString = (str: string, limit = STR_LIMIT) => { + return str.length > limit ? str.substring(0, limit / 2 - 1) + '..' + str.substr(1 - limit / 2) : str +} + +const deleteUndefinedValues = (obj: Record, baseline = obj) => { + Object.keys(obj).forEach((k) => { + if (typeof baseline[k] === 'undefined') { + delete obj[k] + } + }) +} + +export function toBeRequestedWithResponse(...args: unknown[]) { + return toBeRequestedWith.call(this, ...args) +} \ No newline at end of file diff --git a/test-types/jest/types-jest.test.ts b/test-types/jest/types-jest.test.ts index 3d0b3f043..79f4b1c51 100644 --- a/test-types/jest/types-jest.test.ts +++ b/test-types/jest/types-jest.test.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ describe('type assertions', () => { + const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element + const chainableElement = $('findMe') describe('toHaveUrl', () => { const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser @@ -19,21 +21,17 @@ describe('type assertions', () => { }) it('should have ts errors when actual is an element', async () => { - const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element // @ts-expect-error await expect(element).toHaveUrl('https://example.com') }) it('should have ts errors when actual is an ChainableElement', async () => { - const chainableElement = $('findMe') // @ts-expect-error await expect(chainableElement).toHaveUrl('https://example.com') }) }) describe('element type assertions', () => { - const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element - const chainableElement = $('findMe') describe('toBeDisabled', () => { it('should not have ts errors and be able to await the promise for element', async () => { @@ -98,8 +96,6 @@ describe('type assertions', () => { }) describe('toBe', () => { - const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element - const chainableElement = $('findMe') it('should not have ts errors when typing to void when actual is boolean', async () => { // Expect no ts errors @@ -163,4 +159,17 @@ describe('type assertions', () => { const expectRejectsToBeIsVoid: void = expect(booleanPromise).rejects.toBe(true) }) }) + + // describe('Soft Assertions', () => { + + // it('should not have ts errors when used with chainable', async () => { + + // const expectString: string = await $('h1').getText() + // const expectVoid: void = expect.soft(expectString).toEqual('Basketball Shoes') + // // await expect.soft(await $('#price').getText()).toMatch(/€\d+/) + + // // Regular assertions still throw immediately + // // await expect(await $('.add-to-cart').isClickable()).toBe(true) + // }) + // }) }) diff --git a/test/index.test.ts b/test/index.test.ts index 09120e2f8..25c6c95d7 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,8 +1,9 @@ import { test, expect } from 'vitest' -import { setOptions, matchers, utils } from '../src/index.js' +import { setOptions, expect as expectExport, matchers, utils } from '../src/index.js' test('index', () => { expect(setOptions.name).toBe('setDefaultOptions') + expect(expectExport).toBeDefined() expect(utils.compareText).toBeDefined() - expect(matchers.size).toEqual(39) + expect(matchers.size).toEqual(41) }) diff --git a/test/matchers.test.ts b/test/matchers.test.ts index 852e45e5e..f1008985f 100644 --- a/test/matchers.test.ts +++ b/test/matchers.test.ts @@ -46,6 +46,8 @@ const ALL_MATCHERS = [ // mock 'toBeRequested', 'toBeRequestedTimes', + 'toBeRequestedWith', + 'toBeRequestedWithResponse', // snapshot 'toMatchSnapshot', diff --git a/test/matchers/mock/toBeRequestedWith.test.ts b/test/matchers/mock/toBeRequestedWith.test.ts new file mode 100644 index 000000000..f4a03ef7f --- /dev/null +++ b/test/matchers/mock/toBeRequestedWith.test.ts @@ -0,0 +1,487 @@ +import { vi, test, describe, expect, beforeEach, afterEach } from 'vitest' + +import { toBeRequestedWith } from '../../../src/matchers/mock/toBeRequestedWith.js' +import type { local } from 'webdriver' +import { removeColors, getExpectMessage, getExpected, getReceived } from '../../__fixtures__/utils.js' + +vi.mock('@wdio/globals') + +interface Scenario { + name: string + mocks: local.NetworkBaseParameters[] + pass: boolean + params: ExpectWebdriverIO.RequestedWith +} + +class TestMock { + _calls: local.NetworkBaseParameters[] + + constructor() { + this._calls = [] + } + get calls() { + return this._calls + } +} + +function reduceHeaders(headers: local.NetworkHeader[]) { + return Object.entries(headers).reduce((acc, [, value]: [string, local.NetworkHeader]) => { + acc[value.name] = value.value.value + return acc + }, {} as Record) +} + +const authKey = 'Bearer ' + '2'.repeat(128) + +const mockGet: local.NetworkAuthRequiredParameters = { + request: { + url: 'http://localhost:8080/api/search?pages=20', + method: 'GET', + request: '123', + headersSize: 123, + bodySize: 123, + timings: {} as any, + cookies: [], + headers: [{ + name: 'Authorization', + value: { type: 'string', value: authKey } + }, { + name: 'foo', + value: { type: 'string', value: 'bar' } + }] + }, + response: { + headers: {}, + status: 200, + } as any, + // body: JSON.stringify({ + // total: 100, + // page: 1, + // data: { + // aLongValue1: { + // k1: { value1: 'bar1' }, + // k2: { value2: 'bar2' }, + // }, + // foo: { id: 1 }, + // bar: { id: 2 }, + // longValue2: { value: 'foo2' }, + // longValue3: { value: 'foo3' }, + // }, + // }), + // initialPriority: 'Low', + // referrerPolicy: 'origin', +} as any + +const mockPost: local.NetworkAuthRequiredParameters = { + request: { + url: 'https://my-app/api/add-tags', + method: 'POST', + request: '123', + headersSize: 123, + bodySize: 123, + timings: {} as any, + cookies: [], + headers: [{ + name: 'Authorization', + value: { type: 'string', value: authKey } + }, { + name: 'foo', + value: { type: 'string', value: 'bar' } + }, { + name: 'Accept', + value: { type: 'string', value: '*' } + }], + }, + response: { + status: 201, + headers: [] + } as any, + // body: JSON.stringify([ + // { id: 1, name: 'foo' }, + // { id: 2, name: 'bar' }, + // ]), + // postData: JSON.stringify([{ id: 1 }, { search: { name: 'bar' } }]), + // initialPriority: 'Low', + // referrerPolicy: 'origin', +} as any + +describe('toBeRequestedWith', () => { + test('wait for success, exact match', async () => { + const mock: any = new TestMock() + + setTimeout(() => { + mock.calls.push({ ...mockGet }) + }, 5) + setTimeout(() => { + mock.calls.push({ ...mockGet }, { ...mockPost }) + }, 15) + + const params = { + url: mockPost.request.url, + method: mockPost.request.method, + requestHeaders: {}, + statusCode: mockPost.response.status, + responseHeaders: {}, + // postData: mockPost.postData, + // response: JSON.parse(mockPost.body as string), + } + + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + const result = await toBeRequestedWith.call({}, mock, params, { beforeAssertion, afterAssertion }) + expect(result.pass).toBe(true) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toBeRequestedWith', + expectedValue: params, + options: { beforeAssertion, afterAssertion }, + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toBeRequestedWith', + expectedValue: params, + options: { beforeAssertion, afterAssertion }, + result + }) + }) + + test('wait for failure', async () => { + const mock: any = new TestMock() + + setTimeout(() => { + mock.calls.push({ ...mockGet }, { ...mockPost }) + }, 15) + + const params = { + url: 'post.url', + method: 'post.method', + requestHeaders: {}, + responseHeaders: {} + // postData: {}, + // response: 'post.body', + } + + const result = await toBeRequestedWith.call({}, mock, params) + expect(result.pass).toBe(false) + }) + + test('wait for NOT failure, empty params', async () => { + const mock: any = new TestMock() + mock.calls.push({ ...mockGet }, { ...mockPost }) + setTimeout(() => { + mock.calls.push({ ...mockGet }, { ...mockPost }) + }, 10) + + const result = await toBeRequestedWith.call({ isNot: true }, mock, {}) + expect(result.pass).toBe(true) + }) + + test('wait for NOT success', async () => { + const mock: any = new TestMock() + + setTimeout(() => { + mock.calls.push({ ...mockGet }, { ...mockPost }) + }, 10) + + const result = await toBeRequestedWith.call({ isNot: true }, mock, { method: 'DELETE' }) + expect(result.pass).toBe(false) + }) + + const scenarios: Scenario[] = [ + // success + { + name: 'success, url only', + mocks: [{ ...mockPost }], + pass: true, + params: { + url: mockPost.request.url, + }, + }, + { + name: 'success, method only', + mocks: [{ ...mockPost }], + pass: true, + params: { + method: ['DELETE', 'PUT', mockPost.request.method, 'GET'], + }, + }, + { + name: 'success, statusCode only', + mocks: [{ ...mockPost }], + pass: true, + params: { + statusCode: [203, 200, 201], + }, + }, + { + name: 'success, requestHeaders only', + mocks: [{ ...mockPost }], + pass: true, + params: { + requestHeaders: { + Authorization: authKey, + foo: 'bar', + Accept: '*' + }, + }, + }, + { + name: 'success, responseHeaders only', + mocks: [{ ...mockPost }], + pass: true, + params: { + responseHeaders: {}, + }, + }, + // { + // name: 'success, postData only', + // mocks: [{ ...mockPost }], + // pass: true, + // params: { + // postData: JSON.parse(mockPost.postData as string), + // }, + // }, + // { + // name: 'success, response only', + // mocks: [{ ...mockPost }], + // pass: true, + // params: { + // response: mockPost.body, + // }, + // }, + // failure + { + name: 'failure, url only', + mocks: [{ ...mockPost }], + pass: false, + params: { + url: '/api/api', + }, + }, + { + name: 'failure, method only', + mocks: [{ ...mockPost }], + pass: false, + params: { + method: ['DELETE', 'PUT'], + }, + }, + { + name: 'failure, statusCode only', + mocks: [{ ...mockPost }], + pass: false, + params: { + statusCode: [400, 401], + }, + }, + { + name: 'failure, requestHeaders only', + mocks: [{ ...mockPost }], + pass: false, + params: { + requestHeaders: { Cache: 'false' }, + }, + }, + { + name: 'failure, responseHeaders only', + mocks: [{ ...mockPost }], + pass: false, + params: { + responseHeaders: { Cache: 'false' }, + }, + }, + // { + // name: 'failure, postData only', + // mocks: [{ ...mockPost }], + // pass: false, + // params: { + // postData: 'foobar', + // }, + // }, + // { + // name: 'failure, response only', + // mocks: [{ ...mockGet }], + // pass: false, + // params: { + // response: { foobar: true }, + // }, + // }, + // special matcher + { + name: 'special matcher, url', + mocks: [{ ...mockPost }], + pass: true, + params: { + url: expect.stringMatching(/.*\/API\/.*/i), + }, + }, + { + name: 'special matcher, headers', + mocks: [{ ...mockPost }], + pass: true, + params: { + requestHeaders: expect.objectContaining({ + Authorization: expect.stringContaining('Bearer '), + }), + }, + }, + { + name: 'special matcher, postData', + mocks: [{ ...mockPost }], + pass: true, + params: { + postData: expect.stringMatching('"search"'), + }, + }, + { + name: 'special matcher, response', + mocks: [{ ...mockPost }], + pass: true, + params: { + response: expect.arrayContaining([expect.objectContaining({ id: 2 })]), + }, + }, + // function + { + name: 'function, url', + mocks: [{ ...mockPost }], + pass: true, + params: { + url: (url: string) => url.startsWith('https'), + }, + }, + { + name: 'function, headers', + mocks: [{ ...mockPost }], + pass: true, + params: { + requestHeaders: (headers: Record) => headers.foo === 'bar', + }, + }, + { + name: 'function, postData', + mocks: [{ ...mockPost }], + pass: true, + params: { + postData: (r: string) => (JSON.parse(r) as Array>).length === 2, + }, + }, + { + name: 'function, response', + mocks: [{ ...mockPost }], + pass: true, + params: { + response: (r: string) => r.includes('id') && r.includes('name'), + }, + }, + // no postData + // { + // name: 'no postData', + // mocks: [{ ...mockGet }], + // pass: false, + // params: { + // postData: 'something', + // }, + // }, + // body is not a JSON + // { + // name: 'body as string', + // mocks: [{ ...mockGet, body: 'asd' }], + // pass: true, + // params: { + // response: 'asd', + // }, + // }, + // { + // name: 'body as Buffer', + // mocks: [{ ...mockGet, body: Buffer.from('asd') }], + // pass: true, + // params: { + // response: 'asd', + // }, + // }, + // { + // name: 'body as JSON', + // mocks: [{ ...mockGet, body: 'asd' }], + // pass: false, + // params: { + // response: { foo: 'bar' }, + // }, + // }, + ] + + scenarios.forEach((scenario) => { + test(scenario.name, async () => { + const mock: any = new TestMock() + mock.calls.push(...scenario.mocks) + + const result = await toBeRequestedWith.call({}, mock, scenario.params as any) + expect(result.pass).toBe(scenario.pass) + }) + }) + + describe('error messages', () => { + const consoleError = global.console.error + beforeEach(() => { + global.console.error = vi.fn() + }) + + test('unsupported method', async () => { + const mock: any = new TestMock() + mock.calls.push({ ...mockGet }) + + const result = await toBeRequestedWith.call({}, mock, { method: 1234 } as any) + expect(result.pass).toBe(false) + expect(global.console.error).toBeCalledWith( + 'expect.toBeRequestedWith: unsupported value passed to method 1234' + ) + }) + + afterEach(() => { + global.console.error = consoleError + }) + }) + + test('message', async () => { + const mock: any = new TestMock() + + const requested = await toBeRequestedWith.call({}, mock, { + url: () => false, + method: ['DELETE', 'PUT'], + requestHeaders: reduceHeaders(mockPost.request.headers), + responseHeaders: reduceHeaders(mockPost.response.headers), + postData: expect.anything(), + response: [...Array(50).keys()].map((_, id) => ({ id, name: `name_${id}` })), + }) + const wasNotCalled = removeColors(requested.message()) + expect(getExpectMessage(wasNotCalled)).toBe('Expect mock to be called with') + expect(getExpected(wasNotCalled)).toBe( + 'Expected: {' + + '"method": ["DELETE", "PUT"], ' + + '"postData": "Anything ", ' + + '"requestHeaders": {"Accept": "*", "Authorization": "Bearer ..2222222", "foo": "bar"}, ' + + '"response": [{"id": 0, "name": "name_0"}, "... 49 more items"], ' + + '"responseHeaders": {}, ' + + '"url": "() => false"}' + ) + expect(getReceived(wasNotCalled)).toBe('Received: "was not called"') + + mock.calls.push(mockPost) + + const notRequested = await toBeRequestedWith.call({ isNot: true }, mock, { + url: () => true, + method: mockPost.request.method, + }) + const wasCalled = removeColors(notRequested.message()) + expect(wasCalled).toBe( + `Expect mock not to be called with + +- Expected [not] - 1 ++ Received + 1 + + Object { + "method": "POST", +- "url": "() => true", ++ "url": "https://my-app/api/add-tags", + }` + ) + }) +}) \ No newline at end of file From f5d9cbfd0f6bb5f7a442e721a81f50b582ec8681 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sun, 22 Jun 2025 15:33:06 -0400 Subject: [PATCH 09/99] Bring back `toBeRequestedWith` --- test-types/jest/types-jest.test.ts | 87 +++++++++++++++++++++++++++++- types/expect-webdriverio.d.ts | 5 ++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/test-types/jest/types-jest.test.ts b/test-types/jest/types-jest.test.ts index 79f4b1c51..416e7b7d5 100644 --- a/test-types/jest/types-jest.test.ts +++ b/test-types/jest/types-jest.test.ts @@ -2,6 +2,7 @@ describe('type assertions', () => { const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element const chainableElement = $('findMe') + const chainableArray = $$('ul>li') describe('toHaveUrl', () => { const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser @@ -68,7 +69,6 @@ describe('type assertions', () => { }) describe('toMatchSnapshot', () => { - const booleanPromise: Promise = Promise.resolve(true) it('should not have ts errors when typing to Promise for an element', async () => { const expectPromise1: Promise = expect(element).toMatchSnapshot() @@ -93,6 +93,34 @@ describe('type assertions', () => { // const expectNotToBeVoid: void = expect('.findme').toMatchSnapshot() // }) }) + + describe('toMatchInlineSnapshot', () => { + + it('should not have ts errors when typing to Promise for an element', async () => { + const expectPromise1: Promise = expect(element).toMatchInlineSnapshot() + const expectPromise2: Promise = expect(element).toMatchInlineSnapshot('test snapshot') + const expectPromise3: Promise = expect(element).toMatchInlineSnapshot('test snapshot', 'test label') + }) + + it('should not have ts errors when typing to Promise for a chainable', async () => { + const expectPromise1: Promise = expect(chainableElement).toMatchInlineSnapshot() + const expectPromise2: Promise = expect(chainableElement).toMatchInlineSnapshot('test snapshot') + const expectPromise3: Promise = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + }) + + // We need somehow to exclude the Jest types one for this to success + it('should have ts errors when typing to void for an element like', async () => { + //@ts-expect-error + const expectNotToBeVoid1: void = expect(element).toMatchInlineSnapshot() + //@ts-expect-error + const expectPromise2: void = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + }) + + // TODO - conditional types check on T to have the below match void does not work + // it('should not have ts errors when typing to void for a string', async () => { + // const expectNotToBeVoid: void = expect('.findme').toMatchInlineSnapshot() + // }) + }) }) describe('toBe', () => { @@ -160,6 +188,63 @@ describe('type assertions', () => { }) }) + describe('toBeElementsArrayOfSize', async () => { + + it('should not have ts errors when typing to Promise', async () => { + const listItems = await chainableArray + const expectPromise: Promise = expect(listItems).toBeElementsArrayOfSize(5) + const expectPromise1: Promise = expect(listItems).toBeElementsArrayOfSize({ lte: 10 }) + }) + + it('should have ts errors when typing to void', async () => { + const listItems = await chainableArray + // @ts-expect-error + const expectPromise: void = expect(listItems).toBeElementsArrayOfSize(5) + // @ts-expect-error + const expectPromise1: void = expect(listItems).toBeElementsArrayOfSize({ lte: 10 }) + }) + }) + + describe('Network Matchers', () => { + const mock = browser.mock('**/api/todo*') + + it('should not have ts errors when typing to Promise', async () => { + const expectPromise1: Promise = expect(mock).toBeRequested() + const expectPromise2: Promise = expect(mock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + const expectPromise3: Promise = expect(mock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + + const expectPromise4: Promise = expect(mock).toBeRequestedWith({ + url: 'http://localhost:8080/api/todo', // [optional] string | function | custom matcher + method: 'POST', // [optional] string | array + statusCode: 200, // [optional] number | array + requestHeaders: { Authorization: 'foo' }, // [optional] object | function | custom matcher + responseHeaders: { Authorization: 'bar' }, // [optional] object | function | custom matcher + postData: { title: 'foo', description: 'bar' }, // [optional] object | function | custom matcher + response: { success: true }, // [optional] object | function | custom matcher + }) + }) + + it('should have ts errors when typing to void', async () => { + // @ts-expect-error + const expectPromise1: void = expect(mock).toBeRequested() + // @ts-expect-error + const expectPromise2: void = expect(mock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + // @ts-expect-error + const expectPromise3: void = expect(mock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + + // @ts-expect-error + const expectPromise4: void = expect(mock).toBeRequestedWith({ + url: 'http://localhost:8080/api/todo', // [optional] string | function | custom matcher + method: 'POST', // [optional] string | array + statusCode: 200, // [optional] number | array + requestHeaders: { Authorization: 'foo' }, // [optional] object | function | custom matcher + responseHeaders: { Authorization: 'bar' }, // [optional] object | function | custom matcher + postData: { title: 'foo', description: 'bar' }, // [optional] object | function | custom matcher + response: { success: true }, // [optional] object | function | custom matcher + }) + }) + }) + // describe('Soft Assertions', () => { // it('should not have ts errors when used with chainable', async () => { diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 2a7e203a8..211e89ccd 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -256,6 +256,11 @@ interface WdioCustomMatchers extends Record{ times: number | ExpectWebdriverIO.NumberOptions, options?: ExpectWebdriverIO.NumberOptions ): Promise + + /** + * Check that `WebdriverIO.Mock` was called with the specific parameters + */ + toBeRequestedWith(requestedWith: RequestedWith, options?: ExpectWebdriverIO.CommandOptions): Promise } /** From 53317aa2610ad0a3f957283c26b5a0a15ab48519 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sun, 22 Jun 2025 21:56:35 -0400 Subject: [PATCH 10/99] Pretty solid typing for both Jest & mocha (standalone) --- jest.d.ts | 40 ++- package.json | 1 + test-types/jest/tsconfig.json | 2 +- test-types/jest/types-jest.test.ts | 171 +++++++++-- test-types/mocha/tsconfig.json | 3 +- test-types/mocha/types-mocha.test.ts | 287 ++++++++++++++++-- types/expect-webdriverio.d.ts | 46 ++- types/{global.d.ts => standalone-global.d.ts} | 0 types/standalone.d.ts | 26 +- 9 files changed, 523 insertions(+), 53 deletions(-) rename types/{global.d.ts => standalone-global.d.ts} (100%) diff --git a/jest.d.ts b/jest.d.ts index 15fa91e12..8f2ee52f8 100644 --- a/jest.d.ts +++ b/jest.d.ts @@ -1,11 +1,11 @@ /// -// TODO dprevost try to type conditional the toMatchSnapshot later... -/*/// */ - -// type ChainablePromiseElement = ReturnType // type WdioElementLike = WebdriverIO.Element | ChainablePromiseElement +// TODO dprevost - check if we need to add ChainablePromiseElement and or ChainablePromiseArrayElement +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type PromiseLikeType = Promise + declare namespace jest { interface Matchers extends WdioMatchers{ @@ -37,8 +37,36 @@ declare namespace jest { toMatchInlineSnapshot(snapshot?: string, label?: string): Promise } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - interface Expect extends WdioMatchers {} + type MatcherAndInverse = Matchers & AndNot> + interface Expect extends WdioMatchers { + + /** + * Below are the custom Expect of WebdriverIO. + * We need to define them below so that they are correctly typed. We cannot just extend 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 + */ + soft(actual: T): T extends PromiseLikeType ? MatcherAndInverse, T> : MatcherAndInverse + + /** + * 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 + } interface InverseAsymmetricMatchers extends Expect {} } \ No newline at end of file diff --git a/package.json b/package.json index 315719294..0d43ee96f 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "test:types": "node test-types/copy && npm run ts && npm run clean:tests && npm run tsc:root-types", "ts": "run-s ts:*", "ts:jest": "cd test-types/jest && tsc -p ./tsconfig.json --incremental", + "ts:mocha": "cd test-types/mocha && tsc -p ./tsconfig.json --incremental", "watch": "npm run compile -- --watch", "prepare": "husky install" }, diff --git a/test-types/jest/tsconfig.json b/test-types/jest/tsconfig.json index 2dc67605a..7860734fb 100644 --- a/test-types/jest/tsconfig.json +++ b/test-types/jest/tsconfig.json @@ -7,7 +7,7 @@ "skipLibCheck": true, "types": [ "@types/jest", - "../../jest.d.ts", // Needed to be after @types/jest to override the toMatchSnapshot typing + "../../jest.d.ts", // Needed to be after @types/jest to override the toMatchSnapshot typing "@wdio/globals/types" ] } diff --git a/test-types/jest/types-jest.test.ts b/test-types/jest/types-jest.test.ts index 416e7b7d5..57edb81ca 100644 --- a/test-types/jest/types-jest.test.ts +++ b/test-types/jest/types-jest.test.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ + describe('type assertions', () => { const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element const chainableElement = $('findMe') @@ -30,6 +31,13 @@ describe('type assertions', () => { // @ts-expect-error await expect(chainableElement).toHaveUrl('https://example.com') }) + + it('should support stringContaining', async () => { + const expectVoid1: Promise = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) + + // @ts-expect-error + const expectVoid2: void = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) + }) }) describe('element type assertions', () => { @@ -126,7 +134,6 @@ describe('type assertions', () => { describe('toBe', () => { it('should not have ts errors when typing to void when actual is boolean', async () => { - // Expect no ts errors const expectToBeIsVoid: void = expect(true).toBe(true) const expectNotToBeIsVoid: void = expect(true).not.toBe(true) }) @@ -134,16 +141,23 @@ describe('type assertions', () => { it('should have ts errors when typing to Promise when actual is boolean', async () => { //@ts-expect-error const expectToBeIsNotPromiseVoid: Promise = expect(true).toBe(true) + //@ts-expect-error const expectToBeIsNotPromiseVoid: Promise = expect(true).not.toBe(true) }) - it('should not have ts errors when typing to void when actual is an awaited element or chainable', async () => { + it('should expect void when actual is an awaited element/chainable', async () => { const isClickableElement = await element.isClickable() const expectPromiseVoid1: void = expect(isClickableElement).toBe(true) const isClickableChainable: boolean = await chainableElement.isClickable() const expectPromiseVoid2: void = expect(isClickableChainable).toBe(true) + + // @ts-expect-error + const expectPromiseVoid3: Promise = expect(isClickableElement).toBe(true) + + // @ts-expect-error + const expectPromiseVoid4: Promise = expect(isClickableChainable).toBe(true) }) }) @@ -206,14 +220,14 @@ describe('type assertions', () => { }) describe('Network Matchers', () => { - const mock = browser.mock('**/api/todo*') + const promiseNetworkMock = browser.mock('**/api/todo*') it('should not have ts errors when typing to Promise', async () => { - const expectPromise1: Promise = expect(mock).toBeRequested() - const expectPromise2: Promise = expect(mock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) - const expectPromise3: Promise = expect(mock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + const expectPromise1: Promise = expect(promiseNetworkMock).toBeRequested() + const expectPromise2: Promise = expect(promiseNetworkMock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + const expectPromise3: Promise = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 - const expectPromise4: Promise = expect(mock).toBeRequestedWith({ + const expectPromise4: Promise = expect(promiseNetworkMock).toBeRequestedWith({ url: 'http://localhost:8080/api/todo', // [optional] string | function | custom matcher method: 'POST', // [optional] string | array statusCode: 200, // [optional] number | array @@ -226,14 +240,14 @@ describe('type assertions', () => { it('should have ts errors when typing to void', async () => { // @ts-expect-error - const expectPromise1: void = expect(mock).toBeRequested() + const expectPromise1: void = expect(promiseNetworkMock).toBeRequested() // @ts-expect-error - const expectPromise2: void = expect(mock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + const expectPromise2: void = expect(promiseNetworkMock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) // @ts-expect-error - const expectPromise3: void = expect(mock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + const expectPromise3: void = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 // @ts-expect-error - const expectPromise4: void = expect(mock).toBeRequestedWith({ + const expectPromise4: void = expect(promiseNetworkMock).toBeRequestedWith({ url: 'http://localhost:8080/api/todo', // [optional] string | function | custom matcher method: 'POST', // [optional] string | array statusCode: 200, // [optional] number | array @@ -245,16 +259,133 @@ describe('type assertions', () => { }) }) - // describe('Soft Assertions', () => { + 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', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const expectAny1: any = expect.stringContaining('WebdriverIO') + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const expectAny2: any = expect.anything() + }) + + describe('Soft Assertions', async () => { + const expectString: string = await $('h1').getText() + const expectPromise: Promise = $('h1').getText() - // it('should not have ts errors when used with chainable', async () => { + describe('expect.soft', () => { + it('should not have ts error and not need to be awaited/be a promise if actual is non-promise type', async () => { + const expectWdioMatcher: WdioMatchers = expect.soft(expectString) + const expectVoid: void = expect.soft(expectString).toBe('Test Page') + }) - // const expectString: string = await $('h1').getText() - // const expectVoid: void = expect.soft(expectString).toEqual('Basketball Shoes') - // // await expect.soft(await $('#price').getText()).toMatch(/€\d+/) + it('should not have ts error and need to be awaited/be a promise if actual is a promise type', async () => { + const expectWdioMatcher: WdioMatchers, Promise> = expect.soft(expectPromise) + const expectVoid: Promise = expect.soft(expectPromise).toBe('Test Page') - // // Regular assertions still throw immediately - // // await expect(await $('.add-to-cart').isClickable()).toBe(true) - // }) - // }) + await expect.soft(expectPromise).toBe('Test Page') + }) + + it('should have ts error when using await and actual is non-promise type', async () => { + // @ts-expect-error + const expectWdioMatcher: WdioMatchers, string> = expect.soft(expectString) + + // @ts-expect-error + const expectVoid: Promise = expect.soft(expectString).toBe('Test Page') + }) + + it('should not have ts error and need to be awaited/be a promise if actual is a promise type', async () => { + // @ts-expect-error + const expectWdioMatcher: WdioMatchers> = expect.soft(expectPromise) + // @ts-expect-error + const expectVoid: void = expect.soft(expectPromise).toBe('Test Page') + }) + + it('should support chainable element', async () => { + const expectElement: WdioMatchers = expect.soft(element) + const expectElementChainable: WdioMatchers = expect.soft(chainableElement) + + // @ts-expect-error + const expectElement2: WdioMatchers, WebdriverIO.Element> = expect.soft(element) + // @ts-expect-error + const expectElementChainable2: WdioMatchers, typeof chainableElement> = expect.soft(chainableElement) + }) + + it('should support chainable element with wdio Matchers', async () => { + const expectPromise1: Promise = expect.soft(element).toBeDisplayed() + const expectPromise2: Promise = expect.soft(chainableElement).toBeDisplayed() + + // @ts-expect-error + const expectPromise3: void = expect.soft(element).toBeDisplayed() + // @ts-expect-error + const expectPromise4: void = expect.soft(chainableElement).toBeDisplayed() + + await expect.soft(element).toBeDisplayed() + await expect.soft(chainableElement).toBeDisplayed() + }) + + describe('not', async () => { + it('should support not with chainable', async () => { + const expectPromise1: Promise = expect.soft(element).not.toBeDisplayed() + const expectPromise2: Promise = expect.soft(chainableElement).not.toBeDisplayed() + + // @ts-expect-error + const expectPromise3: void = expect.soft(element).not.toBeDisplayed() + // @ts-expect-error + const expectPromise4: void = expect.soft(chainableElement).not.toBeDisplayed() + + await expect.soft(element).not.toBeDisplayed() + await expect.soft(chainableElement).not.toBeDisplayed() + }) + + it('should support not with non-promise', async () => { + const expectVoid1: void = expect.soft(expectString).not.toBe('Test Page') + + // @ts-expect-error + const expectVoid2: Promise = expect.soft(expectString).not.toBe('Test Page') + }) + + it('should support not with promise', async () => { + const expectPromise1: Promise = expect.soft(expectPromise).not.toBe('Test Page') + + // @ts-expect-error + const expectPromise2: void = expect.soft(expectPromise).not.toBe('Test Page') + + await expect.soft(expectPromise).not.toBe('Test Page') + }) + }) + }) + + describe('expect.getSoftFailures', () => { + it('should be of type `SoftFailure`', async () => { + const expectSoftFailure1: ExpectWebdriverIO.SoftFailure[] = expect.getSoftFailures() + + // @ts-expect-error + const expectSoftFailure2: void = expect.getSoftFailures() + }) + }) + + describe('expect.assertSoftFailures', () => { + it('should be of type void', async () => { + const expectVoid1: void = expect.assertSoftFailures() + + // @ts-expect-error + const expectVoid2: Promise = expect.assertSoftFailures() + }) + }) + + describe('expect.clearSoftFailures', () => { + it('should be of type void', async () => { + const expectVoid1: void = expect.clearSoftFailures() + + // @ts-expect-error + const expectVoid2: Promise = expect.clearSoftFailures() + }) + }) + }) + }) }) diff --git a/test-types/mocha/tsconfig.json b/test-types/mocha/tsconfig.json index 7a58c97f8..94868c527 100644 --- a/test-types/mocha/tsconfig.json +++ b/test-types/mocha/tsconfig.json @@ -8,7 +8,8 @@ "types": [ "@types/mocha", "expect", - "../../types/global.d.ts", + "webdriverio", + "../../types/standalone-global.d.ts", // "@wdio/types", // "@wdio/globals/types", // TODO to review adding the global wdio types interfere with local types ] diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index 145459d30..a6dfdecb4 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -1,5 +1,13 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ +/// + +import type { ChainablePromiseElement, ChainablePromiseArray } from 'webdriverio' + describe('type assertions', () => { + const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element + const chainableElement = {} as unknown as ChainablePromiseElement + const chainableArray = {} as ChainablePromiseArray + const networkMock: WebdriverIO.Mock = {} as unknown as WebdriverIO.Mock describe('toHaveUrl', () => { const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser @@ -19,22 +27,25 @@ describe('type assertions', () => { }) it('should have ts errors when actual is an element', async () => { - const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element // @ts-expect-error await expect(element).toHaveUrl('https://example.com') }) - // TODO dprevost find a way to pull @wdio/globals for ChainableElement without affecting local types... - // it('should have ts errors when actual is an ChainableElement', async () => { - // const chainableElement = $('findMe') + it('should have ts errors when actual is an ChainableElement', async () => { + // @ts-expect-error + await expect(chainableElement).toHaveUrl('https://example.com') + }) + + // TODO dprevost fix expect.stringContaining + // it('should support stringContaining', async () => { + // const expectVoid1: Promise = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) + // // @ts-expect-error - // await expect(chainableElement).toHaveUrl('https://example.com') + // const expectVoid2: void = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) // }) }) describe('element type assertions', () => { - const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element - // const chainableElement = $('findMe') describe('toBeDisabled', () => { it('should not have ts errors and be able to await the promise for element', async () => { @@ -46,14 +57,14 @@ describe('type assertions', () => { await expectNotIsPromiseVoid }) - // it('should not have ts errors and be able to await the promise for chainable', async () => { - // // expect no ts errors - // const expectIsPromiseVoid: Promise = expect(chainableElement).toBeDisabled() - // await expectIsPromiseVoid + it('should not have ts errors and be able to await the promise for chainable', async () => { + // expect no ts errors + const expectIsPromiseVoid: Promise = expect(chainableElement).toBeDisabled() + await expectIsPromiseVoid - // const expectNotIsPromiseVoid: Promise = expect(chainableElement).not.toBeDisabled() - // await expectNotIsPromiseVoid - // }) + const expectNotIsPromiseVoid: Promise = expect(chainableElement).not.toBeDisabled() + await expectNotIsPromiseVoid + }) it('should have ts errors when typing to void for element', async () => { // @ts-expect-error @@ -71,17 +82,16 @@ describe('type assertions', () => { }) describe('toMatchSnapshot', () => { - const booleanPromise: Promise = Promise.resolve(true) it('should not have ts errors when typing to Promise for an element', async () => { const expectPromise1: Promise = expect(element).toMatchSnapshot() const expectPromise2: Promise = expect(element).toMatchSnapshot('test label') }) - // it('should not have ts errors when typing to Promise for a chainable', async () => { - // const expectPromise1: Promise = expect(chainableElement).toMatchSnapshot() - // const expectPromise2: Promise = expect(chainableElement).toMatchSnapshot('test label') - // }) + it('should not have ts errors when typing to Promise for a chainable', async () => { + const expectPromise1: Promise = expect(chainableElement).toMatchSnapshot() + const expectPromise2: Promise = expect(chainableElement).toMatchSnapshot('test label') + }) // We need somehow to exclude the Jest types one for this to success it('should have ts errors when typing to void for an element like', async () => { @@ -96,21 +106,64 @@ describe('type assertions', () => { // const expectNotToBeVoid: void = expect('.findme').toMatchSnapshot() // }) }) + + describe('toMatchInlineSnapshot', () => { + + it('should not have ts errors when typing to Promise for an element', async () => { + const expectPromise1: Promise = expect(element).toMatchInlineSnapshot() + const expectPromise2: Promise = expect(element).toMatchInlineSnapshot('test snapshot') + const expectPromise3: Promise = expect(element).toMatchInlineSnapshot('test snapshot', 'test label') + }) + + it('should not have ts errors when typing to Promise for a chainable', async () => { + const expectPromise1: Promise = expect(chainableElement).toMatchInlineSnapshot() + const expectPromise2: Promise = expect(chainableElement).toMatchInlineSnapshot('test snapshot') + const expectPromise3: Promise = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + }) + + // We need somehow to exclude the Jest types one for this to success + it('should have ts errors when typing to void for an element like', async () => { + //@ts-expect-error + const expectNotToBeVoid1: void = expect(element).toMatchInlineSnapshot() + //@ts-expect-error + const expectPromise2: void = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + }) + + // TODO - conditional types check on T to have the below match void does not work + // it('should not have ts errors when typing to void for a string', async () => { + // const expectNotToBeVoid: void = expect('.findme').toMatchInlineSnapshot() + // }) + }) }) - describe('boolean type assertions', () => { - it('should not have ts errors when typing to void', async () => { - // Expect no ts errors + describe('toBe', () => { + + it('should not have ts errors when typing to void when actual is boolean', async () => { const expectToBeIsVoid: void = expect(true).toBe(true) const expectNotToBeIsVoid: void = expect(true).not.toBe(true) }) - it('should have ts errors when typing to Promise', async () => { + it('should have ts errors when typing to Promise when actual is boolean', async () => { //@ts-expect-error const expectToBeIsNotPromiseVoid: Promise = expect(true).toBe(true) + //@ts-expect-error const expectToBeIsNotPromiseVoid: Promise = expect(true).not.toBe(true) }) + + it('should expect void when actual is an awaited element/chainable', async () => { + const isClickableElement = await element.isClickable() + const expectPromiseVoid1: void = expect(isClickableElement).toBe(true) + + const isClickableChainable: boolean = await chainableElement.isClickable() + const expectPromiseVoid2: void = expect(isClickableChainable).toBe(true) + + // @ts-expect-error + const expectPromiseVoid3: Promise = expect(isClickableElement).toBe(true) + + // @ts-expect-error + const expectPromiseVoid4: Promise = expect(isClickableChainable).toBe(true) + }) }) describe('string type assertions', () => { @@ -146,6 +199,7 @@ describe('type assertions', () => { const expectToBeIsNotPromiseVoid: Promise = expect(await booleanPromise).toBe(true) }) + // On standalone, resolves and rejects are not existing // it('should have ts errors when typing resolves and reject is typed to void', async () => { // //@ts-expect-error // const expectResolvesToBeIsVoid: void = expect(booleanPromise).resolves.toBe(true) @@ -153,4 +207,191 @@ describe('type assertions', () => { // const expectRejectsToBeIsVoid: void = expect(booleanPromise).rejects.toBe(true) // }) }) + + describe('toBeElementsArrayOfSize', async () => { + + it('should not have ts errors when typing to Promise', async () => { + const listItems = await chainableArray + const expectPromise: Promise = expect(listItems).toBeElementsArrayOfSize(5) + const expectPromise1: Promise = expect(listItems).toBeElementsArrayOfSize({ lte: 10 }) + }) + + it('should have ts errors when typing to void', async () => { + const listItems = await chainableArray + // @ts-expect-error + const expectPromise: void = expect(listItems).toBeElementsArrayOfSize(5) + // @ts-expect-error + const expectPromise1: void = expect(listItems).toBeElementsArrayOfSize({ lte: 10 }) + }) + }) + + describe('Network Matchers', () => { + const promiseNetworkMock = Promise.resolve(networkMock) + + it('should not have ts errors when typing to Promise', async () => { + const expectPromise1: Promise = expect(promiseNetworkMock).toBeRequested() + const expectPromise2: Promise = expect(promiseNetworkMock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + const expectPromise3: Promise = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + + const expectPromise4: Promise = expect(promiseNetworkMock).toBeRequestedWith({ + url: 'http://localhost:8080/api/todo', // [optional] string | function | custom matcher + method: 'POST', // [optional] string | array + statusCode: 200, // [optional] number | array + requestHeaders: { Authorization: 'foo' }, // [optional] object | function | custom matcher + responseHeaders: { Authorization: 'bar' }, // [optional] object | function | custom matcher + postData: { title: 'foo', description: 'bar' }, // [optional] object | function | custom matcher + response: { success: true }, // [optional] object | function | custom matcher + }) + }) + + it('should have ts errors when typing to void', async () => { + // @ts-expect-error + const expectPromise1: void = expect(mock).toBeRequested() + // @ts-expect-error + const expectPromise2: void = expect(mock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + // @ts-expect-error + const expectPromise3: void = expect(mock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + + // @ts-expect-error + const expectPromise4: void = expect(mock).toBeRequestedWith({ + url: 'http://localhost:8080/api/todo', // [optional] string | function | custom matcher + method: 'POST', // [optional] string | array + statusCode: 200, // [optional] number | array + requestHeaders: { Authorization: 'foo' }, // [optional] object | function | custom matcher + responseHeaders: { Authorization: 'bar' }, // [optional] object | function | custom matcher + postData: { title: 'foo', description: 'bar' }, // [optional] object | function | custom matcher + response: { success: true }, // [optional] object | function | custom matcher + }) + }) + }) + + 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', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const expectAny1: any = expect.stringContaining('WebdriverIO') + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const expectAny2: any = expect.anything() + }) + + describe('Soft Assertions', async () => { + const expectString: string = 'awaited element.getText()' + const expectPromise: Promise = Promise.resolve(expectString) + + describe('expect.soft', () => { + it('should not have ts error and not need to be awaited/be a promise if actual is non-promise type', async () => { + const expectWdioMatcher: WdioMatchers = expect.soft(expectString) + const expectVoid: void = expect.soft(expectString).toBe('Test Page') + }) + + it('should not have ts error and need to be awaited/be a promise if actual is a promise type', async () => { + const expectWdioMatcher: WdioMatchers, Promise> = expect.soft(expectPromise) + const expectVoid: Promise = expect.soft(expectPromise).toBe('Test Page') + + await expect.soft(expectPromise).toBe('Test Page') + }) + + it('should have ts error when using await and actual is non-promise type', async () => { + // @ts-expect-error + const expectWdioMatcher: WdioMatchers, string> = expect.soft(expectString) + + // @ts-expect-error + const expectVoid: Promise = expect.soft(expectString).toBe('Test Page') + }) + + it('should not have ts error and need to be awaited/be a promise if actual is a promise type', async () => { + // @ts-expect-error + const expectWdioMatcher: WdioMatchers> = expect.soft(expectPromise) + // @ts-expect-error + const expectVoid: void = expect.soft(expectPromise).toBe('Test Page') + }) + + it('should support chainable element', async () => { + const expectElement: WdioMatchers = expect.soft(element) + const expectElementChainable: WdioMatchers = expect.soft(chainableElement) + + // @ts-expect-error + const expectElement2: WdioMatchers, WebdriverIO.Element> = expect.soft(element) + // @ts-expect-error + const expectElementChainable2: WdioMatchers, typeof chainableElement> = expect.soft(chainableElement) + }) + + it('should support chainable element with wdio Matchers', async () => { + const expectPromise1: Promise = expect.soft(element).toBeDisplayed() + const expectPromise2: Promise = expect.soft(chainableElement).toBeDisplayed() + + // @ts-expect-error + const expectPromise3: void = expect.soft(element).toBeDisplayed() + // @ts-expect-error + const expectPromise4: void = expect.soft(chainableElement).toBeDisplayed() + + await expect.soft(element).toBeDisplayed() + await expect.soft(chainableElement).toBeDisplayed() + }) + + describe('not', async () => { + it('should support not with chainable', async () => { + const expectPromise1: Promise = expect.soft(element).not.toBeDisplayed() + const expectPromise2: Promise = expect.soft(chainableElement).not.toBeDisplayed() + + // @ts-expect-error + const expectPromise3: void = expect.soft(element).not.toBeDisplayed() + // @ts-expect-error + const expectPromise4: void = expect.soft(chainableElement).not.toBeDisplayed() + + await expect.soft(element).not.toBeDisplayed() + await expect.soft(chainableElement).not.toBeDisplayed() + }) + + it('should support not with non-promise', async () => { + const expectVoid1: void = expect.soft(expectString).not.toBe('Test Page') + + // @ts-expect-error + const expectVoid2: Promise = expect.soft(expectString).not.toBe('Test Page') + }) + + it('should support not with promise', async () => { + const expectPromise1: Promise = expect.soft(expectPromise).not.toBe('Test Page') + + // @ts-expect-error + const expectPromise2: void = expect.soft(expectPromise).not.toBe('Test Page') + + await expect.soft(expectPromise).not.toBe('Test Page') + }) + }) + }) + + describe('expect.getSoftFailures', () => { + it('should be of type `SoftFailure`', async () => { + const expectSoftFailure1: ExpectWebdriverIO.SoftFailure[] = expect.getSoftFailures() + + // @ts-expect-error + const expectSoftFailure2: void = expect.getSoftFailures() + }) + }) + + describe('expect.assertSoftFailures', () => { + it('should be of type void', async () => { + const expectVoid1: void = expect.assertSoftFailures() + + // @ts-expect-error + const expectVoid2: Promise = expect.assertSoftFailures() + }) + }) + + describe('expect.clearSoftFailures', () => { + it('should be of type void', async () => { + const expectVoid1: void = expect.clearSoftFailures() + + // @ts-expect-error + const expectVoid2: Promise = expect.clearSoftFailures() + }) + }) + }) + }) }) diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 211e89ccd..293228573 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -7,12 +7,19 @@ 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 + +// TODO dprevost - check if we need to add ChainablePromiseElement and or ChainablePromiseArrayElement +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type PromiseLikeType = Promise /** * 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. */ +// TODO dprevost have browser matchers and element matchers separated interface WdioCustomMatchers extends Record{ // ===== $ or $$ ===== /** @@ -283,6 +290,37 @@ interface WdioOverloadedMatchers { interface WdioMatchers extends WdioCustomMatchers, WdioOverloadedMatchers {} + +/** + * expect function declaration, containing 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 + */ +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 + */ + soft(actual: T): T extends PromiseLikeType ? Matchers, T> : Matchers + + /** + * 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 +} + declare namespace ExpectWebdriverIO { function setOptions(options: DefaultOptions): void function getConfig(): any @@ -439,11 +477,17 @@ declare namespace ExpectWebdriverIO { asymmetricMatch(...args: any[]): boolean toString(): string } + + interface SoftFailure { + error: Error; + matcherName: string; + location?: string; + } } declare module 'expect-webdriverio' { // TODO dprevost should we also have an expect const here too? - const matchers: WdioCustomMatchers; + const matchers: WdioCustomMatchers; export = matchers; } diff --git a/types/global.d.ts b/types/standalone-global.d.ts similarity index 100% rename from types/global.d.ts rename to types/standalone-global.d.ts diff --git a/types/standalone.d.ts b/types/standalone.d.ts index dc6a97cd1..8a5cf9866 100644 --- a/types/standalone.d.ts +++ b/types/standalone.d.ts @@ -18,6 +18,8 @@ declare namespace ExpectWebdriverIO { interface Matchers extends WdioMatchers, ExpectMatchers {} + type MatchersAndInverse = Matchers & Inverse>; + /** * Mostly derived from the types of `jest-expect` but adapted to work with WebdriverIO. * @see https://github.com/jestjs/jest/blob/main/packages/jest-expect/src/types.ts @@ -29,7 +31,29 @@ declare namespace ExpectWebdriverIO { * * @param actual The value to apply matchers against. */ - (actual: T): Matchers & Inverse> + (actual: T): MatchersAndInverse; + + /** + * 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): T extends PromiseLikeType ? MatchersAndInverse, T> : MatchersAndInverse + + /** + * 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 } interface InverseAsymmetricMatchers extends Expect {} From 2d4fb6e600b696777fee1470fcfec21559bd2ed4 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sun, 22 Jun 2025 23:17:47 -0400 Subject: [PATCH 11/99] Bring back soft assertion + add tsc + code review - Brought back soft assertion with good typing coverage for Jest and mocha (standalone) - Add tsc new script since it was missing - Skip pre-existing tsc error in request network matchers that I'm not sure of --- jest.d.ts | 6 +- src/index.ts | 35 +- src/matchers.ts | 3 +- src/matchers/mock/toBeRequestedWith.ts | 2 +- src/snapshot.ts | 2 +- src/softAssert.ts | 139 +++++ src/softAssertService.ts | 86 ++++ src/softExpect.ts | 105 ++++ test-types/jest/types-jest.test.ts | 8 +- test-types/mocha/types-mocha.test.ts | 8 +- test/matchers.test.ts | 2 +- test/matchers/mock/toBeRequestedTimes.test.ts | 35 +- test/matchers/mock/toBeRequestedWith.test.ts | 2 +- test/softAssertions.test.ts | 483 ++++++++++++++++++ tsconfig.json | 3 +- types/expect-webdriverio.d.ts | 90 +++- types/standalone-global.d.ts | 2 +- 17 files changed, 946 insertions(+), 65 deletions(-) create mode 100644 src/softAssert.ts create mode 100644 src/softAssertService.ts create mode 100644 src/softExpect.ts create mode 100644 test/softAssertions.test.ts diff --git a/jest.d.ts b/jest.d.ts index 8f2ee52f8..2f9c264ba 100644 --- a/jest.d.ts +++ b/jest.d.ts @@ -2,10 +2,6 @@ // type WdioElementLike = WebdriverIO.Element | ChainablePromiseElement -// TODO dprevost - check if we need to add ChainablePromiseElement and or ChainablePromiseArrayElement -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type PromiseLikeType = Promise - declare namespace jest { interface Matchers extends WdioMatchers{ @@ -55,7 +51,7 @@ declare namespace jest { /** * Get all current soft assertion failures */ - getSoftFailures(testId?: string): SoftFailure[] + getSoftFailures(testId?: string): ExpectWebdriverIO.SoftFailure[] /** * Manually assert all soft failures (throws an error if any failures exist) diff --git a/src/index.ts b/src/index.ts index 44fc295f1..e095b64b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,10 @@ /// import { expect as expectLib } from 'expect' import type { RawMatcherFn } 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() const filteredMatchers = {} @@ -31,22 +32,22 @@ expectLib.extend(filteredMatchers as MatchersObject) // Extend the expect object with soft assertions const expectWithSoft = expectLib as unknown as ExpectWebdriverIO.Expect -// Object.defineProperty(expectWithSoft, 'soft', { -// value: (actual: T) => createSoftExpect(actual) -// }) +Object.defineProperty(expectWithSoft, 'soft', { + value: (actual: T) => createSoftExpect(actual) +}) -// // Add soft assertions utility methods -// Object.defineProperty(expectWithSoft, 'getSoftFailures', { -// value: (testId?: string) => SoftAssertService.getInstance().getFailures(testId) -// }) +// Add soft assertions utility methods +Object.defineProperty(expectWithSoft, 'getSoftFailures', { + value: (testId?: string) => SoftAssertService.getInstance().getFailures(testId) +}) -// Object.defineProperty(expectWithSoft, 'assertSoftFailures', { -// value: (testId?: string) => SoftAssertService.getInstance().assertNoFailures(testId) -// }) +Object.defineProperty(expectWithSoft, 'assertSoftFailures', { + value: (testId?: string) => SoftAssertService.getInstance().assertNoFailures(testId) +}) -// Object.defineProperty(expectWithSoft, 'clearSoftFailures', { -// value: (testId?: string) => SoftAssertService.getInstance().clearFailures(testId) -// }) +Object.defineProperty(expectWithSoft, 'clearSoftFailures', { + value: (testId?: string) => SoftAssertService.getInstance().clearFailures(testId) +}) export const expect = expectWithSoft @@ -61,6 +62,12 @@ export const setDefaultOptions = (options = {}): void => { } export const setOptions = setDefaultOptions +/** + * export soft assertion utilities + */ +export { SoftAssertService } from './softAssert.js' +export { SoftAssertionService, type SoftAssertionServiceOptions } from './softAssertService.js' + /** * export snapshot utilities */ diff --git a/src/matchers.ts b/src/matchers.ts index eed5a93b0..8a1f23c4c 100644 --- a/src/matchers.ts +++ b/src/matchers.ts @@ -28,5 +28,4 @@ export * from './matchers/elements/toBeElementsArrayOfSize.js' export * from './matchers/mock/toBeRequested.js' export * from './matchers/mock/toBeRequestedTimes.js' export * from './matchers/mock/toBeRequestedWith.js' -export * from './matchers/snapshot.js' - +export * from './matchers/snapshot.js' \ No newline at end of file diff --git a/src/matchers/mock/toBeRequestedWith.ts b/src/matchers/mock/toBeRequestedWith.ts index a87dec074..ad55a5eb4 100644 --- a/src/matchers/mock/toBeRequestedWith.ts +++ b/src/matchers/mock/toBeRequestedWith.ts @@ -402,4 +402,4 @@ const deleteUndefinedValues = (obj: Record, baseline = obj) => export function toBeRequestedWithResponse(...args: unknown[]) { return toBeRequestedWith.call(this, ...args) -} \ No newline at end of file +} diff --git a/src/snapshot.ts b/src/snapshot.ts index 8223dfafe..39d4af176 100644 --- a/src/snapshot.ts +++ b/src/snapshot.ts @@ -129,4 +129,4 @@ export class SnapshotService implements Services.ServiceInstance { } return service } -} \ No newline at end of file +} diff --git a/src/softAssert.ts b/src/softAssert.ts new file mode 100644 index 000000000..dd3d866b4 --- /dev/null +++ b/src/softAssert.ts @@ -0,0 +1,139 @@ +import type { AssertionError } from 'node:assert' + +interface SoftFailure { + error: AssertionError | Error; + matcherName: string; + location?: string; +} + +interface TestIdentifier { + id: string; + name?: string; + file?: string; +} + +/** + * Soft assertion service to collect failures without stopping test execution + */ +export class SoftAssertService { + private static instance: SoftAssertService + private failureMap: Map = new Map() + private currentTest: TestIdentifier | null = null + + private constructor() { } + + /** + * Get singleton instance + */ + public static getInstance(): SoftAssertService { + if (!SoftAssertService.instance) { + SoftAssertService.instance = new SoftAssertService() + } + return SoftAssertService.instance + } + + /** + * Set the current test context + */ + public setCurrentTest(testId: string, testName?: string, testFile?: string): void { + this.currentTest = { id: testId, name: testName, file: testFile } + if (!this.failureMap.has(testId)) { + this.failureMap.set(testId, []) + } + } + + /** + * Clear the current test context + */ + public clearCurrentTest(): void { + this.currentTest = null + } + + /** + * Get current test ID + */ + public getCurrentTestId(): string | null { + return this.currentTest?.id || null + } + + /** + * Add a soft failure for the current test + */ + public addFailure(error: Error, matcherName: string): void { + const testId = this.getCurrentTestId() + if (!testId) { + throw error // If no test context, throw the error immediately + } + + // Extract stack information to get file and line number + const stackLines = error.stack?.split('\n') || [] + let location = '' + + // Find the first non-expect-webdriverio line in the stack + for (const line of stackLines) { + if (line && !line.includes('expect-webdriverio') && !line.includes('node_modules')) { + location = line.trim() + break + } + } + + const failures = this.failureMap.get(testId) || [] + failures.push({ error, matcherName, location }) + this.failureMap.set(testId, failures) + } + + /** + * Get all failures for a specific test + */ + public getFailures(testId?: string): SoftFailure[] { + const id = testId || this.getCurrentTestId() + if (!id) { + return [] + } + return this.failureMap.get(id) || [] + } + + /** + * Clear failures for a specific test + */ + public clearFailures(testId?: string): void { + const id = testId || this.getCurrentTestId() + if (id) { + this.failureMap.delete(id) + } + } + + /** + * Throw an aggregated error if there are failures for the current test + */ + public assertNoFailures(testId?: string): void { + const id = testId || this.getCurrentTestId() + if (!id) { + return + } + + const failures = this.getFailures(id) + if (failures.length === 0) { + return + } + + // Create a formatted error message with all failures + let message = `${failures.length} soft assertion failure${failures.length > 1 ? 's' : ''}:\n\n` + + failures.forEach((failure, index) => { + message += `${index + 1}) ${failure.matcherName}: ${failure.error.message}\n` + if (failure.location) { + message += ` at ${failure.location}\n` + } + message += '\n' + }) + + // Clear failures for this test to prevent duplicate reporting + this.clearFailures(id) + + // Throw an aggregated error + const error = new Error(message) + error.name = 'SoftAssertionsError' + throw error + } +} \ No newline at end of file diff --git a/src/softAssertService.ts b/src/softAssertService.ts new file mode 100644 index 000000000..85f01b1a4 --- /dev/null +++ b/src/softAssertService.ts @@ -0,0 +1,86 @@ +import type { Services } from '@wdio/types' +import type { Frameworks } from '@wdio/types' +import { SoftAssertService } from './softAssert.js' + +export interface SoftAssertionServiceOptions { + autoAssertOnTestEnd?: boolean; +} + +/** + * WebdriverIO service to integrate soft assertions into the test lifecycle + */ +export class SoftAssertionService implements Services.ServiceInstance { + private softAssertService: SoftAssertService + public options: SoftAssertionServiceOptions + + constructor( + serviceOptions?: SoftAssertionServiceOptions, + ) { + this.softAssertService = SoftAssertService.getInstance() + this.options = { + autoAssertOnTestEnd: true, + ...serviceOptions + } + } + + /** + * Hook before a test starts + */ + beforeTest(test: Frameworks.Test) { + const testId = this.getTestId(test) + this.softAssertService.setCurrentTest(testId, test.title, test.file) + } + + /** + * Hook before a Cucumber step starts + */ + beforeStep(step: Frameworks.PickleStep, scenario: Frameworks.Scenario) { + const stepId = `${scenario.uri || ''}:${scenario.name || ''}:${step.text || ''}` + this.softAssertService.setCurrentTest(stepId, step.text, scenario.uri) + } + + /** + * Hook after a test completes + */ + afterTest(test: Frameworks.Test, _: unknown, result: Frameworks.TestResult) { + // Only assert failures if: + // 1. The test hasn't yet failed for another reason + // 2. Auto-assertion is enabled in the configuration + if (!result.error && this.options.autoAssertOnTestEnd) { + try { + const testId = this.getTestId(test) + this.softAssertService.assertNoFailures(testId) + } catch (error) { + // Update the test result with our aggregated error + result.error = error + result.passed = false + } + } + this.softAssertService.clearCurrentTest() + } + + /** + * Hook after a Cucumber step completes + */ + afterStep(step: Frameworks.PickleStep, scenario: Frameworks.Scenario, result: { passed: boolean, error?: Error }) { + // Only assert failures if the step hasn't already failed for another reason + if (result.passed) { + try { + const stepId = `${scenario.uri || ''}:${scenario.name || ''}:${step.text || ''}` + this.softAssertService.assertNoFailures(stepId) + } catch (error) { + // Update the step result with our aggregated error + result.error = error as Error + result.passed = false + } + } + this.softAssertService.clearCurrentTest() + } + + /** + * Generate a unique test ID from a test object + */ + private getTestId(test: Frameworks.Test): string { + return `${test.file || ''}:${test.parent || ''}:${test.title || ''}` + } +} \ No newline at end of file diff --git a/src/softExpect.ts b/src/softExpect.ts new file mode 100644 index 000000000..270eb6686 --- /dev/null +++ b/src/softExpect.ts @@ -0,0 +1,105 @@ +import { expect, matchers } from './index.js' +import { SoftAssertService } from './softAssert.js' + +/** + * Creates a soft assertion wrapper using lazy evaluation + * Only creates matchers when they're actually accessed + */ +const createSoftExpect = (actual: T): ExpectWebdriverIO.Matchers, T> => { + const softService = SoftAssertService.getInstance() + + // Use a simple proxy that creates matchers on-demand + return new Proxy({} as ExpectWebdriverIO.Matchers, T>, { + get(target, prop) { + const propName = String(prop) + + // Handle .not specially + if (propName === 'not') { + return createSoftNotProxy(actual, softService) + } + + // Handle resolves/rejects (rarely used in WebdriverIO) + if (propName === 'resolves' || propName === 'rejects') { + return createSoftChainProxy(actual, propName, softService) + } + + // Handle matchers + if (matchers.has(propName)) { + return createSoftMatcher(actual, propName, softService) + } + + // For any other properties, return undefined + return undefined + } + }) +} + +/** + * Creates a soft .not proxy + */ +const createSoftNotProxy = (actual: T, softService: SoftAssertService) => { + return new Proxy({} as ExpectWebdriverIO.Matchers, T>, { + get(target, prop) { + const propName = String(prop) + if (matchers.has(propName)) { + return createSoftMatcher(actual, propName, softService, 'not') + } + return undefined + } + }) +} + +/** + * Creates a soft chain proxy (resolves/rejects) + */ +const createSoftChainProxy = (actual: T, chainType: string, softService: SoftAssertService) => { + return new Proxy({} as ExpectWebdriverIO.Matchers, T>, { + get(target, prop) { + const propName = String(prop) + if (matchers.has(propName)) { + return createSoftMatcher(actual, propName, softService, chainType) + } + return undefined + } + }) +} + +/** + * Creates a single soft matcher function + */ +const createSoftMatcher = ( + actual: T, + matcherName: string, + softService: SoftAssertService, + prefix?: string +) => { + return async (...args: unknown[]) => { + try { + // Build the expectation chain + let expectChain = expect(actual) + + if (prefix === 'not') { + expectChain = expectChain.not + } else if (prefix === 'resolves') { + expectChain = expectChain.resolves + } else if (prefix === 'rejects') { + expectChain = expectChain.rejects + } + + return await ((expectChain as unknown) as Record Promise>)[matcherName](...args) + + } catch (error) { + // Record the failure + const fullMatcherName = prefix ? `${prefix}.${matcherName}` : matcherName + softService.addFailure(error as Error, fullMatcherName) + + // Return a passing result to continue execution + return { + pass: true, + message: () => `Soft assertion failed: ${fullMatcherName}` + } + } + } +} + +export default createSoftExpect \ No newline at end of file diff --git a/test-types/jest/types-jest.test.ts b/test-types/jest/types-jest.test.ts index 57edb81ca..fc70dc640 100644 --- a/test-types/jest/types-jest.test.ts +++ b/test-types/jest/types-jest.test.ts @@ -140,10 +140,10 @@ describe('type assertions', () => { it('should have ts errors when typing to Promise when actual is boolean', async () => { //@ts-expect-error - const expectToBeIsNotPromiseVoid: Promise = expect(true).toBe(true) + const expectToBeIsNotPromiseVoid1: Promise = expect(true).toBe(true) //@ts-expect-error - const expectToBeIsNotPromiseVoid: Promise = expect(true).not.toBe(true) + const expectToBeIsNotPromiseVoid2: Promise = expect(true).not.toBe(true) }) it('should expect void when actual is an awaited element/chainable', async () => { @@ -189,9 +189,9 @@ describe('type assertions', () => { it('should have ts errors when typing to Promise', async () => { //@ts-expect-error - const expectToBeIsNotPromiseVoid: Promise = expect(booleanPromise).toBe(true) + const expectToBeIsNotPromiseVoid1: Promise = expect(booleanPromise).toBe(true) //@ts-expect-error - const expectToBeIsNotPromiseVoid: Promise = expect(await booleanPromise).toBe(true) + const expectToBeIsNotPromiseVoid2: Promise = expect(await booleanPromise).toBe(true) }) it('should have ts errors when typing resolves and reject is typed to void', async () => { diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index a6dfdecb4..0fa276f59 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -145,10 +145,10 @@ describe('type assertions', () => { it('should have ts errors when typing to Promise when actual is boolean', async () => { //@ts-expect-error - const expectToBeIsNotPromiseVoid: Promise = expect(true).toBe(true) + const expectToBeIsNotPromiseVoid1: Promise = expect(true).toBe(true) //@ts-expect-error - const expectToBeIsNotPromiseVoid: Promise = expect(true).not.toBe(true) + const expectToBeIsNotPromiseVoid2: Promise = expect(true).not.toBe(true) }) it('should expect void when actual is an awaited element/chainable', async () => { @@ -194,9 +194,9 @@ describe('type assertions', () => { it('should have ts errors when typing to Promise', async () => { //@ts-expect-error - const expectToBeIsNotPromiseVoid: Promise = expect(booleanPromise).toBe(true) + const expectToBeIsNotPromiseVoid1: Promise = expect(booleanPromise).toBe(true) //@ts-expect-error - const expectToBeIsNotPromiseVoid: Promise = expect(await booleanPromise).toBe(true) + const expectToBeIsNotPromiseVoid2: Promise = expect(await booleanPromise).toBe(true) }) // On standalone, resolves and rejects are not existing diff --git a/test/matchers.test.ts b/test/matchers.test.ts index f1008985f..967657415 100644 --- a/test/matchers.test.ts +++ b/test/matchers.test.ts @@ -66,4 +66,4 @@ test('allows to add matcher', () => { //// @ts-expect-error not in types expectLib('foo').toBeCustom('foo') expect(matchers.keys()).toContain('toBeCustom') -}) \ No newline at end of file +}) diff --git a/test/matchers/mock/toBeRequestedTimes.test.ts b/test/matchers/mock/toBeRequestedTimes.test.ts index 22cd9eb78..cf6542324 100644 --- a/test/matchers/mock/toBeRequestedTimes.test.ts +++ b/test/matchers/mock/toBeRequestedTimes.test.ts @@ -7,8 +7,9 @@ import { removeColors, getReceived, getExpected, getExpectMessage } from '../../ vi.mock('@wdio/globals') -class TestMock implements Mock { - _calls: Matches[] +//@ts-ignore TODO fix me +class TestMock implements WebdriverIO.Mock { + _calls: local.NetworkResponseCompletedParameters[] constructor () { this._calls = [] @@ -17,16 +18,17 @@ class TestMock implements Mock { return this._calls } on = vi.fn() - abort () { return Promise.resolve() } - abortOnce () { return Promise.resolve() } - respond () { return Promise.resolve() } - respondOnce () { return Promise.resolve() } - clear () { return Promise.resolve() } - restore () { return Promise.resolve() } + abort () { return this } + abortOnce () { return this } + respond () { return this } + respondOnce () { return this } + clear () { return this } + restore () { return Promise.resolve(this) } waitForResponse () { return Promise.resolve(true) } } -const mockMatch: Matches = { +const mockMatch: local.NetworkResponseCompletedParameters = { + //@ts-ignore TODO fix me body: 'foo', url: '/foo/bar', method: 'POST', @@ -39,7 +41,8 @@ const mockMatch: Matches = { describe('toBeRequestedTimes', () => { test('wait for success', async () => { - const mock: Mock = new TestMock() + //@ts-ignore TODO fix me + const mock: WebdriverIO.Mock = new TestMock() setTimeout(() => { mock.calls.push(mockMatch) @@ -63,7 +66,8 @@ describe('toBeRequestedTimes', () => { }) test('wait for success using number options', async () => { - const mock: Mock = new TestMock() + //@ts-ignore TODO fix me + const mock: WebdriverIO.Mock = new TestMock() setTimeout(() => { mock.calls.push(mockMatch) @@ -76,7 +80,8 @@ describe('toBeRequestedTimes', () => { }) test('wait but failure', async () => { - const mock: Mock = new TestMock() + //@ts-ignore TODO fix me + const mock: WebdriverIO.Mock = new TestMock() const result = await toBeRequestedTimes.call({}, mock, 1) expect(result.pass).toBe(false) @@ -98,7 +103,8 @@ describe('toBeRequestedTimes', () => { }) test('not to be called', async () => { - const mock: Mock = new TestMock() + //@ts-ignore TODO fix me + const mock: WebdriverIO.Mock = new TestMock() // expect(mock).not.toBeRequestedTimes(0) should fail const result = await toBeRequestedTimes.call({ isNot: true }, mock, 0) @@ -120,7 +126,8 @@ describe('toBeRequestedTimes', () => { }) test('message', async () => { - const mock: Mock = new TestMock() + //@ts-ignore TODO fix me + const mock: WebdriverIO.Mock = new TestMock() const result = await toBeRequestedTimes.call({}, mock, 0) expect(result.message()).toContain('Expect mock to be called 0 times') diff --git a/test/matchers/mock/toBeRequestedWith.test.ts b/test/matchers/mock/toBeRequestedWith.test.ts index f4a03ef7f..0fd51ff70 100644 --- a/test/matchers/mock/toBeRequestedWith.test.ts +++ b/test/matchers/mock/toBeRequestedWith.test.ts @@ -484,4 +484,4 @@ describe('toBeRequestedWith', () => { }` ) }) -}) \ No newline at end of file +}) diff --git a/test/softAssertions.test.ts b/test/softAssertions.test.ts new file mode 100644 index 000000000..c6c998d0c --- /dev/null +++ b/test/softAssertions.test.ts @@ -0,0 +1,483 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { $ } from '@wdio/globals' +import { expect as expectWdio, SoftAssertionService, SoftAssertService } from '../src/index.js' +import type { TestResult } from '@wdio/types/build/Frameworks' + +vi.mock('@wdio/globals') + +describe('Soft Assertions', () => { + // Setup a mock element for testing + let el: any + + beforeEach(async () => { + el = $('sel') + // We need to mock getText() which is what the toHaveText matcher actually calls + el.getText = vi.fn().mockImplementation(() => 'Actual Text') + // Clear any soft assertion failures before each test + expectWdio.clearSoftFailures() + }) + + describe('expect.soft', () => { + it('should not throw immediately on failure', async () => { + const softService = SoftAssertService.getInstance() + softService.setCurrentTest('test-1', 'test name', 'test file') + + await expectWdio.soft(el).toHaveText('Expected Text') + + // Verify the failure was recorded + const failures = expectWdio.getSoftFailures() + expect(failures.length).toBe(1) + expect(failures[0].matcherName).toBe('toHaveText') + expect(failures[0].error.message).toContain('text') + }) + + it('should support chained assertions with .not', async () => { + // Setup a test ID for this test + const softService = SoftAssertService.getInstance() + softService.setCurrentTest('test-2', 'test name', 'test file') + + // This should not throw even though it fails + await expectWdio.soft(el).not.toHaveText('Actual Text') + + // Verify the failure was recorded + const failures = expectWdio.getSoftFailures() + expect(failures.length).toBe(1) + expect(failures[0].matcherName).toBe('not.toHaveText') + }) + + it('should support multiple soft failures in the same test', async () => { + // Setup a test ID for this test + const softService = SoftAssertService.getInstance() + softService.setCurrentTest('test-3', 'test name', 'test file') + + // These should not throw even though they fail + await expectWdio.soft(el).toHaveText('First Expected') + await expectWdio.soft(el).toHaveText('Second Expected') + await expectWdio.soft(el).toHaveText('Third Expected') + + // Verify all failures were recorded + const failures = expectWdio.getSoftFailures() + expect(failures.length).toBe(3) + expect(failures[0].matcherName).toBe('toHaveText') + expect(failures[1].matcherName).toBe('toHaveText') + expect(failures[2].matcherName).toBe('toHaveText') + }) + + it('should allow passing assertions', async () => { + // Set up a test ID for this test + const softService = SoftAssertService.getInstance() + softService.setCurrentTest('test-4', 'test name', 'test file') + + // This should pass normally + await expectWdio.soft(el).toHaveText('Actual Text') + + // Verify no failures were recorded + const failures = expectWdio.getSoftFailures() + expect(failures.length).toBe(0) + }) + + it('assertSoftFailures should throw if failures exist', async () => { + // Setup a test ID for this test + const softService = SoftAssertService.getInstance() + softService.setCurrentTest('test-5', 'test name', 'test file') + + // Record a failure + await expectWdio.soft(el).toHaveText('Expected Text') + + // Should throw when asserting failures + await expect(() => expectWdio.assertSoftFailures()).toThrow(/1 soft assertion failure/) + }) + + it('clearSoftFailures should remove all failures', async () => { + // Setup a test ID for this test + const softService = SoftAssertService.getInstance() + softService.setCurrentTest('test-6', 'test name', 'test file') + + // Record failures + await expectWdio.soft(el).toHaveText('First Expected') + await expectWdio.soft(el).toHaveText('Second Expected') + + // Verify failures were recorded + expect(expectWdio.getSoftFailures().length).toBe(2) + + // Clear failures + expectWdio.clearSoftFailures() + + // Should be no failures now + expect(expectWdio.getSoftFailures().length).toBe(0) + }) + }) + + describe('SoftAssertService hooks', () => { + it('afterTest should throw if soft failures exist', () => { + const softService = SoftAssertService.getInstance() + softService.setCurrentTest('test-hooks-1', 'test hooks', 'test file') + + const error = new Error('Test failure') + softService.addFailure(error, 'toBeDisplayed') + + // Create mock test result object + const testResult = { passed: true, error: 'undefined' } as TestResult + + // Create a mock service + const service = new SoftAssertionService() + + // Call afterTest hook - this should update the result + service.afterTest({ + file: 'test file', parent: '', title: 'test hooks', + fullName: '', + ctx: undefined, + type: '', + fullTitle: '', + pending: false + }, null, testResult) + + // Verify the test result was updated + expect(testResult.passed).toBe(true) + expect(testResult.error).toBeDefined() + }) + }) + + describe('Different Matcher Types', () => { + beforeEach(async () => { + el = $('sel') + // Mock different methods for different matchers + el.getText = vi.fn().mockImplementation(() => 'Actual Text') + el.isDisplayed = vi.fn().mockImplementation(() => false) + el.getAttribute = vi.fn().mockImplementation(() => 'actual-class') + el.isClickable = vi.fn().mockImplementation(() => false) + expectWdio.clearSoftFailures() + }) + + it('should handle boolean matchers', async () => { + const softService = SoftAssertService.getInstance() + softService.setCurrentTest('boolean-test', 'boolean test', 'test file') + + // Test boolean matcher + await expectWdio.soft(el).toBeDisplayed() + await expectWdio.soft(el).toBeClickable() + + const failures = expectWdio.getSoftFailures() + expect(failures.length).toBe(2) + expect(failures[0].matcherName).toBe('toBeDisplayed') + expect(failures[1].matcherName).toBe('toBeClickable') + }) + + it('should handle attribute matchers with multiple parameters', async () => { + const softService = SoftAssertService.getInstance() + softService.setCurrentTest('attribute-test', 'attribute test', 'test file') + + await expectWdio.soft(el).toHaveAttribute('class', 'expected-class') + + const failures = expectWdio.getSoftFailures() + expect(failures.length).toBe(1) + expect(failures[0].matcherName).toBe('toHaveAttribute') + }) + + it('should handle matchers with options', async () => { + const softService = SoftAssertService.getInstance() + softService.setCurrentTest('options-test', 'options test', 'test file') + + await expectWdio.soft(el).toHaveText('Expected', { ignoreCase: true, wait: 1000 }) + + const failures = expectWdio.getSoftFailures() + expect(failures.length).toBe(1) + expect(failures[0].matcherName).toBe('toHaveText') + }) + }) + + describe('Test Isolation', () => { + it('should isolate failures between different test contexts', async () => { + const softService = SoftAssertService.getInstance() + + // Test 1 + softService.setCurrentTest('isolation-test-1', 'test 1', 'file1') + await expectWdio.soft(el).toHaveText('Expected Text 1') + expect(expectWdio.getSoftFailures().length).toBe(1) + + // Test 2 - should have separate failures + softService.setCurrentTest('isolation-test-2', 'test 2', 'file2') + await expectWdio.soft(el).toHaveText('Expected Text 2') + + // Test 2 should only see its own failure + expect(expectWdio.getSoftFailures('isolation-test-2').length).toBe(1) + expect(expectWdio.getSoftFailures('isolation-test-1').length).toBe(1) + + // Clear one test shouldn't affect the other + expectWdio.clearSoftFailures('isolation-test-1') + expect(expectWdio.getSoftFailures('isolation-test-1').length).toBe(0) + expect(expectWdio.getSoftFailures('isolation-test-2').length).toBe(1) + }) + + it('should handle calls without test context gracefully', async () => { + const softService = SoftAssertService.getInstance() + softService.clearCurrentTest() // No test context + + // Should throw immediately when no test context + await expect(async () => { + await expectWdio.soft(el).toHaveText('Expected Text') + }).rejects.toThrow() + }) + + it('should handle rapid concurrent soft assertions', async () => { + const softService = SoftAssertService.getInstance() + softService.setCurrentTest('concurrent-test', 'concurrent', 'test file') + + el.getText = vi.fn().mockImplementation(() => 'Actual Text') + el.isDisplayed = vi.fn().mockImplementation(() => false) + el.isClickable = vi.fn().mockImplementation(() => false) + + // Fire multiple assertions rapidly + const promises = [ + expectWdio.soft(el).toHaveText('Expected 1'), + expectWdio.soft(el).toBeDisplayed(), + expectWdio.soft(el).toBeClickable() + ] + + await Promise.all(promises) + + const failures = expectWdio.getSoftFailures() + + expect(failures.length).toBe(3) + + // Verify all different matchers were recorded + const matcherNames = failures.map(f => f.matcherName) + expect(matcherNames).toContain('toHaveText') + expect(matcherNames).toContain('toBeDisplayed') + expect(matcherNames).toContain('toBeClickable') + }) + }) + + describe('Edge Cases & Error Handling', () => { + it('should handle matcher that throws non-standard errors', async () => { + const softService = SoftAssertService.getInstance() + softService.setCurrentTest('error-test', 'error test', 'test file') + + // Mock a matcher that throws a unique error + const originalMethod = el.getText + el.getText = vi.fn().mockImplementation(() => { + throw new TypeError('Weird browser error') + }) + + await expectWdio.soft(el).toHaveText('Expected Text') + + const failures = expectWdio.getSoftFailures() + expect(failures.length).toBe(1) + expect(failures[0].error).toBeInstanceOf(Error) + expect(failures[0].error.message).toContain('Weird browser error') + + // Restore + el.getText = originalMethod + }) + + it('should handle very long error messages', async () => { + const softService = SoftAssertService.getInstance() + softService.setCurrentTest('long-error-test', 'long error', 'test file') + + const veryLongText = 'A'.repeat(10000) + await expectWdio.soft(el).toHaveText(veryLongText) + + const failures = expectWdio.getSoftFailures() + expect(failures.length).toBe(1) + expect(failures[0].error.message.length).toBeGreaterThan(0) + }) + + it('should handle null/undefined values gracefully', async () => { + const softService = SoftAssertService.getInstance() + softService.setCurrentTest('null-test', 'null test', 'test file') + + // Test with null/undefined values + await expectWdio.soft(el).toHaveText(null as any) + await expectWdio.soft(el).toHaveAttribute('class', undefined as any) + + const failures = expectWdio.getSoftFailures() + expect(failures.length).toBe(2) + }) + + it('should capture error location information', async () => { + const softService = SoftAssertService.getInstance() + softService.setCurrentTest('location-test', 'location test', 'test file') + + await expectWdio.soft(el).toHaveText('Expected Text') + + const failures = expectWdio.getSoftFailures() + expect(failures.length).toBe(1) + + // Should have location info (if implemented) + if (failures[0].location) { + expect(failures[0].location).toBeTruthy() + expect(typeof failures[0].location).toBe('string') + } + }) + + it('should handle maximum failure limits', async () => { + const softService = SoftAssertService.getInstance() + softService.setCurrentTest('limit-test', 'limit test', 'test file') + + // Generate many failures + const promises = [] + for (let i = 0; i < 150; i++) { + promises.push(expectWdio.soft(el).toHaveText(`Expected ${i}`)) + } + + await Promise.all(promises) + + const failures = expectWdio.getSoftFailures() + // Should either limit failures or handle large numbers gracefully + expect(failures.length).toBeGreaterThan(0) + expect(failures.length).toBeLessThanOrEqual(150) + }) + }) + + describe('SoftAssertionService Configuration', () => { + beforeEach(() => { + expectWdio.clearSoftFailures() + }) + + it('should auto-assert failures by default', () => { + const softService = SoftAssertService.getInstance() + + const testId = 'test file::config default' + softService.setCurrentTest(testId, 'config default', 'test file') + + const error = new Error('Test failure') + softService.addFailure(error, 'toBeDisplayed') + + const service = new SoftAssertionService() + + const testResult = { passed: true, error: undefined } as TestResult + + service.afterTest({ + file: 'test file', + parent: '', + title: 'config default', + fullName: '', + ctx: undefined, + type: '', + fullTitle: '', + pending: false + }, null, testResult) + + expect(testResult.passed).toBe(false) + expect(testResult.error).toBeDefined() + }) + + it('should not auto-assert when autoAssertOnTestEnd is false', () => { + const softService = SoftAssertService.getInstance() + + const testId = 'test file::config disabled' + softService.setCurrentTest(testId, 'config disabled', 'test file') + + const error = new Error('Test failure') + softService.addFailure(error, 'toBeDisplayed') + + const service = new SoftAssertionService({ autoAssertOnTestEnd: false }) + + // Create mock test result object + const testResult = { passed: true, error: undefined } as TestResult + + // Call afterTest hook - should NOT update the result because auto-assert is disabled + service.afterTest({ + file: 'test file', + parent: '', + title: 'config disabled', + fullName: '', + ctx: undefined, + type: '', + fullTitle: '', + pending: false + }, null, testResult) + + expect(testResult.passed).toBe(true) + expect(testResult.error).toBeUndefined() + + const failures = expectWdio.getSoftFailures(testId) + expect(failures.length).toBe(1) + }) + + it('should still auto-assert with explicit autoAssertOnTestEnd: true', () => { + const softService = SoftAssertService.getInstance() + + const testId = 'test file::config explicit' + softService.setCurrentTest(testId, 'config explicit', 'test file') + + const error = new Error('Test failure') + softService.addFailure(error, 'toBeDisplayed') + + const service = new SoftAssertionService({ autoAssertOnTestEnd: true }) + + const testResult = { passed: true, error: undefined } as TestResult + + service.afterTest({ + file: 'test file', + parent: '', + title: 'config explicit', + fullName: '', + ctx: undefined, + type: '', + fullTitle: '', + pending: false + }, null, testResult) + + expect(testResult.passed).toBe(false) + expect(testResult.error).toBeDefined() + }) + + it('should skip auto-assert if test already has an error', () => { + const softService = SoftAssertService.getInstance() + softService.setCurrentTest('config-existing-error-test', 'config existing error', 'test file') + + const error = new Error('Soft assertion failure') + softService.addFailure(error, 'toBeDisplayed') + + const service = new SoftAssertionService() + + const existingError = new Error('Pre-existing test error') + const testResult = { passed: false, error: existingError } as TestResult + + service.afterTest({ + file: 'test file', + parent: '', + title: 'config existing error', + fullName: '', + ctx: undefined, + type: '', + fullTitle: '', + pending: false + }, null, testResult) + + expect(testResult.passed).toBe(false) + expect(testResult.error).toBe(existingError) + expect(testResult.error?.message).toBe('Pre-existing test error') + }) + + it('should accept undefined options and use defaults', () => { + const service = new SoftAssertionService(undefined) + expect(service).toBeDefined() + + const softService = SoftAssertService.getInstance() + + const testId = 'test file::config undefined' + softService.setCurrentTest(testId, 'config undefined', 'test file') + + const error = new Error('Test failure') + softService.addFailure(error, 'toBeDisplayed') + + const testResult = { passed: true, error: undefined } as TestResult + + service.afterTest({ + file: 'test file', + parent: '', + title: 'config undefined', + fullName: '', + ctx: undefined, + type: '', + fullTitle: '', + pending: false + }, null, testResult) + + expect(testResult.passed).toBe(false) + expect(testResult.error).toBeDefined() + }) + }) +}) diff --git a/tsconfig.json b/tsconfig.json index bd84d6376..dedda6553 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ }, "include": [ "./test/**/*.ts", - "./src/**/*.ts" + "./src/**/*.ts", + "./src/matcher/mock/**/*.ts", ] } diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 293228573..4318ada78 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -267,7 +267,7 @@ interface WdioCustomMatchers extends Record{ /** * Check that `WebdriverIO.Mock` was called with the specific parameters */ - toBeRequestedWith(requestedWith: RequestedWith, options?: ExpectWebdriverIO.CommandOptions): Promise + toBeRequestedWith(requestedWith: ExpectWebdriverIO.RequestedWith, options?: ExpectWebdriverIO.CommandOptions): Promise } /** @@ -325,20 +325,59 @@ declare namespace ExpectWebdriverIO { function setOptions(options: DefaultOptions): void function getConfig(): any + interface SnapshotServiceArgs { + updateState?: SnapshotUpdateState + resolveSnapshotPath?: (path: string, extension: string) => string + } + + class SnapshotService { + static initiate(options: SnapshotServiceArgs): ServiceInstance & { + results: SnapshotResult[] + } + } + + interface SoftFailure { + error: Error; + matcherName: string; + location?: string; + } + + class SoftAssertService { + static getInstance(): SoftAssertService; + setCurrentTest(testId: string, testName?: string, testFile?: string): void; + clearCurrentTest(): void; + getCurrentTestId(): string | null; + addFailure(error: Error, matcherName: string): void; + getFailures(testId?: string): SoftFailure[]; + clearFailures(testId?: string): void; + assertNoFailures(testId?: string): void; + } + + interface SoftAssertionServiceOptions { + autoAssertOnTestEnd?: boolean; + } + + class SoftAssertionService implements ServiceInstance { + constructor(serviceOptions?: SoftAssertionServiceOptions, capabilities?: any, config?: any); + beforeTest(test: Test): void; + beforeStep(step: PickleStep, scenario: Scenario): void; + afterTest(test: Test, context: any, result: TestResult): void; + afterStep(step: PickleStep, scenario: Scenario, result: { passed: boolean, error?: Error }): void; + } + interface AssertionResult { pass: boolean message(): string } - 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 - > + // TODO dprevost - to review + // const matchers: Map< + // string, + // ( + // actual: any, + // ...expected: any[] + // ) => Promise + // > interface AssertionHookParams { /** @@ -464,6 +503,31 @@ declare namespace ExpectWebdriverIO { */ gte?: number } + + type RequestedWith = { + url?: string | ExpectWebdriverIO.PartialMatcher | ((url: string) => boolean) + method?: string | Array + statusCode?: number | Array + requestHeaders?: + | Record + | ExpectWebdriverIO.PartialMatcher + | ((headers: Record) => boolean) + responseHeaders?: + | Record + | ExpectWebdriverIO.PartialMatcher + | ((headers: Record) => boolean) + postData?: + | string + | ExpectWebdriverIO.JsonCompatible + | ExpectWebdriverIO.PartialMatcher + | ((r: string | undefined) => boolean) + response?: + | string + | ExpectWebdriverIO.JsonCompatible + | ExpectWebdriverIO.PartialMatcher + | ((r: string) => boolean) + } + type jsonPrimitive = string | number | boolean | null type jsonObject = { [x: string]: jsonPrimitive | jsonObject | jsonArray } type jsonArray = Array @@ -477,12 +541,6 @@ declare namespace ExpectWebdriverIO { asymmetricMatch(...args: any[]): boolean toString(): string } - - interface SoftFailure { - error: Error; - matcherName: string; - location?: string; - } } declare module 'expect-webdriverio' { diff --git a/types/standalone-global.d.ts b/types/standalone-global.d.ts index 20abc4c89..4988da689 100644 --- a/types/standalone-global.d.ts +++ b/types/standalone-global.d.ts @@ -8,4 +8,4 @@ declare namespace NodeJS { interface Global { expect: ExpectWebdriverIO.Expect } -} \ No newline at end of file +} From 4cac925b692dae50b2dc1c9db1a31dfa941df879 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sun, 22 Jun 2025 23:28:25 -0400 Subject: [PATCH 12/99] Code review --- src/index.ts | 10 +++++----- src/matchers.ts | 2 +- src/snapshot.ts | 1 + src/softAssert.ts | 2 +- src/softAssertService.ts | 2 +- src/softExpect.ts | 2 +- test/softAssertions.test.ts | 2 +- types/expect-webdriverio.d.ts | 8 ++++---- 8 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/index.ts b/src/index.ts index e095b64b8..62c7e9419 100644 --- a/src/index.ts +++ b/src/index.ts @@ -63,15 +63,15 @@ export const setDefaultOptions = (options = {}): void => { export const setOptions = setDefaultOptions /** - * export soft assertion utilities + * export snapshot utilities */ -export { SoftAssertService } from './softAssert.js' -export { SoftAssertionService, type SoftAssertionServiceOptions } from './softAssertService.js' +export { SnapshotService } from './snapshot.js' /** - * export snapshot utilities + * export soft assertion utilities */ -export { SnapshotService } from './snapshot.js' +export { SoftAssertService } from './softAssert.js' +export { SoftAssertionService, type SoftAssertionServiceOptions } from './softAssertService.js' /** * export utils diff --git a/src/matchers.ts b/src/matchers.ts index 8a1f23c4c..323fafc05 100644 --- a/src/matchers.ts +++ b/src/matchers.ts @@ -28,4 +28,4 @@ export * from './matchers/elements/toBeElementsArrayOfSize.js' export * from './matchers/mock/toBeRequested.js' export * from './matchers/mock/toBeRequestedTimes.js' export * from './matchers/mock/toBeRequestedWith.js' -export * from './matchers/snapshot.js' \ No newline at end of file +export * from './matchers/snapshot.js' diff --git a/src/snapshot.ts b/src/snapshot.ts index 39d4af176..91b7b99e9 100644 --- a/src/snapshot.ts +++ b/src/snapshot.ts @@ -130,3 +130,4 @@ export class SnapshotService implements Services.ServiceInstance { return service } } + diff --git a/src/softAssert.ts b/src/softAssert.ts index dd3d866b4..73f1a9903 100644 --- a/src/softAssert.ts +++ b/src/softAssert.ts @@ -136,4 +136,4 @@ export class SoftAssertService { error.name = 'SoftAssertionsError' throw error } -} \ No newline at end of file +} diff --git a/src/softAssertService.ts b/src/softAssertService.ts index 85f01b1a4..eb9507348 100644 --- a/src/softAssertService.ts +++ b/src/softAssertService.ts @@ -83,4 +83,4 @@ export class SoftAssertionService implements Services.ServiceInstance { private getTestId(test: Frameworks.Test): string { return `${test.file || ''}:${test.parent || ''}:${test.title || ''}` } -} \ No newline at end of file +} diff --git a/src/softExpect.ts b/src/softExpect.ts index 270eb6686..4aa2dfbb9 100644 --- a/src/softExpect.ts +++ b/src/softExpect.ts @@ -102,4 +102,4 @@ const createSoftMatcher = ( } } -export default createSoftExpect \ No newline at end of file +export default createSoftExpect diff --git a/test/softAssertions.test.ts b/test/softAssertions.test.ts index c6c998d0c..9fcddc5ee 100644 --- a/test/softAssertions.test.ts +++ b/test/softAssertions.test.ts @@ -480,4 +480,4 @@ describe('Soft Assertions', () => { expect(testResult.error).toBeDefined() }) }) -}) +}) \ No newline at end of file diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 4318ada78..5908b5ba0 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -335,13 +335,13 @@ declare namespace ExpectWebdriverIO { results: SnapshotResult[] } } - + interface SoftFailure { error: Error; matcherName: string; location?: string; - } - + } + class SoftAssertService { static getInstance(): SoftAssertService; setCurrentTest(testId: string, testName?: string, testFile?: string): void; @@ -526,7 +526,7 @@ declare namespace ExpectWebdriverIO { | ExpectWebdriverIO.JsonCompatible | ExpectWebdriverIO.PartialMatcher | ((r: string) => boolean) - } + } type jsonPrimitive = string | number | boolean | null type jsonObject = { [x: string]: jsonPrimitive | jsonObject | jsonArray } From fc92f7e5c870909e7342b3bfbf87d2731f27803c Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sun, 22 Jun 2025 23:53:54 -0400 Subject: [PATCH 13/99] Code review + add linter in types --- test-types/types.ts | 14 ---------- test/softAssertions.test.ts | 3 +- types/expect-webdriverio.d.ts | 52 ++++++++++++++++------------------- types/standalone.d.ts | 35 +++++++++++++++-------- 4 files changed, 50 insertions(+), 54 deletions(-) delete mode 100644 test-types/types.ts diff --git a/test-types/types.ts b/test-types/types.ts deleted file mode 100644 index d9848ef9b..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/softAssertions.test.ts b/test/softAssertions.test.ts index 9fcddc5ee..bef7e9603 100644 --- a/test/softAssertions.test.ts +++ b/test/softAssertions.test.ts @@ -480,4 +480,5 @@ describe('Soft Assertions', () => { expect(testResult.error).toBeDefined() }) }) -}) \ No newline at end of file +}) + diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 5908b5ba0..885f9333f 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -20,7 +20,8 @@ type PromiseLikeType = Promise */ // TODO dprevost have browser matchers and element matchers separated -interface WdioCustomMatchers extends Record{ +/* eslint-disable @typescript-eslint/no-explicit-any */ +interface WdioCustomMatchers extends Record { // ===== $ or $$ ===== /** * `WebdriverIO.Element` -> `isDisplayed` @@ -82,7 +83,7 @@ interface WdioCustomMatchers extends Record{ */ toHaveElementProperty( property: string | RegExp | ExpectWebdriverIO.PartialMatcher, - value?: any, + value?: unknown, options?: ExpectWebdriverIO.StringOptions ): Promise @@ -267,14 +268,13 @@ interface WdioCustomMatchers extends Record{ /** * Check that `WebdriverIO.Mock` was called with the specific parameters */ - toBeRequestedWith(requestedWith: ExpectWebdriverIO.RequestedWith, options?: ExpectWebdriverIO.CommandOptions): Promise + toBeRequestedWith(requestedWith: ExpectWebdriverIO.RequestedWith, options?: ExpectWebdriverIO.CommandOptions): Promise } /** * Those need to be also duplicated in jest.d.ts in order for the typing to correctly overload the matchers (we cannot just extend the Matchers interface) - * @see */ -interface WdioOverloadedMatchers { +interface WdioOverloadedMatchers { /** * snapshot matcher * @param label optional snapshot label @@ -285,18 +285,17 @@ interface WdioOverloadedMatchers { * @param snapshot snapshot string (autogenerated if not specified) * @param label optional snapshot label */ - toMatchInlineSnapshot(snapshot?: string, label?: string): Promise + toMatchInlineSnapshot(snapshot?: string, label?: string): Promise } interface WdioMatchers extends WdioCustomMatchers, WdioOverloadedMatchers {} - /** * expect function declaration, containing 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 */ -interface WdioCustomExpect { +interface WdioCustomExpect { /** * Creates a soft assertion wrapper around standard expect @@ -337,20 +336,20 @@ declare namespace ExpectWebdriverIO { } interface SoftFailure { - error: Error; - matcherName: string; - location?: string; + error: Error + matcherName: string + location?: string } class SoftAssertService { - static getInstance(): SoftAssertService; - setCurrentTest(testId: string, testName?: string, testFile?: string): void; - clearCurrentTest(): void; - getCurrentTestId(): string | null; - addFailure(error: Error, matcherName: string): void; - getFailures(testId?: string): SoftFailure[]; - clearFailures(testId?: string): void; - assertNoFailures(testId?: string): void; + static getInstance(): SoftAssertService + setCurrentTest(testId: string, testName?: string, testFile?: string): void + clearCurrentTest(): void + getCurrentTestId(): string | null + addFailure(error: Error, matcherName: string): void + getFailures(testId?: string): SoftFailure[] + clearFailures(testId?: string): void + assertNoFailures(testId?: string): void } interface SoftAssertionServiceOptions { @@ -358,11 +357,11 @@ declare namespace ExpectWebdriverIO { } class SoftAssertionService implements ServiceInstance { - constructor(serviceOptions?: SoftAssertionServiceOptions, capabilities?: any, config?: any); - beforeTest(test: Test): void; - beforeStep(step: PickleStep, scenario: Scenario): void; - afterTest(test: Test, context: any, result: TestResult): void; - afterStep(step: PickleStep, scenario: Scenario, result: { passed: boolean, error?: Error }): void; + constructor(serviceOptions?: SoftAssertionServiceOptions, capabilities?: unknown, config?: un) + beforeTest(test: Test): void + beforeStep(step: PickleStep, scenario: Scenario): void + afterTest(test: Test, context: any, result: TestResult): void + afterStep(step: PickleStep, scenario: Scenario, result: { passed: boolean, error?: Error }): void } interface AssertionResult { @@ -544,8 +543,5 @@ declare namespace ExpectWebdriverIO { } declare module 'expect-webdriverio' { - - // TODO dprevost should we also have an expect const here too? - const matchers: WdioCustomMatchers; - export = matchers; + export = ExpectWebdriverIO } diff --git a/types/standalone.d.ts b/types/standalone.d.ts index 8a5cf9866..0e707310f 100644 --- a/types/standalone.d.ts +++ b/types/standalone.d.ts @@ -2,23 +2,36 @@ /// /// -type ExpectAsymmetricMatchers = import('expect').AsymmetricMatchers; -type ExpectBaseExpect = import('expect').BaseExpect; -type ExpectMatchers = import('expect').Matchers; +type ExpectAsymmetricMatchers = import('expect').AsymmetricMatchers +type ExpectBaseExpect = import('expect').BaseExpect +type ExpectMatchers = import('expect').Matchers // Not exportable from 'expect' type Inverse = { - /** - * Inverse next matcher. If you know how to test something, `.not` lets you test its opposite. - */ - not: Matchers; -}; + /** + * Inverse next matcher. If you know how to test something, `.not` lets you test its opposite. + */ + not: Matchers; +} declare namespace ExpectWebdriverIO { - interface Matchers extends WdioMatchers, ExpectMatchers {} + interface Matchers extends WdioMatchers, ExpectMatchers { + /** + * 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 + } - type MatchersAndInverse = Matchers & Inverse>; + type MatchersAndInverse = Matchers & Inverse> /** * Mostly derived from the types of `jest-expect` but adapted to work with WebdriverIO. @@ -53,7 +66,7 @@ declare namespace ExpectWebdriverIO { /** * Clear all current soft assertion failures */ - clearSoftFailures(testId?: string): void + clearSoftFailures(testId?: string): void } interface InverseAsymmetricMatchers extends Expect {} From 47faaee98782367e6fed2c77a2b8fc03535b27b8 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Mon, 23 Jun 2025 01:08:47 -0400 Subject: [PATCH 14/99] Add decent not finished typing for Jasmine + soft assertion --- jasmine.d.ts | 35 +- package-lock.json | 7 + package.json | 1 + test-types/jasmine/tsconfig.json | 15 + test-types/jasmine/types-jasmine.test.ts | 391 +++++++++++++++++++++++ types/jasmine-soft-extend.d.ts | 30 ++ 6 files changed, 477 insertions(+), 2 deletions(-) create mode 100644 test-types/jasmine/tsconfig.json create mode 100644 test-types/jasmine/types-jasmine.test.ts create mode 100644 types/jasmine-soft-extend.d.ts diff --git a/jasmine.d.ts b/jasmine.d.ts index 0d9ddcddd..072fc6cf2 100644 --- a/jasmine.d.ts +++ b/jasmine.d.ts @@ -1,6 +1,37 @@ /// declare namespace jasmine { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - interface AsyncMatchers extends ExpectWebdriverIO.Matchers, T> {} + + interface Matchers extends WdioMatchers{ + + /** + * Below are overloaded Jest's matchers not part of `expect` but of `jest-snapshot`. + * We need to define them below so that they are correctly typed overloaded + * @see https://github.com/jestjs/jest/blob/73dbef5d2d3195a1e55fb254c54cce70d3036252/packages/jest-snapshot/src/types.ts#L37 + */ + + /** + * snapshot matcher + * @param label optional snapshot label + */ + toMatchSnapshot(label?: string): Promise; + + // TODO - this is not working as expected, need to investigate + /** + * snapshot matcher + * @param label optional snapshot label + */ + // toMatchSnapshot: T extends WdioElementLike ? (label: string) => Promise : (hint?: string) => R; + + /** + * inline snapshot matcher + * @param snapshot snapshot string (autogenerated if not specified) + * @param label optional snapshot label + */ + toMatchInlineSnapshot(snapshot?: string, label?: string): Promise + } + + // type MatcherAndInverse = Matchers + interface AsyncMatchers extends WdioCustomMatchers {} } + diff --git a/package-lock.json b/package-lock.json index 391370a59..369c8a4b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ }, "devDependencies": { "@types/debug": "^4.1.12", + "@types/jasmine": "^5.1.8", "@types/jest": "^30.0.0", "@types/lodash.isequal": "^4.5.8", "@types/mocha": "^10.0.10", @@ -2082,6 +2083,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", diff --git a/package.json b/package.json index 0d43ee96f..1bea99835 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ }, "devDependencies": { "@types/debug": "^4.1.12", + "@types/jasmine": "^5.1.8", "@types/jest": "^30.0.0", "@types/lodash.isequal": "^4.5.8", "@types/mocha": "^10.0.10", diff --git a/test-types/jasmine/tsconfig.json b/test-types/jasmine/tsconfig.json new file mode 100644 index 000000000..82ea23a1f --- /dev/null +++ b/test-types/jasmine/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "outDir": "dist", + "noImplicitAny": true, + "target": "ES2020", + "module": "Node16", + "skipLibCheck": true, + "types": [ + "../../types/jasmine-soft-extend.d.ts", + "@types/jasmine", + "../../jasmine.d.ts", + "@wdio/globals/types" + ] + } +} \ 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..59c5c51b8 --- /dev/null +++ b/test-types/jasmine/types-jasmine.test.ts @@ -0,0 +1,391 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +describe('type assertions', () => { + const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element + const chainableElement = $('findMe') + const chainableArray = $$('ul>li') + + describe('toHaveUrl', () => { + const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser + it('should not have ts errors and be able to await the promise when actual is browser', async () => { + const expectPromiseVoid: Promise = expect(browser).toHaveUrl('https://example.com') + await expectPromiseVoid + + const expectNotPromiseVoid: Promise = expect(browser).not.toHaveUrl('https://example.com') + await expectNotPromiseVoid + }) + + it('should have ts errors and not need to await the promise when actual is browser', async () => { + // @ts-expect-error + const expectVoid: void = expect(browser).toHaveUrl('https://example.com') + // @ts-expect-error + const expectNotVoid: void = expect(browser).not.toHaveUrl('https://example.com') + }) + + it('should have ts errors when actual is an element', async () => { + // @ts-expect-error + await expect(element).toHaveUrl('https://example.com') + }) + + it('should have ts errors when actual is an ChainableElement', async () => { + // @ts-expect-error + await expect(chainableElement).toHaveUrl('https://example.com') + }) + + it('should support stringContaining', async () => { + const expectVoid1: Promise = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) + + // @ts-expect-error + const expectVoid2: void = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) + }) + }) + + describe('element type assertions', () => { + + describe('toBeDisabled', () => { + it('should not have ts errors and be able to await the promise for element', async () => { + // expect no ts errors + const expectIsPromiseVoid: Promise = expect(element).toBeDisabled() + await expectIsPromiseVoid + + const expectNotIsPromiseVoid: Promise = expect(element).not.toBeDisabled() + await expectNotIsPromiseVoid + }) + + it('should not have ts errors and be able to await the promise for chainable', async () => { + // expect no ts errors + const expectIsPromiseVoid: Promise = expect(chainableElement).toBeDisabled() + await expectIsPromiseVoid + + const expectNotIsPromiseVoid: Promise = expect(chainableElement).not.toBeDisabled() + await expectNotIsPromiseVoid + }) + + it('should have ts errors when typing to void for element', async () => { + // @ts-expect-error + const expectToBeIsVoid: void = expect(element).toBeDisabled() + // @ts-expect-error + const expectNotToBeIsVoid: void = expect(element).not.toBeDisabled() + }) + + it('should have ts errors when typing to void for chainable', async () => { + // @ts-expect-error + const expectToBeIsVoid: void = expect(chainableElement).toBeDisabled() + // @ts-expect-error + const expectNotToBeIsVoid: void = expect(chainableElement).not.toBeDisabled() + }) + }) + + describe('toMatchSnapshot', () => { + + it('should not have ts errors when typing to Promise for an element', async () => { + const expectPromise1: Promise = expect(element).toMatchSnapshot() + const expectPromise2: Promise = expect(element).toMatchSnapshot('test label') + }) + + it('should not have ts errors when typing to Promise for a chainable', async () => { + const expectPromise1: Promise = expect(chainableElement).toMatchSnapshot() + const expectPromise2: Promise = expect(chainableElement).toMatchSnapshot('test label') + }) + + // We need somehow to exclude the Jest types one for this to success + it('should have ts errors when typing to void for an element like', async () => { + //@ts-expect-error + const expectNotToBeVoid1: void = expect(element).toMatchSnapshot() + //@ts-expect-error + const expectNotToBeVoid2: void = expect(chainableElement).toMatchSnapshot() + }) + + // TODO - conditional types check on T to have the below match void does not work + // it('should not have ts errors when typing to void for a string', async () => { + // const expectNotToBeVoid: void = expect('.findme').toMatchSnapshot() + // }) + }) + + describe('toMatchInlineSnapshot', () => { + + it('should not have ts errors when typing to Promise for an element', async () => { + const expectPromise1: Promise = expect(element).toMatchInlineSnapshot() + const expectPromise2: Promise = expect(element).toMatchInlineSnapshot('test snapshot') + const expectPromise3: Promise = expect(element).toMatchInlineSnapshot('test snapshot', 'test label') + }) + + it('should not have ts errors when typing to Promise for a chainable', async () => { + const expectPromise1: Promise = expect(chainableElement).toMatchInlineSnapshot() + const expectPromise2: Promise = expect(chainableElement).toMatchInlineSnapshot('test snapshot') + const expectPromise3: Promise = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + }) + + // We need somehow to exclude the Jest types one for this to success + it('should have ts errors when typing to void for an element like', async () => { + //@ts-expect-error + const expectNotToBeVoid1: void = expect(element).toMatchInlineSnapshot() + //@ts-expect-error + const expectPromise2: void = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + }) + + // TODO - conditional types check on T to have the below match void does not work + // it('should not have ts errors when typing to void for a string', async () => { + // const expectNotToBeVoid: void = expect('.findme').toMatchInlineSnapshot() + // }) + }) + }) + + describe('toBe', () => { + + it('should not have ts errors when typing to void when actual is boolean', async () => { + const expectToBeIsVoid: void = expect(true).toBe(true) + const expectNotToBeIsVoid: void = expect(true).not.toBe(true) + }) + + it('should have ts errors when typing to Promise when actual is boolean', async () => { + //@ts-expect-error + const expectToBeIsNotPromiseVoid1: Promise = expect(true).toBe(true) + + //@ts-expect-error + const expectToBeIsNotPromiseVoid2: Promise = expect(true).not.toBe(true) + }) + + it('should expect void when actual is an awaited element/chainable', async () => { + const isClickableElement = await element.isClickable() + const expectPromiseVoid1: void = expect(isClickableElement).toBe(true) + + const isClickableChainable: boolean = await chainableElement.isClickable() + const expectPromiseVoid2: void = expect(isClickableChainable).toBe(true) + + // @ts-expect-error + const expectPromiseVoid3: Promise = expect(isClickableElement).toBe(true) + + // @ts-expect-error + const expectPromiseVoid4: Promise = expect(isClickableChainable).toBe(true) + }) + }) + + describe('string type assertions', () => { + it('should not have ts errors when typing to void', async () => { + // Expect no ts errors + const expectToBeIsVoid: void = expect('test').toBe('test') + }) + + it('should have ts errors when typing to Promise', async () => { + //@ts-expect-error + const expectToBeIsNotPromiseVoid: Promise = expect('test').toBe('test') + }) + }) + + describe('Promise<> type assertions', () => { + const booleanPromise: Promise = Promise.resolve(true) + + it('should not have ts errors when typing to void', async () => { + // const expectToBeIsVoid: void = expect(booleanPromise).toBe(true) + const expectAwaitToBeIsVoid: void = expect(await booleanPromise).toBe(true) + }) + + it('should not have ts errors when resolves and rejects is typed to Promise', async () => { + // TODO the below needs to return Promise but currently returns void + const expectResolvesToBeIsVoid: Promise = expect(booleanPromise).resolves.toBe(true) + const expectRejectsToBeIsVoid: Promise = expect(booleanPromise).rejects.toBe(true) + }) + + it('should have ts errors when typing to Promise', async () => { + //@ts-expect-error + const expectToBeIsNotPromiseVoid1: Promise = expect(booleanPromise).toBe(true) + //@ts-expect-error + const expectToBeIsNotPromiseVoid2: Promise = expect(await booleanPromise).toBe(true) + }) + + // it('should have ts errors when typing resolves and reject is typed to void', async () => { + // //@ts-expect-error + // const expectResolvesToBeIsVoid: void = expect(booleanPromise).resolves.toBe(true) + // //@ts-expect-error + // const expectRejectsToBeIsVoid: void = expect(booleanPromise).rejects.toBe(true) + // }) + }) + + describe('toBeElementsArrayOfSize', async () => { + + it('should not have ts errors when typing to Promise', async () => { + const listItems = await chainableArray + const expectPromise: Promise = expect(listItems).toBeElementsArrayOfSize(5) + const expectPromise1: Promise = expect(listItems).toBeElementsArrayOfSize({ lte: 10 }) + }) + + it('should have ts errors when typing to void', async () => { + const listItems = await chainableArray + // @ts-expect-error + const expectPromise: void = expect(listItems).toBeElementsArrayOfSize(5) + // @ts-expect-error + const expectPromise1: void = expect(listItems).toBeElementsArrayOfSize({ lte: 10 }) + }) + }) + + describe('Network Matchers', () => { + const promiseNetworkMock = browser.mock('**/api/todo*') + + it('should not have ts errors when typing to Promise', async () => { + const expectPromise1: Promise = expect(promiseNetworkMock).toBeRequested() + const expectPromise2: Promise = expect(promiseNetworkMock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + const expectPromise3: Promise = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + + const expectPromise4: Promise = expect(promiseNetworkMock).toBeRequestedWith({ + url: 'http://localhost:8080/api/todo', // [optional] string | function | custom matcher + method: 'POST', // [optional] string | array + statusCode: 200, // [optional] number | array + requestHeaders: { Authorization: 'foo' }, // [optional] object | function | custom matcher + responseHeaders: { Authorization: 'bar' }, // [optional] object | function | custom matcher + postData: { title: 'foo', description: 'bar' }, // [optional] object | function | custom matcher + response: { success: true }, // [optional] object | function | custom matcher + }) + }) + + it('should have ts errors when typing to void', async () => { + // @ts-expect-error + const expectPromise1: void = expect(promiseNetworkMock).toBeRequested() + // @ts-expect-error + const expectPromise2: void = expect(promiseNetworkMock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + // @ts-expect-error + const expectPromise3: void = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + + // @ts-expect-error + const expectPromise4: void = expect(promiseNetworkMock).toBeRequestedWith({ + url: 'http://localhost:8080/api/todo', // [optional] string | function | custom matcher + method: 'POST', // [optional] string | array + statusCode: 200, // [optional] number | array + requestHeaders: { Authorization: 'foo' }, // [optional] object | function | custom matcher + responseHeaders: { Authorization: 'bar' }, // [optional] object | function | custom matcher + postData: { title: 'foo', description: 'bar' }, // [optional] object | function | custom matcher + response: { success: true }, // [optional] object | function | custom matcher + }) + }) + }) + + 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', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const expectAny1: any = expect.stringContaining('WebdriverIO') + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const expectAny2: any = expect.anything() + }) + + describe('Soft Assertions', async () => { + const expectString: string = await $('h1').getText() + const expectPromise: Promise = $('h1').getText() + + describe('expect.soft', () => { + it('should not have ts error and not need to be awaited/be a promise if actual is non-promise type', async () => { + const expectWdioMatcher: WdioMatchers = expect.soft(expectString) + const expectVoid: void = expect.soft(expectString).toBe('Test Page') + }) + + it('should not have ts error and need to be awaited/be a promise if actual is a promise type', async () => { + const expectWdioMatcher: WdioMatchers, Promise> = expect.soft(expectPromise) + const expectVoid: Promise = expect.soft(expectPromise).toBe('Test Page') + + await expect.soft(expectPromise).toBe('Test Page') + }) + + it('should have ts error when using await and actual is non-promise type', async () => { + // @ts-expect-error + const expectWdioMatcher: WdioMatchers, string> = expect.soft(expectString) + + // @ts-expect-error + const expectVoid: Promise = expect.soft(expectString).toBe('Test Page') + }) + + it('should not have ts error and need to be awaited/be a promise if actual is a promise type', async () => { + // @ts-expect-error + const expectWdioMatcher: WdioMatchers> = expect.soft(expectPromise) + // @ts-expect-error + const expectVoid: void = expect.soft(expectPromise).toBe('Test Page') + }) + + it('should support chainable element', async () => { + const expectElement: WdioMatchers = expect.soft(element) + const expectElementChainable: WdioMatchers = expect.soft(chainableElement) + + // @ts-expect-error + const expectElement2: WdioMatchers, WebdriverIO.Element> = expect.soft(element) + // @ts-expect-error + const expectElementChainable2: WdioMatchers, typeof chainableElement> = expect.soft(chainableElement) + }) + + it('should support chainable element with wdio Matchers', async () => { + const expectPromise1: Promise = expect.soft(element).toBeDisplayed() + const expectPromise2: Promise = expect.soft(chainableElement).toBeDisplayed() + + // @ts-expect-error + const expectPromise3: void = expect.soft(element).toBeDisplayed() + // @ts-expect-error + const expectPromise4: void = expect.soft(chainableElement).toBeDisplayed() + + await expect.soft(element).toBeDisplayed() + await expect.soft(chainableElement).toBeDisplayed() + }) + + describe('not', async () => { + it('should support not with chainable', async () => { + const expectPromise1: Promise = expect.soft(element).not.toBeDisplayed() + const expectPromise2: Promise = expect.soft(chainableElement).not.toBeDisplayed() + + // @ts-expect-error + const expectPromise3: void = expect.soft(element).not.toBeDisplayed() + // @ts-expect-error + const expectPromise4: void = expect.soft(chainableElement).not.toBeDisplayed() + + await expect.soft(element).not.toBeDisplayed() + await expect.soft(chainableElement).not.toBeDisplayed() + }) + + it('should support not with non-promise', async () => { + const expectVoid1: void = expect.soft(expectString).not.toBe('Test Page') + + // @ts-expect-error + const expectVoid2: Promise = expect.soft(expectString).not.toBe('Test Page') + }) + + it('should support not with promise', async () => { + const expectPromise1: Promise = expect.soft(expectPromise).not.toBe('Test Page') + + // @ts-expect-error + const expectPromise2: void = expect.soft(expectPromise).not.toBe('Test Page') + + await expect.soft(expectPromise).not.toBe('Test Page') + }) + }) + }) + + describe('expect.getSoftFailures', () => { + it('should be of type `SoftFailure`', async () => { + const expectSoftFailure1: ExpectWebdriverIO.SoftFailure[] = expect.getSoftFailures() + + // @ts-expect-error + const expectSoftFailure2: void = expect.getSoftFailures() + }) + }) + + describe('expect.assertSoftFailures', () => { + it('should be of type void', async () => { + const expectVoid1: void = expect.assertSoftFailures() + + // @ts-expect-error + const expectVoid2: Promise = expect.assertSoftFailures() + }) + }) + + describe('expect.clearSoftFailures', () => { + it('should be of type void', async () => { + const expectVoid1: void = expect.clearSoftFailures() + + // @ts-expect-error + const expectVoid2: Promise = expect.clearSoftFailures() + }) + }) + }) + }) +}) diff --git a/types/jasmine-soft-extend.d.ts b/types/jasmine-soft-extend.d.ts new file mode 100644 index 000000000..c8770e136 --- /dev/null +++ b/types/jasmine-soft-extend.d.ts @@ -0,0 +1,30 @@ +export {} + +declare global { + function expect(actual: T): jasmine.Matchers + namespace expect { + + /** + * 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 + */ + function soft(actual: T): jasmine.Matchers + // soft(actual: T): T extends PromiseLikeType ? Matchers, T> : Matchers + + /** + * Get all current soft assertion failures + */ + function getSoftFailures(testId?: string): WebdriverIO.SoftFailure[] + + /** + * Manually assert all soft failures (throws an error if any failures exist) + */ + function assertSoftFailures(testId?: string): void + + /** + * Clear all current soft assertion failures + */ + function clearSoftFailures(testId?: string): void + } +} \ No newline at end of file From a75fecc45ea1ab6221a1b48b6ebfca8d4ba324f2 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Tue, 24 Jun 2025 11:12:57 -0400 Subject: [PATCH 15/99] ish working with jasmine expect and matcher + fix unimplemented method - Have some working base line with Jasmine expect and even asyncExpect - Fix expect.unimplemented method not in TS error because of `extends Record` on matcher --- jasmine.d.ts | 2 +- test-types/jasmine/types-jasmine.test.ts | 167 +++++++++++++++++- test-types/mocha/types-mocha.test.ts | 9 +- test/matchers.test.ts | 3 +- test/matchers/mock/toBeRequestedTimes.test.ts | 35 ++-- types/expect-webdriverio.d.ts | 11 +- types/jasmine-soft-extend.d.ts | 64 ++++++- 7 files changed, 248 insertions(+), 43 deletions(-) diff --git a/jasmine.d.ts b/jasmine.d.ts index 072fc6cf2..c7f140e3d 100644 --- a/jasmine.d.ts +++ b/jasmine.d.ts @@ -32,6 +32,6 @@ declare namespace jasmine { } // type MatcherAndInverse = Matchers - interface AsyncMatchers extends WdioCustomMatchers {} + // interface AsyncMatchers extends WdioMatchers {} } diff --git a/test-types/jasmine/types-jasmine.test.ts b/test-types/jasmine/types-jasmine.test.ts index 59c5c51b8..f87e4e759 100644 --- a/test-types/jasmine/types-jasmine.test.ts +++ b/test-types/jasmine/types-jasmine.test.ts @@ -182,9 +182,12 @@ describe('type assertions', () => { }) it('should not have ts errors when resolves and rejects is typed to Promise', async () => { - // TODO the below needs to return Promise but currently returns void - const expectResolvesToBeIsVoid: Promise = expect(booleanPromise).resolves.toBe(true) - const expectRejectsToBeIsVoid: Promise = expect(booleanPromise).rejects.toBe(true) + + // @ts-expect-error + expect(booleanPromise).resolves.toBe(true) + + // @ts-expect-error + expect(booleanPromise).rejects.toBe(true) }) it('should have ts errors when typing to Promise', async () => { @@ -273,18 +276,18 @@ describe('type assertions', () => { const expectAny2: any = expect.anything() }) - describe('Soft Assertions', async () => { + describe('Expect Soft Assertions', async () => { const expectString: string = await $('h1').getText() const expectPromise: Promise = $('h1').getText() describe('expect.soft', () => { it('should not have ts error and not need to be awaited/be a promise if actual is non-promise type', async () => { - const expectWdioMatcher: WdioMatchers = expect.soft(expectString) + const expectWdioMatcher: jasmine.Matchers = expect.soft(expectString) const expectVoid: void = expect.soft(expectString).toBe('Test Page') }) it('should not have ts error and need to be awaited/be a promise if actual is a promise type', async () => { - const expectWdioMatcher: WdioMatchers, Promise> = expect.soft(expectPromise) + const expectWdioMatcher: jasmine.Matchers> = expect.soft(expectPromise) const expectVoid: Promise = expect.soft(expectPromise).toBe('Test Page') await expect.soft(expectPromise).toBe('Test Page') @@ -306,8 +309,8 @@ describe('type assertions', () => { }) it('should support chainable element', async () => { - const expectElement: WdioMatchers = expect.soft(element) - const expectElementChainable: WdioMatchers = expect.soft(chainableElement) + const expectElement: jasmine.Matchers = expect.soft(element) + const expectElementChainable: jasmine.Matchers = expect.soft(chainableElement) // @ts-expect-error const expectElement2: WdioMatchers, WebdriverIO.Element> = expect.soft(element) @@ -387,5 +390,153 @@ describe('type assertions', () => { }) }) }) + + describe('ExpectAsync Soft Assertions', async () => { + const expectString: string = await $('h1').getText() + const expectPromise: Promise = $('h1').getText() + + describe('expectAsync.soft', () => { + it('should not have ts error and not need to be awaited/be a promise if actual is non-promise type', async () => { + const expectWdioMatcher: jasmine.Matchers = expectAsync.soft(expectString) + const expectVoid: void = expectAsync.soft(expectString).toBe('Test Page') + }) + + it('should not have ts error and need to be awaited/be a promise if actual is a promise type', async () => { + const expectWdioMatcher: jasmine.Matchers> = expectAsync.soft(expectPromise) + const expectVoid: Promise = expectAsync.soft(expectPromise).toBeResolvedTo('Test Page') + + await expectAsync.soft(expectPromise).toBeResolvedTo('Test Page') + }) + + it('should have ts error when using await and actual is non-promise type', async () => { + // @ts-expect-error + const expectWdioMatcher: WdioMatchers, string> = expect.soft(expectString) + + // @ts-expect-error + const expectVoid: Promise = expect.soft(expectString).toBe('Test Page') + }) + + it('should not have ts error and need to be awaited/be a promise if actual is a promise type', async () => { + // @ts-expect-error + const expectWdioMatcher: WdioMatchers> = expect.soft(expectPromise) + // @ts-expect-error + const expectVoid: void = expect.soft(expectPromise).toBe('Test Page') + }) + + it('should support chainable element', async () => { + const expectElement: jasmine.Matchers = expect.soft(element) + const expectElementChainable: jasmine.Matchers = expect.soft(chainableElement) + + // @ts-expect-error + const expectElement2: WdioMatchers, WebdriverIO.Element> = expect.soft(element) + // @ts-expect-error + const expectElementChainable2: WdioMatchers, typeof chainableElement> = expect.soft(chainableElement) + }) + + it('should support chainable element with wdio Matchers', async () => { + const expectPromise1: Promise = expect.soft(element).toBeDisplayed() + const expectPromise2: Promise = expect.soft(chainableElement).toBeDisplayed() + + // @ts-expect-error + const expectPromise3: void = expect.soft(element).toBeDisplayed() + // @ts-expect-error + const expectPromise4: void = expect.soft(chainableElement).toBeDisplayed() + + await expect.soft(element).toBeDisplayed() + await expect.soft(chainableElement).toBeDisplayed() + }) + + describe('not', async () => { + it('should support not with chainable', async () => { + const expectPromise1: Promise = expect.soft(element).not.toBeDisplayed() + const expectPromise2: Promise = expect.soft(chainableElement).not.toBeDisplayed() + + // @ts-expect-error + const expectPromise3: void = expect.soft(element).not.toBeDisplayed() + // @ts-expect-error + const expectPromise4: void = expect.soft(chainableElement).not.toBeDisplayed() + + await expect.soft(element).not.toBeDisplayed() + await expect.soft(chainableElement).not.toBeDisplayed() + }) + + it('should support not with non-promise', async () => { + const expectVoid1: void = expect.soft(expectString).not.toBe('Test Page') + + // @ts-expect-error + const expectVoid2: Promise = expect.soft(expectString).not.toBe('Test Page') + }) + + it('should support not with promise', async () => { + const expectPromise1: Promise = expect.soft(expectPromise).not.toBe('Test Page') + + // @ts-expect-error + const expectPromise2: void = expect.soft(expectPromise).not.toBe('Test Page') + + await expect.soft(expectPromise).not.toBe('Test Page') + }) + }) + }) + + describe('expect.getSoftFailures', () => { + it('should be of type `SoftFailure`', async () => { + const expectSoftFailure1: ExpectWebdriverIO.SoftFailure[] = expect.getSoftFailures() + + // @ts-expect-error + const expectSoftFailure2: void = expect.getSoftFailures() + }) + }) + + describe('expect.assertSoftFailures', () => { + it('should be of type void', async () => { + const expectVoid1: void = expect.assertSoftFailures() + + // @ts-expect-error + const expectVoid2: Promise = expect.assertSoftFailures() + }) + }) + + describe('expect.clearSoftFailures', () => { + it('should be of type void', async () => { + const expectVoid1: void = expect.clearSoftFailures() + + // @ts-expect-error + const expectVoid2: Promise = expect.clearSoftFailures() + }) + }) + }) + + describe('Jasmine', async () => { + const string = 'Test Page' + const promiseString = Promise.resolve('Test Page') + + it('should correctly support expect.toBe', async () => { + const expectNonPromiseMatched1: jasmine.Matchers = expect(string) + const expectVoid1: void = expect(string).toBe(string) + + // @ts-expect-error + const expectNonPromiseMatched2: jasmine.Matchers> = expect(string) + // @ts-expect-error + const expectVoid2: Promise = expect(string).toBe(string) + + // @ts-expect-error + expect(string).unimplementedFunction() + }) + + it('should correctly support expect.toBeResolvedTo', async () => { + const expectNonPromiseMatched1: jasmine.AsyncMatchers = expectAsync(promiseString) + const expectVoid1: PromiseLike = expectAsync(promiseString).toBeResolvedTo('Test Page') + + // @ts-expect-error + const expectNonPromiseMatched2: jasmine.AsyncMatchers, unknown> = expectAsync(promiseString) + // @ts-expect-error + const expectVoid2: void = expectAsync(promiseString).toBeResolvedTo('Test Page') + + await expectAsync(promiseString).toBeResolvedTo('Test Page') + + // @ts-expect-error + expectAsync(promiseString).unimplementedFunction() + }) + }) }) }) diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index 0fa276f59..2fefc3443 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -187,9 +187,12 @@ describe('type assertions', () => { }) it('should not have ts errors when resolves and rejects is typed to Promise', async () => { - // TODO the below needs to return Promise but currently returns void - const expectResolvesToBeIsVoid: Promise = expect(booleanPromise).resolves.toBe(true) - const expectRejectsToBeIsVoid: Promise = expect(booleanPromise).rejects.toBe(true) + // TODO should we support resolves and rejects in standalone or with mocha? + + /// @ts-expect-error + expect(booleanPromise).resolves.toBe(true) + /// @ts-expect-error + expect(booleanPromise).rejects.toBe(true) }) it('should have ts errors when typing to Promise', async () => { diff --git a/test/matchers.test.ts b/test/matchers.test.ts index 967657415..6c5ae808a 100644 --- a/test/matchers.test.ts +++ b/test/matchers.test.ts @@ -62,8 +62,7 @@ test('allows to add matcher', () => { const matcher: any = vi.fn((actual: any, expected: any) => ({ pass: actual === expected })) expectLib.extend({ toBeCustom: matcher }) - // TODO dprevost see later if we really the below to expect a ts error since it is not working anymore... - //// @ts-expect-error not in types + // @ts-expect-error not in types expectLib('foo').toBeCustom('foo') expect(matchers.keys()).toContain('toBeCustom') }) diff --git a/test/matchers/mock/toBeRequestedTimes.test.ts b/test/matchers/mock/toBeRequestedTimes.test.ts index cf6542324..22cd9eb78 100644 --- a/test/matchers/mock/toBeRequestedTimes.test.ts +++ b/test/matchers/mock/toBeRequestedTimes.test.ts @@ -7,9 +7,8 @@ import { removeColors, getReceived, getExpected, getExpectMessage } from '../../ vi.mock('@wdio/globals') -//@ts-ignore TODO fix me -class TestMock implements WebdriverIO.Mock { - _calls: local.NetworkResponseCompletedParameters[] +class TestMock implements Mock { + _calls: Matches[] constructor () { this._calls = [] @@ -18,17 +17,16 @@ class TestMock implements WebdriverIO.Mock { return this._calls } on = vi.fn() - abort () { return this } - abortOnce () { return this } - respond () { return this } - respondOnce () { return this } - clear () { return this } - restore () { return Promise.resolve(this) } + abort () { return Promise.resolve() } + abortOnce () { return Promise.resolve() } + respond () { return Promise.resolve() } + respondOnce () { return Promise.resolve() } + clear () { return Promise.resolve() } + restore () { return Promise.resolve() } waitForResponse () { return Promise.resolve(true) } } -const mockMatch: local.NetworkResponseCompletedParameters = { - //@ts-ignore TODO fix me +const mockMatch: Matches = { body: 'foo', url: '/foo/bar', method: 'POST', @@ -41,8 +39,7 @@ const mockMatch: local.NetworkResponseCompletedParameters = { describe('toBeRequestedTimes', () => { test('wait for success', async () => { - //@ts-ignore TODO fix me - const mock: WebdriverIO.Mock = new TestMock() + const mock: Mock = new TestMock() setTimeout(() => { mock.calls.push(mockMatch) @@ -66,8 +63,7 @@ describe('toBeRequestedTimes', () => { }) test('wait for success using number options', async () => { - //@ts-ignore TODO fix me - const mock: WebdriverIO.Mock = new TestMock() + const mock: Mock = new TestMock() setTimeout(() => { mock.calls.push(mockMatch) @@ -80,8 +76,7 @@ describe('toBeRequestedTimes', () => { }) test('wait but failure', async () => { - //@ts-ignore TODO fix me - const mock: WebdriverIO.Mock = new TestMock() + const mock: Mock = new TestMock() const result = await toBeRequestedTimes.call({}, mock, 1) expect(result.pass).toBe(false) @@ -103,8 +98,7 @@ describe('toBeRequestedTimes', () => { }) test('not to be called', async () => { - //@ts-ignore TODO fix me - const mock: WebdriverIO.Mock = new TestMock() + const mock: Mock = new TestMock() // expect(mock).not.toBeRequestedTimes(0) should fail const result = await toBeRequestedTimes.call({ isNot: true }, mock, 0) @@ -126,8 +120,7 @@ describe('toBeRequestedTimes', () => { }) test('message', async () => { - //@ts-ignore TODO fix me - const mock: WebdriverIO.Mock = new TestMock() + const mock: Mock = new TestMock() const result = await toBeRequestedTimes.call({}, mock, 0) expect(result.message()).toContain('Expect mock to be called 0 times') diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 885f9333f..91c8387ab 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -6,6 +6,7 @@ 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 ExpectLibAsymmetricMatchers = import('expect').AsymmetricMatchers // type ChainablePromiseElement = import('webdriverio').ChainablePromiseElement // type ChainablePromiseArray = import('webdriverio').ChainablePromiseArray @@ -20,8 +21,9 @@ type PromiseLikeType = Promise */ // TODO dprevost have browser matchers and element matchers separated +// TODO extending extends Record remove ts error on unimplemented matchers /* eslint-disable @typescript-eslint/no-explicit-any */ -interface WdioCustomMatchers extends Record { +interface WdioCustomMatchers /*extends Record*/ { // ===== $ or $$ ===== /** * `WebdriverIO.Element` -> `isDisplayed` @@ -295,7 +297,9 @@ interface WdioMatchers extends WdioCustomMatchers, WdioOve * - 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 */ -interface WdioCustomExpect { +// TODO dprevost should we extends Expect from expect lib or just AsyncMatchers? +// TODO dprevost ExpectLibAsymmetricMatchers add arrayOf and closeTo previously not there! and not was there previously but is no more? +interface WdioCustomExpect extends ExpectLibAsymmetricMatchers { /** * Creates a soft assertion wrapper around standard expect @@ -393,7 +397,6 @@ declare namespace ExpectWebdriverIO { * expect(el).toHaveAttribute('attr', 'value', { ... }) // expectedValue is `['attr', 'value]` * ``` */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any expectedValue?: any, /** * Options that the user has passed in, e.g. `expect(el).toHaveText('foo', { ignoreCase: true })` -> `{ ignoreCase: true }` @@ -533,10 +536,8 @@ declare namespace ExpectWebdriverIO { 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 } diff --git a/types/jasmine-soft-extend.d.ts b/types/jasmine-soft-extend.d.ts index c8770e136..d083e1b0e 100644 --- a/types/jasmine-soft-extend.d.ts +++ b/types/jasmine-soft-extend.d.ts @@ -1,9 +1,55 @@ -export {} +/// + +type UnwrapPromise = T extends Promise ? U : T declare global { + + // TODO dprevost might need to override the Array too (and more?) function expect(actual: T): jasmine.Matchers + + // function expectAsync(actual: T | PromiseLike): jasmine.AsyncMatchers namespace expect { + // TODO should we use expectAsync here instead? + /** Wdio soft assertion */ + /** + * 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 + */ + function soft(actual: T): jasmine.Matchers + // soft(actual: T): T extends PromiseLikeType ? Matchers, T> : Matchers + + /** + * Get all current soft assertion failures + */ + function getSoftFailures(testId?: string): ExpectWebdriverIO.SoftFailure[] + + /** + * Manually assert all soft failures (throws an error if any failures exist) + */ + function assertSoftFailures(testId?: string): void + + /** + * Clear all current soft assertion failures + */ + function clearSoftFailures(testId?: string): void + + /** Expect Asymmetric Matchers */ + function any(sample: unknown): AsyncMatcher + function anything(): AsyncMatcher + function arrayContaining(sample: Array): AsyncMatcher + function arrayOf(sample: unknown): AsyncMatcher + function closeTo(sample: number, precision?: number): AsyncMatcher + function objectContaining(sample: Record): AsyncMatcher + function stringContaining(sample: string): AsyncMatcher + function stringMatching(sample: string | RegExp): AsyncMatcher + } + + namespace expectAsync { + + // TODO should we use expectAsync here instead? + /** Wdio soft assertion */ /** * Creates a soft assertion wrapper around standard expect * Soft assertions record failures but don't throw errors immediately @@ -15,7 +61,7 @@ declare global { /** * Get all current soft assertion failures */ - function getSoftFailures(testId?: string): WebdriverIO.SoftFailure[] + function getSoftFailures(testId?: string): ExpectWebdriverIO.SoftFailure[] /** * Manually assert all soft failures (throws an error if any failures exist) @@ -26,5 +72,17 @@ declare global { * Clear all current soft assertion failures */ function clearSoftFailures(testId?: string): void + + /** Expect Asymmetric Matchers */ + function any(sample: unknown): AsyncMatcher + function anything(): AsyncMatcher + function arrayContaining(sample: Array): AsyncMatcher + function arrayOf(sample: unknown): AsyncMatcher + function closeTo(sample: number, precision?: number): AsyncMatcher + function objectContaining(sample: Record): AsyncMatcher + function stringContaining(sample: string): AsyncMatcher + function stringMatching(sample: string | RegExp): AsyncMatcher } -} \ No newline at end of file +} + +export {} \ No newline at end of file From ff52916bc375ed55ac8ecb4dc0a1c549e08bbb40 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Tue, 24 Jun 2025 13:22:38 -0400 Subject: [PATCH 16/99] Use `ExpectWebdriverIO` where we can to be more verbose --- types/standalone.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/standalone.d.ts b/types/standalone.d.ts index 0e707310f..de6934efb 100644 --- a/types/standalone.d.ts +++ b/types/standalone.d.ts @@ -31,7 +31,7 @@ declare namespace ExpectWebdriverIO { toMatchInlineSnapshot(snapshot?: string, label?: string): Promise } - type MatchersAndInverse = Matchers & Inverse> + type MatchersAndInverse = ExpectWebdriverIO.Matchers & Inverse> /** * Mostly derived from the types of `jest-expect` but adapted to work with WebdriverIO. @@ -69,5 +69,5 @@ declare namespace ExpectWebdriverIO { clearSoftFailures(testId?: string): void } - interface InverseAsymmetricMatchers extends Expect {} + interface InverseAsymmetricMatchers extends ExpectWebdriverIO.Expect {} } From 85e0fad7911c1cde1b5856193476c5a0fbb274d4 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Tue, 24 Jun 2025 13:25:13 -0400 Subject: [PATCH 17/99] Separate Browser, mock and element matcher + better typing --- test-types/jest/types-jest.test.ts | 76 ++++++---- test-types/mocha/types-mocha.test.ts | 6 +- types/expect-webdriverio.d.ts | 214 +++++++++++++++++---------- 3 files changed, 187 insertions(+), 109 deletions(-) diff --git a/test-types/jest/types-jest.test.ts b/test-types/jest/types-jest.test.ts index fc70dc640..eee0ad782 100644 --- a/test-types/jest/types-jest.test.ts +++ b/test-types/jest/types-jest.test.ts @@ -5,38 +5,40 @@ describe('type assertions', () => { const chainableElement = $('findMe') const chainableArray = $$('ul>li') - describe('toHaveUrl', () => { + describe('Browser', () => { const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser - it('should not have ts errors and be able to await the promise when actual is browser', async () => { - const expectPromiseVoid: Promise = expect(browser).toHaveUrl('https://example.com') - await expectPromiseVoid + describe('toHaveUrl', () => { + it('should not have ts errors and be able to await the promise when actual is browser', async () => { + const expectPromiseVoid: Promise = expect(browser).toHaveUrl('https://example.com') + await expectPromiseVoid - const expectNotPromiseVoid: Promise = expect(browser).not.toHaveUrl('https://example.com') - await expectNotPromiseVoid - }) + const expectNotPromiseVoid: Promise = expect(browser).not.toHaveUrl('https://example.com') + await expectNotPromiseVoid + }) - it('should have ts errors and not need to await the promise when actual is browser', async () => { + it('should have ts errors and not need to await the promise when actual is browser', async () => { // @ts-expect-error - const expectVoid: void = expect(browser).toHaveUrl('https://example.com') - // @ts-expect-error - const expectNotVoid: void = expect(browser).not.toHaveUrl('https://example.com') - }) + const expectVoid: void = expect(browser).toHaveUrl('https://example.com') + // @ts-expect-error + const expectNotVoid: void = expect(browser).not.toHaveUrl('https://example.com') + }) - it('should have ts errors when actual is an element', async () => { + it('should have ts errors when actual is an element', async () => { // @ts-expect-error - await expect(element).toHaveUrl('https://example.com') - }) + await expect(element).toHaveUrl('https://example.com') + }) - it('should have ts errors when actual is an ChainableElement', async () => { + it('should have ts errors when actual is an ChainableElement', async () => { // @ts-expect-error - await expect(chainableElement).toHaveUrl('https://example.com') - }) + await expect(chainableElement).toHaveUrl('https://example.com') + }) - it('should support stringContaining', async () => { - const expectVoid1: Promise = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) + it('should support stringContaining', async () => { + const expectVoid1: Promise = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) - // @ts-expect-error - const expectVoid2: void = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) + // @ts-expect-error + const expectVoid2: void = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) + }) }) }) @@ -227,7 +229,11 @@ describe('type assertions', () => { const expectPromise2: Promise = expect(promiseNetworkMock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) const expectPromise3: Promise = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 - const expectPromise4: Promise = expect(promiseNetworkMock).toBeRequestedWith({ + const expectPromise4: Promise = expect(promiseNetworkMock).not.toBeRequested() + const expectPromise5: Promise = expect(promiseNetworkMock).not.toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + const expectPromise6: Promise = expect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + + const expectPromise7: Promise = expect(promiseNetworkMock).toBeRequestedWith({ url: 'http://localhost:8080/api/todo', // [optional] string | function | custom matcher method: 'POST', // [optional] string | array statusCode: 200, // [optional] number | array @@ -236,6 +242,10 @@ describe('type assertions', () => { postData: { title: 'foo', description: 'bar' }, // [optional] object | function | custom matcher response: { success: true }, // [optional] object | function | custom matcher }) + + const expectPromise8: Promise = expect(promiseNetworkMock).toBeRequestedWith(expect.objectContaining({ + response: { success: true }, // [optional] object | function | custom matcher + })) }) it('should have ts errors when typing to void', async () => { @@ -247,7 +257,14 @@ describe('type assertions', () => { const expectPromise3: void = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 // @ts-expect-error - const expectPromise4: void = expect(promiseNetworkMock).toBeRequestedWith({ + const expectPromise4: void = expect(promiseNetworkMock).not.toBeRequested() + // @ts-expect-error + const expectPromise5: void = expect(promiseNetworkMock).not.toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + // @ts-expect-error + const expectPromise6: void = expect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + + // @ts-expect-error + const expectPromise7: void = expect(promiseNetworkMock).toBeRequestedWith({ url: 'http://localhost:8080/api/todo', // [optional] string | function | custom matcher method: 'POST', // [optional] string | array statusCode: 200, // [optional] number | array @@ -256,6 +273,11 @@ describe('type assertions', () => { postData: { title: 'foo', description: 'bar' }, // [optional] object | function | custom matcher response: { success: true }, // [optional] object | function | custom matcher }) + + // @ts-expect-error + const expectPromise8: void = expect(promiseNetworkMock).toBeRequestedWith(expect.objectContaining({ + response: { success: true }, // [optional] object | function | custom matcher + })) }) }) @@ -292,15 +314,15 @@ describe('type assertions', () => { it('should have ts error when using await and actual is non-promise type', async () => { // @ts-expect-error - const expectWdioMatcher: WdioMatchers, string> = expect.soft(expectString) + const expectWdioMatcher: jest.MatcherAndInverse, string> = expect.soft(expectString) // @ts-expect-error const expectVoid: Promise = expect.soft(expectString).toBe('Test Page') }) it('should not have ts error and need to be awaited/be a promise if actual is a promise type', async () => { - // @ts-expect-error - const expectWdioMatcher: WdioMatchers> = expect.soft(expectPromise) + // @ts-expect-error + const expectWdioMatcher: jest.MatcherAndInverse> = expect.soft(expectPromise) // @ts-expect-error const expectVoid: void = expect.soft(expectPromise).toBe('Test Page') }) diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index 2fefc3443..2a4a9120b 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -301,15 +301,15 @@ describe('type assertions', () => { it('should have ts error when using await and actual is non-promise type', async () => { // @ts-expect-error - const expectWdioMatcher: WdioMatchers, string> = expect.soft(expectString) + const expectWdioMatcher: ExpectWebdriverIO.MatchersAndInverse, string> = expect.soft(expectString) // @ts-expect-error const expectVoid: Promise = expect.soft(expectString).toBe('Test Page') }) it('should not have ts error and need to be awaited/be a promise if actual is a promise type', async () => { - // @ts-expect-error - const expectWdioMatcher: WdioMatchers> = expect.soft(expectPromise) + // @ts-expect-error + const expectWdioMatcher: ExpectWebdriverIO.MatchersAndInverse> = expect.soft(expectPromise) // @ts-expect-error const expectVoid: void = expect.soft(expectPromise).toBe('Test Page') }) diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 91c8387ab..dfdd17eda 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -7,13 +7,51 @@ type Scenario = import('@wdio/types').Frameworks.Scenario type SnapshotResult = import('@vitest/snapshot').SnapshotResult type SnapshotUpdateState = import('@vitest/snapshot').SnapshotUpdateState type ExpectLibAsymmetricMatchers = import('expect').AsymmetricMatchers +type ChainablePromiseElement = import('webdriverio').ChainablePromiseElement +type ChainablePromiseArray = import('webdriverio').ChainablePromiseArray -// type ChainablePromiseElement = import('webdriverio').ChainablePromiseElement -// type ChainablePromiseArray = import('webdriverio').ChainablePromiseArray +// type PromiseLike = import('expect').PromiseLike -// TODO dprevost - check if we need to add ChainablePromiseElement and or ChainablePromiseArrayElement // eslint-disable-next-line @typescript-eslint/no-explicit-any type PromiseLikeType = Promise +type UnwrapPromise = T extends Promise ? U : T + +interface WdioBrowserMatchers{ + /** + * `WebdriverIO.Browser` -> `getUrl` + */ + toHaveUrl: T extends WebdriverIO.Browser ? (url: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions) => Promise: never; + + /** + * `WebdriverIO.Browser` -> `getTitle` + */ + toHaveTitle: T extends WebdriverIO.Browser ? (title: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions) => Promise: never; + + /** + * `WebdriverIO.Browser` -> `execute` + */ + toHaveClipboardText: T extends WebdriverIO.Browser ? (clipboardText: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions) => Promise: never; +} + +type MockPromise = Promise +interface WdioMockMatchers { + /** + * Check that `WebdriverIO.Mock` was called + */ + toBeRequested: T extends MockPromise ? (options?: ExpectWebdriverIO.CommandOptions) => Promise : never + /** + * Check that `WebdriverIO.Mock` was called N times + */ + toBeRequestedTimes: T extends MockPromise ? ( + times: number | ExpectWebdriverIO.NumberOptions, + options?: ExpectWebdriverIO.NumberOptions + ) => Promise : never + + /** + * Check that `WebdriverIO.Mock` was called with the specific parameters + */ + toBeRequestedWith: T extends MockPromise ? (requestedWith: ExpectWebdriverIO.RequestedWith, options?: ExpectWebdriverIO.CommandOptions) => Promise : never +} /** * Note we are defining Matchers outside of the namespace as done in jest library until we can make every typing work correctly. @@ -22,45 +60,59 @@ type PromiseLikeType = Promise // TODO dprevost have browser matchers and element matchers separated // TODO extending extends Record remove ts error on unimplemented matchers -/* eslint-disable @typescript-eslint/no-explicit-any */ -interface WdioCustomMatchers /*extends Record*/ { + +// TODO dprevost - check if custom matchers (https://webdriver.io/docs/custommatchers/) will still work aka webdriverio/expect-webdriverio#1408 +type ElementOrArrayLike = ElementLike | ElementArrayLike +type ElementLike = WebdriverIO.Element | ChainablePromiseElement +type ElementArrayLike = WebdriverIO.ElementArray | ChainablePromiseArray +interface WdioCustomMatchers { // ===== $ or $$ ===== /** * `WebdriverIO.Element` -> `isDisplayed` */ - toBeDisplayed(options?: ExpectWebdriverIO.CommandOptions): Promise + toBeDisplayed: T extends ElementOrArrayLike ? (options?: ExpectWebdriverIO.CommandOptions) => Promise : never /** * `WebdriverIO.Element` -> `isExisting` */ - toExist(options?: ExpectWebdriverIO.CommandOptions): Promise + toExist: T extends ElementOrArrayLike ? (options?: ExpectWebdriverIO.CommandOptions) => Promise : never + /** * `WebdriverIO.Element` -> `isExisting` */ - toBePresent(options?: ExpectWebdriverIO.CommandOptions): Promise + toBePresent: T extends ElementOrArrayLike ? (options?: ExpectWebdriverIO.CommandOptions) => Promise : never + /** * `WebdriverIO.Element` -> `isExisting` */ - toBeExisting(options?: ExpectWebdriverIO.CommandOptions): Promise + toBeExisting: T extends ElementOrArrayLike ? (options?: ExpectWebdriverIO.CommandOptions) => Promise : never /** * `WebdriverIO.Element` -> `getAttribute` */ - toHaveAttribute( + toHaveAttribute: T extends ElementOrArrayLike ? ( attribute: string, value?: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions - ): Promise + ) => Promise : never + /** * `WebdriverIO.Element` -> `getAttribute` */ - toHaveAttr(attribute: string, value?: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): Promise + toHaveAttr: T extends ElementOrArrayLike ? ( + attribute: string, + value?: string | RegExp | ExpectWebdriverIO.PartialMatcher, + options?: ExpectWebdriverIO.StringOptions + ) => Promise : never /** * `WebdriverIO.Element` -> `getAttribute` class * @deprecated since v1.3.1 - use `toHaveElementClass` instead. */ - toHaveClass(className: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): Promise + toHaveClass: T extends ElementOrArrayLike ? ( + className: string | RegExp | ExpectWebdriverIO.PartialMatcher, + options?: ExpectWebdriverIO.StringOptions + ) => Promise : never /** * `WebdriverIO.Element` -> `getAttribute` class @@ -78,84 +130,103 @@ interface WdioCustomMatchers /*extends Record*/ { * await expect(element).toHaveElementClass(['btn', 'btn-large']); * ``` */ - toHaveElementClass(className: string | RegExp | Array | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): Promise + toHaveElementClass: T extends ElementOrArrayLike ? ( + className: string | RegExp | Array | ExpectWebdriverIO.PartialMatcher, + options?: ExpectWebdriverIO.StringOptions + ) => Promise : never /** * `WebdriverIO.Element` -> `getProperty` */ - toHaveElementProperty( + toHaveElementProperty: T extends ElementOrArrayLike ? ( property: string | RegExp | ExpectWebdriverIO.PartialMatcher, value?: unknown, options?: ExpectWebdriverIO.StringOptions - ): Promise + ) => Promise : never /** * `WebdriverIO.Element` -> `getProperty` value */ - toHaveValue(value: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): Promise + toHaveValue: T extends ElementOrArrayLike ? ( + value: string | RegExp | ExpectWebdriverIO.PartialMatcher, + options?: ExpectWebdriverIO.StringOptions + ) => Promise : never /** * `WebdriverIO.Element` -> `isClickable` */ - toBeClickable(options?: ExpectWebdriverIO.StringOptions): Promise + toBeClickable: T extends ElementOrArrayLike ? (options?: ExpectWebdriverIO.StringOptions) => Promise : never /** * `WebdriverIO.Element` -> `!isEnabled` */ - toBeDisabled(options?: ExpectWebdriverIO.StringOptions): Promise + toBeDisabled: T extends ElementOrArrayLike ? (options?: ExpectWebdriverIO.StringOptions) => Promise : never /** * `WebdriverIO.Element` -> `isDisplayedInViewport` */ - toBeDisplayedInViewport(options?: ExpectWebdriverIO.StringOptions): Promise + toBeDisplayedInViewport: T extends ElementOrArrayLike ? (options?: ExpectWebdriverIO.StringOptions) => Promise : never /** * `WebdriverIO.Element` -> `isEnabled` */ - toBeEnabled(options?: ExpectWebdriverIO.StringOptions): Promise + toBeEnabled: T extends ElementOrArrayLike ? (options?: ExpectWebdriverIO.StringOptions) => Promise : never /** * `WebdriverIO.Element` -> `isFocused` */ - toBeFocused(options?: ExpectWebdriverIO.StringOptions): Promise + toBeFocused: T extends ElementOrArrayLike ? (options?: ExpectWebdriverIO.StringOptions) => Promise : never /** * `WebdriverIO.Element` -> `isSelected` */ - toBeSelected(options?: ExpectWebdriverIO.StringOptions): Promise + toBeSelected: T extends ElementOrArrayLike ? (options?: ExpectWebdriverIO.StringOptions) => Promise : never /** * `WebdriverIO.Element` -> `isSelected` */ - toBeChecked(options?: ExpectWebdriverIO.StringOptions): Promise + toBeChecked: T extends ElementOrArrayLike ? (options?: ExpectWebdriverIO.StringOptions) => Promise : never /** * `WebdriverIO.Element` -> `$$('./*').length` * supports less / greater then or equals to be passed in options */ - toHaveChildren( + toHaveChildren: T extends ElementOrArrayLike ? ( size?: number | ExpectWebdriverIO.NumberOptions, options?: ExpectWebdriverIO.NumberOptions - ): Promise + ) => Promise : never /** * `WebdriverIO.Element` -> `getAttribute` href */ - toHaveHref(href: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): Promise + toHaveHref: T extends ElementOrArrayLike ? ( + href: string | RegExp | ExpectWebdriverIO.PartialMatcher, + options?: ExpectWebdriverIO.StringOptions + ) => Promise : never + /** * `WebdriverIO.Element` -> `getAttribute` href */ - toHaveLink(href: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): Promise + toHaveLink: T extends ElementOrArrayLike ? ( + href: string | RegExp | ExpectWebdriverIO.PartialMatcher, + options?: ExpectWebdriverIO.StringOptions + ) => Promise : never /** * `WebdriverIO.Element` -> `getProperty` value */ - toHaveId(id: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): Promise + toHaveId: T extends ElementOrArrayLike ? ( + id: string | RegExp | ExpectWebdriverIO.PartialMatcher, + options?: ExpectWebdriverIO.StringOptions + ) => Promise : never /** * `WebdriverIO.Element` -> `getSize` value */ - toHaveSize(size: { height: number; width: number }, options?: ExpectWebdriverIO.StringOptions): Promise + toHaveSize: T extends ElementOrArrayLike ? ( + size: { height: number; width: number }, + options?: ExpectWebdriverIO.StringOptions + ) => Promise : never /** * `WebdriverIO.Element` -> `getText` @@ -177,100 +248,80 @@ interface WdioCustomMatchers /*extends Record*/ { * ``` */ toHaveText( - text: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array ExpectWebdriverIO.PartialMatcher | ExpectWebdriverIO.PartialMatcher, - options?: ExpectWebdriverIO.StringOptions - ): Promise + text: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, + ) => Promise : never /** * `WebdriverIO.Element` -> `getHTML` * Element's html equals the html provided */ - toHaveHTML(html: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, options?: ExpectWebdriverIO.HTMLOptions): Promise + toHaveHTML: T extends ElementOrArrayLike ? ( + html: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, + options?: ExpectWebdriverIO.HTMLOptions + ) => Promise : never /** * `WebdriverIO.Element` -> `getComputedLabel` * Element's computed label equals the computed label provided */ - toHaveComputedLabel( + toHaveComputedLabel: T extends ElementOrArrayLike ? ( computedLabel: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, options?: ExpectWebdriverIO.StringOptions - ): Promise + ) => Promise : never /** * `WebdriverIO.Element` -> `getComputedRole` * Element's computed role equals the computed role provided */ - toHaveComputedRole( + toHaveComputedRole: T extends ElementOrArrayLike ? ( computedRole: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, options?: ExpectWebdriverIO.StringOptions - ): Promise + ) => Promise : never /** * `WebdriverIO.Element` -> `getSize('width')` * Element's width equals the width provided */ - toHaveWidth(width: number, options?: ExpectWebdriverIO.CommandOptions): Promise + toHaveWidth: T extends ElementOrArrayLike ? ( + width: number, + options?: ExpectWebdriverIO.CommandOptions + ) => Promise : never /** * `WebdriverIO.Element` -> `getSize('height')` * Element's height equals the height provided */ - toHaveHeight(height: number, options?: ExpectWebdriverIO.CommandOptions): Promise + toHaveHeight: T extends ElementOrArrayLike ? ( + height: number, + options?: ExpectWebdriverIO.CommandOptions + ) => Promise : never /** * `WebdriverIO.Element` -> `getSize()` * Element's size equals the size provided */ - toHaveHeight(size: { height: number; width: number }, options?: ExpectWebdriverIO.CommandOptions): Promise + toHaveHeight: T extends ElementOrArrayLike ? ( + size: { height: number; width: number }, + options?: ExpectWebdriverIO.CommandOptions + ) => Promise : never /** * `WebdriverIO.Element` -> `getAttribute("style")` */ - toHaveStyle(style: { [key: string]: string }, options?: ExpectWebdriverIO.StringOptions): Promise - - // ===== browser only ===== - /** - * `WebdriverIO.Browser` -> `getUrl` - */ - toHaveUrl: T extends WebdriverIO.Browser ? (url: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions) => Promise: never; - - /** - * `WebdriverIO.Browser` -> `getTitle` - */ - toHaveTitle(title: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): Promise - - /** - * `WebdriverIO.Browser` -> `execute` - */ - toHaveClipboardText(clipboardText: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): Promise + toHaveStyle: T extends ElementOrArrayLike ? ( + style: { [key: string]: string }, + options?: ExpectWebdriverIO.StringOptions + ) => Promise : never // ===== $$ only ===== /** * `WebdriverIO.ElementArray` -> `$$('...').length` * supports less / greater then or equals to be passed in options */ - toBeElementsArrayOfSize( + toBeElementsArrayOfSize: T extends ElementArrayLike ? ( size: number | ExpectWebdriverIO.NumberOptions, options?: ExpectWebdriverIO.NumberOptions - ): Promise & Promise; - - // ==== network mock ==== - /** - * Check that `WebdriverIO.Mock` was called - */ - toBeRequested(options?: ExpectWebdriverIO.CommandOptions): Promise - /** - * Check that `WebdriverIO.Mock` was called N times - */ - toBeRequestedTimes( - times: number | ExpectWebdriverIO.NumberOptions, - options?: ExpectWebdriverIO.NumberOptions - ): Promise - - /** - * Check that `WebdriverIO.Mock` was called with the specific parameters - */ - toBeRequestedWith(requestedWith: ExpectWebdriverIO.RequestedWith, options?: ExpectWebdriverIO.CommandOptions): Promise + ) => Promise & Promise : never } /** @@ -290,7 +341,7 @@ interface WdioOverloadedMatchers { toMatchInlineSnapshot(snapshot?: string, label?: string): Promise } -interface WdioMatchers extends WdioCustomMatchers, WdioOverloadedMatchers {} +interface WdioMatchers extends WdioOverloadedMatchers, WdioBrowserMatchers, WdioCustomMatchers, WdioMockMatchers {} /** * expect function declaration, containing two generics: @@ -326,6 +377,7 @@ interface WdioCustomExpect extends ExpectLibAsymmetricMatchers { declare namespace ExpectWebdriverIO { function setOptions(options: DefaultOptions): void + // eslint-disable-next-line @typescript-eslint/no-explicit-any function getConfig(): any interface SnapshotServiceArgs { @@ -364,6 +416,7 @@ declare namespace ExpectWebdriverIO { constructor(serviceOptions?: SoftAssertionServiceOptions, capabilities?: unknown, config?: un) beforeTest(test: Test): void beforeStep(step: PickleStep, scenario: Scenario): void + // eslint-disable-next-line @typescript-eslint/no-explicit-any afterTest(test: Test, context: any, result: TestResult): void afterStep(step: PickleStep, scenario: Scenario, result: { passed: boolean, error?: Error }): void } @@ -397,6 +450,7 @@ declare namespace ExpectWebdriverIO { * expect(el).toHaveAttribute('attr', 'value', { ... }) // expectedValue is `['attr', 'value]` * ``` */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any expectedValue?: any, /** * Options that the user has passed in, e.g. `expect(el).toHaveText('foo', { ignoreCase: true })` -> `{ ignoreCase: true }` @@ -536,8 +590,10 @@ declare namespace ExpectWebdriverIO { 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 } From b7fa0a133b6e5d282e940a9a054753e9caf5f7e4 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Tue, 24 Jun 2025 14:04:22 -0400 Subject: [PATCH 18/99] fix rebase + add more UT typing cases --- src/matchers/element/toHaveAttribute.ts | 3 ++ src/matchers/element/toHaveClass.ts | 1 + src/matchers/element/toHaveHref.ts | 1 + src/matchers/element/toHaveId.ts | 1 + test-types/jest/types-jest.test.ts | 63 +++++++++++++++---------- types/expect-webdriverio.d.ts | 4 +- 6 files changed, 46 insertions(+), 27 deletions(-) diff --git a/src/matchers/element/toHaveAttribute.ts b/src/matchers/element/toHaveAttribute.ts index cd0a2b141..acbe2bffe 100644 --- a/src/matchers/element/toHaveAttribute.ts +++ b/src/matchers/element/toHaveAttribute.ts @@ -56,12 +56,14 @@ async function toHaveAttributeFn(received: WdioElementMaybePromise, attribute: s let el = await received?.getElement() const pass = await waitUntil(async () => { + // @ts-ignore TODO dprevost fix me const result = await executeCommand.call(this, el, conditionAttr, {}, [attribute]) el = result.el as WebdriverIO.Element return result.success }, isNot, {}) + // @ts-ignore TODO dprevost fix me const message = enhanceError(el, !isNot, pass, this, verb, expectation, attribute, {}) return { @@ -84,6 +86,7 @@ export async function toHaveAttribute( const result = typeof value !== 'undefined' // Name and value is passed in e.g. el.toHaveAttribute('attr', 'value', (opts)) + // @ts-ignore TODO dprevost fix me ? await toHaveAttributeAndValue.call(this, received, attribute, value, options) // Only name is passed in e.g. el.toHaveAttribute('attr') : await toHaveAttributeFn.call(this, received, attribute) diff --git a/src/matchers/element/toHaveClass.ts b/src/matchers/element/toHaveClass.ts index 5cb5d4791..18df08712 100644 --- a/src/matchers/element/toHaveClass.ts +++ b/src/matchers/element/toHaveClass.ts @@ -84,6 +84,7 @@ export async function toHaveElementClass( * @deprecated */ export function toHaveClassContaining(el: WebdriverIO.Element, className: string | RegExp | ExpectWebdriverIO.PartialMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS) { + // @ts-ignore TODO dprevost fix me return toHaveAttributeAndValue.call(this, el, 'class', className, { ...options, containing: true diff --git a/src/matchers/element/toHaveHref.ts b/src/matchers/element/toHaveHref.ts index 05920c262..2aa67b0f8 100644 --- a/src/matchers/element/toHaveHref.ts +++ b/src/matchers/element/toHaveHref.ts @@ -13,6 +13,7 @@ export async function toHaveHref( options, }) + // @ts-ignore TODO dprevost fix me const result = await toHaveAttributeAndValue.call(this, el, 'href', expectedValue, options) await options.afterAssertion?.({ diff --git a/src/matchers/element/toHaveId.ts b/src/matchers/element/toHaveId.ts index 99be37be3..240ba40f3 100644 --- a/src/matchers/element/toHaveId.ts +++ b/src/matchers/element/toHaveId.ts @@ -13,6 +13,7 @@ export async function toHaveId( options, }) + // @ts-ignore TODO dprevost fix me const result: ExpectWebdriverIO.AssertionResult = await toHaveAttributeAndValue.call(this, el, 'id', expectedValue, options) await options.afterAssertion?.({ diff --git a/test-types/jest/types-jest.test.ts b/test-types/jest/types-jest.test.ts index eee0ad782..28deaa66d 100644 --- a/test-types/jest/types-jest.test.ts +++ b/test-types/jest/types-jest.test.ts @@ -5,39 +5,35 @@ describe('type assertions', () => { const chainableElement = $('findMe') const chainableArray = $$('ul>li') + // Type assertions + let expectPromiseVoid: Promise + let expectVoid: void + describe('Browser', () => { const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser - describe('toHaveUrl', () => { - it('should not have ts errors and be able to await the promise when actual is browser', async () => { - const expectPromiseVoid: Promise = expect(browser).toHaveUrl('https://example.com') - await expectPromiseVoid - const expectNotPromiseVoid: Promise = expect(browser).not.toHaveUrl('https://example.com') - await expectNotPromiseVoid - }) + describe('toHaveUrl', () => { + it('should be supported correctly', async () => { + expectPromiseVoid = expect(browser).toHaveUrl('https://example.com') + expectPromiseVoid = expect(browser).not.toHaveUrl('https://example.com') + expectPromiseVoid = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) + expectPromiseVoid = expect(browser).toHaveUrl(expect.any(String)) + expectPromiseVoid = expect(browser).toHaveUrl(expect.anything()) + // TODO add more asymmetric matchers - it('should have ts errors and not need to await the promise when actual is browser', async () => { - // @ts-expect-error - const expectVoid: void = expect(browser).toHaveUrl('https://example.com') // @ts-expect-error - const expectNotVoid: void = expect(browser).not.toHaveUrl('https://example.com') + 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')) }) - it('should have ts errors when actual is an element', async () => { - // @ts-expect-error + it('should have ts errors when actual is not a Browser element', async () => { + // @ts-expect-error await expect(element).toHaveUrl('https://example.com') - }) - - it('should have ts errors when actual is an ChainableElement', async () => { - // @ts-expect-error - await expect(chainableElement).toHaveUrl('https://example.com') - }) - - it('should support stringContaining', async () => { - const expectVoid1: Promise = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) - // @ts-expect-error - const expectVoid2: void = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) + await expect(element).not.toHaveUrl('https://example.com') }) }) }) @@ -78,6 +74,25 @@ describe('type assertions', () => { }) }) + describe('toHaveText', () => { + it('should be supported correctly', async () => { + const expectPromise1: Promise = expect(element).toHaveText('text') + const expectPromise2: Promise = expect(element).toHaveText(/text/) + const expectPromise3: Promise = expect(element).toHaveText(['text1', 'text2']) + const expectPromise4: Promise = expect(element).toHaveText([expect.stringContaining('text1'), expect.stringContaining('text2')]) + const expectPromise5: Promise = expect(element).toHaveText([/text1/, /text2/]) + const expectPromise6: Promise = expect(element).toHaveText(['text1', /text1/, expect.stringContaining('text3')]) + + const expectPromise7: Promise = expect(element).not.toHaveText('text') + + // @ts-expect-error + const expectTsError7: void = expect(element).toHaveText('text') + + // @ts-expect-error + await expect(browser).toHaveText('text') + }) + }) + describe('toMatchSnapshot', () => { it('should not have ts errors when typing to Promise for an element', async () => { diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index dfdd17eda..bf7621149 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -247,9 +247,7 @@ interface WdioCustomMatchers { * await expect(elem).toHaveText(['Coffee', 'Tea', 'Milk']) * ``` */ - toHaveText( - text: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, - ) => Promise : never + toHaveText: T extends ElementOrArrayLike ? (text: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array) => Promise : never /** * `WebdriverIO.Element` -> `getHTML` From 7ecd093dd323efed9086a077ca0b5cbb0eda9bc5 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Tue, 24 Jun 2025 18:21:46 -0400 Subject: [PATCH 19/99] Review test template to be better --- test-types/jest/types-jest.test.ts | 496 +++++++++++++++++------------ 1 file changed, 290 insertions(+), 206 deletions(-) diff --git a/test-types/jest/types-jest.test.ts b/test-types/jest/types-jest.test.ts index 28deaa66d..fff425b7d 100644 --- a/test-types/jest/types-jest.test.ts +++ b/test-types/jest/types-jest.test.ts @@ -1,9 +1,11 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -describe('type assertions', () => { - const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element - const chainableElement = $('findMe') - const chainableArray = $$('ul>li') +describe('type assertions', async () => { + const chainableElement: ChainablePromiseElement = $('findMe') + const chainableArray: ChainablePromiseArray = $$('ul>li') + const element: WebdriverIO.Element = await chainableElement?.getElement() + // TODO dprevost: Need more test with this type? + // const ElementArray: WebdriverIO.ElementArray = await chainableArray?.getElements() // Type assertions let expectPromiseVoid: Promise @@ -16,6 +18,8 @@ describe('type assertions', () => { 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.any(String)) expectPromiseVoid = expect(browser).toHaveUrl(expect.anything()) @@ -34,205 +38,278 @@ describe('type assertions', () => { 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('element type assertions', () => { + describe('element', () => { describe('toBeDisabled', () => { - it('should not have ts errors and be able to await the promise for element', async () => { - // expect no ts errors - const expectIsPromiseVoid: Promise = expect(element).toBeDisabled() - await expectIsPromiseVoid - - const expectNotIsPromiseVoid: Promise = expect(element).not.toBeDisabled() - await expectNotIsPromiseVoid - }) + it('should be supported correctly', async () => { + // Element + expectPromiseVoid = expect(element).toBeDisabled() + expectPromiseVoid = expect(element).not.toBeDisabled() - it('should not have ts errors and be able to await the promise for chainable', async () => { - // expect no ts errors - const expectIsPromiseVoid: Promise = expect(chainableElement).toBeDisabled() - await expectIsPromiseVoid + // Chainable element + expectPromiseVoid = expect(chainableElement).toBeDisabled() + expectPromiseVoid = expect(chainableElement).not.toBeDisabled() - const expectNotIsPromiseVoid: Promise = expect(chainableElement).not.toBeDisabled() - await expectNotIsPromiseVoid - }) + // Chainable element array + expectPromiseVoid = expect(chainableArray).toBeDisabled() + expectPromiseVoid = expect(chainableArray).not.toBeDisabled() - it('should have ts errors when typing to void for element', async () => { // @ts-expect-error - const expectToBeIsVoid: void = expect(element).toBeDisabled() + expectVoid = expect(element).toBeDisabled() // @ts-expect-error - const expectNotToBeIsVoid: void = expect(element).not.toBeDisabled() + expectVoid = expect(element).not.toBeDisabled() }) - it('should have ts errors when typing to void for chainable', async () => { + it('should have ts errors when actual is not an element', async () => { // @ts-expect-error - const expectToBeIsVoid: void = expect(chainableElement).toBeDisabled() + await expect(browser).toBeDisabled() // @ts-expect-error - const expectNotToBeIsVoid: void = expect(chainableElement).not.toBeDisabled() + 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 () => { - const expectPromise1: Promise = expect(element).toHaveText('text') - const expectPromise2: Promise = expect(element).toHaveText(/text/) - const expectPromise3: Promise = expect(element).toHaveText(['text1', 'text2']) - const expectPromise4: Promise = expect(element).toHaveText([expect.stringContaining('text1'), expect.stringContaining('text2')]) - const expectPromise5: Promise = expect(element).toHaveText([/text1/, /text2/]) - const expectPromise6: Promise = expect(element).toHaveText(['text1', /text1/, expect.stringContaining('text3')]) + 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')]) - const expectPromise7: Promise = expect(element).not.toHaveText('text') + expectPromiseVoid = expect(element).not.toHaveText('text') // @ts-expect-error - const expectTsError7: void = expect(element).toHaveText('text') + expectVoid = expect(element).toHaveText('text') + // @ts-expect-error + await expect(element).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('toMatchSnapshot', () => { - it('should not have ts errors when typing to Promise for an element', async () => { - const expectPromise1: Promise = expect(element).toMatchSnapshot() - const expectPromise2: Promise = expect(element).toMatchSnapshot('test label') - }) + it('should be supported correctly', async () => { + expectPromiseVoid = expect(element).toMatchSnapshot() + expectPromiseVoid = expect(element).toMatchSnapshot('test label') + expectPromiseVoid = expect(element).not.toMatchSnapshot('test label') - it('should not have ts errors when typing to Promise for a chainable', async () => { - const expectPromise1: Promise = expect(chainableElement).toMatchSnapshot() - const expectPromise2: Promise = expect(chainableElement).toMatchSnapshot('test label') - }) + expectPromiseVoid = expect(chainableElement).toMatchSnapshot() + expectPromiseVoid = expect(chainableElement).toMatchSnapshot('test label') + expectPromiseVoid = expect(chainableElement).not.toMatchSnapshot('test label') - // We need somehow to exclude the Jest types one for this to success - it('should have ts errors when typing to void for an element like', async () => { //@ts-expect-error - const expectNotToBeVoid1: void = expect(element).toMatchSnapshot() + expectVoid = expect(element).toMatchSnapshot() + //@ts-expect-error + expectVoid = expect(element).not.toMatchSnapshot() + //@ts-expect-error + expectVoid = expect(chainableElement).toMatchSnapshot() //@ts-expect-error - const expectNotToBeVoid2: void = expect(chainableElement).toMatchSnapshot() + expectVoid = expect(chainableElement).not.toMatchSnapshot() }) - // TODO - conditional types check on T to have the below match void does not work - // it('should not have ts errors when typing to void for a string', async () => { - // const expectNotToBeVoid: void = expect('.findme').toMatchSnapshot() + // TODO - since we are overloading the `toMatchSnapshot` of jest.toMatchSnapshot, I wonder if we can achieve the below... + // it('should have ts errors when not an element or chainable', async () => { + // //@ts-expect-error + // await expect('.findme').toMatchSnapshot() // }) }) describe('toMatchInlineSnapshot', () => { - it('should not have ts errors when typing to Promise for an element', async () => { - const expectPromise1: Promise = expect(element).toMatchInlineSnapshot() - const expectPromise2: Promise = expect(element).toMatchInlineSnapshot('test snapshot') - const expectPromise3: Promise = expect(element).toMatchInlineSnapshot('test snapshot', 'test label') - }) + it('should be correctly supported', async () => { + expectPromiseVoid = expect(element).toMatchInlineSnapshot() + expectPromiseVoid = expect(element).toMatchInlineSnapshot('test snapshot') + expectPromiseVoid = expect(element).toMatchInlineSnapshot('test snapshot', 'test label') - it('should not have ts errors when typing to Promise for a chainable', async () => { - const expectPromise1: Promise = expect(chainableElement).toMatchInlineSnapshot() - const expectPromise2: Promise = expect(chainableElement).toMatchInlineSnapshot('test snapshot') - const expectPromise3: Promise = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') - }) + expectPromiseVoid = expect(chainableElement).toMatchInlineSnapshot() + expectPromiseVoid = expect(chainableElement).toMatchInlineSnapshot('test snapshot') + expectPromiseVoid = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') - // We need somehow to exclude the Jest types one for this to success - it('should have ts errors when typing to void for an element like', async () => { //@ts-expect-error - const expectNotToBeVoid1: void = expect(element).toMatchInlineSnapshot() + expectVoid = expect(element).toMatchInlineSnapshot() //@ts-expect-error - const expectPromise2: void = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + expectVoid = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') }) + }) - // TODO - conditional types check on T to have the below match void does not work - // it('should not have ts errors when typing to void for a string', async () => { - // const expectNotToBeVoid: void = expect('.findme').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('toBe', () => { - it('should not have ts errors when typing to void when actual is boolean', async () => { - const expectToBeIsVoid: void = expect(true).toBe(true) - const expectNotToBeIsVoid: void = expect(true).not.toBe(true) - }) + it('should expect void type when actual is a boolean', async () => { + expectVoid = expect(true).toBe(true) + expectVoid = expect(true).not.toBe(true) - it('should have ts errors when typing to Promise when actual is boolean', async () => { //@ts-expect-error - const expectToBeIsNotPromiseVoid1: Promise = expect(true).toBe(true) + 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 is not supported', async () => { + expectVoid = expect(chainableElement).toBe(true) + expectVoid = expect(chainableElement).not.toBe(true) + + //@ts-expect-error + expectPromiseVoid = expect(chainableElement).toBe(true) //@ts-expect-error - const expectToBeIsNotPromiseVoid2: Promise = expect(true).not.toBe(true) + expectPromiseVoid = expect(chainableElement).not.toBe(true) }) - it('should expect void when actual is an awaited element/chainable', async () => { - const isClickableElement = await element.isClickable() - const expectPromiseVoid1: void = expect(isClickableElement).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) - const isClickableChainable: boolean = await chainableElement.isClickable() - const expectPromiseVoid2: void = expect(isClickableChainable).toBe(true) + expectVoid = expect(promiseBoolean).toBe(true) + expectVoid = expect(promiseBoolean).not.toBe(true) - // @ts-expect-error - const expectPromiseVoid3: Promise = expect(isClickableElement).toBe(true) + //@ts-expect-error + expectPromiseVoid = expect(promiseBoolean).toBe(true) + //@ts-expect-error + expectPromiseVoid = expect(promiseBoolean).toBe(true) + }) - // @ts-expect-error - const expectPromiseVoid4: Promise = expect(isClickableChainable).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('string type assertions', () => { - it('should not have ts errors when typing to void', async () => { - // Expect no ts errors - const expectToBeIsVoid: void = expect('test').toBe('test') - }) + describe('Jest original Matchers', () => { + const propertyMatchers: Partial<{}> = {} + const snapshotName: string = 'test-snapshot' + describe('toMatchSnapshot', () => { - it('should have ts errors when typing to Promise', async () => { - //@ts-expect-error - const expectToBeIsNotPromiseVoid: Promise = expect('test').toBe('test') + it('should have original jest Matcher still works', async () => { + expectVoid = expect(element).toMatchSnapshot(propertyMatchers) + expectVoid = expect(element).toMatchSnapshot(propertyMatchers, snapshotName) + expectVoid = expect(element).toMatchInlineSnapshot(propertyMatchers) + expectVoid = expect(element).toMatchInlineSnapshot(propertyMatchers, snapshotName) + + expectVoid = expect(element).not.toMatchSnapshot(propertyMatchers) + expectVoid = expect(element).not.toMatchSnapshot(propertyMatchers, snapshotName) + expectVoid = expect(element).not.toMatchInlineSnapshot(propertyMatchers) + expectVoid = expect(element).not.toMatchInlineSnapshot(propertyMatchers, snapshotName) + + // @ts-expect-error + expectPromiseVoid = expect(element).toMatchSnapshot(propertyMatchers) + // @ts-expect-error + expectPromiseVoid = expect(element).toMatchSnapshot(propertyMatchers, snapshotName) + // @ts-expect-error + expectPromiseVoid = expect(element).toMatchInlineSnapshot(propertyMatchers) + // @ts-expect-error + expectPromiseVoid = expect(element).toMatchInlineSnapshot(propertyMatchers, snapshotName) + // @ts-expect-error + expectPromiseVoid = expect(element).not.toMatchSnapshot(propertyMatchers) + // @ts-expect-error + expectPromiseVoid = expect(element).not.toMatchSnapshot(propertyMatchers, snapshotName) + // @ts-expect-error + expectPromiseVoid = expect(element).not.toMatchInlineSnapshot(propertyMatchers) + // @ts-expect-error + expectPromiseVoid = expect(element).not.toMatchInlineSnapshot(propertyMatchers, snapshotName) + }) }) }) - describe('Promise<> type assertions', () => { + describe('Promise type assertions', () => { const booleanPromise: Promise = Promise.resolve(true) - it('should not have ts errors when typing to void', async () => { - const expectToBeIsVoid: void = expect(booleanPromise).toBe(true) - const expectAwaitToBeIsVoid: void = expect(await booleanPromise).toBe(true) - }) + it('should expect a Promise of type', async () => { + const expectPromiseBoolean1: jest.JestMatchers> = expect(booleanPromise) + const expectPromiseBoolean2: jest.Matchers> = expect(booleanPromise).not - it('should not have ts errors when resolves and rejects is typed to Promise', async () => { - // TODO the below needs to return Promise but currently returns void - const expectResolvesToBeIsVoid: Promise = expect(booleanPromise).resolves.toBe(true) - const expectRejectsToBeIsVoid: Promise = expect(booleanPromise).rejects.toBe(true) + // @ts-expect-error + const expectPromiseBoolean3: jest.JestMatchers = expect(booleanPromise) + //// @ts-expect-error + // const expectPromiseBoolean4: jest.Matchers = expect(booleanPromise).not }) - it('should have ts errors when typing to Promise', async () => { - //@ts-expect-error - const expectToBeIsNotPromiseVoid1: Promise = expect(booleanPromise).toBe(true) - //@ts-expect-error - const expectToBeIsNotPromiseVoid2: Promise = expect(await booleanPromise).toBe(true) - }) + it('should work with resolves & rejects correctly', async () => { + expectPromiseVoid = expect(booleanPromise).resolves.toBe(true) + expectPromiseVoid = expect(booleanPromise).rejects.toBe(true) - it('should have ts errors when typing resolves and reject is typed to void', async () => { //@ts-expect-error - const expectResolvesToBeIsVoid: void = expect(booleanPromise).resolves.toBe(true) + expectVoid = expect(booleanPromise).resolves.toBe(true) //@ts-expect-error - const expectRejectsToBeIsVoid: void = expect(booleanPromise).rejects.toBe(true) - }) - }) + expectVoid = expect(booleanPromise).rejects.toBe(true) - describe('toBeElementsArrayOfSize', async () => { - - it('should not have ts errors when typing to Promise', async () => { - const listItems = await chainableArray - const expectPromise: Promise = expect(listItems).toBeElementsArrayOfSize(5) - const expectPromise1: Promise = expect(listItems).toBeElementsArrayOfSize({ lte: 10 }) }) - it('should have ts errors when typing to void', async () => { - const listItems = await chainableArray - // @ts-expect-error - const expectPromise: void = expect(listItems).toBeElementsArrayOfSize(5) - // @ts-expect-error - const expectPromise1: void = expect(listItems).toBeElementsArrayOfSize({ lte: 10 }) + 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) }) }) @@ -240,15 +317,15 @@ describe('type assertions', () => { const promiseNetworkMock = browser.mock('**/api/todo*') it('should not have ts errors when typing to Promise', async () => { - const expectPromise1: Promise = expect(promiseNetworkMock).toBeRequested() - const expectPromise2: Promise = expect(promiseNetworkMock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) - const expectPromise3: Promise = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + expectPromiseVoid = expect(promiseNetworkMock).toBeRequested() + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 - const expectPromise4: Promise = expect(promiseNetworkMock).not.toBeRequested() - const expectPromise5: Promise = expect(promiseNetworkMock).not.toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) - const expectPromise6: Promise = expect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequested() + expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 - const expectPromise7: Promise = expect(promiseNetworkMock).toBeRequestedWith({ + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith({ url: 'http://localhost:8080/api/todo', // [optional] string | function | custom matcher method: 'POST', // [optional] string | array statusCode: 200, // [optional] number | array @@ -258,28 +335,28 @@ describe('type assertions', () => { response: { success: true }, // [optional] object | function | custom matcher }) - const expectPromise8: Promise = expect(promiseNetworkMock).toBeRequestedWith(expect.objectContaining({ + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith(expect.objectContaining({ response: { success: true }, // [optional] object | function | custom matcher })) }) it('should have ts errors when typing to void', async () => { // @ts-expect-error - const expectPromise1: void = expect(promiseNetworkMock).toBeRequested() + expectVoid = expect(promiseNetworkMock).toBeRequested() // @ts-expect-error - const expectPromise2: void = expect(promiseNetworkMock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + expectVoid = expect(promiseNetworkMock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) // @ts-expect-error - const expectPromise3: void = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + expectVoid = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 // @ts-expect-error - const expectPromise4: void = expect(promiseNetworkMock).not.toBeRequested() + expectVoid = expect(promiseNetworkMock).not.toBeRequested() // @ts-expect-error - const expectPromise5: void = expect(promiseNetworkMock).not.toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + expectVoid = expect(promiseNetworkMock).not.toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) // @ts-expect-error - const expectPromise6: void = expect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + expectVoid = expect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 // @ts-expect-error - const expectPromise7: void = expect(promiseNetworkMock).toBeRequestedWith({ + expectVoid = expect(promiseNetworkMock).toBeRequestedWith({ url: 'http://localhost:8080/api/todo', // [optional] string | function | custom matcher method: 'POST', // [optional] string | array statusCode: 200, // [optional] number | array @@ -290,7 +367,7 @@ describe('type assertions', () => { }) // @ts-expect-error - const expectPromise8: void = expect(promiseNetworkMock).toBeRequestedWith(expect.objectContaining({ + expectVoid = expect(promiseNetworkMock).toBeRequestedWith(expect.objectContaining({ response: { success: true }, // [optional] object | function | custom matcher })) }) @@ -302,44 +379,65 @@ describe('type assertions', () => { expect.unimplementedFunction() }) - it('should support stringContaining, anything', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const expectAny1: any = expect.stringContaining('WebdriverIO') - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const expectAny2: any = expect.anything() + it('should support stringContaining, anything and more', async () => { + expect.stringContaining('WebdriverIO') + expect.arrayContaining(['WebdriverIO', 'Test']) + expect.objectContaining({ name: '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.arrayContaining(['WebdriverIO', 'Test']) + expect.not.objectContaining({ name: '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 expectString: string = await $('h1').getText() - const expectPromise: Promise = $('h1').getText() + const actualString: string = await $('h1').getText() + const actualPromiseString: Promise = $('h1').getText() describe('expect.soft', () => { - it('should not have ts error and not need to be awaited/be a promise if actual is non-promise type', async () => { - const expectWdioMatcher: WdioMatchers = expect.soft(expectString) - const expectVoid: void = expect.soft(expectString).toBe('Test Page') - }) - - it('should not have ts error and need to be awaited/be a promise if actual is a promise type', async () => { - const expectWdioMatcher: WdioMatchers, Promise> = expect.soft(expectPromise) - const expectVoid: Promise = expect.soft(expectPromise).toBe('Test Page') - - await expect.soft(expectPromise).toBe('Test Page') - }) + it('should not need to be awaited/be a promise if actual is non-promise type', async () => { + const expectWdioMatcher1: WdioMatchers = 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')) - it('should have ts error when using await and actual is non-promise type', async () => { // @ts-expect-error - const expectWdioMatcher: jest.MatcherAndInverse, string> = expect.soft(expectString) - + expectPromiseVoid = expect.soft(actualString).toBe('Test Page') + // @ts-expect-error + expectPromiseVoid = expect.soft(actualString).not.toBe('Test Page') // @ts-expect-error - const expectVoid: Promise = expect.soft(expectString).toBe('Test Page') + expectPromiseVoid = expect.soft(actualString).not.toBe(expect.stringContaining('Test Page')) }) - it('should not have ts error and need to be awaited/be a promise if actual is a promise type', async () => { + it('should need to be awaited/be a promise if actual is promise type', async () => { + const expectWdioMatcher1: jest.MatcherAndInverse, 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 - const expectWdioMatcher: jest.MatcherAndInverse> = expect.soft(expectPromise) + expectVoid = expect.soft(actualPromiseString).not.toBe('Test Page') // @ts-expect-error - const expectVoid: void = expect.soft(expectPromise).toBe('Test Page') + expectVoid = expect.soft(actualPromiseString).not.toBe(expect.stringContaining('Test Page')) }) it('should support chainable element', async () => { @@ -353,47 +451,33 @@ describe('type assertions', () => { }) it('should support chainable element with wdio Matchers', async () => { - const expectPromise1: Promise = expect.soft(element).toBeDisplayed() - const expectPromise2: Promise = expect.soft(chainableElement).toBeDisplayed() - - // @ts-expect-error - const expectPromise3: void = expect.soft(element).toBeDisplayed() - // @ts-expect-error - const expectPromise4: void = expect.soft(chainableElement).toBeDisplayed() - + 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() - }) - - describe('not', async () => { - it('should support not with chainable', async () => { - const expectPromise1: Promise = expect.soft(element).not.toBeDisplayed() - const expectPromise2: Promise = expect.soft(chainableElement).not.toBeDisplayed() - - // @ts-expect-error - const expectPromise3: void = expect.soft(element).not.toBeDisplayed() - // @ts-expect-error - const expectPromise4: void = expect.soft(chainableElement).not.toBeDisplayed() - - await expect.soft(element).not.toBeDisplayed() - await expect.soft(chainableElement).not.toBeDisplayed() - }) - - it('should support not with non-promise', async () => { - const expectVoid1: void = expect.soft(expectString).not.toBe('Test Page') - - // @ts-expect-error - const expectVoid2: Promise = expect.soft(expectString).not.toBe('Test Page') - }) + await expect.soft(chainableArray).toBeDisplayed() - it('should support not with promise', async () => { - const expectPromise1: Promise = expect.soft(expectPromise).not.toBe('Test Page') + 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 - const expectPromise2: void = expect.soft(expectPromise).not.toBe('Test Page') + // @ts-expect-error + expectVoid = expect.soft(element).toBeDisplayed() + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toBeDisplayed() + // @ts-expect-error + expectVoid = expect.soft(chainableArray).toBeDisplayed() - await expect.soft(expectPromise).not.toBe('Test Page') - }) + // @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() }) }) @@ -402,25 +486,25 @@ describe('type assertions', () => { const expectSoftFailure1: ExpectWebdriverIO.SoftFailure[] = expect.getSoftFailures() // @ts-expect-error - const expectSoftFailure2: void = expect.getSoftFailures() + expectVoid = expect.getSoftFailures() }) }) describe('expect.assertSoftFailures', () => { it('should be of type void', async () => { - const expectVoid1: void = expect.assertSoftFailures() + expectVoid = expect.assertSoftFailures() // @ts-expect-error - const expectVoid2: Promise = expect.assertSoftFailures() + expectPromiseVoid = expect.assertSoftFailures() }) }) describe('expect.clearSoftFailures', () => { it('should be of type void', async () => { - const expectVoid1: void = expect.clearSoftFailures() + expectVoid = expect.clearSoftFailures() // @ts-expect-error - const expectVoid2: Promise = expect.clearSoftFailures() + expectPromiseVoid = expect.clearSoftFailures() }) }) }) From a1d32a597bedfeb795f048b62d59a1b6bbfbb317 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Tue, 24 Jun 2025 19:46:05 -0400 Subject: [PATCH 20/99] Add a working case of custom matchers with types! --- .../jest/customMatchers/customMatchers.d.ts | 11 +++++++++ .../jest/customMatchers/toBeCustomPromise.ts | 18 ++++++++++++++ test-types/jest/tsconfig.json | 12 +++++++--- test-types/jest/types-jest.test.ts | 24 +++++++++++++++++++ 4 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 test-types/jest/customMatchers/customMatchers.d.ts create mode 100644 test-types/jest/customMatchers/toBeCustomPromise.ts diff --git a/test-types/jest/customMatchers/customMatchers.d.ts b/test-types/jest/customMatchers/customMatchers.d.ts new file mode 100644 index 000000000..33f197873 --- /dev/null +++ b/test-types/jest/customMatchers/customMatchers.d.ts @@ -0,0 +1,11 @@ +// TODO dprevost should we review this to have the wdio namespace or maybe the expect namespace? +// Name jest is required to augment the jest.Matchers interface +declare namespace jest { + interface AsymmetricMatchers { + toBeCustom(): void; + } + interface Matchers { + toBeCustom(): R; + toBeCustomPromise: T extends ChainablePromiseElement ? (expected?: object) => Promise : never; + } +} \ No newline at end of file diff --git a/test-types/jest/customMatchers/toBeCustomPromise.ts b/test-types/jest/customMatchers/toBeCustomPromise.ts new file mode 100644 index 000000000..8ef9f8a01 --- /dev/null +++ b/test-types/jest/customMatchers/toBeCustomPromise.ts @@ -0,0 +1,18 @@ +import type { MatcherFunction } from 'expect' + +const toBeCustom: MatcherFunction = + function (actual: ChainablePromiseElement) { + const pass = actual + if (pass) { + return { + message: () => 'failed to be custom', + pass: true, + } + } + return { + message: () => 'failed to not be custom', + pass: false, + } + } + +expect.extend({ toBeCustom }) \ No newline at end of file diff --git a/test-types/jest/tsconfig.json b/test-types/jest/tsconfig.json index 7860734fb..2a08907a4 100644 --- a/test-types/jest/tsconfig.json +++ b/test-types/jest/tsconfig.json @@ -5,10 +5,16 @@ "target": "ES2020", "module": "Node16", "skipLibCheck": true, + "typeRoots": [ + "../../node_modules/", + "../../", + "./customMatchers", + ], "types": [ "@types/jest", - "../../jest.d.ts", // Needed to be after @types/jest to override the toMatchSnapshot typing - "@wdio/globals/types" - ] + "jest.d.ts", // Needed to be after @types/jest to override the toMatchSnapshot typing + "@wdio/globals/types", + "customMatchers.d.ts", + ], } } diff --git a/test-types/jest/types-jest.test.ts b/test-types/jest/types-jest.test.ts index fff425b7d..787424ed4 100644 --- a/test-types/jest/types-jest.test.ts +++ b/test-types/jest/types-jest.test.ts @@ -194,6 +194,30 @@ describe('type assertions', async () => { }) }) + describe('Custom matchers', () => { + 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() + }) + + it('should supported correctly a promise custom matcher with only chainableElement as actual', async () => { + expectPromiseVoid = expect(chainableElement).toBeCustomPromise() + expectPromiseVoid = expect(chainableElement).toBeCustomPromise(expect.objectContaining({})) + + // @ts-expect-error + expect('test').toBeCustomPromise() + // @ts-expect-error + expectVoid = expect(chainableElement).toBeCustomPromise() + // @ts-expect-error + expectVoid = expect(chainableElement).toBeCustomPromise(expect.objectContaining({})) + }) + }) + describe('toBe', () => { it('should expect void type when actual is a boolean', async () => { From 59a287c5e37da2a94e79f63b087136c4c4bf8a83 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Tue, 24 Jun 2025 20:57:06 -0400 Subject: [PATCH 21/99] Remove this file --- .DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 1432c941fcc593c5975dc5d3e9cbe911d25e285e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKyG{c^3>-s{B4|=l?hjD#2dgOgg8Tplhz11&0g3K9emmm_kZ?y!fyR<|ZoM9_ zZi@36fGuBduYm=CIo%N-zKqS!-A8s(5l4#}SJ>elPq@R&D0@2K+;g&=vBLrXZS%Z+ zc--D+ZJd2q{&6y0S5YY-1*Cu!kOER*S^@TKxA{e)Rw*C_q`;Q~{(We4$6h!j#;1cz zi~z(L!(n`mS%TO+KA|o_QDlw^6BZeiN@m6`ga7au#EN*6KiD^+hDy`1;jM!%td8gnC^OSEEQ iv|?_&6<;3YHGk%LFB}qs&Uny?`VnwlWK!TS6xai)x*AOY From ece01bdecf38f06a535feb61d13119e0e1bfa5dd Mon Sep 17 00:00:00 2001 From: David Prevost Date: Tue, 24 Jun 2025 21:38:26 -0400 Subject: [PATCH 22/99] Fix rebase --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 1bea99835..7fe2a0447 100644 --- a/package.json +++ b/package.json @@ -46,15 +46,15 @@ "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": "echo 'TODO dprevost to bring back' && exit 0 && tsc jasmine.d.ts jest.d.ts", "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", + "test:types": "npm run ts && npm run clean:tests && npm run tsc:root-types", "ts": "run-s ts:*", - "ts:jest": "cd test-types/jest && tsc -p ./tsconfig.json --incremental", - "ts:mocha": "cd test-types/mocha && tsc -p ./tsconfig.json --incremental", + "ts:jest": "cd test-types/jest && tsc -p ./tsconfig.json --incremental --noEmit", + "ts:mocha": "cd test-types/mocha && tsc -p ./tsconfig.json --incremental --noEmit", "watch": "npm run compile -- --watch", "prepare": "husky install" }, From 144611bf653e02ee8d9b6461cb7e28fcb98a6fc5 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Tue, 24 Jun 2025 22:01:59 -0400 Subject: [PATCH 23/99] Copy better type testing to mocha + clean package.json - Copied the newer test file from Jest to mocha - Add customMarcher type into Mocha - Need to fix AsymmetricMatcher not working - Review script and have tsc using `--noEmit` instead of using cleaning scripts --- package.json | 8 +- .../jest/customMatchers/customMatchers.d.ts | 2 +- .../jest/customMatchers/toBeCustomPromise.ts | 18 - test-types/jest/tsconfig.json | 9 +- test-types/jest/types-jest.test.ts | 76 +-- .../mocha/customMatchers/customMatchers.d.ts | 10 + test-types/mocha/tsconfig.json | 1 + test-types/mocha/types-mocha.test.ts | 542 +++++++++++------- types/standalone.d.ts | 6 +- 9 files changed, 385 insertions(+), 287 deletions(-) delete mode 100644 test-types/jest/customMatchers/toBeCustomPromise.ts create mode 100644 test-types/mocha/customMatchers/customMatchers.d.ts diff --git a/package.json b/package.json index 7fe2a0447..600a53992 100644 --- a/package.json +++ b/package.json @@ -44,17 +44,17 @@ "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": "echo 'TODO dprevost to bring back' && exit 0 && tsc jasmine.d.ts jest.d.ts", "test": "run-s test:*", "test:tsc": "tsc --project tsconfig.json --noEmit", "test:lint": "eslint .", "test:unit": "vitest --run", - "test:types": "npm run ts && npm run clean:tests && npm run tsc:root-types", + "test:types": "npm run ts && npm run tsc:root-types", "ts": "run-s ts:*", - "ts:jest": "cd test-types/jest && tsc -p ./tsconfig.json --incremental --noEmit", - "ts:mocha": "cd test-types/mocha && tsc -p ./tsconfig.json --incremental --noEmit", + "ts:jest": "cd test-types/jest && tsc --project ./tsconfig.json --noEmit", + "ts:mocha": "cd test-types/mocha && tsc --project ./tsconfig.json --noEmit", + "ts:jasmine": "echo 'TODO dprevost to bring back' && exit 0 && cd test-types/jasmine && tsc --project ./tsconfig.json --noEmit", "watch": "npm run compile -- --watch", "prepare": "husky install" }, diff --git a/test-types/jest/customMatchers/customMatchers.d.ts b/test-types/jest/customMatchers/customMatchers.d.ts index 33f197873..6e7e61dcc 100644 --- a/test-types/jest/customMatchers/customMatchers.d.ts +++ b/test-types/jest/customMatchers/customMatchers.d.ts @@ -6,6 +6,6 @@ declare namespace jest { } interface Matchers { toBeCustom(): R; - toBeCustomPromise: T extends ChainablePromiseElement ? (expected?: object) => Promise : never; + toBeCustomPromise: T extends ChainablePromiseElement ? (expected?: string) => Promise : never; } } \ No newline at end of file diff --git a/test-types/jest/customMatchers/toBeCustomPromise.ts b/test-types/jest/customMatchers/toBeCustomPromise.ts deleted file mode 100644 index 8ef9f8a01..000000000 --- a/test-types/jest/customMatchers/toBeCustomPromise.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { MatcherFunction } from 'expect' - -const toBeCustom: MatcherFunction = - function (actual: ChainablePromiseElement) { - const pass = actual - if (pass) { - return { - message: () => 'failed to be custom', - pass: true, - } - } - return { - message: () => 'failed to not be custom', - pass: false, - } - } - -expect.extend({ toBeCustom }) \ No newline at end of file diff --git a/test-types/jest/tsconfig.json b/test-types/jest/tsconfig.json index 2a08907a4..756d10eb4 100644 --- a/test-types/jest/tsconfig.json +++ b/test-types/jest/tsconfig.json @@ -5,16 +5,11 @@ "target": "ES2020", "module": "Node16", "skipLibCheck": true, - "typeRoots": [ - "../../node_modules/", - "../../", - "./customMatchers", - ], "types": [ "@types/jest", - "jest.d.ts", // Needed to be after @types/jest to override the toMatchSnapshot typing + "../../jest.d.ts", // Needed to be after @types/jest to override the toMatchSnapshot typing "@wdio/globals/types", - "customMatchers.d.ts", + "./customMatchers/customMatchers.d.ts", ], } } diff --git a/test-types/jest/types-jest.test.ts b/test-types/jest/types-jest.test.ts index 787424ed4..c6378ee02 100644 --- a/test-types/jest/types-jest.test.ts +++ b/test-types/jest/types-jest.test.ts @@ -207,14 +207,14 @@ describe('type assertions', async () => { it('should supported correctly a promise custom matcher with only chainableElement as actual', async () => { expectPromiseVoid = expect(chainableElement).toBeCustomPromise() - expectPromiseVoid = expect(chainableElement).toBeCustomPromise(expect.objectContaining({})) + expectPromiseVoid = expect(chainableElement).toBeCustomPromise(expect.stringContaining('test')) // @ts-expect-error expect('test').toBeCustomPromise() // @ts-expect-error expectVoid = expect(chainableElement).toBeCustomPromise() // @ts-expect-error - expectVoid = expect(chainableElement).toBeCustomPromise(expect.objectContaining({})) + expectVoid = expect(chainableElement).toBeCustomPromise(expect.stringContaining('test')) }) }) @@ -269,42 +269,6 @@ describe('type assertions', async () => { }) }) - describe('Jest original Matchers', () => { - const propertyMatchers: Partial<{}> = {} - const snapshotName: string = 'test-snapshot' - describe('toMatchSnapshot', () => { - - it('should have original jest Matcher still works', async () => { - expectVoid = expect(element).toMatchSnapshot(propertyMatchers) - expectVoid = expect(element).toMatchSnapshot(propertyMatchers, snapshotName) - expectVoid = expect(element).toMatchInlineSnapshot(propertyMatchers) - expectVoid = expect(element).toMatchInlineSnapshot(propertyMatchers, snapshotName) - - expectVoid = expect(element).not.toMatchSnapshot(propertyMatchers) - expectVoid = expect(element).not.toMatchSnapshot(propertyMatchers, snapshotName) - expectVoid = expect(element).not.toMatchInlineSnapshot(propertyMatchers) - expectVoid = expect(element).not.toMatchInlineSnapshot(propertyMatchers, snapshotName) - - // @ts-expect-error - expectPromiseVoid = expect(element).toMatchSnapshot(propertyMatchers) - // @ts-expect-error - expectPromiseVoid = expect(element).toMatchSnapshot(propertyMatchers, snapshotName) - // @ts-expect-error - expectPromiseVoid = expect(element).toMatchInlineSnapshot(propertyMatchers) - // @ts-expect-error - expectPromiseVoid = expect(element).toMatchInlineSnapshot(propertyMatchers, snapshotName) - // @ts-expect-error - expectPromiseVoid = expect(element).not.toMatchSnapshot(propertyMatchers) - // @ts-expect-error - expectPromiseVoid = expect(element).not.toMatchSnapshot(propertyMatchers, snapshotName) - // @ts-expect-error - expectPromiseVoid = expect(element).not.toMatchInlineSnapshot(propertyMatchers) - // @ts-expect-error - expectPromiseVoid = expect(element).not.toMatchInlineSnapshot(propertyMatchers, snapshotName) - }) - }) - }) - describe('Promise type assertions', () => { const booleanPromise: Promise = Promise.resolve(true) @@ -533,4 +497,40 @@ describe('type assertions', async () => { }) }) }) + + describe('@types/jest only - original Matchers', () => { + const propertyMatchers: Partial<{}> = {} + const snapshotName: string = 'test-snapshot' + describe('toMatchSnapshot', () => { + + it('should have original jest Matcher still works', async () => { + expectVoid = expect(element).toMatchSnapshot(propertyMatchers) + expectVoid = expect(element).toMatchSnapshot(propertyMatchers, snapshotName) + expectVoid = expect(element).toMatchInlineSnapshot(propertyMatchers) + expectVoid = expect(element).toMatchInlineSnapshot(propertyMatchers, snapshotName) + + expectVoid = expect(element).not.toMatchSnapshot(propertyMatchers) + expectVoid = expect(element).not.toMatchSnapshot(propertyMatchers, snapshotName) + expectVoid = expect(element).not.toMatchInlineSnapshot(propertyMatchers) + expectVoid = expect(element).not.toMatchInlineSnapshot(propertyMatchers, snapshotName) + + // @ts-expect-error + expectPromiseVoid = expect(element).toMatchSnapshot(propertyMatchers) + // @ts-expect-error + expectPromiseVoid = expect(element).toMatchSnapshot(propertyMatchers, snapshotName) + // @ts-expect-error + expectPromiseVoid = expect(element).toMatchInlineSnapshot(propertyMatchers) + // @ts-expect-error + expectPromiseVoid = expect(element).toMatchInlineSnapshot(propertyMatchers, snapshotName) + // @ts-expect-error + expectPromiseVoid = expect(element).not.toMatchSnapshot(propertyMatchers) + // @ts-expect-error + expectPromiseVoid = expect(element).not.toMatchSnapshot(propertyMatchers, snapshotName) + // @ts-expect-error + expectPromiseVoid = expect(element).not.toMatchInlineSnapshot(propertyMatchers) + // @ts-expect-error + expectPromiseVoid = expect(element).not.toMatchInlineSnapshot(propertyMatchers, snapshotName) + }) + }) + }) }) diff --git a/test-types/mocha/customMatchers/customMatchers.d.ts b/test-types/mocha/customMatchers/customMatchers.d.ts new file mode 100644 index 000000000..b326f7306 --- /dev/null +++ b/test-types/mocha/customMatchers/customMatchers.d.ts @@ -0,0 +1,10 @@ +// Name jest is required to augment the jest.Matchers interface +declare namespace ExpectWebdriverIO { + interface AsymmetricMatchers { + toBeCustom(): void; + } + interface Matchers { + toBeCustom(): R; + toBeCustomPromise: T extends ChainablePromiseElement ? (expected?: string) => Promise : never; + } +} \ No newline at end of file diff --git a/test-types/mocha/tsconfig.json b/test-types/mocha/tsconfig.json index 94868c527..1890b3d8e 100644 --- a/test-types/mocha/tsconfig.json +++ b/test-types/mocha/tsconfig.json @@ -10,6 +10,7 @@ "expect", "webdriverio", "../../types/standalone-global.d.ts", + "./customMatchers/customMatchers.d.ts" // "@wdio/types", // "@wdio/globals/types", // TODO to review adding the global wdio types interfere with local types ] diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index 2a4a9120b..4bb1b2a86 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -8,223 +8,304 @@ describe('type assertions', () => { const chainableElement = {} as unknown as ChainablePromiseElement const chainableArray = {} as ChainablePromiseArray const networkMock: WebdriverIO.Mock = {} as unknown as WebdriverIO.Mock + // TODO dprevost: Need more test with this type? + // const ElementArray: WebdriverIO.ElementArray = await chainableArray?.getElements() - describe('toHaveUrl', () => { + // Type assertions + let expectPromiseVoid: Promise + let expectVoid: void + + describe('Browser', () => { const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser - it('should not have ts errors and be able to await the promise when actual is browser', async () => { - const expectPromiseVoid: Promise = expect(browser).toHaveUrl('https://example.com') - await expectPromiseVoid - const expectNotPromiseVoid: Promise = expect(browser).not.toHaveUrl('https://example.com') - await expectNotPromiseVoid - }) + describe('toHaveUrl', () => { + it('should be supported correctly', async () => { + expectPromiseVoid = expect(browser).toHaveUrl('https://example.com') + expectPromiseVoid = expect(browser).not.toHaveUrl('https://example.com') - it('should have ts errors and not need to await the promise when actual is browser', async () => { - // @ts-expect-error - const expectVoid: void = expect(browser).toHaveUrl('https://example.com') - // @ts-expect-error - const expectNotVoid: void = expect(browser).not.toHaveUrl('https://example.com') - }) + // Asymmetric matchers - TODO dprevost to fix + // expectPromiseVoid = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) + // expectPromiseVoid = expect(browser).toHaveUrl(expect.any(String)) + // expectPromiseVoid = expect(browser).toHaveUrl(expect.anything()) + // TODO add more asymmetric matchers - it('should have ts errors when actual is an element', async () => { - // @ts-expect-error - await expect(element).toHaveUrl('https://example.com') - }) + // @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')) + }) - it('should have ts errors when actual is an ChainableElement', async () => { - // @ts-expect-error - await expect(chainableElement).toHaveUrl('https://example.com') + 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') + }) }) - - // TODO dprevost fix expect.stringContaining - // it('should support stringContaining', async () => { - // const expectVoid1: Promise = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) - - // // @ts-expect-error - // const expectVoid2: void = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) - // }) }) - describe('element type assertions', () => { + describe('element', () => { describe('toBeDisabled', () => { - it('should not have ts errors and be able to await the promise for element', async () => { - // expect no ts errors - const expectIsPromiseVoid: Promise = expect(element).toBeDisabled() - await expectIsPromiseVoid + it('should be supported correctly', async () => { + // Element + expectPromiseVoid = expect(element).toBeDisabled() + expectPromiseVoid = expect(element).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() + }) - const expectNotIsPromiseVoid: Promise = expect(element).not.toBeDisabled() - await expectNotIsPromiseVoid + 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']) + // TODO dprevost: to fix + // 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')]) - it('should not have ts errors and be able to await the promise for chainable', async () => { - // expect no ts errors - const expectIsPromiseVoid: Promise = expect(chainableElement).toBeDisabled() - await expectIsPromiseVoid + expectPromiseVoid = expect(element).not.toHaveText('text') + + // @ts-expect-error + expectVoid = expect(element).toHaveText('text') + // @ts-expect-error + await expect(element).toHaveText(6) - const expectNotIsPromiseVoid: Promise = expect(chainableElement).not.toBeDisabled() - await expectNotIsPromiseVoid + // @ts-expect-error + await expect(browser).toHaveText('text') }) - it('should have ts errors when typing to void for element', async () => { + it('should have ts errors when actual is not an element', async () => { + // @ts-expect-error + await expect(browser).toHaveText('text') // @ts-expect-error - const expectToBeIsVoid: void = expect(element).toBeDisabled() + await expect(browser).not.toHaveText('text') // @ts-expect-error - const expectNotToBeIsVoid: void = expect(element).not.toBeDisabled() + await expect(true).toHaveText('text') + // @ts-expect-error + await expect(true).toHaveText('text') }) - it('should have ts errors when typing to void for chainable', async () => { + it('should have ts errors when actual is string or Promise', async () => { + // @ts-expect-error + await expect('text').toHaveText('text') // @ts-expect-error - const expectToBeIsVoid: void = expect(chainableElement).toBeDisabled() + await expect('text').not.toHaveText('text') // @ts-expect-error - const expectNotToBeIsVoid: void = expect(chainableElement).not.toBeDisabled() + await expect(Promise.resolve('text')).toHaveText('text') + // @ts-expect-error + await expect(Promise.resolve('text')).toHaveText('text') }) }) describe('toMatchSnapshot', () => { - it('should not have ts errors when typing to Promise for an element', async () => { - const expectPromise1: Promise = expect(element).toMatchSnapshot() - const expectPromise2: Promise = expect(element).toMatchSnapshot('test label') - }) + it('should be supported correctly', async () => { + expectPromiseVoid = expect(element).toMatchSnapshot() + expectPromiseVoid = expect(element).toMatchSnapshot('test label') + expectPromiseVoid = expect(element).not.toMatchSnapshot('test label') - it('should not have ts errors when typing to Promise for a chainable', async () => { - const expectPromise1: Promise = expect(chainableElement).toMatchSnapshot() - const expectPromise2: Promise = expect(chainableElement).toMatchSnapshot('test label') - }) + expectPromiseVoid = expect(chainableElement).toMatchSnapshot() + expectPromiseVoid = expect(chainableElement).toMatchSnapshot('test label') + expectPromiseVoid = expect(chainableElement).not.toMatchSnapshot('test label') - // We need somehow to exclude the Jest types one for this to success - it('should have ts errors when typing to void for an element like', async () => { //@ts-expect-error - const expectNotToBeVoid1: void = expect(element).toMatchSnapshot() + expectVoid = expect(element).toMatchSnapshot() + //@ts-expect-error + expectVoid = expect(element).not.toMatchSnapshot() //@ts-expect-error - const expectNotToBeVoid2: void = expect(chainableElement).toMatchSnapshot() + expectVoid = expect(chainableElement).toMatchSnapshot() + //@ts-expect-error + expectVoid = expect(chainableElement).not.toMatchSnapshot() }) - // TODO - conditional types check on T to have the below match void does not work - // it('should not have ts errors when typing to void for a string', async () => { - // const expectNotToBeVoid: void = expect('.findme').toMatchSnapshot() + // TODO - since we are overloading the `toMatchSnapshot` of jest.toMatchSnapshot, I wonder if we can achieve the below... + // it('should have ts errors when not an element or chainable', async () => { + // //@ts-expect-error + // await expect('.findme').toMatchSnapshot() // }) }) describe('toMatchInlineSnapshot', () => { - it('should not have ts errors when typing to Promise for an element', async () => { - const expectPromise1: Promise = expect(element).toMatchInlineSnapshot() - const expectPromise2: Promise = expect(element).toMatchInlineSnapshot('test snapshot') - const expectPromise3: Promise = expect(element).toMatchInlineSnapshot('test snapshot', 'test label') - }) + it('should be correctly supported', async () => { + expectPromiseVoid = expect(element).toMatchInlineSnapshot() + expectPromiseVoid = expect(element).toMatchInlineSnapshot('test snapshot') + expectPromiseVoid = expect(element).toMatchInlineSnapshot('test snapshot', 'test label') - it('should not have ts errors when typing to Promise for a chainable', async () => { - const expectPromise1: Promise = expect(chainableElement).toMatchInlineSnapshot() - const expectPromise2: Promise = expect(chainableElement).toMatchInlineSnapshot('test snapshot') - const expectPromise3: Promise = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') - }) + expectPromiseVoid = expect(chainableElement).toMatchInlineSnapshot() + expectPromiseVoid = expect(chainableElement).toMatchInlineSnapshot('test snapshot') + expectPromiseVoid = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') - // We need somehow to exclude the Jest types one for this to success - it('should have ts errors when typing to void for an element like', async () => { //@ts-expect-error - const expectNotToBeVoid1: void = expect(element).toMatchInlineSnapshot() + expectVoid = expect(element).toMatchInlineSnapshot() //@ts-expect-error - const expectPromise2: void = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + expectVoid = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') }) - - // TODO - conditional types check on T to have the below match void does not work - // it('should not have ts errors when typing to void for a string', async () => { - // const expectNotToBeVoid: void = expect('.findme').toMatchInlineSnapshot() - // }) }) - }) - describe('toBe', () => { + describe('toBeElementsArrayOfSize', async () => { - it('should not have ts errors when typing to void when actual is boolean', async () => { - const expectToBeIsVoid: void = expect(true).toBe(true) - const expectNotToBeIsVoid: void = expect(true).not.toBe(true) - }) + it('should work correctly when actual is chainableArray', async () => { + expectPromiseVoid = expect(chainableArray).toBeElementsArrayOfSize(5) + expectPromiseVoid = expect(chainableArray).toBeElementsArrayOfSize({ lte: 10 }) - it('should have ts errors when typing to Promise when actual is boolean', async () => { - //@ts-expect-error - const expectToBeIsNotPromiseVoid1: Promise = expect(true).toBe(true) + // @ts-expect-error + expectVoid = expect(chainableArray).toBeElementsArrayOfSize(5) + // @ts-expect-error + expectVoid = expect(chainableArray).toBeElementsArrayOfSize({ lte: 10 }) + }) - //@ts-expect-error - const expectToBeIsNotPromiseVoid2: Promise = expect(true).not.toBe(true) + 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 }) + }) }) + }) - it('should expect void when actual is an awaited element/chainable', async () => { - const isClickableElement = await element.isClickable() - const expectPromiseVoid1: void = expect(isClickableElement).toBe(true) - - const isClickableChainable: boolean = await chainableElement.isClickable() - const expectPromiseVoid2: void = expect(isClickableChainable).toBe(true) + // TODO dprevost + describe('Custom matchers', () => { + it('should supported correctly a non-promise custom matcher', async () => { + expectVoid = expect('test').toBeCustom() + expectVoid = expect('test').not.toBeCustom() // @ts-expect-error - const expectPromiseVoid3: Promise = expect(isClickableElement).toBe(true) + expectPromiseVoid = expect('test').toBeCustom() + // @ts-expect-error + expectPromiseVoid = expect('test').not.toBeCustom() + }) + it('should supported correctly a promise custom matcher with only chainableElement as actual', async () => { + expectPromiseVoid = expect(chainableElement).toBeCustomPromise() + // TODO dprevost: to fix + // expectPromiseVoid = expect(chainableElement).toBeCustomPromise(expect.stringContaining('test')) + + // @ts-expect-error + expect('test').toBeCustomPromise() // @ts-expect-error - const expectPromiseVoid4: Promise = expect(isClickableChainable).toBe(true) + expectVoid = expect(chainableElement).toBeCustomPromise() + // @ts-expect-error + expectVoid = expect(chainableElement).toBeCustomPromise(expect.stringContaining('test')) }) }) - describe('string type assertions', () => { - it('should not have ts errors when typing to void', async () => { - // Expect no ts errors - const expectToBeIsVoid: void = expect('test').toBe('test') - }) + describe('toBe', () => { - it('should have ts errors when typing to Promise', async () => { + 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 - const expectToBeIsNotPromiseVoid: Promise = expect('test').toBe('test') + expectPromiseVoid = expect(true).not.toBe(true) }) - }) - describe('Promise<> type assertions', () => { - const booleanPromise: Promise = Promise.resolve(true) + it('should not expect Promise when actual is a chainable since toBe is not supported', async () => { + expectVoid = expect(chainableElement).toBe(true) + expectVoid = expect(chainableElement).not.toBe(true) - it('should not have ts errors when typing to void', async () => { - const expectToBeIsVoid: void = expect(booleanPromise).toBe(true) - const expectAwaitToBeIsVoid: void = expect(await booleanPromise).toBe(true) + //@ts-expect-error + expectPromiseVoid = expect(chainableElement).toBe(true) + //@ts-expect-error + expectPromiseVoid = expect(chainableElement).not.toBe(true) }) - it('should not have ts errors when resolves and rejects is typed to Promise', async () => { - // TODO should we support resolves and rejects in standalone or with mocha? + it('should still expect void type when actual is a Promise since we do not overload them', async () => { + const promiseBoolean = Promise.resolve(true) - /// @ts-expect-error - expect(booleanPromise).resolves.toBe(true) - /// @ts-expect-error - expect(booleanPromise).rejects.toBe(true) - }) + expectVoid = expect(promiseBoolean).toBe(true) + expectVoid = expect(promiseBoolean).not.toBe(true) - it('should have ts errors when typing to Promise', async () => { //@ts-expect-error - const expectToBeIsNotPromiseVoid1: Promise = expect(booleanPromise).toBe(true) + expectPromiseVoid = expect(promiseBoolean).toBe(true) //@ts-expect-error - const expectToBeIsNotPromiseVoid2: Promise = expect(await booleanPromise).toBe(true) + expectPromiseVoid = expect(promiseBoolean).toBe(true) }) - // On standalone, resolves and rejects are not existing - // it('should have ts errors when typing resolves and reject is typed to void', async () => { - // //@ts-expect-error - // const expectResolvesToBeIsVoid: void = expect(booleanPromise).resolves.toBe(true) - // //@ts-expect-error - // const expectRejectsToBeIsVoid: void = expect(booleanPromise).rejects.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('toBeElementsArrayOfSize', async () => { + describe('Promise type assertions', () => { + const booleanPromise: Promise = Promise.resolve(true) - it('should not have ts errors when typing to Promise', async () => { - const listItems = await chainableArray - const expectPromise: Promise = expect(listItems).toBeElementsArrayOfSize(5) - const expectPromise1: Promise = expect(listItems).toBeElementsArrayOfSize({ lte: 10 }) - }) + it('should expect a Promise of type', async () => { + const expectPromiseBoolean1: ExpectWebdriverIO.MatchersAndInverse> = expect(booleanPromise) + const expectPromiseBoolean2: ExpectWebdriverIO.Matchers> = expect(booleanPromise).not - it('should have ts errors when typing to void', async () => { - const listItems = await chainableArray - // @ts-expect-error - const expectPromise: void = expect(listItems).toBeElementsArrayOfSize(5) // @ts-expect-error - const expectPromise1: void = expect(listItems).toBeElementsArrayOfSize({ lte: 10 }) + const expectPromiseBoolean3: jest.JestMatchers = expect(booleanPromise) + //// @ts-expect-error + // const expectPromiseBoolean4: jest.Matchers = expect(booleanPromise).not + }) + + it('should work with resolves & rejects correctly', async () => { + // TODO dprevost should we support this in Wdio since we do not even use it or document it? + // 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) }) }) @@ -232,11 +313,15 @@ describe('type assertions', () => { const promiseNetworkMock = Promise.resolve(networkMock) it('should not have ts errors when typing to Promise', async () => { - const expectPromise1: Promise = expect(promiseNetworkMock).toBeRequested() - const expectPromise2: Promise = expect(promiseNetworkMock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) - const expectPromise3: Promise = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + expectPromiseVoid = expect(promiseNetworkMock).toBeRequested() + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 - const expectPromise4: Promise = expect(promiseNetworkMock).toBeRequestedWith({ + expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequested() + expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith({ url: 'http://localhost:8080/api/todo', // [optional] string | function | custom matcher method: 'POST', // [optional] string | array statusCode: 200, // [optional] number | array @@ -245,18 +330,30 @@ describe('type assertions', () => { postData: { title: 'foo', description: 'bar' }, // [optional] object | function | custom matcher response: { success: true }, // [optional] object | function | custom matcher }) + + // TODO dprevost: to fix + // expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith(expect.objectContaining({ + // response: { success: true }, // [optional] object | function | custom matcher + // })) }) it('should have ts errors when typing to void', async () => { // @ts-expect-error - const expectPromise1: void = expect(mock).toBeRequested() + expectVoid = expect(promiseNetworkMock).toBeRequested() // @ts-expect-error - const expectPromise2: void = expect(mock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + expectVoid = expect(promiseNetworkMock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) // @ts-expect-error - const expectPromise3: void = expect(mock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + expectVoid = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 // @ts-expect-error - const expectPromise4: void = expect(mock).toBeRequestedWith({ + 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/todo', // [optional] string | function | custom matcher method: 'POST', // [optional] string | array statusCode: 200, // [optional] number | array @@ -265,6 +362,11 @@ describe('type assertions', () => { postData: { title: 'foo', description: 'bar' }, // [optional] object | function | custom matcher response: { success: true }, // [optional] object | function | custom matcher }) + + // @ts-expect-error + expectVoid = expect(promiseNetworkMock).toBeRequestedWith(expect.objectContaining({ + response: { success: true }, // [optional] object | function | custom matcher + })) }) }) @@ -274,44 +376,66 @@ describe('type assertions', () => { expect.unimplementedFunction() }) - it('should support stringContaining, anything', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const expectAny1: any = expect.stringContaining('WebdriverIO') - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const expectAny2: any = expect.anything() + it('should support stringContaining, anything and more', async () => { + expect.stringContaining('WebdriverIO') + expect.arrayContaining(['WebdriverIO', 'Test']) + expect.objectContaining({ name: '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.arrayContaining(['WebdriverIO', 'Test']) + expect.not.objectContaining({ name: 'WebdriverIO' }) + + // TODO dprevost: Should we support these? + // 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 expectString: string = 'awaited element.getText()' - const expectPromise: Promise = Promise.resolve(expectString) + const actualString: string = 'Test Page' + const actualPromiseString: Promise = Promise.resolve('Test Page') describe('expect.soft', () => { - it('should not have ts error and not need to be awaited/be a promise if actual is non-promise type', async () => { - const expectWdioMatcher: WdioMatchers = expect.soft(expectString) - const expectVoid: void = expect.soft(expectString).toBe('Test Page') - }) - - it('should not have ts error and need to be awaited/be a promise if actual is a promise type', async () => { - const expectWdioMatcher: WdioMatchers, Promise> = expect.soft(expectPromise) - const expectVoid: Promise = expect.soft(expectPromise).toBe('Test Page') - - await expect.soft(expectPromise).toBe('Test Page') - }) + it('should not need to be awaited/be a promise if actual is non-promise type', async () => { + const expectWdioMatcher1: WdioMatchers = 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')) - it('should have ts error when using await and actual is non-promise type', async () => { // @ts-expect-error - const expectWdioMatcher: ExpectWebdriverIO.MatchersAndInverse, string> = expect.soft(expectString) - + expectPromiseVoid = expect.soft(actualString).toBe('Test Page') + // @ts-expect-error + expectPromiseVoid = expect.soft(actualString).not.toBe('Test Page') // @ts-expect-error - const expectVoid: Promise = expect.soft(expectString).toBe('Test Page') + expectPromiseVoid = expect.soft(actualString).not.toBe(expect.stringContaining('Test Page')) }) - it('should not have ts error and need to be awaited/be a promise if actual is a promise type', async () => { + 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 - const expectWdioMatcher: ExpectWebdriverIO.MatchersAndInverse> = expect.soft(expectPromise) + expectVoid = expect.soft(actualPromiseString).not.toBe('Test Page') // @ts-expect-error - const expectVoid: void = expect.soft(expectPromise).toBe('Test Page') + expectVoid = expect.soft(actualPromiseString).not.toBe(expect.stringContaining('Test Page')) }) it('should support chainable element', async () => { @@ -325,47 +449,33 @@ describe('type assertions', () => { }) it('should support chainable element with wdio Matchers', async () => { - const expectPromise1: Promise = expect.soft(element).toBeDisplayed() - const expectPromise2: Promise = expect.soft(chainableElement).toBeDisplayed() - - // @ts-expect-error - const expectPromise3: void = expect.soft(element).toBeDisplayed() - // @ts-expect-error - const expectPromise4: void = expect.soft(chainableElement).toBeDisplayed() - + 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() - }) - - describe('not', async () => { - it('should support not with chainable', async () => { - const expectPromise1: Promise = expect.soft(element).not.toBeDisplayed() - const expectPromise2: Promise = expect.soft(chainableElement).not.toBeDisplayed() - - // @ts-expect-error - const expectPromise3: void = expect.soft(element).not.toBeDisplayed() - // @ts-expect-error - const expectPromise4: void = expect.soft(chainableElement).not.toBeDisplayed() - - await expect.soft(element).not.toBeDisplayed() - await expect.soft(chainableElement).not.toBeDisplayed() - }) + await expect.soft(chainableArray).toBeDisplayed() - it('should support not with non-promise', async () => { - const expectVoid1: void = expect.soft(expectString).not.toBe('Test Page') + 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 - const expectVoid2: Promise = expect.soft(expectString).not.toBe('Test Page') - }) - - it('should support not with promise', async () => { - const expectPromise1: Promise = expect.soft(expectPromise).not.toBe('Test Page') - - // @ts-expect-error - const expectPromise2: void = expect.soft(expectPromise).not.toBe('Test Page') + // @ts-expect-error + expectVoid = expect.soft(element).toBeDisplayed() + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toBeDisplayed() + // @ts-expect-error + expectVoid = expect.soft(chainableArray).toBeDisplayed() - await expect.soft(expectPromise).not.toBe('Test Page') - }) + // @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() }) }) @@ -374,25 +484,25 @@ describe('type assertions', () => { const expectSoftFailure1: ExpectWebdriverIO.SoftFailure[] = expect.getSoftFailures() // @ts-expect-error - const expectSoftFailure2: void = expect.getSoftFailures() + expectVoid = expect.getSoftFailures() }) }) describe('expect.assertSoftFailures', () => { it('should be of type void', async () => { - const expectVoid1: void = expect.assertSoftFailures() + expectVoid = expect.assertSoftFailures() // @ts-expect-error - const expectVoid2: Promise = expect.assertSoftFailures() + expectPromiseVoid = expect.assertSoftFailures() }) }) describe('expect.clearSoftFailures', () => { it('should be of type void', async () => { - const expectVoid1: void = expect.clearSoftFailures() + expectVoid = expect.clearSoftFailures() // @ts-expect-error - const expectVoid2: Promise = expect.clearSoftFailures() + expectPromiseVoid = expect.clearSoftFailures() }) }) }) diff --git a/types/standalone.d.ts b/types/standalone.d.ts index de6934efb..a717713ef 100644 --- a/types/standalone.d.ts +++ b/types/standalone.d.ts @@ -7,11 +7,11 @@ type ExpectBaseExpect = import('expect').BaseExpect type ExpectMatchers = import('expect').Matchers // Not exportable from 'expect' -type Inverse = { +type Inverse = { /** * Inverse next matcher. If you know how to test something, `.not` lets you test its opposite. */ - not: Matchers; + not: M; } declare namespace ExpectWebdriverIO { @@ -31,7 +31,7 @@ declare namespace ExpectWebdriverIO { toMatchInlineSnapshot(snapshot?: string, label?: string): Promise } - type MatchersAndInverse = ExpectWebdriverIO.Matchers & Inverse> + type MatchersAndInverse = ExpectWebdriverIO.Matchers & Inverse> /** * Mostly derived from the types of `jest-expect` but adapted to work with WebdriverIO. From e8b943215d991b6d90f7d3c248d55a5bc78b010f Mon Sep 17 00:00:00 2001 From: David Prevost Date: Wed, 25 Jun 2025 21:03:31 -0400 Subject: [PATCH 24/99] Working asymmetric matcher --- src/matchers/browser/toHaveClipboardText.ts | 2 +- src/matchers/browser/toHaveTitle.ts | 2 +- src/matchers/browser/toHaveUrl.ts | 2 +- src/matchers/element/toHaveAttribute.ts | 6 +- src/matchers/element/toHaveClass.ts | 6 +- src/matchers/element/toHaveComputedLabel.ts | 4 +- src/matchers/element/toHaveComputedRole.ts | 4 +- src/matchers/element/toHaveElementProperty.ts | 4 +- src/matchers/element/toHaveHTML.ts | 4 +- src/matchers/element/toHaveId.ts | 2 +- src/matchers/element/toHaveText.ts | 4 +- src/matchers/element/toHaveValue.ts | 2 +- src/matchers/mock/toBeRequestedWith.ts | 10 +-- src/utils.ts | 12 +-- .../mocha/customMatchers/customMatchers.d.ts | 2 +- test-types/mocha/types-mocha.test.ts | 80 +++++++++++++++---- types/expect-webdriverio.d.ts | 70 +++++++++------- types/standalone.d.ts | 4 +- 18 files changed, 138 insertions(+), 82 deletions(-) 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 acbe2bffe..d7ceb6b65 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 @@ -75,7 +75,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 18df08712..25c26f718 100644 --- a/src/matchers/element/toHaveClass.ts +++ b/src/matchers/element/toHaveClass.ts @@ -3,7 +3,7 @@ import type { WdioElementMaybePromise } from '../../types.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 } @@ -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) { // @ts-ignore TODO dprevost fix me return toHaveAttributeAndValue.call(this, el, 'class', className, { ...options, 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 240ba40f3..0d72b8b16 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/utils.ts b/src/utils.ts index f5ecb0f23..d8dd0f32e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -16,7 +16,7 @@ const asymmetricMatcher = ? Symbol.for('jest.asymmetricMatcher') : 0x13_57_a5 -export function isAsymmetricMatcher(expected: unknown): expected is ExpectWebdriverIO.PartialMatcher { +export function isAsymmetricMatcher(expected: unknown): expected is WdioAsymmetricMatcher { return ( typeof expected === 'object' && typeof expected === 'object' && @@ -28,7 +28,7 @@ export function isAsymmetricMatcher(expected: unknown): expected is ExpectWebdri ) as boolean } -function isStringContainingMatcher(expected: unknown): expected is ExpectWebdriverIO.PartialMatcher { +function isStringContainingMatcher(expected: unknown): expected is WdioAsymmetricMatcher { return isAsymmetricMatcher(expected) && ['StringContaining', 'StringNotContaining'].includes(expected.toString()) } @@ -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,7 +174,7 @@ 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 } } @@ -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 }) diff --git a/test-types/mocha/customMatchers/customMatchers.d.ts b/test-types/mocha/customMatchers/customMatchers.d.ts index b326f7306..66d0e49a3 100644 --- a/test-types/mocha/customMatchers/customMatchers.d.ts +++ b/test-types/mocha/customMatchers/customMatchers.d.ts @@ -5,6 +5,6 @@ declare namespace ExpectWebdriverIO { } interface Matchers { toBeCustom(): R; - toBeCustomPromise: T extends ChainablePromiseElement ? (expected?: string) => Promise : never; + toBeCustomPromise: T extends ChainablePromiseElement ? (expected?: string | ExpectWebdriverIO.PartialMatcher) => Promise : never; } } \ No newline at end of file diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index 4bb1b2a86..7e008a591 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -24,9 +24,10 @@ describe('type assertions', () => { expectPromiseVoid = expect(browser).not.toHaveUrl('https://example.com') // Asymmetric matchers - TODO dprevost to fix - // expectPromiseVoid = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) - // expectPromiseVoid = expect(browser).toHaveUrl(expect.any(String)) - // expectPromiseVoid = expect(browser).toHaveUrl(expect.anything()) + 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()) // TODO add more asymmetric matchers // @ts-expect-error @@ -35,6 +36,11 @@ describe('type assertions', () => { 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) + //// @ts-expect-error TODO dprevost can we make the below fail? + // await expect(browser).toHaveUrl(expect.objectContaining({})) }) it('should have ts errors when actual is not a Browser element', async () => { @@ -48,6 +54,37 @@ describe('type assertions', () => { 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()) + // TODO add more asymmetric matchers + + // @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', () => { @@ -89,10 +126,9 @@ describe('type assertions', () => { expectPromiseVoid = expect(element).toHaveText('text') expectPromiseVoid = expect(element).toHaveText(/text/) expectPromiseVoid = expect(element).toHaveText(['text1', 'text2']) - // TODO dprevost: to fix - // 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')]) + 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')]) expectPromiseVoid = expect(element).not.toHaveText('text') @@ -214,7 +250,7 @@ describe('type assertions', () => { it('should supported correctly a promise custom matcher with only chainableElement as actual', async () => { expectPromiseVoid = expect(chainableElement).toBeCustomPromise() // TODO dprevost: to fix - // expectPromiseVoid = expect(chainableElement).toBeCustomPromise(expect.stringContaining('test')) + expectPromiseVoid = expect(chainableElement).toBeCustomPromise(expect.stringContaining('test')) // @ts-expect-error expect('test').toBeCustomPromise() @@ -222,6 +258,8 @@ describe('type assertions', () => { expectVoid = expect(chainableElement).toBeCustomPromise() // @ts-expect-error expectVoid = expect(chainableElement).toBeCustomPromise(expect.stringContaining('test')) + // @ts-expect-error + expect(chainableElement).toBeCustomPromise(expect.stringContaining(6)) }) }) @@ -322,19 +360,29 @@ describe('type assertions', () => { expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith({ - url: 'http://localhost:8080/api/todo', // [optional] string | function | custom matcher - method: 'POST', // [optional] string | array - statusCode: 200, // [optional] number | array - requestHeaders: { Authorization: 'foo' }, // [optional] object | function | custom matcher - responseHeaders: { Authorization: 'bar' }, // [optional] object | function | custom matcher - postData: { title: 'foo', description: 'bar' }, // [optional] object | function | custom matcher - response: { success: true }, // [optional] object | function | custom matcher + url: 'http://localhost:8080/api/todo', + method: 'POST', + statusCode: 200, + requestHeaders: { Authorization: 'foo' }, + responseHeaders: { Authorization: 'bar' }, + postData: { title: 'foo', description: 'bar' }, + response: { success: true }, }) - // TODO dprevost: to fix + // TODO dprevost: Asymmetric matcher is not defined on the entire object in the .d.ts file, it is a bug? // expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith(expect.objectContaining({ // response: { success: true }, // [optional] object | function | custom matcher // })) + + 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 }), + }) }) it('should have ts errors when typing to void', async () => { diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index bf7621149..d5c2bc762 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -9,6 +9,7 @@ type SnapshotUpdateState = import('@vitest/snapshot').SnapshotUpdateState type ExpectLibAsymmetricMatchers = import('expect').AsymmetricMatchers type ChainablePromiseElement = import('webdriverio').ChainablePromiseElement type ChainablePromiseArray = import('webdriverio').ChainablePromiseArray +type ExpectLibAsymmetricMatcher = import('expect').AsymmetricMatcher // type PromiseLike = import('expect').PromiseLike @@ -20,17 +21,17 @@ interface WdioBrowserMatchers{ /** * `WebdriverIO.Browser` -> `getUrl` */ - toHaveUrl: T extends WebdriverIO.Browser ? (url: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions) => Promise: never; + toHaveUrl: T extends WebdriverIO.Browser ? (url: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions) => Promise: never; /** * `WebdriverIO.Browser` -> `getTitle` */ - toHaveTitle: T extends WebdriverIO.Browser ? (title: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions) => Promise: never; + toHaveTitle: T extends WebdriverIO.Browser ? (title: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions) => Promise: never; /** * `WebdriverIO.Browser` -> `execute` */ - toHaveClipboardText: T extends WebdriverIO.Browser ? (clipboardText: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions) => Promise: never; + toHaveClipboardText: T extends WebdriverIO.Browser ? (clipboardText: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions) => Promise: never; } type MockPromise = Promise @@ -92,7 +93,7 @@ interface WdioCustomMatchers { */ toHaveAttribute: T extends ElementOrArrayLike ? ( attribute: string, - value?: string | RegExp | ExpectWebdriverIO.PartialMatcher, + value?: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions ) => Promise : never @@ -101,7 +102,7 @@ interface WdioCustomMatchers { */ toHaveAttr: T extends ElementOrArrayLike ? ( attribute: string, - value?: string | RegExp | ExpectWebdriverIO.PartialMatcher, + value?: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions ) => Promise : never @@ -110,7 +111,7 @@ interface WdioCustomMatchers { * @deprecated since v1.3.1 - use `toHaveElementClass` instead. */ toHaveClass: T extends ElementOrArrayLike ? ( - className: string | RegExp | ExpectWebdriverIO.PartialMatcher, + className: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions ) => Promise : never @@ -131,7 +132,7 @@ interface WdioCustomMatchers { * ``` */ toHaveElementClass: T extends ElementOrArrayLike ? ( - className: string | RegExp | Array | ExpectWebdriverIO.PartialMatcher, + className: string | RegExp | Array | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions ) => Promise : never @@ -139,7 +140,7 @@ interface WdioCustomMatchers { * `WebdriverIO.Element` -> `getProperty` */ toHaveElementProperty: T extends ElementOrArrayLike ? ( - property: string | RegExp | ExpectWebdriverIO.PartialMatcher, + property: string | RegExp | ExpectWebdriverIO.PartialMatcher, value?: unknown, options?: ExpectWebdriverIO.StringOptions ) => Promise : never @@ -148,7 +149,7 @@ interface WdioCustomMatchers { * `WebdriverIO.Element` -> `getProperty` value */ toHaveValue: T extends ElementOrArrayLike ? ( - value: string | RegExp | ExpectWebdriverIO.PartialMatcher, + value: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions ) => Promise : never @@ -200,7 +201,7 @@ interface WdioCustomMatchers { * `WebdriverIO.Element` -> `getAttribute` href */ toHaveHref: T extends ElementOrArrayLike ? ( - href: string | RegExp | ExpectWebdriverIO.PartialMatcher, + href: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions ) => Promise : never @@ -208,7 +209,7 @@ interface WdioCustomMatchers { * `WebdriverIO.Element` -> `getAttribute` href */ toHaveLink: T extends ElementOrArrayLike ? ( - href: string | RegExp | ExpectWebdriverIO.PartialMatcher, + href: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions ) => Promise : never @@ -216,7 +217,7 @@ interface WdioCustomMatchers { * `WebdriverIO.Element` -> `getProperty` value */ toHaveId: T extends ElementOrArrayLike ? ( - id: string | RegExp | ExpectWebdriverIO.PartialMatcher, + id: string | RegExp | ExpectWebdriverIO.PartialMatche, options?: ExpectWebdriverIO.StringOptions ) => Promise : never @@ -247,14 +248,14 @@ interface WdioCustomMatchers { * await expect(elem).toHaveText(['Coffee', 'Tea', 'Milk']) * ``` */ - toHaveText: T extends ElementOrArrayLike ? (text: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array) => Promise : never + toHaveText: T extends ElementOrArrayLike ? (text: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array>) => Promise : never /** * `WebdriverIO.Element` -> `getHTML` * Element's html equals the html provided */ toHaveHTML: T extends ElementOrArrayLike ? ( - html: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, + html: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, options?: ExpectWebdriverIO.HTMLOptions ) => Promise : never @@ -263,7 +264,7 @@ interface WdioCustomMatchers { * Element's computed label equals the computed label provided */ toHaveComputedLabel: T extends ElementOrArrayLike ? ( - computedLabel: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, + computedLabel: string | RegExp | ExpectWebdriverIO.PartialMatcher| Array, options?: ExpectWebdriverIO.StringOptions ) => Promise : never @@ -272,7 +273,7 @@ interface WdioCustomMatchers { * Element's computed role equals the computed role provided */ toHaveComputedRole: T extends ElementOrArrayLike ? ( - computedRole: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, + computedRole: string | RegExp | ExpectWebdriverIO.PartialMatcher| Array, options?: ExpectWebdriverIO.StringOptions ) => Promise : never @@ -341,6 +342,17 @@ interface WdioOverloadedMatchers { interface WdioMatchers extends WdioOverloadedMatchers, WdioBrowserMatchers, WdioCustomMatchers, WdioMockMatchers {} +type WdioAsymmetricMatchers = ExpectLibAsymmetricMatchers + +/** + * Implementation of the asymmetric matcher. Equivalent as he PartialMatcher but with sample used by implementations. + * // TODO dprevost - might be needed in the namespace for custom matchers implementation? + */ +type WdioAsymmetricMatcher = ExpectWebdriverIO.PartialMatcher & { + // Overwrite protected properties of expect.AsymmetricMatcher to access them + sample: T; +} + /** * expect function declaration, containing two generics: * - T: the type of the actual value, e.g. any type, not just WebdriverIO.Browser or WebdriverIO.Element @@ -348,8 +360,7 @@ interface WdioMatchers extends WdioOverloadedMatchers, Wdi */ // TODO dprevost should we extends Expect from expect lib or just AsyncMatchers? // TODO dprevost ExpectLibAsymmetricMatchers add arrayOf and closeTo previously not there! and not was there previously but is no more? -interface WdioCustomExpect extends ExpectLibAsymmetricMatchers { - +interface WdioCustomExpect extends WdioAsymmetricMatchers { /** * Creates a soft assertion wrapper around standard expect * Soft assertions record failures but don't throw errors immediately @@ -559,25 +570,25 @@ declare namespace ExpectWebdriverIO { } 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 + | ExpectWebdriverIO.PartialMatcher | ((r: string | undefined) => boolean) response?: | string - | ExpectWebdriverIO.JsonCompatible + | ExpectWebdriverIO.JsonCompatible | ExpectWebdriverIO.PartialMatcher | ((r: string) => boolean) } @@ -587,14 +598,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 - } + /** + * Allow to partially matches value. Same as asymmetric matcher in jest. + * Some properties are omitted for the type check to work correctly. + */ + type PartialMatcher = Omit, 'sample' | 'inverse' | '$$typeof'> } declare module 'expect-webdriverio' { diff --git a/types/standalone.d.ts b/types/standalone.d.ts index a717713ef..762ca42b3 100644 --- a/types/standalone.d.ts +++ b/types/standalone.d.ts @@ -2,9 +2,9 @@ /// /// -type ExpectAsymmetricMatchers = import('expect').AsymmetricMatchers type ExpectBaseExpect = import('expect').BaseExpect type ExpectMatchers = import('expect').Matchers +type ExpectLibExpect = import('expect').Expect // Not exportable from 'expect' type Inverse = { @@ -37,7 +37,7 @@ declare namespace ExpectWebdriverIO { * Mostly derived from the types of `jest-expect` but adapted to work with WebdriverIO. * @see https://github.com/jestjs/jest/blob/main/packages/jest-expect/src/types.ts */ - interface Expect extends ExpectBaseExpect, ExpectAsymmetricMatchers, Inverse> { + interface Expect extends ExpectLibExpect { /** * The `expect` function is used every time you want to test a value. * You will rarely call `expect` by itself. From 267b4dadf2aaafe8a717f04476f89afb3783a3f7 Mon Sep 17 00:00:00 2001 From: David Prevost <77302423+dprevost-LMI@users.noreply.github.com> Date: Wed, 25 Jun 2025 10:18:40 -0400 Subject: [PATCH 25/99] Apply suggestions from code review --- test-types/mocha/customMatchers/customMatchers.d.ts | 1 - types/expect-webdriverio.d.ts | 2 -- types/standalone-global.d.ts | 4 ++-- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/test-types/mocha/customMatchers/customMatchers.d.ts b/test-types/mocha/customMatchers/customMatchers.d.ts index 66d0e49a3..883539e12 100644 --- a/test-types/mocha/customMatchers/customMatchers.d.ts +++ b/test-types/mocha/customMatchers/customMatchers.d.ts @@ -1,4 +1,3 @@ -// Name jest is required to augment the jest.Matchers interface declare namespace ExpectWebdriverIO { interface AsymmetricMatchers { toBeCustom(): void; diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index d5c2bc762..091b9e89b 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -11,8 +11,6 @@ type ChainablePromiseElement = import('webdriverio').ChainablePromiseElement type ChainablePromiseArray = import('webdriverio').ChainablePromiseArray type ExpectLibAsymmetricMatcher = import('expect').AsymmetricMatcher -// type PromiseLike = import('expect').PromiseLike - // eslint-disable-next-line @typescript-eslint/no-explicit-any type PromiseLikeType = Promise type UnwrapPromise = T extends Promise ? U : T diff --git a/types/standalone-global.d.ts b/types/standalone-global.d.ts index 4988da689..af271a75b 100644 --- a/types/standalone-global.d.ts +++ b/types/standalone-global.d.ts @@ -1,7 +1,7 @@ /// -// On IDE restart, it seems to conflict with one defined in `types/jest` -// @ts-ignore +// We override the existing one, probably coming from `types/jest` +// @ts-expect-error declare const expect: ExpectWebdriverIO.Expect declare namespace NodeJS { From c597d881eb9efbf1e973fc45a6dc768db9763b8a Mon Sep 17 00:00:00 2001 From: David Prevost <77302423+dprevost-LMI@users.noreply.github.com> Date: Thu, 26 Jun 2025 20:03:34 -0400 Subject: [PATCH 26/99] Update types/expect-webdriverio.d.ts --- types/expect-webdriverio.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 091b9e89b..f2072c110 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -416,7 +416,7 @@ declare namespace ExpectWebdriverIO { } interface SoftAssertionServiceOptions { - autoAssertOnTestEnd?: boolean; + autoAssertOnTestEnd?: boolean } class SoftAssertionService implements ServiceInstance { From ed635251b1e6c46673eac24d3291ceb80bb002b6 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Thu, 26 Jun 2025 20:10:18 -0400 Subject: [PATCH 27/99] Remove unneeded changes --- tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index dedda6553..b605359a4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,6 +20,5 @@ "include": [ "./test/**/*.ts", "./src/**/*.ts", - "./src/matcher/mock/**/*.ts", ] } From 912faabaf89d39949a0d1c7c7404c5a845c12c12 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Thu, 26 Jun 2025 20:28:25 -0400 Subject: [PATCH 28/99] Use built-in PromiseLike --- jest.d.ts | 2 +- test/snapshot.test.ts | 2 +- tsconfig.json | 2 +- types/expect-webdriverio.d.ts | 4 +--- types/jasmine-soft-extend.d.ts | 36 +++++++++++++++++----------------- types/standalone.d.ts | 2 +- 6 files changed, 23 insertions(+), 25 deletions(-) diff --git a/jest.d.ts b/jest.d.ts index 2f9c264ba..269552130 100644 --- a/jest.d.ts +++ b/jest.d.ts @@ -46,7 +46,7 @@ declare namespace jest { * 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): T extends PromiseLikeType ? MatcherAndInverse, T> : MatcherAndInverse + soft(actual: T): T extends PromiseLike ? MatcherAndInverse, T> : MatcherAndInverse /** * Get all current soft assertion failures diff --git a/test/snapshot.test.ts b/test/snapshot.test.ts index 8d45559b3..b757bb16b 100644 --- a/test/snapshot.test.ts +++ b/test/snapshot.test.ts @@ -72,4 +72,4 @@ test('supports cucumber snapshot testing', async () => { const expectedSnapfileExist = await fs.access(path.resolve(__dirname, 'file.feature.snap')) .then(() => true, () => false) expect(expectedSnapfileExist).toBe(true) -}) \ No newline at end of file +}) diff --git a/tsconfig.json b/tsconfig.json index b605359a4..bd84d6376 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,6 @@ }, "include": [ "./test/**/*.ts", - "./src/**/*.ts", + "./src/**/*.ts" ] } diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index f2072c110..9f524b346 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -11,8 +11,6 @@ type ChainablePromiseElement = import('webdriverio').ChainablePromiseElement type ChainablePromiseArray = import('webdriverio').ChainablePromiseArray type ExpectLibAsymmetricMatcher = import('expect').AsymmetricMatcher -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type PromiseLikeType = Promise type UnwrapPromise = T extends Promise ? U : T interface WdioBrowserMatchers{ @@ -364,7 +362,7 @@ interface WdioCustomExpect extends WdioAsymmetricMatchers { * 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): T extends PromiseLikeType ? Matchers, T> : Matchers + soft(actual: T): T extends PromiseLike ? Matchers, T> : Matchers /** * Get all current soft assertion failures diff --git a/types/jasmine-soft-extend.d.ts b/types/jasmine-soft-extend.d.ts index d083e1b0e..03fc09ab0 100644 --- a/types/jasmine-soft-extend.d.ts +++ b/types/jasmine-soft-extend.d.ts @@ -18,7 +18,7 @@ declare global { * All failures are collected and reported at the end of the test */ function soft(actual: T): jasmine.Matchers - // soft(actual: T): T extends PromiseLikeType ? Matchers, T> : Matchers + // soft(actual: T): T extends PromiseLike ? Matchers, T> : Matchers /** * Get all current soft assertion failures @@ -36,14 +36,14 @@ declare global { function clearSoftFailures(testId?: string): void /** Expect Asymmetric Matchers */ - function any(sample: unknown): AsyncMatcher - function anything(): AsyncMatcher - function arrayContaining(sample: Array): AsyncMatcher - function arrayOf(sample: unknown): AsyncMatcher - function closeTo(sample: number, precision?: number): AsyncMatcher - function objectContaining(sample: Record): AsyncMatcher - function stringContaining(sample: string): AsyncMatcher - function stringMatching(sample: string | RegExp): AsyncMatcher + // function any(sample: unknown): AsyncMatcher + // function anything(): AsyncMatcher + // function arrayContaining(sample: Array): AsyncMatcher + // function arrayOf(sample: unknown): AsyncMatcher + // function closeTo(sample: number, precision?: number): AsyncMatcher + // function objectContaining(sample: Record): AsyncMatcher + // function stringContaining(sample: string): AsyncMatcher + // function stringMatching(sample: string | RegExp): AsyncMatcher } namespace expectAsync { @@ -56,7 +56,7 @@ declare global { * All failures are collected and reported at the end of the test */ function soft(actual: T): jasmine.Matchers - // soft(actual: T): T extends PromiseLikeType ? Matchers, T> : Matchers + // soft(actual: T): T extends PromiseLike ? Matchers, T> : Matchers /** * Get all current soft assertion failures @@ -74,14 +74,14 @@ declare global { function clearSoftFailures(testId?: string): void /** Expect Asymmetric Matchers */ - function any(sample: unknown): AsyncMatcher - function anything(): AsyncMatcher - function arrayContaining(sample: Array): AsyncMatcher - function arrayOf(sample: unknown): AsyncMatcher - function closeTo(sample: number, precision?: number): AsyncMatcher - function objectContaining(sample: Record): AsyncMatcher - function stringContaining(sample: string): AsyncMatcher - function stringMatching(sample: string | RegExp): AsyncMatcher + // function any(sample: unknown): AsyncMatcher + // function anything(): AsyncMatcher + // function arrayContaining(sample: Array): AsyncMatcher + // function arrayOf(sample: unknown): AsyncMatcher + // function closeTo(sample: number, precision?: number): AsyncMatcher + // function objectContaining(sample: Record): AsyncMatcher + // function stringContaining(sample: string): AsyncMatcher + // function stringMatching(sample: string | RegExp): AsyncMatcher } } diff --git a/types/standalone.d.ts b/types/standalone.d.ts index 762ca42b3..349024520 100644 --- a/types/standalone.d.ts +++ b/types/standalone.d.ts @@ -51,7 +51,7 @@ declare namespace ExpectWebdriverIO { * 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): T extends PromiseLikeType ? MatchersAndInverse, T> : MatchersAndInverse + soft(actual: T): T extends PromiseLike ? MatchersAndInverse, T> : MatchersAndInverse /** * Get all current soft assertion failures From 5c68ef4c6c1cde2413eccaf8054b4e674673f535 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Thu, 26 Jun 2025 21:46:18 -0400 Subject: [PATCH 29/99] Have snapshot use await only on promises --- jest.d.ts | 12 +--- test-types/jest/types-jest.test.ts | 90 +++++++++++++++++++--------- test-types/mocha/types-mocha.test.ts | 18 +++--- types/expect-webdriverio.d.ts | 39 ++++++------ types/jasmine-soft-extend.d.ts | 2 - types/standalone.d.ts | 6 +- 6 files changed, 98 insertions(+), 69 deletions(-) diff --git a/jest.d.ts b/jest.d.ts index 269552130..582219489 100644 --- a/jest.d.ts +++ b/jest.d.ts @@ -12,25 +12,19 @@ declare namespace jest { * @see https://github.com/jestjs/jest/blob/73dbef5d2d3195a1e55fb254c54cce70d3036252/packages/jest-snapshot/src/types.ts#L37 */ + // TODO dprevost: how can we make both Wdio snapshot and Jest snapshot work together? /** * snapshot matcher * @param label optional snapshot label */ - toMatchSnapshot(label?: string): Promise; - - // TODO - this is not working as expected, need to investigate - /** - * snapshot matcher - * @param label optional snapshot label - */ - // toMatchSnapshot: T extends WdioElementLike ? (label: string) => Promise : (hint?: string) => R; + toMatchSnapshot(label?: string): T extends WdioPromiseLike ? Promise : R; /** * inline snapshot matcher * @param snapshot snapshot string (autogenerated if not specified) * @param label optional snapshot label */ - toMatchInlineSnapshot(snapshot?: string, label?: string): Promise + toMatchInlineSnapshot(snapshot?: string, label?: string): T extends WdioPromiseLike ? Promise : R; } type MatcherAndInverse = Matchers & AndNot> diff --git a/test-types/jest/types-jest.test.ts b/test-types/jest/types-jest.test.ts index c6378ee02..476147b6c 100644 --- a/test-types/jest/types-jest.test.ts +++ b/test-types/jest/types-jest.test.ts @@ -126,18 +126,18 @@ describe('type assertions', async () => { describe('toMatchSnapshot', () => { it('should be supported correctly', async () => { - expectPromiseVoid = expect(element).toMatchSnapshot() - expectPromiseVoid = expect(element).toMatchSnapshot('test label') - expectPromiseVoid = expect(element).not.toMatchSnapshot('test label') + 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 - expectVoid = expect(element).toMatchSnapshot() + expectPromiseVoid = expect(element).toMatchSnapshot() //@ts-expect-error - expectVoid = expect(element).not.toMatchSnapshot() + expectPromiseVoid = expect(element).not.toMatchSnapshot() //@ts-expect-error expectVoid = expect(chainableElement).toMatchSnapshot() //@ts-expect-error @@ -154,16 +154,16 @@ describe('type assertions', async () => { describe('toMatchInlineSnapshot', () => { it('should be correctly supported', async () => { - expectPromiseVoid = expect(element).toMatchInlineSnapshot() - expectPromiseVoid = expect(element).toMatchInlineSnapshot('test snapshot') - expectPromiseVoid = expect(element).toMatchInlineSnapshot('test snapshot', 'test label') + 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 - expectVoid = expect(element).toMatchInlineSnapshot() + expectPromiseVoid = expect(element).toMatchInlineSnapshot() //@ts-expect-error expectVoid = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') }) @@ -499,37 +499,69 @@ describe('type assertions', async () => { }) describe('@types/jest only - original Matchers', () => { - const propertyMatchers: Partial<{}> = {} - const snapshotName: string = 'test-snapshot' - describe('toMatchSnapshot', () => { + 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 have original jest Matcher still works', async () => { - expectVoid = expect(element).toMatchSnapshot(propertyMatchers) - expectVoid = expect(element).toMatchSnapshot(propertyMatchers, snapshotName) - expectVoid = expect(element).toMatchInlineSnapshot(propertyMatchers) - expectVoid = expect(element).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(element).not.toMatchSnapshot(propertyMatchers) - expectVoid = expect(element).not.toMatchSnapshot(propertyMatchers, snapshotName) - expectVoid = expect(element).not.toMatchInlineSnapshot(propertyMatchers) - expectVoid = expect(element).not.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(element).toMatchSnapshot(propertyMatchers) + expectPromiseVoid = expect(treeObject).toMatchSnapshot(propertyMatchers) // @ts-expect-error - expectPromiseVoid = expect(element).toMatchSnapshot(propertyMatchers, snapshotName) + expectPromiseVoid = expect(treeObject).toMatchSnapshot(propertyMatchers, snapshotName) // @ts-expect-error - expectPromiseVoid = expect(element).toMatchInlineSnapshot(propertyMatchers) + expectPromiseVoid = expect(treeObject).toMatchInlineSnapshot(propertyMatchers) // @ts-expect-error - expectPromiseVoid = expect(element).toMatchInlineSnapshot(propertyMatchers, snapshotName) + expectPromiseVoid = expect(treeObject).toMatchInlineSnapshot(propertyMatchers, snapshotName) // @ts-expect-error - expectPromiseVoid = expect(element).not.toMatchSnapshot(propertyMatchers) + expectPromiseVoid = expect(treeObject).not.toMatchSnapshot(propertyMatchers) // @ts-expect-error - expectPromiseVoid = expect(element).not.toMatchSnapshot(propertyMatchers, snapshotName) + expectPromiseVoid = expect(treeObject).not.toMatchSnapshot(propertyMatchers, snapshotName) // @ts-expect-error - expectPromiseVoid = expect(element).not.toMatchInlineSnapshot(propertyMatchers) + expectPromiseVoid = expect(treeObject).not.toMatchInlineSnapshot(propertyMatchers) // @ts-expect-error - expectPromiseVoid = expect(element).not.toMatchInlineSnapshot(propertyMatchers, snapshotName) + expectPromiseVoid = expect(treeObject).not.toMatchInlineSnapshot(propertyMatchers, snapshotName) }) }) }) diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index 7e008a591..906ebd4f5 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -167,18 +167,18 @@ describe('type assertions', () => { describe('toMatchSnapshot', () => { it('should be supported correctly', async () => { - expectPromiseVoid = expect(element).toMatchSnapshot() - expectPromiseVoid = expect(element).toMatchSnapshot('test label') - expectPromiseVoid = expect(element).not.toMatchSnapshot('test label') + 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 - expectVoid = expect(element).toMatchSnapshot() + expectPromiseVoid = expect(element).toMatchSnapshot() //@ts-expect-error - expectVoid = expect(element).not.toMatchSnapshot() + expectPromiseVoid = expect(element).not.toMatchSnapshot() //@ts-expect-error expectVoid = expect(chainableElement).toMatchSnapshot() //@ts-expect-error @@ -195,16 +195,16 @@ describe('type assertions', () => { describe('toMatchInlineSnapshot', () => { it('should be correctly supported', async () => { - expectPromiseVoid = expect(element).toMatchInlineSnapshot() - expectPromiseVoid = expect(element).toMatchInlineSnapshot('test snapshot') - expectPromiseVoid = expect(element).toMatchInlineSnapshot('test snapshot', 'test label') + 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 - expectVoid = expect(element).toMatchInlineSnapshot() + expectPromiseVoid = expect(element).toMatchInlineSnapshot() //@ts-expect-error expectVoid = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') }) diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 9f524b346..b11080b35 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -4,13 +4,21 @@ 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 ExpectLibAsymmetricMatchers = import('expect').AsymmetricMatchers + type ChainablePromiseElement = import('webdriverio').ChainablePromiseElement type ChainablePromiseArray = import('webdriverio').ChainablePromiseArray + +type ExpectLibAsymmetricMatchers = import('expect').AsymmetricMatchers type ExpectLibAsymmetricMatcher = import('expect').AsymmetricMatcher +type WdioPromiseLike = PromiseLike | ChainablePromiseElement | ChainablePromiseArray +type ElementPromise = Promise +type ElementArrayPromise = Promise +type WdioOnlyPromiseLike = ElementPromise | ElementArrayPromise | ChainablePromiseElement | ChainablePromiseArray + type UnwrapPromise = T extends Promise ? U : T interface WdioBrowserMatchers{ @@ -55,10 +63,9 @@ interface WdioMockMatchers { * Once we have all types working, we could check to bring those back into the `ExpectWebdriverIO` namespace. */ -// TODO dprevost have browser matchers and element matchers separated -// TODO extending extends Record remove ts error on unimplemented matchers - // TODO dprevost - check if custom matchers (https://webdriver.io/docs/custommatchers/) will still work aka webdriverio/expect-webdriverio#1408 + +// TODO dprevost - can we be better and return void for Element/ElementArray but Promise for ElementPromise/ElementArrayPromise? type ElementOrArrayLike = ElementLike | ElementArrayLike type ElementLike = WebdriverIO.Element | ChainablePromiseElement type ElementArrayLike = WebdriverIO.ElementArray | ChainablePromiseArray @@ -213,7 +220,7 @@ interface WdioCustomMatchers { * `WebdriverIO.Element` -> `getProperty` value */ toHaveId: T extends ElementOrArrayLike ? ( - id: string | RegExp | ExpectWebdriverIO.PartialMatche, + id: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions ) => Promise : never @@ -338,17 +345,6 @@ interface WdioOverloadedMatchers { interface WdioMatchers extends WdioOverloadedMatchers, WdioBrowserMatchers, WdioCustomMatchers, WdioMockMatchers {} -type WdioAsymmetricMatchers = ExpectLibAsymmetricMatchers - -/** - * Implementation of the asymmetric matcher. Equivalent as he PartialMatcher but with sample used by implementations. - * // TODO dprevost - might be needed in the namespace for custom matchers implementation? - */ -type WdioAsymmetricMatcher = ExpectWebdriverIO.PartialMatcher & { - // Overwrite protected properties of expect.AsymmetricMatcher to access them - sample: T; -} - /** * expect function declaration, containing two generics: * - T: the type of the actual value, e.g. any type, not just WebdriverIO.Browser or WebdriverIO.Element @@ -356,7 +352,7 @@ type WdioAsymmetricMatcher = ExpectWebdriverIO.PartialMatcher & { */ // TODO dprevost should we extends Expect from expect lib or just AsyncMatchers? // TODO dprevost ExpectLibAsymmetricMatchers add arrayOf and closeTo previously not there! and not was there previously but is no more? -interface WdioCustomExpect extends WdioAsymmetricMatchers { +interface WdioCustomExpect extends ExpectLibAsymmetricMatchers { /** * Creates a soft assertion wrapper around standard expect * Soft assertions record failures but don't throw errors immediately @@ -380,6 +376,15 @@ interface WdioCustomExpect extends WdioAsymmetricMatchers { clearSoftFailures(testId?: string): void } +/** + * 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: T; +} + declare namespace ExpectWebdriverIO { function setOptions(options: DefaultOptions): void // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/types/jasmine-soft-extend.d.ts b/types/jasmine-soft-extend.d.ts index 03fc09ab0..37ed6724c 100644 --- a/types/jasmine-soft-extend.d.ts +++ b/types/jasmine-soft-extend.d.ts @@ -1,7 +1,5 @@ /// -type UnwrapPromise = T extends Promise ? U : T - declare global { // TODO dprevost might need to override the Array too (and more?) diff --git a/types/standalone.d.ts b/types/standalone.d.ts index 349024520..468ac7037 100644 --- a/types/standalone.d.ts +++ b/types/standalone.d.ts @@ -6,7 +6,7 @@ type ExpectBaseExpect = import('expect').BaseExpect type ExpectMatchers = import('expect').Matchers type ExpectLibExpect = import('expect').Expect -// Not exportable from 'expect' +// To remove when exportable from 'expect'. See https://github.com/jestjs/jest/pull/15704 (already merged) type Inverse = { /** * Inverse next matcher. If you know how to test something, `.not` lets you test its opposite. @@ -21,14 +21,14 @@ declare namespace ExpectWebdriverIO { * snapshot matcher * @param label optional snapshot label */ - toMatchSnapshot(label?: string): Promise + toMatchSnapshot(label?: string): T extends WdioPromiseLike ? Promise : R; /** * inline snapshot matcher * @param snapshot snapshot string (autogenerated if not specified) * @param label optional snapshot label */ - toMatchInlineSnapshot(snapshot?: string, label?: string): Promise + toMatchInlineSnapshot(snapshot?: string, label?: string): T extends WdioPromiseLike ? Promise : R; } type MatchersAndInverse = ExpectWebdriverIO.Matchers & Inverse> From 5dc6e898fbd9bab8bc4885e1c3bb0211ad140935 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Thu, 26 Jun 2025 23:04:56 -0400 Subject: [PATCH 30/99] More reusable types --- test-types/mocha/types-mocha.test.ts | 47 +++++++ types/expect-webdriverio.d.ts | 184 ++++++++++++++------------- 2 files changed, 144 insertions(+), 87 deletions(-) diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index 906ebd4f5..55e69dae4 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -5,6 +5,7 @@ import type { ChainablePromiseElement, ChainablePromiseArray } from 'webdriverio describe('type assertions', () => { const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element + const elementArray: WebdriverIO.ElementArray = [] as unknown as WebdriverIO.ElementArray const chainableElement = {} as unknown as ChainablePromiseElement const chainableArray = {} as ChainablePromiseArray const networkMock: WebdriverIO.Mock = {} as unknown as WebdriverIO.Mock @@ -95,6 +96,10 @@ describe('type assertions', () => { 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() @@ -137,6 +142,48 @@ describe('type assertions', () => { // @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') }) diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index b11080b35..1ed21ae3e 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -14,109 +14,134 @@ type ChainablePromiseArray = import('webdriverio').ChainablePromiseArray type ExpectLibAsymmetricMatchers = import('expect').AsymmetricMatchers type ExpectLibAsymmetricMatcher = import('expect').AsymmetricMatcher +/** + * 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 + +/** + * Type potentially leading to a promise + */ +type WdioMaybePromise = PromiseLike | WdioOnlyMaybePromiseLike + type UnwrapPromise = T extends Promise ? U : T +// TODO dprevost - check if custom matchers (https://webdriver.io/docs/custommatchers/) will still work aka webdriverio/expect-webdriverio#1408 + +/** + * 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 ElementOrArrayLike = ElementLike | ElementArrayLike +type ElementLike = WebdriverIO.Element | ChainablePromiseElement +type ElementArrayLike = WebdriverIO.ElementArray | ChainablePromiseArray +type MockPromise = Promise + +/** + * Helper so the return type is a Promise only when needed to avoid using `await` when not a promise. + */ +type Promise = T extends WdioPromiseLike ? Promise : R +// type WdioPromiseVoidLike = T extends (WdioPromiseLike | WdioOnlyPromiseLike) ? Promise : never +// type WdioVoidLike = WdioPromiseVoidLike extends never ? void : never + +/** + * Helpers function allowing to use the function when the expect(actual: T) is of the expected type T. + */ +type FnWhenBrowser = T extends WebdriverIO.Browser ? Fn : never +type FnWhenMock = T extends MockPromise ? Fn : never +type FnWhenElementOrArrayLike = T extends ElementOrArrayLike ? Fn : never +type FnWhenElementArrayLike = T extends ElementArrayLike ? Fn : never interface WdioBrowserMatchers{ /** * `WebdriverIO.Browser` -> `getUrl` */ - toHaveUrl: T extends WebdriverIO.Browser ? (url: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions) => Promise: never; + toHaveUrl: FnWhenBrowser, options?: ExpectWebdriverIO.StringOptions) => Promise> /** * `WebdriverIO.Browser` -> `getTitle` */ - toHaveTitle: T extends WebdriverIO.Browser ? (title: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions) => Promise: never; + toHaveTitle: FnWhenBrowser, options?: ExpectWebdriverIO.StringOptions) => Promise> /** * `WebdriverIO.Browser` -> `execute` */ - toHaveClipboardText: T extends WebdriverIO.Browser ? (clipboardText: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions) => Promise: never; + toHaveClipboardText: FnWhenBrowser, options?: ExpectWebdriverIO.StringOptions) => Promise> } -type MockPromise = Promise interface WdioMockMatchers { /** * Check that `WebdriverIO.Mock` was called */ - toBeRequested: T extends MockPromise ? (options?: ExpectWebdriverIO.CommandOptions) => Promise : never + toBeRequested: FnWhenMock Promise> /** * Check that `WebdriverIO.Mock` was called N times */ - toBeRequestedTimes: T extends MockPromise ? ( - times: number | ExpectWebdriverIO.NumberOptions, - options?: ExpectWebdriverIO.NumberOptions - ) => Promise : never + toBeRequestedTimes: FnWhenMock Promise> /** * Check that `WebdriverIO.Mock` was called with the specific parameters */ - toBeRequestedWith: T extends MockPromise ? (requestedWith: ExpectWebdriverIO.RequestedWith, options?: ExpectWebdriverIO.CommandOptions) => Promise : never + toBeRequestedWith: FnWhenMock Promise> } - -/** - * 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. - */ - -// TODO dprevost - check if custom matchers (https://webdriver.io/docs/custommatchers/) will still work aka webdriverio/expect-webdriverio#1408 - -// TODO dprevost - can we be better and return void for Element/ElementArray but Promise for ElementPromise/ElementArrayPromise? -type ElementOrArrayLike = ElementLike | ElementArrayLike -type ElementLike = WebdriverIO.Element | ChainablePromiseElement -type ElementArrayLike = WebdriverIO.ElementArray | ChainablePromiseArray interface WdioCustomMatchers { // ===== $ or $$ ===== /** * `WebdriverIO.Element` -> `isDisplayed` */ - toBeDisplayed: T extends ElementOrArrayLike ? (options?: ExpectWebdriverIO.CommandOptions) => Promise : never + toBeDisplayed: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isExisting` */ - toExist: T extends ElementOrArrayLike ? (options?: ExpectWebdriverIO.CommandOptions) => Promise : never + toExist: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isExisting` */ - toBePresent: T extends ElementOrArrayLike ? (options?: ExpectWebdriverIO.CommandOptions) => Promise : never + toBePresent: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isExisting` */ - toBeExisting: T extends ElementOrArrayLike ? (options?: ExpectWebdriverIO.CommandOptions) => Promise : never + toBeExisting: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `getAttribute` */ - toHaveAttribute: T extends ElementOrArrayLike ? ( - attribute: string, - value?: string | RegExp | ExpectWebdriverIO.PartialMatcher, - options?: ExpectWebdriverIO.StringOptions - ) => Promise : never + toHaveAttribute: FnWhenElementOrArrayLike, + options?: ExpectWebdriverIO.StringOptions) + => Promise> /** * `WebdriverIO.Element` -> `getAttribute` */ - toHaveAttr: T extends ElementOrArrayLike ? ( - attribute: string, - value?: string | RegExp | ExpectWebdriverIO.PartialMatcher, + toHaveAttr: FnWhenElementOrArrayLike, options?: ExpectWebdriverIO.StringOptions - ) => Promise : never + ) => Promise> /** * `WebdriverIO.Element` -> `getAttribute` class * @deprecated since v1.3.1 - use `toHaveElementClass` instead. */ - toHaveClass: T extends ElementOrArrayLike ? ( + toHaveClass: FnWhenElementOrArrayLike, options?: ExpectWebdriverIO.StringOptions - ) => Promise : never + ) => Promise> /** * `WebdriverIO.Element` -> `getAttribute` class @@ -134,103 +159,103 @@ interface WdioCustomMatchers { * await expect(element).toHaveElementClass(['btn', 'btn-large']); * ``` */ - toHaveElementClass: T extends ElementOrArrayLike ? ( + toHaveElementClass: FnWhenElementOrArrayLike | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions - ) => Promise : never + ) => Promise> /** * `WebdriverIO.Element` -> `getProperty` */ - toHaveElementProperty: T extends ElementOrArrayLike ? ( + toHaveElementProperty: FnWhenElementOrArrayLike, value?: unknown, options?: ExpectWebdriverIO.StringOptions - ) => Promise : never + ) => Promise> /** * `WebdriverIO.Element` -> `getProperty` value */ - toHaveValue: T extends ElementOrArrayLike ? ( + toHaveValue: FnWhenElementOrArrayLike, options?: ExpectWebdriverIO.StringOptions - ) => Promise : never + ) => Promise> /** * `WebdriverIO.Element` -> `isClickable` */ - toBeClickable: T extends ElementOrArrayLike ? (options?: ExpectWebdriverIO.StringOptions) => Promise : never + toBeClickable: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `!isEnabled` */ - toBeDisabled: T extends ElementOrArrayLike ? (options?: ExpectWebdriverIO.StringOptions) => Promise : never + toBeDisabled: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isDisplayedInViewport` */ - toBeDisplayedInViewport: T extends ElementOrArrayLike ? (options?: ExpectWebdriverIO.StringOptions) => Promise : never + toBeDisplayedInViewport: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isEnabled` */ - toBeEnabled: T extends ElementOrArrayLike ? (options?: ExpectWebdriverIO.StringOptions) => Promise : never + toBeEnabled: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isFocused` */ - toBeFocused: T extends ElementOrArrayLike ? (options?: ExpectWebdriverIO.StringOptions) => Promise : never + toBeFocused: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isSelected` */ - toBeSelected: T extends ElementOrArrayLike ? (options?: ExpectWebdriverIO.StringOptions) => Promise : never + toBeSelected: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isSelected` */ - toBeChecked: T extends ElementOrArrayLike ? (options?: ExpectWebdriverIO.StringOptions) => Promise : never + toBeChecked: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `$$('./*').length` * supports less / greater then or equals to be passed in options */ - toHaveChildren: T extends ElementOrArrayLike ? ( + toHaveChildren: FnWhenElementOrArrayLike Promise : never + ) => Promise> /** * `WebdriverIO.Element` -> `getAttribute` href */ - toHaveHref: T extends ElementOrArrayLike ? ( + toHaveHref: FnWhenElementOrArrayLike, options?: ExpectWebdriverIO.StringOptions - ) => Promise : never + ) => Promise> /** * `WebdriverIO.Element` -> `getAttribute` href */ - toHaveLink: T extends ElementOrArrayLike ? ( + toHaveLink: FnWhenElementOrArrayLike, options?: ExpectWebdriverIO.StringOptions - ) => Promise : never + ) => Promise> /** * `WebdriverIO.Element` -> `getProperty` value */ - toHaveId: T extends ElementOrArrayLike ? ( + toHaveId: FnWhenElementOrArrayLike, options?: ExpectWebdriverIO.StringOptions - ) => Promise : never + ) => Promise> /** * `WebdriverIO.Element` -> `getSize` value */ - toHaveSize: T extends ElementOrArrayLike ? ( + toHaveSize: FnWhenElementOrArrayLike Promise : never + ) => Promise> /** * `WebdriverIO.Element` -> `getText` @@ -251,79 +276,64 @@ interface WdioCustomMatchers { * await expect(elem).toHaveText(['Coffee', 'Tea', 'Milk']) * ``` */ - toHaveText: T extends ElementOrArrayLike ? (text: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array>) => Promise : never + toHaveText: FnWhenElementOrArrayLike | Array>) => Promise> /** * `WebdriverIO.Element` -> `getHTML` * Element's html equals the html provided */ - toHaveHTML: T extends ElementOrArrayLike ? ( - html: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, - options?: ExpectWebdriverIO.HTMLOptions - ) => Promise : never + toHaveHTML: FnWhenElementOrArrayLike | Array) => Promise> /** * `WebdriverIO.Element` -> `getComputedLabel` * Element's computed label equals the computed label provided */ - toHaveComputedLabel: T extends ElementOrArrayLike ? ( + toHaveComputedLabel: FnWhenElementOrArrayLike| Array, options?: ExpectWebdriverIO.StringOptions - ) => Promise : never + ) => Promise> /** * `WebdriverIO.Element` -> `getComputedRole` * Element's computed role equals the computed role provided */ - toHaveComputedRole: T extends ElementOrArrayLike ? ( + toHaveComputedRole: FnWhenElementOrArrayLike| Array, options?: ExpectWebdriverIO.StringOptions - ) => Promise : never + ) => Promise> /** * `WebdriverIO.Element` -> `getSize('width')` * Element's width equals the width provided */ - toHaveWidth: T extends ElementOrArrayLike ? ( - width: number, - options?: ExpectWebdriverIO.CommandOptions - ) => Promise : never + toHaveWidth: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `getSize('height')` * Element's height equals the height provided */ - toHaveHeight: T extends ElementOrArrayLike ? ( - height: number, - options?: ExpectWebdriverIO.CommandOptions - ) => Promise : never + toHaveHeight: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `getSize()` * Element's size equals the size provided */ - toHaveHeight: T extends ElementOrArrayLike ? ( - size: { height: number; width: number }, - options?: ExpectWebdriverIO.CommandOptions - ) => Promise : never + toHaveHeight: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `getAttribute("style")` */ - toHaveStyle: T extends ElementOrArrayLike ? ( - style: { [key: string]: string }, - options?: ExpectWebdriverIO.StringOptions - ) => Promise : never + toHaveStyle: FnWhenElementOrArrayLike Promise> // ===== $$ only ===== /** * `WebdriverIO.ElementArray` -> `$$('...').length` * supports less / greater then or equals to be passed in options */ - toBeElementsArrayOfSize: T extends ElementArrayLike ? ( + toBeElementsArrayOfSize: FnWhenElementArrayLike Promise & Promise : never + ) => Promise & Promise> } /** From 75caff75cb578a53943fd089a50a9be4b5262c9d Mon Sep 17 00:00:00 2001 From: David Prevost Date: Fri, 27 Jun 2025 00:40:13 -0400 Subject: [PATCH 31/99] Validated that asymmetric matchers previously not included works --- src/utils.ts | 1 + test-types/mocha/types-mocha.test.ts | 96 ++++++++++++++++++++-------- test/matchers.test.ts | 5 ++ 3 files changed, 77 insertions(+), 25 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index d8dd0f32e..15aafa086 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -21,6 +21,7 @@ export function isAsymmetricMatcher(expected: unknown): expected is WdioAsymmetr typeof expected === 'object' && typeof expected === 'object' && expected && + // TODO dprevost will this one still work? '$$typeof' in expected && 'asymmetricMatch' in expected && expected.$$typeof === asymmetricMatcher && diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index 55e69dae4..710815220 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -9,8 +9,6 @@ describe('type assertions', () => { const chainableElement = {} as unknown as ChainablePromiseElement const chainableArray = {} as ChainablePromiseArray const networkMock: WebdriverIO.Mock = {} as unknown as WebdriverIO.Mock - // TODO dprevost: Need more test with this type? - // const ElementArray: WebdriverIO.ElementArray = await chainableArray?.getElements() // Type assertions let expectPromiseVoid: Promise @@ -24,12 +22,11 @@ describe('type assertions', () => { expectPromiseVoid = expect(browser).toHaveUrl('https://example.com') expectPromiseVoid = expect(browser).not.toHaveUrl('https://example.com') - // Asymmetric matchers - TODO dprevost to fix + // 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()) - // TODO add more asymmetric matchers // @ts-expect-error expectVoid = expect(browser).toHaveUrl('https://example.com') @@ -65,7 +62,6 @@ describe('type assertions', () => { expectPromiseVoid = expect(browser).toHaveTitle(expect.stringContaining('WebdriverIO')) expectPromiseVoid = expect(browser).toHaveTitle(expect.any(String)) expectPromiseVoid = expect(browser).toHaveTitle(expect.anything()) - // TODO add more asymmetric matchers // @ts-expect-error expectVoid = expect(browser).toHaveTitle('https://example.com') @@ -255,6 +251,21 @@ describe('type assertions', () => { //@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 () => { @@ -282,7 +293,6 @@ describe('type assertions', () => { }) }) - // TODO dprevost describe('Custom matchers', () => { it('should supported correctly a non-promise custom matcher', async () => { expectVoid = expect('test').toBeCustom() @@ -296,7 +306,6 @@ describe('type assertions', () => { it('should supported correctly a promise custom matcher with only chainableElement as actual', async () => { expectPromiseVoid = expect(chainableElement).toBeCustomPromise() - // TODO dprevost: to fix expectPromiseVoid = expect(chainableElement).toBeCustomPromise(expect.stringContaining('test')) // @ts-expect-error @@ -322,7 +331,7 @@ describe('type assertions', () => { expectPromiseVoid = expect(true).not.toBe(true) }) - it('should not expect Promise when actual is a chainable since toBe is not supported', async () => { + 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) @@ -367,11 +376,6 @@ describe('type assertions', () => { it('should expect a Promise of type', async () => { const expectPromiseBoolean1: ExpectWebdriverIO.MatchersAndInverse> = expect(booleanPromise) const expectPromiseBoolean2: ExpectWebdriverIO.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 () => { @@ -399,12 +403,12 @@ describe('type assertions', () => { it('should not have ts errors when typing to Promise', async () => { expectPromiseVoid = expect(promiseNetworkMock).toBeRequested() - expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) - expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedTimes(2) + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequested() - expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) - expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequestedTimes(2) + expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith({ url: 'http://localhost:8080/api/todo', @@ -430,6 +434,15 @@ describe('type assertions', () => { 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 () => { @@ -449,18 +462,18 @@ describe('type assertions', () => { // @ts-expect-error expectVoid = expect(promiseNetworkMock).toBeRequestedWith({ - url: 'http://localhost:8080/api/todo', // [optional] string | function | custom matcher - method: 'POST', // [optional] string | array - statusCode: 200, // [optional] number | array - requestHeaders: { Authorization: 'foo' }, // [optional] object | function | custom matcher - responseHeaders: { Authorization: 'bar' }, // [optional] object | function | custom matcher - postData: { title: 'foo', description: 'bar' }, // [optional] object | function | custom matcher - response: { success: true }, // [optional] object | function | custom matcher + url: 'http://localhost:8080/api/todo', + 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 }, // [optional] object | function | custom matcher + response: { success: true }, })) }) }) @@ -473,8 +486,14 @@ describe('type assertions', () => { 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) @@ -486,8 +505,12 @@ describe('type assertions', () => { 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')) // TODO dprevost: Should we support these? // expect.not.anything() @@ -572,6 +595,12 @@ describe('type assertions', () => { // @ts-expect-error expectVoid = expect.soft(chainableArray).not.toBeDisplayed() }) + + it('should have ts (or lint) errors when actual is a chainable not awaited', async () => { + // TODO dprevost: see if an eslint rule could help us here to detect missing await when not using wdio matchers + // expectPromiseVoid = expect.soft(chainableElement.getText()).toEqual('Basketball Shoes') + // expectPromiseVoid = expect.soft(chainableElement.getText()).toMatch(/€\d+/) + }) }) describe('expect.getSoftFailures', () => { @@ -602,4 +631,21 @@ describe('type assertions', () => { }) }) }) + + 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/matchers.test.ts b/test/matchers.test.ts index 6c5ae808a..f0d642eb4 100644 --- a/test/matchers.test.ts +++ b/test/matchers.test.ts @@ -66,3 +66,8 @@ test('allows to add matcher', () => { 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))) +}) From 2d0d0583e72b9b2d250b7b4758f889d30e185f2d Mon Sep 17 00:00:00 2001 From: David Prevost Date: Fri, 27 Jun 2025 00:46:38 -0400 Subject: [PATCH 32/99] ignore not needed anymore! --- src/matchers/element/toHaveAttribute.ts | 3 --- src/matchers/element/toHaveClass.ts | 1 - src/matchers/element/toHaveHref.ts | 1 - src/matchers/element/toHaveId.ts | 1 - 4 files changed, 6 deletions(-) diff --git a/src/matchers/element/toHaveAttribute.ts b/src/matchers/element/toHaveAttribute.ts index d7ceb6b65..615dab685 100644 --- a/src/matchers/element/toHaveAttribute.ts +++ b/src/matchers/element/toHaveAttribute.ts @@ -56,14 +56,12 @@ async function toHaveAttributeFn(received: WdioElementMaybePromise, attribute: s let el = await received?.getElement() const pass = await waitUntil(async () => { - // @ts-ignore TODO dprevost fix me const result = await executeCommand.call(this, el, conditionAttr, {}, [attribute]) el = result.el as WebdriverIO.Element return result.success }, isNot, {}) - // @ts-ignore TODO dprevost fix me const message = enhanceError(el, !isNot, pass, this, verb, expectation, attribute, {}) return { @@ -86,7 +84,6 @@ export async function toHaveAttribute( const result = typeof value !== 'undefined' // Name and value is passed in e.g. el.toHaveAttribute('attr', 'value', (opts)) - // @ts-ignore TODO dprevost fix me ? await toHaveAttributeAndValue.call(this, received, attribute, value, options) // Only name is passed in e.g. el.toHaveAttribute('attr') : await toHaveAttributeFn.call(this, received, attribute) diff --git a/src/matchers/element/toHaveClass.ts b/src/matchers/element/toHaveClass.ts index 25c26f718..864d4ad04 100644 --- a/src/matchers/element/toHaveClass.ts +++ b/src/matchers/element/toHaveClass.ts @@ -84,7 +84,6 @@ export async function toHaveElementClass( * @deprecated */ export function toHaveClassContaining(el: WebdriverIO.Element, className: string | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS) { - // @ts-ignore TODO dprevost fix me return toHaveAttributeAndValue.call(this, el, 'class', className, { ...options, containing: true diff --git a/src/matchers/element/toHaveHref.ts b/src/matchers/element/toHaveHref.ts index 2aa67b0f8..05920c262 100644 --- a/src/matchers/element/toHaveHref.ts +++ b/src/matchers/element/toHaveHref.ts @@ -13,7 +13,6 @@ export async function toHaveHref( options, }) - // @ts-ignore TODO dprevost fix me const result = await toHaveAttributeAndValue.call(this, el, 'href', expectedValue, options) await options.afterAssertion?.({ diff --git a/src/matchers/element/toHaveId.ts b/src/matchers/element/toHaveId.ts index 0d72b8b16..6bc3d4ae2 100644 --- a/src/matchers/element/toHaveId.ts +++ b/src/matchers/element/toHaveId.ts @@ -13,7 +13,6 @@ export async function toHaveId( options, }) - // @ts-ignore TODO dprevost fix me const result: ExpectWebdriverIO.AssertionResult = await toHaveAttributeAndValue.call(this, el, 'id', expectedValue, options) await options.afterAssertion?.({ From e68e916a4f7d8a4b625fb7f251c9f8997e619401 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Fri, 27 Jun 2025 01:32:29 -0400 Subject: [PATCH 33/99] Fix some missing options pararmter + dig matchers and still not sure! --- test-types/mocha/types-mocha.test.ts | 6 ++++++ types/expect-webdriverio.d.ts | 16 +++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index 710815220..e490c5248 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -130,6 +130,12 @@ describe('type assertions', () => { 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') diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 1ed21ae3e..96992a441 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -276,13 +276,19 @@ interface WdioCustomMatchers { * await expect(elem).toHaveText(['Coffee', 'Tea', 'Milk']) * ``` */ - toHaveText: FnWhenElementOrArrayLike | Array>) => Promise> + toHaveText: FnWhenElementOrArrayLike | Array>, + options?: ExpectWebdriverIO.StringOptions + ) => Promise> /** * `WebdriverIO.Element` -> `getHTML` * Element's html equals the html provided */ - toHaveHTML: FnWhenElementOrArrayLike | Array) => Promise> + toHaveHTML: FnWhenElementOrArrayLike | Array, + options?: ExpectWebdriverIO.StringOptions + ) => Promise> /** * `WebdriverIO.Element` -> `getComputedLabel` @@ -446,12 +452,12 @@ declare namespace ExpectWebdriverIO { message(): string } - // TODO dprevost - to review + // TODO dprevost: what is this, I'm unable to find it in the codebase, was a function before, seems to override something from Jasmine in the past? // const matchers: Map< // string, // ( - // actual: any, - // ...expected: any[] + // actual: unknown, + // ...expected: unknown[] // ) => Promise // > From 3fbf20c5f21cbcf899ddd29efaadfcabc43b85de Mon Sep 17 00:00:00 2001 From: David Prevost Date: Fri, 27 Jun 2025 10:07:52 -0400 Subject: [PATCH 34/99] Moving types into expect-webdriverio.d.ts and remove standalone.d.ts --- jest.d.ts | 4 +- src/index.ts | 2 +- test-types/jest/types-jest.test.ts | 10 +- test-types/mocha/tsconfig.json | 1 - test-types/mocha/types-mocha.test.ts | 12 +- types/expect-webdriverio.d.ts | 199 ++++++++++++++++++--------- types/standalone-global.d.ts | 2 +- types/standalone.d.ts | 73 ---------- 8 files changed, 150 insertions(+), 153 deletions(-) delete mode 100644 types/standalone.d.ts diff --git a/jest.d.ts b/jest.d.ts index 582219489..e77217a9e 100644 --- a/jest.d.ts +++ b/jest.d.ts @@ -4,7 +4,7 @@ declare namespace jest { - interface Matchers extends WdioMatchers{ + interface Matchers extends WdioCustomMatchers{ /** * Below are overloaded Jest's matchers not part of `expect` but of `jest-snapshot`. @@ -28,7 +28,7 @@ declare namespace jest { } type MatcherAndInverse = Matchers & AndNot> - interface Expect extends WdioMatchers { + interface Expect extends WdioCustomMatchers { /** * Below are the custom Expect of WebdriverIO. diff --git a/src/index.ts b/src/index.ts index 62c7e9419..8da64d398 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -/// +/// import { expect as expectLib } from 'expect' import type { RawMatcherFn } from './types.js' import * as wdioMatchers from './matchers.js' diff --git a/test-types/jest/types-jest.test.ts b/test-types/jest/types-jest.test.ts index 476147b6c..380dfd648 100644 --- a/test-types/jest/types-jest.test.ts +++ b/test-types/jest/types-jest.test.ts @@ -401,7 +401,7 @@ describe('type assertions', async () => { describe('expect.soft', () => { it('should not need to be awaited/be a promise if actual is non-promise type', async () => { - const expectWdioMatcher1: WdioMatchers = expect.soft(actualString) + 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')) @@ -429,13 +429,13 @@ describe('type assertions', async () => { }) it('should support chainable element', async () => { - const expectElement: WdioMatchers = expect.soft(element) - const expectElementChainable: WdioMatchers = expect.soft(chainableElement) + const expectElement: WdioCustomMatchers = expect.soft(element) + const expectElementChainable: WdioCustomMatchers = expect.soft(chainableElement) // @ts-expect-error - const expectElement2: WdioMatchers, WebdriverIO.Element> = expect.soft(element) + const expectElement2: WdioCustomMatchers, WebdriverIO.Element> = expect.soft(element) // @ts-expect-error - const expectElementChainable2: WdioMatchers, typeof chainableElement> = expect.soft(chainableElement) + const expectElementChainable2: WdioCustomMatchers, typeof chainableElement> = expect.soft(chainableElement) }) it('should support chainable element with wdio Matchers', async () => { diff --git a/test-types/mocha/tsconfig.json b/test-types/mocha/tsconfig.json index 1890b3d8e..b1ff7414b 100644 --- a/test-types/mocha/tsconfig.json +++ b/test-types/mocha/tsconfig.json @@ -11,7 +11,6 @@ "webdriverio", "../../types/standalone-global.d.ts", "./customMatchers/customMatchers.d.ts" - // "@wdio/types", // "@wdio/globals/types", // TODO to review adding the global wdio types interfere with local types ] } diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index e490c5248..038abc4a7 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -535,7 +535,7 @@ describe('type assertions', () => { describe('expect.soft', () => { it('should not need to be awaited/be a promise if actual is non-promise type', async () => { - const expectWdioMatcher1: WdioMatchers = expect.soft(actualString) + 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')) @@ -549,7 +549,7 @@ describe('type assertions', () => { }) it('should need to be awaited/be a promise if actual is promise type', async () => { - const expectWdioMatcher1: ExpectWebdriverIO.MatchersAndInverse, Promise> = expect.soft(actualPromiseString) + const expectWdioMatcher1: WdioMatchersAndInverse, 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')) @@ -563,13 +563,13 @@ describe('type assertions', () => { }) it('should support chainable element', async () => { - const expectElement: WdioMatchers = expect.soft(element) - const expectElementChainable: WdioMatchers = expect.soft(chainableElement) + const expectElement: WdioCustomMatchers = expect.soft(element) + const expectElementChainable: WdioCustomMatchers = expect.soft(chainableElement) // @ts-expect-error - const expectElement2: WdioMatchers, WebdriverIO.Element> = expect.soft(element) + const expectElement2: WdioCustomMatchers, WebdriverIO.Element> = expect.soft(element) // @ts-expect-error - const expectElementChainable2: WdioMatchers, typeof chainableElement> = expect.soft(chainableElement) + const expectElementChainable2: WdioCustomMatchers, typeof chainableElement> = expect.soft(chainableElement) }) it('should support chainable element with wdio Matchers', async () => { diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 96992a441..f9da48e69 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -13,6 +13,17 @@ type ChainablePromiseArray = import('webdriverio').ChainablePromiseArray type ExpectLibAsymmetricMatchers = import('expect').AsymmetricMatchers type ExpectLibAsymmetricMatcher = import('expect').AsymmetricMatcher +type ExpectLibBaseExpect = import('expect').BaseExpect +type ExpectLibMatchers = import('expect').Matchers +type ExpectLibExpect = import('expect').Expect + +// To remove when exportable from 'expect'. See https://github.com/jestjs/jest/pull/15704 (already merged) +type Inverse = { + /** + * Inverse next matcher. If you know how to test something, `.not` lets you test its opposite. + */ + not: M; +} /** * Real Promise and wdio chainable promise types. @@ -45,83 +56,96 @@ type UnwrapPromise = T extends Promise ? U : T * 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 user 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 /** - * Helper so the return type is a Promise only when needed to avoid using `await` when not a promise. + * Type helpers allowing to use the function when the expect(actual: T) is of the expected type T. */ -type Promise = T extends WdioPromiseLike ? Promise : R -// type WdioPromiseVoidLike = T extends (WdioPromiseLike | WdioOnlyPromiseLike) ? Promise : never -// type WdioVoidLike = WdioPromiseVoidLike extends never ? void : never +type FnWhenBrowser = ActualT extends WebdriverIO.Browser ? Fn : never +type FnWhenMock = ActualT extends MockPromise ? Fn : never +type FnWhenElementOrArrayLike = ActualT extends ElementOrArrayLike ? Fn : never +type FnWhenElementArrayLike = ActualT extends ElementArrayLike ? Fn : never /** - * Helpers function allowing to use the function when the expect(actual: T) is of the expected type T. + * 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. */ -type FnWhenBrowser = T extends WebdriverIO.Browser ? Fn : never -type FnWhenMock = T extends MockPromise ? Fn : never -type FnWhenElementOrArrayLike = T extends ElementOrArrayLike ? Fn : never -type FnWhenElementArrayLike = T extends ElementArrayLike ? Fn : never -interface WdioBrowserMatchers{ +interface WdioBrowserMatchers{ /** * `WebdriverIO.Browser` -> `getUrl` */ - toHaveUrl: FnWhenBrowser, options?: ExpectWebdriverIO.StringOptions) => Promise> + toHaveUrl: FnWhenBrowser, options?: ExpectWebdriverIO.StringOptions) => Promise> /** * `WebdriverIO.Browser` -> `getTitle` */ - toHaveTitle: FnWhenBrowser, options?: ExpectWebdriverIO.StringOptions) => Promise> + toHaveTitle: FnWhenBrowser, options?: ExpectWebdriverIO.StringOptions) => Promise> /** * `WebdriverIO.Browser` -> `execute` */ - toHaveClipboardText: FnWhenBrowser, options?: ExpectWebdriverIO.StringOptions) => Promise> + toHaveClipboardText: FnWhenBrowser, options?: ExpectWebdriverIO.StringOptions) => Promise> } -interface WdioMockMatchers { +/** + * 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 { /** * Check that `WebdriverIO.Mock` was called */ - toBeRequested: FnWhenMock Promise> + toBeRequested: FnWhenMock Promise> /** * Check that `WebdriverIO.Mock` was called N times */ - toBeRequestedTimes: FnWhenMock Promise> + toBeRequestedTimes: FnWhenMock Promise> /** * Check that `WebdriverIO.Mock` was called with the specific parameters */ - toBeRequestedWith: FnWhenMock Promise> + toBeRequestedWith: FnWhenMock Promise> } -interface WdioCustomMatchers { + +/** + * 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 { // ===== $ or $$ ===== /** * `WebdriverIO.Element` -> `isDisplayed` */ - toBeDisplayed: FnWhenElementOrArrayLike Promise> + toBeDisplayed: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isExisting` */ - toExist: FnWhenElementOrArrayLike Promise> + toExist: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isExisting` */ - toBePresent: FnWhenElementOrArrayLike Promise> + toBePresent: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isExisting` */ - toBeExisting: FnWhenElementOrArrayLike Promise> + toBeExisting: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `getAttribute` */ - toHaveAttribute: FnWhenElementOrArrayLike, options?: ExpectWebdriverIO.StringOptions) => Promise> @@ -129,7 +153,7 @@ interface WdioCustomMatchers { /** * `WebdriverIO.Element` -> `getAttribute` */ - toHaveAttr: FnWhenElementOrArrayLike, options?: ExpectWebdriverIO.StringOptions ) => Promise> @@ -138,7 +162,7 @@ interface WdioCustomMatchers { * `WebdriverIO.Element` -> `getAttribute` class * @deprecated since v1.3.1 - use `toHaveElementClass` instead. */ - toHaveClass: FnWhenElementOrArrayLike, options?: ExpectWebdriverIO.StringOptions ) => Promise> @@ -159,7 +183,7 @@ interface WdioCustomMatchers { * await expect(element).toHaveElementClass(['btn', 'btn-large']); * ``` */ - toHaveElementClass: FnWhenElementOrArrayLike | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions ) => Promise> @@ -167,7 +191,7 @@ interface WdioCustomMatchers { /** * `WebdriverIO.Element` -> `getProperty` */ - toHaveElementProperty: FnWhenElementOrArrayLike, value?: unknown, options?: ExpectWebdriverIO.StringOptions @@ -176,7 +200,7 @@ interface WdioCustomMatchers { /** * `WebdriverIO.Element` -> `getProperty` value */ - toHaveValue: FnWhenElementOrArrayLike, options?: ExpectWebdriverIO.StringOptions ) => Promise> @@ -184,43 +208,43 @@ interface WdioCustomMatchers { /** * `WebdriverIO.Element` -> `isClickable` */ - toBeClickable: FnWhenElementOrArrayLike Promise> + toBeClickable: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `!isEnabled` */ - toBeDisabled: FnWhenElementOrArrayLike Promise> + toBeDisabled: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isDisplayedInViewport` */ - toBeDisplayedInViewport: FnWhenElementOrArrayLike Promise> + toBeDisplayedInViewport: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isEnabled` */ - toBeEnabled: FnWhenElementOrArrayLike Promise> + toBeEnabled: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isFocused` */ - toBeFocused: FnWhenElementOrArrayLike Promise> + toBeFocused: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isSelected` */ - toBeSelected: FnWhenElementOrArrayLike Promise> + toBeSelected: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isSelected` */ - toBeChecked: FnWhenElementOrArrayLike Promise> + toBeChecked: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `$$('./*').length` * supports less / greater then or equals to be passed in options */ - toHaveChildren: FnWhenElementOrArrayLike Promise> @@ -228,7 +252,7 @@ interface WdioCustomMatchers { /** * `WebdriverIO.Element` -> `getAttribute` href */ - toHaveHref: FnWhenElementOrArrayLike, options?: ExpectWebdriverIO.StringOptions ) => Promise> @@ -236,7 +260,7 @@ interface WdioCustomMatchers { /** * `WebdriverIO.Element` -> `getAttribute` href */ - toHaveLink: FnWhenElementOrArrayLike, options?: ExpectWebdriverIO.StringOptions ) => Promise> @@ -244,7 +268,7 @@ interface WdioCustomMatchers { /** * `WebdriverIO.Element` -> `getProperty` value */ - toHaveId: FnWhenElementOrArrayLike, options?: ExpectWebdriverIO.StringOptions ) => Promise> @@ -252,7 +276,7 @@ interface WdioCustomMatchers { /** * `WebdriverIO.Element` -> `getSize` value */ - toHaveSize: FnWhenElementOrArrayLike Promise> @@ -276,7 +300,7 @@ interface WdioCustomMatchers { * await expect(elem).toHaveText(['Coffee', 'Tea', 'Milk']) * ``` */ - toHaveText: FnWhenElementOrArrayLike | Array>, options?: ExpectWebdriverIO.StringOptions ) => Promise> @@ -285,7 +309,7 @@ interface WdioCustomMatchers { * `WebdriverIO.Element` -> `getHTML` * Element's html equals the html provided */ - toHaveHTML: FnWhenElementOrArrayLike | Array, options?: ExpectWebdriverIO.StringOptions ) => Promise> @@ -294,8 +318,8 @@ interface WdioCustomMatchers { * `WebdriverIO.Element` -> `getComputedLabel` * Element's computed label equals the computed label provided */ - toHaveComputedLabel: FnWhenElementOrArrayLike| Array, + toHaveComputedLabel: FnWhenElementOrArrayLike| Array, options?: ExpectWebdriverIO.StringOptions ) => Promise> @@ -303,8 +327,8 @@ interface WdioCustomMatchers { * `WebdriverIO.Element` -> `getComputedRole` * Element's computed role equals the computed role provided */ - toHaveComputedRole: FnWhenElementOrArrayLike| Array, + toHaveComputedRole: FnWhenElementOrArrayLike| Array, options?: ExpectWebdriverIO.StringOptions ) => Promise> @@ -312,69 +336,87 @@ interface WdioCustomMatchers { * `WebdriverIO.Element` -> `getSize('width')` * Element's width equals the width provided */ - toHaveWidth: FnWhenElementOrArrayLike Promise> + toHaveWidth: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `getSize('height')` * Element's height equals the height provided */ - toHaveHeight: FnWhenElementOrArrayLike Promise> + toHaveHeight: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `getSize()` * Element's size equals the size provided */ - toHaveHeight: FnWhenElementOrArrayLike Promise> + toHaveHeight: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `getAttribute("style")` */ - toHaveStyle: FnWhenElementOrArrayLike Promise> + 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 { // ===== $$ 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. + * Warning: these matchers overload the similar matchers from jest-expect library. + * When the actual is a WebdriverIO.Element, we need to await the `outerHTML` therefore the return type is a Promise. + * TODO dprevost: Review for better typings... + * * Those need to be also duplicated in jest.d.ts in order for the typing to correctly overload the matchers (we cannot just extend the Matchers interface) */ -interface WdioOverloadedMatchers { +interface WdioJestOverloadedMatchers { /** * snapshot matcher * @param label optional snapshot label */ - toMatchSnapshot(label?: string): Promise + toMatchSnapshot(label?: string): ActualT extends WdioPromiseLike ? Promise : R; /** * inline snapshot matcher * @param snapshot snapshot string (autogenerated if not specified) * @param label optional snapshot label */ - toMatchInlineSnapshot(snapshot?: string, label?: string): Promise + toMatchInlineSnapshot(snapshot?: string, label?: string): ActualT extends WdioPromiseLike ? Promise : R; } -interface WdioMatchers extends WdioOverloadedMatchers, WdioBrowserMatchers, WdioCustomMatchers, WdioMockMatchers {} +/** + * 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 = WdioCustomMatchers & ExpectLibMatchers + +type WdioMatchersAndInverse = WdioMatchers & Inverse> /** - * expect function declaration, containing 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 + * Expects specific to WebdriverIO, excluding the generic expect matchers. */ -// TODO dprevost should we extends Expect from expect lib or just AsyncMatchers? -// TODO dprevost ExpectLibAsymmetricMatchers add arrayOf and closeTo previously not there! and not was there previously but is no more? -interface WdioCustomExpect extends ExpectLibAsymmetricMatchers { +interface WdioCustomExpect extends ExpectLibBaseExpect { /** * 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): T extends PromiseLike ? Matchers, T> : Matchers + soft(actual: T): T extends PromiseLike ? ExpectWebdriverIO.MatchersAndInverse, T> : ExpectWebdriverIO.MatchersAndInverse; /** * Get all current soft assertion failures @@ -392,13 +434,18 @@ interface WdioCustomExpect extends ExpectLibAsymmetricMatchers { clearSoftFailures(testId?: string): void } +/** + * Expects supported by the expect-webdriverio library, including the generic expect matchers. + */ +type WdioExpect = ExpectLibExpect & WdioCustomExpect + /** * 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 & { +type WdioAsymmetricMatcher = ExpectWebdriverIO.PartialMatcher & { // Overwrite protected properties of expect.AsymmetricMatcher to access them - sample: T; + sample: R; } declare namespace ExpectWebdriverIO { @@ -406,6 +453,30 @@ declare namespace ExpectWebdriverIO { // eslint-disable-next-line @typescript-eslint/no-explicit-any function getConfig(): any + /** + * Supported Matchers for expect-webdriverio. + * The Type T (ActualT) needs to keep it's name to overload the Matchers from the expect library. + */ + interface Matchers extends WdioMatchers {} + + type MatchersAndInverse = ExpectWebdriverIO.Matchers & Inverse> + + interface Expect extends 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; + } + interface SnapshotServiceArgs { updateState?: SnapshotUpdateState resolveSnapshotPath?: (path: string, extension: string) => string diff --git a/types/standalone-global.d.ts b/types/standalone-global.d.ts index af271a75b..92223b418 100644 --- a/types/standalone-global.d.ts +++ b/types/standalone-global.d.ts @@ -1,4 +1,4 @@ -/// +/// // We override the existing one, probably coming from `types/jest` // @ts-expect-error diff --git a/types/standalone.d.ts b/types/standalone.d.ts deleted file mode 100644 index 468ac7037..000000000 --- a/types/standalone.d.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* eslint-disable @typescript-eslint/consistent-type-imports*/ -/// -/// - -type ExpectBaseExpect = import('expect').BaseExpect -type ExpectMatchers = import('expect').Matchers -type ExpectLibExpect = import('expect').Expect - -// To remove when exportable from 'expect'. See https://github.com/jestjs/jest/pull/15704 (already merged) -type Inverse = { - /** - * Inverse next matcher. If you know how to test something, `.not` lets you test its opposite. - */ - not: M; -} - -declare namespace ExpectWebdriverIO { - - interface Matchers extends WdioMatchers, ExpectMatchers { - /** - * snapshot matcher - * @param label optional snapshot label - */ - toMatchSnapshot(label?: string): T extends WdioPromiseLike ? Promise : R; - - /** - * 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 : R; - } - - type MatchersAndInverse = ExpectWebdriverIO.Matchers & Inverse> - - /** - * Mostly derived from the types of `jest-expect` but adapted to work with WebdriverIO. - * @see https://github.com/jestjs/jest/blob/main/packages/jest-expect/src/types.ts - */ - interface Expect extends ExpectLibExpect { - /** - * The `expect` function is used every time you want to test a value. - * You will rarely call `expect` by itself. - * - * @param actual The value to apply matchers against. - */ - (actual: T): MatchersAndInverse; - - /** - * 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): T extends PromiseLike ? MatchersAndInverse, T> : MatchersAndInverse - - /** - * 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 - } - - interface InverseAsymmetricMatchers extends ExpectWebdriverIO.Expect {} -} From 2b138b017ba1e90f39aaa63b5f514eb661bd3cb6 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Fri, 27 Jun 2025 18:39:57 -0400 Subject: [PATCH 35/99] Move new mocha test to jest, review + simplify jest.d.ts --- jest.d.ts | 26 +--- test-types/jest/types-jest.test.ts | 210 +++++++++++++++++++++++---- test-types/mocha/types-mocha.test.ts | 6 +- types/expect-webdriverio.d.ts | 2 + types/standalone-global.d.ts | 5 + 5 files changed, 196 insertions(+), 53 deletions(-) diff --git a/jest.d.ts b/jest.d.ts index e77217a9e..07a94994d 100644 --- a/jest.d.ts +++ b/jest.d.ts @@ -1,10 +1,8 @@ /// -// type WdioElementLike = WebdriverIO.Element | ChainablePromiseElement - declare namespace jest { - interface Matchers extends WdioCustomMatchers{ + interface Matchers extends WdioMatchers, WdioJestOverloadedMatchers { /** * Below are overloaded Jest's matchers not part of `expect` but of `jest-snapshot`. @@ -28,12 +26,7 @@ declare namespace jest { } type MatcherAndInverse = Matchers & AndNot> - interface Expect extends WdioCustomMatchers { - - /** - * Below are the custom Expect of WebdriverIO. - * We need to define them below so that they are correctly typed. We cannot just extend WdioCustomExpect - */ + interface Expect extends ExpectWebdriverIO.Expect { /** * Creates a soft assertion wrapper around standard expect @@ -41,21 +34,6 @@ declare namespace jest { * All failures are collected and reported at the end of the test */ soft(actual: T): T extends PromiseLike ? MatcherAndInverse, T> : MatcherAndInverse - - /** - * 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 } interface InverseAsymmetricMatchers extends Expect {} diff --git a/test-types/jest/types-jest.test.ts b/test-types/jest/types-jest.test.ts index 380dfd648..e8054c0cf 100644 --- a/test-types/jest/types-jest.test.ts +++ b/test-types/jest/types-jest.test.ts @@ -3,9 +3,9 @@ describe('type assertions', async () => { const chainableElement: ChainablePromiseElement = $('findMe') const chainableArray: ChainablePromiseArray = $$('ul>li') + const element: WebdriverIO.Element = await chainableElement?.getElement() - // TODO dprevost: Need more test with this type? - // const ElementArray: WebdriverIO.ElementArray = await chainableArray?.getElements() + const elementArray: WebdriverIO.ElementArray = await chainableArray?.getElements() // Type assertions let expectPromiseVoid: Promise @@ -21,9 +21,9 @@ describe('type assertions', async () => { // 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()) - // TODO add more asymmetric matchers // @ts-expect-error expectVoid = expect(browser).toHaveUrl('https://example.com') @@ -31,6 +31,11 @@ describe('type assertions', async () => { 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) + //// @ts-expect-error TODO dprevost can we make the below fail? + // await expect(browser).toHaveUrl(expect.objectContaining({})) }) it('should have ts errors when actual is not a Browser element', async () => { @@ -44,6 +49,36 @@ describe('type assertions', async () => { 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', () => { @@ -54,6 +89,10 @@ describe('type assertions', async () => { 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() @@ -88,6 +127,12 @@ describe('type assertions', async () => { 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') @@ -96,6 +141,48 @@ describe('type assertions', async () => { // @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') }) @@ -167,6 +254,21 @@ describe('type assertions', async () => { //@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 () => { @@ -215,6 +317,8 @@ describe('type assertions', async () => { expectVoid = expect(chainableElement).toBeCustomPromise() // @ts-expect-error expectVoid = expect(chainableElement).toBeCustomPromise(expect.stringContaining('test')) + // @ts-expect-error + expect(chainableElement).toBeCustomPromise(expect.stringContaining(6)) }) }) @@ -230,7 +334,7 @@ describe('type assertions', async () => { expectPromiseVoid = expect(true).not.toBe(true) }) - it('should not expect Promise when actual is a chainable since toBe is not supported', async () => { + 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) @@ -306,26 +410,46 @@ describe('type assertions', async () => { it('should not have ts errors when typing to Promise', async () => { expectPromiseVoid = expect(promiseNetworkMock).toBeRequested() - expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) - expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedTimes(2) + expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequested() - expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) - expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequestedTimes(2) + expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith({ - url: 'http://localhost:8080/api/todo', // [optional] string | function | custom matcher - method: 'POST', // [optional] string | array - statusCode: 200, // [optional] number | array - requestHeaders: { Authorization: 'foo' }, // [optional] object | function | custom matcher - responseHeaders: { Authorization: 'bar' }, // [optional] object | function | custom matcher - postData: { title: 'foo', description: 'bar' }, // [optional] object | function | custom matcher - response: { success: true }, // [optional] object | function | custom matcher + url: 'http://localhost:8080/api/todo', + method: 'POST', + statusCode: 200, + requestHeaders: { Authorization: 'foo' }, + responseHeaders: { Authorization: 'bar' }, + postData: { title: 'foo', description: 'bar' }, + response: { success: true }, }) - expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith(expect.objectContaining({ - response: { success: true }, // [optional] object | function | custom matcher - })) + // TODO dprevost: Asymmetric matcher is not defined on the entire object in the .d.ts file, it is a bug? + // expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith(expect.objectContaining({ + // response: { success: true }, // [optional] object | function | custom matcher + // })) + + 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 () => { @@ -345,18 +469,18 @@ describe('type assertions', async () => { // @ts-expect-error expectVoid = expect(promiseNetworkMock).toBeRequestedWith({ - url: 'http://localhost:8080/api/todo', // [optional] string | function | custom matcher - method: 'POST', // [optional] string | array - statusCode: 200, // [optional] number | array - requestHeaders: { Authorization: 'foo' }, // [optional] object | function | custom matcher - responseHeaders: { Authorization: 'bar' }, // [optional] object | function | custom matcher - postData: { title: 'foo', description: 'bar' }, // [optional] object | function | custom matcher - response: { success: true }, // [optional] object | function | custom matcher + url: 'http://localhost:8080/api/todo', + 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 }, // [optional] object | function | custom matcher + response: { success: true }, })) }) }) @@ -369,8 +493,14 @@ describe('type assertions', async () => { 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) @@ -382,9 +512,12 @@ describe('type assertions', async () => { 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) @@ -467,6 +600,12 @@ describe('type assertions', async () => { // @ts-expect-error expectVoid = expect.soft(chainableArray).not.toBeDisplayed() }) + + it('should have ts (or lint) errors when actual is a chainable not awaited', async () => { + // TODO dprevost: see if an eslint rule could help us here to detect missing await when not using wdio matchers + // expectPromiseVoid = expect.soft(chainableElement.getText()).toEqual('Basketball Shoes') + // expectPromiseVoid = expect.soft(chainableElement.getText()).toMatch(/€\d+/) + }) }) describe('expect.getSoftFailures', () => { @@ -498,6 +637,23 @@ describe('type assertions', async () => { }) }) + 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' diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index 038abc4a7..341957eab 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -4,10 +4,12 @@ import type { ChainablePromiseElement, ChainablePromiseArray } from 'webdriverio' describe('type assertions', () => { - const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element - const elementArray: WebdriverIO.ElementArray = [] as unknown as WebdriverIO.ElementArray 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 diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index f9da48e69..aff102319 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -17,6 +17,8 @@ type ExpectLibBaseExpect = import('expect').BaseExpect type ExpectLibMatchers = import('expect').Matchers type ExpectLibExpect = import('expect').Expect +// TODO dprevost: a suggestion would be to move any code outside of the namespace to separate types.ts file, so that we can import the types. + // To remove when exportable from 'expect'. See https://github.com/jestjs/jest/pull/15704 (already merged) type Inverse = { /** diff --git a/types/standalone-global.d.ts b/types/standalone-global.d.ts index 92223b418..929f502f9 100644 --- a/types/standalone-global.d.ts +++ b/types/standalone-global.d.ts @@ -1,5 +1,10 @@ /// +/** + * Global declaration file for WebdriverIO's Expect library when not pair with another expect library like Jest. or Jasmine. + * One example is mocha without the chai expect library. + */ + // We override the existing one, probably coming from `types/jest` // @ts-expect-error declare const expect: ExpectWebdriverIO.Expect From e28a899f9f5871fde0b9f943cebe431b32da7ba9 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Fri, 27 Jun 2025 19:01:01 -0400 Subject: [PATCH 36/99] Simplify further jest.d.ts --- jest.d.ts | 20 ++++++-------------- test-types/jest/tsconfig.json | 2 +- test-types/jest/types-jest.test.ts | 26 ++++++++++++++++++-------- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/jest.d.ts b/jest.d.ts index 07a94994d..9555bde36 100644 --- a/jest.d.ts +++ b/jest.d.ts @@ -2,15 +2,16 @@ declare namespace jest { - interface Matchers extends WdioMatchers, WdioJestOverloadedMatchers { + interface Matchers extends ExpectWebdriverIO.Matchers { /** * Below are overloaded Jest's matchers not part of `expect` but of `jest-snapshot`. - * We need to define them below so that they are correctly typed overloaded * @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. */ - // TODO dprevost: how can we make both Wdio snapshot and Jest snapshot work together? /** * snapshot matcher * @param label optional snapshot label @@ -25,16 +26,7 @@ declare namespace jest { toMatchInlineSnapshot(snapshot?: string, label?: string): T extends WdioPromiseLike ? Promise : R; } - type MatcherAndInverse = Matchers & AndNot> - interface Expect extends ExpectWebdriverIO.Expect { + interface Expect extends ExpectWebdriverIO.Expect {} - /** - * 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): T extends PromiseLike ? MatcherAndInverse, T> : MatcherAndInverse - } - - interface InverseAsymmetricMatchers extends Expect {} + interface InverseAsymmetricMatchers extends ExpectWebdriverIO.Expect {} } \ No newline at end of file diff --git a/test-types/jest/tsconfig.json b/test-types/jest/tsconfig.json index 756d10eb4..bf4637544 100644 --- a/test-types/jest/tsconfig.json +++ b/test-types/jest/tsconfig.json @@ -8,7 +8,7 @@ "types": [ "@types/jest", "../../jest.d.ts", // Needed to be after @types/jest to override the toMatchSnapshot typing - "@wdio/globals/types", + // "@wdio/globals/types", // This types interfere with the local types see how to workaround this "./customMatchers/customMatchers.d.ts", ], } diff --git a/test-types/jest/types-jest.test.ts b/test-types/jest/types-jest.test.ts index e8054c0cf..8ee8d0a5b 100644 --- a/test-types/jest/types-jest.test.ts +++ b/test-types/jest/types-jest.test.ts @@ -1,11 +1,20 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ describe('type assertions', async () => { - const chainableElement: ChainablePromiseElement = $('findMe') - const chainableArray: ChainablePromiseArray = $$('ul>li') + // TODO dprevost: using @wdio/globals/types overlap with the local types/expect-webdriverio.d.ts, find how to work with this + // const chainableElement: ChainablePromiseElement = $('findMe') + // const chainableArray: ChainablePromiseArray = $$('ul>li') - const element: WebdriverIO.Element = await chainableElement?.getElement() - const elementArray: WebdriverIO.ElementArray = await chainableArray?.getElements() + // const element: WebdriverIO.Element = await chainableElement?.getElement() + // const elementArray: WebdriverIO.ElementArray = await chainableArray?.getElements() + + 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 @@ -406,7 +415,8 @@ describe('type assertions', async () => { }) describe('Network Matchers', () => { - const promiseNetworkMock = browser.mock('**/api/todo*') + // const promiseNetworkMock = browser.mock('**/api/todo*') + const promiseNetworkMock = Promise.resolve(networkMock) it('should not have ts errors when typing to Promise', async () => { expectPromiseVoid = expect(promiseNetworkMock).toBeRequested() @@ -529,8 +539,8 @@ describe('type assertions', async () => { }) describe('Soft Assertions', async () => { - const actualString: string = await $('h1').getText() - const actualPromiseString: Promise = $('h1').getText() + 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 () => { @@ -548,7 +558,7 @@ describe('type assertions', async () => { }) it('should need to be awaited/be a promise if actual is promise type', async () => { - const expectWdioMatcher1: jest.MatcherAndInverse, Promise> = expect.soft(actualPromiseString) + 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')) From 20aa50aea0f49e5c06ecb7c360b5d0de5bba7d86 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Fri, 27 Jun 2025 19:19:16 -0400 Subject: [PATCH 37/99] Finalize simplification of Jest namespace --- jest.d.ts | 5 +++++ types/expect-webdriverio.d.ts | 8 +++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/jest.d.ts b/jest.d.ts index 9555bde36..f563b7a22 100644 --- a/jest.d.ts +++ b/jest.d.ts @@ -1,5 +1,10 @@ /// +/** + * 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 { diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index aff102319..5dd6c9264 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -377,11 +377,13 @@ interface WdioElementArrayOnlyMatchers { /** * Matchers supporting basic snapshot tests as well as DOM snapshot testing. - * Warning: these matchers overload the similar matchers from jest-expect library. * When the actual is a WebdriverIO.Element, we need to await the `outerHTML` therefore the return type is a Promise. - * TODO dprevost: Review for better typings... * - * Those need to be also duplicated in jest.d.ts in order for the typing to correctly overload the matchers (we cannot just extend the Matchers interface) + * ⚠️ 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 + * + * TODO dprevost: Review for better typings... */ interface WdioJestOverloadedMatchers { /** From ea039f9888c68f950faa1e54afa542aa12f2194e Mon Sep 17 00:00:00 2001 From: David Prevost Date: Fri, 27 Jun 2025 19:40:26 -0400 Subject: [PATCH 38/99] Test `ExpectWebdriverIO` for custom matcher when using Jest! --- test-types/jest/customMatchers/customMatchers.d.ts | 6 +++--- types/expect-webdriverio.d.ts | 9 +++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/test-types/jest/customMatchers/customMatchers.d.ts b/test-types/jest/customMatchers/customMatchers.d.ts index 6e7e61dcc..fde1d39b6 100644 --- a/test-types/jest/customMatchers/customMatchers.d.ts +++ b/test-types/jest/customMatchers/customMatchers.d.ts @@ -1,6 +1,6 @@ -// TODO dprevost should we review this to have the wdio namespace or maybe the expect namespace? -// Name jest is required to augment the jest.Matchers interface -declare namespace jest { +declare namespace ExpectWebdriverIO { + + // TODO dprevost: ensure we still allow custom asymmetric matchers interface AsymmetricMatchers { toBeCustom(): void; } diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 5dd6c9264..69ff8a29c 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -695,6 +695,15 @@ declare namespace ExpectWebdriverIO { * Some properties are omitted for the type check to work correctly. */ type PartialMatcher = Omit, 'sample' | 'inverse' | '$$typeof'> + + //TODO dprevost: ensure we do not break custom AsymmetricMatchers from expect library + // declare global { + // namespace ExpectWebdriverIO { + // interface AsymmetricMatchers { + // myCustomMatcher(value: string): ExpectWebdriverIO.PartialMatcher; + // } + // } + // } } declare module 'expect-webdriverio' { From b65fe358c52403f2dd95b6ad7187c8935b51c9ef Mon Sep 17 00:00:00 2001 From: David Prevost Date: Fri, 27 Jun 2025 20:01:36 -0400 Subject: [PATCH 39/99] Remove unused code! --- types/expect-webdriverio.d.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 69ff8a29c..44133dc30 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -49,8 +49,6 @@ type WdioOnlyMaybePromiseLike = ElementPromise | ElementArrayPromise | Chainable */ type WdioMaybePromise = PromiseLike | WdioOnlyMaybePromiseLike -type UnwrapPromise = T extends Promise ? U : T - // TODO dprevost - check if custom matchers (https://webdriver.io/docs/custommatchers/) will still work aka webdriverio/expect-webdriverio#1408 /** From 1d0e64bdaed1f5cd8f1aa6224f4e3925e5ac4a09 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Fri, 27 Jun 2025 23:00:19 -0400 Subject: [PATCH 40/99] Have better tsc check on types but filtering out node_module errors --- package.json | 2 +- tsconfig.types.json | 32 ++++++++++++++++++++ types-checks-filter-out-node_modules.js | 40 +++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 tsconfig.types.json create mode 100644 types-checks-filter-out-node_modules.js diff --git a/package.json b/package.json index 600a53992..dd89186a8 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "clean": "run-p clean:*", "clean:build": "rimraf ./lib", "compile": "tsc --build tsconfig.build.json", - "tsc:root-types": "echo 'TODO dprevost to bring back' && exit 0 && 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 .", diff --git a/tsconfig.types.json b/tsconfig.types.json new file mode 100644 index 000000000..0202615ee --- /dev/null +++ b/tsconfig.types.json @@ -0,0 +1,32 @@ +{ + /** + * 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": ["jest.d.ts", + /*"jasmine.d.ts",*/ // TODO dprevost: bring back + "expect-webdriverio.d.ts"], + "exclude": [ + "**/urlpattern-polyfill/**" + ] +} \ 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..1b2a373a4 --- /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('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 From 53db48c35d3a587be45fb4efe8c3fad272af4e48 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Fri, 27 Jun 2025 23:15:27 -0400 Subject: [PATCH 41/99] Brin back tsc on types + exclude file in coverage and review % - To be able to tsc check the lib types, we had to filter out the errors coming from the node_modules with a custom scripts - Adding the script lower the % so we have exclude it from vitest with more files and review the coverage - Fix raise problem in .d.ts - Combine the `toHaveHeight` signature since with the new FN/never pattern we cannot use function overloading! --- jest.d.ts | 2 +- test-types/mocha/types-mocha.test.ts | 38 ++++++++++++++++++ types-checks-filter-out-node_modules.js | 2 +- types/expect-webdriverio.d.ts | 52 +++++++++++++------------ vitest.config.ts | 13 ++++--- 5 files changed, 75 insertions(+), 32 deletions(-) diff --git a/jest.d.ts b/jest.d.ts index f563b7a22..42b5a28ce 100644 --- a/jest.d.ts +++ b/jest.d.ts @@ -7,7 +7,7 @@ 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`. diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index 341957eab..e5f8c959f 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -215,6 +215,44 @@ describe('type assertions', () => { }) }) + 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 () => { diff --git a/types-checks-filter-out-node_modules.js b/types-checks-filter-out-node_modules.js index 1b2a373a4..9b59fcc4e 100644 --- a/types-checks-filter-out-node_modules.js +++ b/types-checks-filter-out-node_modules.js @@ -32,7 +32,7 @@ node.exec('tsc --project tsconfig.types.json --noEmit', (error, stdout, stderr) } if (!found) { - console.log('No type errors outside the exclusion list.') + 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) diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 44133dc30..80e89155f 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -14,7 +14,7 @@ type ChainablePromiseArray = import('webdriverio').ChainablePromiseArray type ExpectLibAsymmetricMatchers = import('expect').AsymmetricMatchers type ExpectLibAsymmetricMatcher = import('expect').AsymmetricMatcher type ExpectLibBaseExpect = import('expect').BaseExpect -type ExpectLibMatchers = import('expect').Matchers +type ExpectLibMatchers, T> = import('expect').Matchers type ExpectLibExpect = import('expect').Expect // TODO dprevost: a suggestion would be to move any code outside of the namespace to separate types.ts file, so that we can import the types. @@ -44,11 +44,6 @@ type WdioOnlyPromiseLike = ElementPromise | ElementArrayPromise | ChainablePromi */ type WdioOnlyMaybePromiseLike = ElementPromise | ElementArrayPromise | ChainablePromiseElement | ChainablePromiseArray | WebdriverIO.Browser | WebdriverIO.Element | WebdriverIO.ElementArray -/** - * Type potentially leading to a promise - */ -type WdioMaybePromise = PromiseLike | WdioOnlyMaybePromiseLike - // TODO dprevost - check if custom matchers (https://webdriver.io/docs/custommatchers/) will still work aka webdriverio/expect-webdriverio#1408 /** @@ -339,16 +334,22 @@ interface WdioElementOrArrayMatchers { toHaveWidth: FnWhenElementOrArrayLike Promise> /** - * `WebdriverIO.Element` -> `getSize('height')` - * Element's height equals the height provided - */ - toHaveHeight: FnWhenElementOrArrayLike Promise> - - /** - * `WebdriverIO.Element` -> `getSize()` - * Element's size equals the size provided + * `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> + toHaveHeight: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `getAttribute("style")` @@ -405,9 +406,9 @@ type WdioCustomMatchers = WdioJestOverloadedMatchers & W /** * All the matchers that WebdriverIO Library supports including the generic matchers from the expect library. */ -type WdioMatchers = WdioCustomMatchers & ExpectLibMatchers +type WdioMatchers, ActualT> = WdioCustomMatchers & ExpectLibMatchers -type WdioMatchersAndInverse = WdioMatchers & Inverse> +type WdioMatchersAndInverse, ActualT> = WdioMatchers & Inverse> /** * Expects specific to WebdriverIO, excluding the generic expect matchers. @@ -423,7 +424,7 @@ interface WdioCustomExpect extends ExpectLibBaseExpect { /** * Get all current soft assertion failures */ - getSoftFailures(testId?: string): SoftFailure[] + getSoftFailures(testId?: string): ExpectWebdriverIO.SoftFailure[] /** * Manually assert all soft failures (throws an error if any failures exist) @@ -459,9 +460,9 @@ declare namespace ExpectWebdriverIO { * Supported Matchers for expect-webdriverio. * The Type T (ActualT) needs to keep it's name to overload the Matchers from the expect library. */ - interface Matchers extends WdioMatchers {} + interface Matchers, T> extends WdioMatchers {} - type MatchersAndInverse = ExpectWebdriverIO.Matchers & Inverse> + type MatchersAndInverse, ActualT> = ExpectWebdriverIO.Matchers & Inverse> interface Expect extends WdioExpect { /** @@ -512,7 +513,8 @@ declare namespace ExpectWebdriverIO { } class SoftAssertionService implements ServiceInstance { - constructor(serviceOptions?: SoftAssertionServiceOptions, capabilities?: unknown, config?: un) + // eslint-disable-next-line @typescript-eslint/no-explicit-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 @@ -675,12 +677,12 @@ declare namespace ExpectWebdriverIO { | string | ExpectWebdriverIO.JsonCompatible | ExpectWebdriverIO.PartialMatcher - | ((r: string | undefined) => boolean) + | ((postData: string | undefined) => boolean) response?: | string - | ExpectWebdriverIO.JsonCompatible - | ExpectWebdriverIO.PartialMatcher - | ((r: string) => boolean) + | ExpectWebdriverIO.JsonCompatible + | ExpectWebdriverIO.PartialMatcher + | ((response: unknown) => boolean) } type jsonPrimitive = string | number | boolean | null 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, } } } From 27968b1bcfe713926d24087d081f7a142d99aad1 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sun, 29 Jun 2025 08:37:15 -0400 Subject: [PATCH 42/99] fix and add UT for custom asymmetric matchers --- test-types/jest/types-jest.test.ts | 38 +++ .../customMatchers-module-expect.d.ts | 22 ++ ...mMatchers-namespace-expectwebdriverio.d.ts | 12 + .../mocha/customMatchers/customMatchers.d.ts | 9 - test-types/mocha/tsconfig.json | 1 - test-types/mocha/types-mocha.test.ts | 219 ++++++++++++++++-- tsconfig.types.json | 9 +- types/expect-webdriverio.d.ts | 16 +- types/standalone-global.d.ts | 2 - 9 files changed, 292 insertions(+), 36 deletions(-) create mode 100644 test-types/mocha/customMatchers/customMatchers-module-expect.d.ts create mode 100644 test-types/mocha/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts delete mode 100644 test-types/mocha/customMatchers/customMatchers.d.ts diff --git a/test-types/jest/types-jest.test.ts b/test-types/jest/types-jest.test.ts index 8ee8d0a5b..780053bca 100644 --- a/test-types/jest/types-jest.test.ts +++ b/test-types/jest/types-jest.test.ts @@ -219,6 +219,44 @@ describe('type assertions', async () => { }) }) + 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 () => { 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..e25671893 --- /dev/null +++ b/test-types/mocha/customMatchers/customMatchers-module-expect.d.ts @@ -0,0 +1,22 @@ +import 'expect' + +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: + // Required 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) + // 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..b8e4fed42 --- /dev/null +++ b/test-types/mocha/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts @@ -0,0 +1,12 @@ +declare namespace ExpectWebdriverIO { + // TODO dprevost: This is not working probably because we need to redefine the asymmetric matchers in the ExpectWebdriverIO namespace so we use that namespace instead of the ExpectLib namespace. + // But will that breaks the `declare module 'expect'` way? + interface AsymmetricMatchers { + toBeCustom(): void; + 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/customMatchers/customMatchers.d.ts b/test-types/mocha/customMatchers/customMatchers.d.ts deleted file mode 100644 index 883539e12..000000000 --- a/test-types/mocha/customMatchers/customMatchers.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -declare namespace ExpectWebdriverIO { - interface AsymmetricMatchers { - toBeCustom(): void; - } - interface Matchers { - toBeCustom(): R; - toBeCustomPromise: T extends ChainablePromiseElement ? (expected?: string | ExpectWebdriverIO.PartialMatcher) => Promise : never; - } -} \ No newline at end of file diff --git a/test-types/mocha/tsconfig.json b/test-types/mocha/tsconfig.json index b1ff7414b..e2f7dcd43 100644 --- a/test-types/mocha/tsconfig.json +++ b/test-types/mocha/tsconfig.json @@ -10,7 +10,6 @@ "expect", "webdriverio", "../../types/standalone-global.d.ts", - "./customMatchers/customMatchers.d.ts" // "@wdio/globals/types", // TODO to review adding the global wdio types interfere with local types ] } diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index e5f8c959f..c2c865b9a 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -340,28 +340,135 @@ describe('type assertions', () => { }) describe('Custom matchers', () => { - it('should supported correctly a non-promise custom matcher', async () => { - expectVoid = expect('test').toBeCustom() - expectVoid = expect('test').not.toBeCustom() + 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() + // @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 () => { + expectVoid = expect.toBeCustom() + // TODO dprevost: to fix + //expectVoid = expect.not.toBeCustom() + + // @ts-expect-error + expectPromiseVoid = expect.toBeCustom() + // @ts-expect-error + expectPromiseVoid = expect.not.toBeCustom() + + }) }) - 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')) + describe('using `expect` module declaration', () => { - // @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 - expect(chainableElement).toBeCustomPromise(expect.stringContaining(6)) + 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') + + // TODO how to make the below fails when the await is missing inf front of the expect from the asymmetric matcher? + // expectPromiseVoid = expect(chainableElement).toHaveCustomProperty( + // expect.toHaveCustomProperty(chainableElement) + // ) + + // @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) + + // TODO how to make the below fails when the await is missing inf front of the expect from the asymmetric matcher? + // expectPromiseVoid = expect(chainableElement).toHaveCustomProperty( + // expect.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) + ) + }) + + // TODO this is not supported in Wdio right now, maybe one day we can support it + // it('should support an async asymmetric matcher on a non async matcher', async () => { + // expectPromiseVoid = expect({ value: 5 }).toEqual({ + // value: expect.toHaveCustomProperty(chainableElement) + // }) + + // // @ts-expect-error + // expectVoid = expect({ value: 5 }).toEqual({ + // value: expect.toHaveCustomProperty(chainableElement) + // }) + + // }) }) }) @@ -647,6 +754,82 @@ describe('type assertions', () => { // expectPromiseVoid = expect.soft(chainableElement.getText()).toEqual('Basketball Shoes') // expectPromiseVoid = expect.soft(chainableElement.getText()).toMatch(/€\d+/) }) + + 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', () => { diff --git a/tsconfig.types.json b/tsconfig.types.json index 0202615ee..f6a0362c5 100644 --- a/tsconfig.types.json +++ b/tsconfig.types.json @@ -23,10 +23,11 @@ "noEmit": true, }, - "include": ["jest.d.ts", - /*"jasmine.d.ts",*/ // TODO dprevost: bring back - "expect-webdriverio.d.ts"], + "include": ["*.d.ts", + "./types/*.d.ts"], "exclude": [ - "**/urlpattern-polyfill/**" + "**/urlpattern-polyfill/**", + "jasmine.d.ts", // TODO dprevost: to remove + "types/jasmine-soft-extend.d.ts", // TODO dprevost: to remove ] } \ No newline at end of file diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 80e89155f..ab938f2b3 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -440,7 +440,13 @@ interface WdioCustomExpect extends ExpectLibBaseExpect { /** * Expects supported by the expect-webdriverio library, including the generic expect matchers. */ -type WdioExpect = ExpectLibExpect & WdioCustomExpect +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. @@ -456,6 +462,7 @@ declare namespace ExpectWebdriverIO { // eslint-disable-next-line @typescript-eslint/no-explicit-any function getConfig(): any + /** expect lib type/interface override to have everything under the ExpectWebDriverIO namespace */ /** * Supported Matchers for expect-webdriverio. * The Type T (ActualT) needs to keep it's name to overload the Matchers from the expect library. @@ -464,7 +471,11 @@ declare namespace ExpectWebdriverIO { type MatchersAndInverse, ActualT> = ExpectWebdriverIO.Matchers & Inverse> - interface Expect extends WdioExpect { + /** + * Overloaded from `expect` library to allow using the `ExpectWebdriverIO` namespace to define custom asymmetric matchers. + */ + type AsymmetricMatchers = WdioAsymmetricMatchers + interface Expect extends AsymmetricMatchers, WdioExpect { /** * The `expect` function is used every time you want to test a value. * You will rarely call `expect` by itself. @@ -696,6 +707,7 @@ declare namespace ExpectWebdriverIO { */ type PartialMatcher = Omit, 'sample' | 'inverse' | '$$typeof'> + // interface AsymmetricMatchers extends WdioAsymmetricMatchers {} //TODO dprevost: ensure we do not break custom AsymmetricMatchers from expect library // declare global { // namespace ExpectWebdriverIO { diff --git a/types/standalone-global.d.ts b/types/standalone-global.d.ts index 929f502f9..aed133da8 100644 --- a/types/standalone-global.d.ts +++ b/types/standalone-global.d.ts @@ -5,8 +5,6 @@ * One example is mocha without the chai expect library. */ -// We override the existing one, probably coming from `types/jest` -// @ts-expect-error declare const expect: ExpectWebdriverIO.Expect declare namespace NodeJS { From d01529b9d5d28174d83fb8ef3b2aad0cafc1e28c Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sun, 29 Jun 2025 08:49:36 -0400 Subject: [PATCH 43/99] Review overloaded functions from `expect` lib --- types/expect-webdriverio.d.ts | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index ab938f2b3..f798ded51 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -462,20 +462,18 @@ declare namespace ExpectWebdriverIO { // eslint-disable-next-line @typescript-eslint/no-explicit-any function getConfig(): any - /** expect lib type/interface override to have everything under the ExpectWebDriverIO namespace */ /** - * Supported Matchers for expect-webdriverio. - * The Type T (ActualT) needs to keep it's name to overload the Matchers from the expect library. + * This block are overloaded types from the expect library. + * They are required to show function 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. */ - interface Matchers, T> extends WdioMatchers {} - - type MatchersAndInverse, ActualT> = ExpectWebdriverIO.Matchers & Inverse> /** - * Overloaded from `expect` library to allow using the `ExpectWebdriverIO` namespace to define custom asymmetric matchers. + * Expect defining the custom wdio expect and also pulling on asymmetric matchers. + * T needs to stay named T to correctly overload the expect function from the expect library. */ - type AsymmetricMatchers = WdioAsymmetricMatchers - interface Expect extends AsymmetricMatchers, WdioExpect { + interface Expect extends ExpectWebdriverIO.AsymmetricMatchers, WdioExpect { /** * The `expect` function is used every time you want to test a value. * You will rarely call `expect` by itself. @@ -491,6 +489,23 @@ declare namespace ExpectWebdriverIO { (actual: T): ExpectWebdriverIO.MatchersAndInverse; } + /** + * Supported Matchers for expect-webdriverio. + * The Type T (ActualT) needs to keep it's name to overload the Matchers from the expect library. + */ + interface Matchers, T> extends WdioMatchers {} + + /** + * Overloaded from `expect` library to allow using the `ExpectWebdriverIO` namespace to define custom asymmetric matchers. + */ + type AsymmetricMatchers = WdioAsymmetricMatchers + + /** + * End of block overloading types from the expect library. + */ + + type MatchersAndInverse, ActualT> = ExpectWebdriverIO.Matchers & Inverse> + interface SnapshotServiceArgs { updateState?: SnapshotUpdateState resolveSnapshotPath?: (path: string, extension: string) => string From 2f8182b47364188fe8860e53118402754fabe075 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sun, 29 Jun 2025 09:31:45 -0400 Subject: [PATCH 44/99] Make inverse matcher works under the ExpectWebDriverIO namespace! Add doc around custom matcher support --- .../customMatchers-module-expect.d.ts | 4 ++ ...mMatchers-namespace-expectwebdriverio.d.ts | 8 +-- test-types/mocha/types-mocha.test.ts | 11 ++-- types/expect-webdriverio.d.ts | 50 ++++++------------- 4 files changed, 32 insertions(+), 41 deletions(-) diff --git a/test-types/mocha/customMatchers/customMatchers-module-expect.d.ts b/test-types/mocha/customMatchers/customMatchers-module-expect.d.ts index e25671893..c4df1e5f8 100644 --- a/test-types/mocha/customMatchers/customMatchers-module-expect.d.ts +++ b/test-types/mocha/customMatchers/customMatchers-module-expect.d.ts @@ -1,5 +1,9 @@ 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 diff --git a/test-types/mocha/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts b/test-types/mocha/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts index b8e4fed42..46efd6715 100644 --- a/test-types/mocha/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts +++ b/test-types/mocha/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts @@ -1,8 +1,10 @@ +/** + * Custom matchers under the `ExpectWebdriverIO` namespace. + * @see {@link https://webdriver.io/docs/custommatchers/#typescript-support} + */ declare namespace ExpectWebdriverIO { - // TODO dprevost: This is not working probably because we need to redefine the asymmetric matchers in the ExpectWebdriverIO namespace so we use that namespace instead of the ExpectLib namespace. - // But will that breaks the `declare module 'expect'` way? interface AsymmetricMatchers { - toBeCustom(): void; + toBeCustom(): string; toBeCustomPromise(chainableElement: ChainablePromiseElement): Promise>; } interface Matchers { diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index c2c865b9a..9ef1fd7a6 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -371,15 +371,18 @@ describe('type assertions', () => { }) it('should support custom asymmetric matcher', async () => { - expectVoid = expect.toBeCustom() - // TODO dprevost: to fix - //expectVoid = expect.not.toBeCustom() + const expectString1 : string = expect.toBeCustom() + const expectString2 : string = 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()) }) }) @@ -696,7 +699,7 @@ describe('type assertions', () => { }) it('should need to be awaited/be a promise if actual is promise type', async () => { - const expectWdioMatcher1: WdioMatchersAndInverse, Promise> = expect.soft(actualPromiseString) + 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')) diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index f798ded51..509b65cd6 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -13,20 +13,11 @@ type ChainablePromiseArray = import('webdriverio').ChainablePromiseArray type ExpectLibAsymmetricMatchers = import('expect').AsymmetricMatchers type ExpectLibAsymmetricMatcher = import('expect').AsymmetricMatcher -type ExpectLibBaseExpect = import('expect').BaseExpect type ExpectLibMatchers, T> = import('expect').Matchers type ExpectLibExpect = import('expect').Expect // TODO dprevost: a suggestion would be to move any code outside of the namespace to separate types.ts file, so that we can import the types. -// To remove when exportable from 'expect'. See https://github.com/jestjs/jest/pull/15704 (already merged) -type Inverse = { - /** - * Inverse next matcher. If you know how to test something, `.not` lets you test its opposite. - */ - not: M; -} - /** * Real Promise and wdio chainable promise types. */ @@ -408,12 +399,10 @@ type WdioCustomMatchers = WdioJestOverloadedMatchers & W */ type WdioMatchers, ActualT> = WdioCustomMatchers & ExpectLibMatchers -type WdioMatchersAndInverse, ActualT> = WdioMatchers & Inverse> - /** * Expects specific to WebdriverIO, excluding the generic expect matchers. */ -interface WdioCustomExpect extends ExpectLibBaseExpect { +interface WdioCustomExpect { /** * Creates a soft assertion wrapper around standard expect * Soft assertions record failures but don't throw errors immediately @@ -463,17 +452,18 @@ declare namespace ExpectWebdriverIO { function getConfig(): any /** - * This block are overloaded types from the expect library. - * They are required to show function under the `ExpectWebdriverIO` namespace. + * 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. - * T needs to stay named T to correctly overload the expect function from the expect library. + * `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, WdioExpect { + interface Expect extends ExpectWebdriverIO.AsymmetricMatchers, Inverse>, WdioExpect { /** * The `expect` function is used every time you want to test a value. * You will rarely call `expect` by itself. @@ -489,16 +479,17 @@ declare namespace ExpectWebdriverIO { (actual: T): ExpectWebdriverIO.MatchersAndInverse; } - /** - * Supported Matchers for expect-webdriverio. - * The Type T (ActualT) needs to keep it's name to overload the Matchers from the expect library. - */ interface Matchers, T> extends WdioMatchers {} - /** - * Overloaded from `expect` library to allow using the `ExpectWebdriverIO` namespace to define custom asymmetric matchers. - */ - type AsymmetricMatchers = WdioAsymmetricMatchers + // To remove when exportable from 'expect'. See https://github.com/jestjs/jest/pull/15704 (already merged) + interface Inverse { + /** + * Inverse next matcher. If you know how to test something, `.not` lets you test its opposite. + */ + not: Matchers; + } + + interface AsymmetricMatchers extends WdioAsymmetricMatchers {} /** * End of block overloading types from the expect library. @@ -720,17 +711,8 @@ declare namespace ExpectWebdriverIO { * Allow to partially matches value. Same as asymmetric matcher in jest. * Some properties are omitted for the type check to work correctly. */ + // TODO dprevost: verify if we do breaking changes on this PartialMatcher, since before it was the AsymmetricMatcher interface used everywhere. type PartialMatcher = Omit, 'sample' | 'inverse' | '$$typeof'> - - // interface AsymmetricMatchers extends WdioAsymmetricMatchers {} - //TODO dprevost: ensure we do not break custom AsymmetricMatchers from expect library - // declare global { - // namespace ExpectWebdriverIO { - // interface AsymmetricMatchers { - // myCustomMatcher(value: string): ExpectWebdriverIO.PartialMatcher; - // } - // } - // } } declare module 'expect-webdriverio' { From ba08e5ab7ce2a8c059dc708769dbda922915c724 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sun, 29 Jun 2025 10:26:33 -0400 Subject: [PATCH 45/99] Make inverse asymmetric matcher works under Jest --- jest.d.ts | 2 +- .../customMatchers-module-expect.d.ts | 26 ++ ...mMatchers-namespace-expectwebdriverio.d.ts | 14 ++ .../jest/customMatchers/customMatchers.d.ts | 11 - test-types/jest/tsconfig.json | 1 - test-types/jest/types-jest.test.ts | 222 ++++++++++++++++-- .../customMatchers-module-expect.d.ts | 8 +- ...mMatchers-namespace-expectwebdriverio.d.ts | 4 +- test-types/mocha/types-mocha.test.ts | 8 +- 9 files changed, 255 insertions(+), 41 deletions(-) create mode 100644 test-types/jest/customMatchers/customMatchers-module-expect.d.ts create mode 100644 test-types/jest/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts delete mode 100644 test-types/jest/customMatchers/customMatchers.d.ts diff --git a/jest.d.ts b/jest.d.ts index 42b5a28ce..e9177927f 100644 --- a/jest.d.ts +++ b/jest.d.ts @@ -33,5 +33,5 @@ declare namespace jest { interface Expect extends ExpectWebdriverIO.Expect {} - interface InverseAsymmetricMatchers extends ExpectWebdriverIO.Expect {} + interface InverseAsymmetricMatchers extends ExpectWebdriverIO.AsymmetricMatchers {} } \ No newline at end of file diff --git a/test-types/jest/customMatchers/customMatchers-module-expect.d.ts b/test-types/jest/customMatchers/customMatchers-module-expect.d.ts new file mode 100644 index 000000000..750d6e1ff --- /dev/null +++ b/test-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/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts b/test-types/jest/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts new file mode 100644 index 000000000..7a833bd87 --- /dev/null +++ b/test-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/customMatchers/customMatchers.d.ts b/test-types/jest/customMatchers/customMatchers.d.ts deleted file mode 100644 index fde1d39b6..000000000 --- a/test-types/jest/customMatchers/customMatchers.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -declare namespace ExpectWebdriverIO { - - // TODO dprevost: ensure we still allow custom asymmetric matchers - interface AsymmetricMatchers { - toBeCustom(): void; - } - interface Matchers { - toBeCustom(): R; - toBeCustomPromise: T extends ChainablePromiseElement ? (expected?: string) => Promise : never; - } -} \ No newline at end of file diff --git a/test-types/jest/tsconfig.json b/test-types/jest/tsconfig.json index bf4637544..5c879a556 100644 --- a/test-types/jest/tsconfig.json +++ b/test-types/jest/tsconfig.json @@ -9,7 +9,6 @@ "@types/jest", "../../jest.d.ts", // Needed to be after @types/jest to override the toMatchSnapshot typing // "@wdio/globals/types", // This types interfere with the local types see how to workaround this - "./customMatchers/customMatchers.d.ts", ], } } diff --git a/test-types/jest/types-jest.test.ts b/test-types/jest/types-jest.test.ts index 780053bca..78e1ea608 100644 --- a/test-types/jest/types-jest.test.ts +++ b/test-types/jest/types-jest.test.ts @@ -344,28 +344,138 @@ describe('type assertions', async () => { }) describe('Custom matchers', () => { - it('should supported correctly a non-promise custom matcher', async () => { - expectVoid = expect('test').toBeCustom() - expectVoid = expect('test').not.toBeCustom() + 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() + // @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()) + }) }) - 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')) + describe('using `expect` module declaration', () => { - // @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 - expect(chainableElement).toBeCustomPromise(expect.stringContaining(6)) + 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') + + // TODO how to make the below fails when the await is missing inf front of the expect from the asymmetric matcher? + // expectPromiseVoid = expect(chainableElement).toHaveCustomProperty( + // expect.toHaveCustomProperty(chainableElement) + // ) + + // @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) + + // TODO how to make the below fails when the await is missing inf front of the expect from the asymmetric matcher? + // expectPromiseVoid = expect(chainableElement).toHaveCustomProperty( + // expect.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) + ) + }) + + // TODO this is not supported in Wdio right now, maybe one day we can support it + // it('should support an async asymmetric matcher on a non async matcher', async () => { + // expectPromiseVoid = expect({ value: 5 }).toEqual({ + // value: expect.toHaveCustomProperty(chainableElement) + // }) + + // // @ts-expect-error + // expectVoid = expect({ value: 5 }).toEqual({ + // value: expect.toHaveCustomProperty(chainableElement) + // }) + + // }) }) }) @@ -654,6 +764,82 @@ describe('type assertions', async () => { // expectPromiseVoid = expect.soft(chainableElement.getText()).toEqual('Basketball Shoes') // expectPromiseVoid = expect.soft(chainableElement.getText()).toMatch(/€\d+/) }) + + 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', () => { diff --git a/test-types/mocha/customMatchers/customMatchers-module-expect.d.ts b/test-types/mocha/customMatchers/customMatchers-module-expect.d.ts index c4df1e5f8..750d6e1ff 100644 --- a/test-types/mocha/customMatchers/customMatchers-module-expect.d.ts +++ b/test-types/mocha/customMatchers/customMatchers-module-expect.d.ts @@ -8,19 +8,19 @@ declare module 'expect' { interface AsymmetricMatchers { toBeWithinRange(floor: number, ceiling: number): void toHaveSimpleCustomProperty(string: string): string - toHaveCustomProperty(element: ChainablePromiseElement | WebdriverIO.Element): Promise + toHaveCustomProperty(element: ChainablePromiseElement | WebdriverIO.Element): Promise> } interface Matchers { toBeWithinRange(floor: number, ceiling: number): R toHaveSimpleCustomProperty(string: string | ExpectWebdriverIO.PartialMatcher): Promise toHaveCustomProperty: - // Required to typecheck the custom matcher so it is only used on elements + // 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) - // Never blocks the call on non-element types + 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 index 46efd6715..7a833bd87 100644 --- a/test-types/mocha/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts +++ b/test-types/mocha/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts @@ -4,11 +4,11 @@ */ declare namespace ExpectWebdriverIO { interface AsymmetricMatchers { - toBeCustom(): string; + toBeCustom(): ExpectWebdriverIO.PartialMatcher; toBeCustomPromise(chainableElement: ChainablePromiseElement): Promise>; } interface Matchers { toBeCustom(): R; - toBeCustomPromise: T extends ChainablePromiseElement ? (expected?: string | ExpectWebdriverIO.PartialMatcher | Promise>) => Promise : never; + toBeCustomPromise: T extends ChainablePromiseElement ? (expected?: string | ExpectWebdriverIO.PartialMatcher | Promise>) => Promise : never; } } \ No newline at end of file diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index 9ef1fd7a6..d6d7c7ae4 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -371,8 +371,8 @@ describe('type assertions', () => { }) it('should support custom asymmetric matcher', async () => { - const expectString1 : string = expect.toBeCustom() - const expectString2 : string = expect.not.toBeCustom() + const expectString1 : ExpectWebdriverIO.PartialMatcher = expect.toBeCustom() + const expectString2 : ExpectWebdriverIO.PartialMatcher = expect.not.toBeCustom() expectPromiseVoid = expect(chainableElement).toBeCustomPromise(expect.toBeCustom()) @@ -437,8 +437,8 @@ describe('type assertions', () => { expectPromiseVoid = expect(chainableElement).toHaveCustomProperty( await expect.toHaveCustomProperty(chainableElement) ) - const expectPromiseWdioElement1: Promise = expect.toHaveCustomProperty(chainableElement) - const expectPromiseWdioElement2: Promise = expect.not.toHaveCustomProperty(chainableElement) + const expectPromiseWdioElement1: Promise> = expect.toHaveCustomProperty(chainableElement) + const expectPromiseWdioElement2: Promise> = expect.not.toHaveCustomProperty(chainableElement) // TODO how to make the below fails when the await is missing inf front of the expect from the asymmetric matcher? // expectPromiseVoid = expect(chainableElement).toHaveCustomProperty( From 82b31ded517481c6505cd2dd0a2677a40234c212 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sun, 29 Jun 2025 11:45:39 -0400 Subject: [PATCH 46/99] Upgrade the ts config to more recent values --- package.json | 6 +++--- test-types/jasmine/tsconfig.json | 6 +++--- test-types/jest/tsconfig.json | 6 +++--- test-types/mocha/tsconfig.json | 6 +++--- tsconfig.json | 13 +++++++++---- types/standalone-global.d.ts | 1 + 6 files changed, 22 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index dd89186a8..92110aefa 100644 --- a/package.json +++ b/package.json @@ -52,9 +52,9 @@ "test:unit": "vitest --run", "test:types": "npm run ts && npm run tsc:root-types", "ts": "run-s ts:*", - "ts:jest": "cd test-types/jest && tsc --project ./tsconfig.json --noEmit", - "ts:mocha": "cd test-types/mocha && tsc --project ./tsconfig.json --noEmit", - "ts:jasmine": "echo 'TODO dprevost to bring back' && exit 0 && cd test-types/jasmine && tsc --project ./tsconfig.json --noEmit", + "ts:jest": "cd test-types/jest && tsc --project ./tsconfig.json", + "ts:mocha": "cd test-types/mocha && tsc --project ./tsconfig.json", + "ts:jasmine": "echo 'TODO dprevost to bring back' && exit 0 && cd test-types/jasmine && tsc --project ./tsconfig.json", "watch": "npm run compile -- --watch", "prepare": "husky install" }, diff --git a/test-types/jasmine/tsconfig.json b/test-types/jasmine/tsconfig.json index 82ea23a1f..73fd29f08 100644 --- a/test-types/jasmine/tsconfig.json +++ b/test-types/jasmine/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { - "outDir": "dist", + "noEmit": true, "noImplicitAny": true, - "target": "ES2020", - "module": "Node16", + "target": "es2022", + "module": "node18", "skipLibCheck": true, "types": [ "../../types/jasmine-soft-extend.d.ts", diff --git a/test-types/jest/tsconfig.json b/test-types/jest/tsconfig.json index 5c879a556..4de10105e 100644 --- a/test-types/jest/tsconfig.json +++ b/test-types/jest/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { - "outDir": "dist", + "noEmit": true, "noImplicitAny": true, - "target": "ES2020", - "module": "Node16", + "target": "es2022", + "module": "node18", "skipLibCheck": true, "types": [ "@types/jest", diff --git a/test-types/mocha/tsconfig.json b/test-types/mocha/tsconfig.json index e2f7dcd43..718b4f9c8 100644 --- a/test-types/mocha/tsconfig.json +++ b/test-types/mocha/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { - "outDir": "dist", + "noEmit": true, "noImplicitAny": true, - "target": "ES2020", - "module": "Node16", + "target": "es2022", + "module": "node18", "skipLibCheck": true, "types": [ "@types/mocha", diff --git a/tsconfig.json b/tsconfig.json index bd84d6376..c57148314 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,21 @@ { + /** + * Let's keep aligned with the WebdriverIO tsconfig.json. + * @see https://github.com/webdriverio/webdriverio/blob/main/tsconfig.json#L5 + * TODO: dprevost, why using moduleResolution node16 in the above link? + */ "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", // node is actually node10, this should be reviewed but node16 as in WebdriverIO main project does not work. "allowSyntheticDefaultImports": true, "types": [ "node", diff --git a/types/standalone-global.d.ts b/types/standalone-global.d.ts index aed133da8..b8f9eb535 100644 --- a/types/standalone-global.d.ts +++ b/types/standalone-global.d.ts @@ -5,6 +5,7 @@ * One example is mocha without the chai expect library. */ +//// @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 { From a33f00708d3e47a2edee05afb1da1fe4af9f59bc Mon Sep 17 00:00:00 2001 From: David Prevost Date: Tue, 1 Jul 2025 13:57:34 -0400 Subject: [PATCH 47/99] Working types under jasmine --- jasmine.d.ts | 37 - .../customMatchers-module-expect.d.ts | 26 + ...mMatchers-namespace-expectwebdriverio.d.ts | 14 + test-types/jasmine/tsconfig.json | 4 +- test-types/jasmine/types-jasmine.test.ts | 1066 +++++++++++------ test-types/mocha/tsconfig.json | 3 +- types/expect-webdriverio.d.ts | 18 +- 7 files changed, 766 insertions(+), 402 deletions(-) delete mode 100644 jasmine.d.ts create mode 100644 test-types/jasmine/customMatchers/customMatchers-module-expect.d.ts create mode 100644 test-types/jasmine/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts diff --git a/jasmine.d.ts b/jasmine.d.ts deleted file mode 100644 index c7f140e3d..000000000 --- a/jasmine.d.ts +++ /dev/null @@ -1,37 +0,0 @@ -/// - -declare namespace jasmine { - - interface Matchers extends WdioMatchers{ - - /** - * Below are overloaded Jest's matchers not part of `expect` but of `jest-snapshot`. - * We need to define them below so that they are correctly typed overloaded - * @see https://github.com/jestjs/jest/blob/73dbef5d2d3195a1e55fb254c54cce70d3036252/packages/jest-snapshot/src/types.ts#L37 - */ - - /** - * snapshot matcher - * @param label optional snapshot label - */ - toMatchSnapshot(label?: string): Promise; - - // TODO - this is not working as expected, need to investigate - /** - * snapshot matcher - * @param label optional snapshot label - */ - // toMatchSnapshot: T extends WdioElementLike ? (label: string) => Promise : (hint?: string) => R; - - /** - * inline snapshot matcher - * @param snapshot snapshot string (autogenerated if not specified) - * @param label optional snapshot label - */ - toMatchInlineSnapshot(snapshot?: string, label?: string): Promise - } - - // type MatcherAndInverse = Matchers - // interface AsyncMatchers extends WdioMatchers {} -} - 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..750d6e1ff --- /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): 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/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts b/test-types/jasmine/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts new file mode 100644 index 000000000..7a833bd87 --- /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>) => Promise : never; + } +} \ No newline at end of file diff --git a/test-types/jasmine/tsconfig.json b/test-types/jasmine/tsconfig.json index 73fd29f08..06c6f4314 100644 --- a/test-types/jasmine/tsconfig.json +++ b/test-types/jasmine/tsconfig.json @@ -6,10 +6,8 @@ "module": "node18", "skipLibCheck": true, "types": [ - "../../types/jasmine-soft-extend.d.ts", + "../../types/standalone-global.d.ts", "@types/jasmine", - "../../jasmine.d.ts", - "@wdio/globals/types" ] } } \ No newline at end of file diff --git a/test-types/jasmine/types-jasmine.test.ts b/test-types/jasmine/types-jasmine.test.ts index f87e4e759..9c5521445 100644 --- a/test-types/jasmine/types-jasmine.test.ts +++ b/test-types/jasmine/types-jasmine.test.ts @@ -1,264 +1,636 @@ /* 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 chainableElement = $('findMe') - const chainableArray = $$('ul>li') + const elementArray: WebdriverIO.ElementArray = [] as unknown as WebdriverIO.ElementArray + + const networkMock: WebdriverIO.Mock = {} as unknown as WebdriverIO.Mock - describe('toHaveUrl', () => { + // Type assertions + let expectPromiseVoid: Promise + let expectVoid: void + + describe('Browser', () => { const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser - it('should not have ts errors and be able to await the promise when actual is browser', async () => { - const expectPromiseVoid: Promise = expect(browser).toHaveUrl('https://example.com') - await expectPromiseVoid - const expectNotPromiseVoid: Promise = expect(browser).not.toHaveUrl('https://example.com') - await expectNotPromiseVoid - }) + describe('toHaveUrl', () => { + it('should be supported correctly', async () => { + expectPromiseVoid = expect(browser).toHaveUrl('https://example.com') + expectPromiseVoid = expect(browser).not.toHaveUrl('https://example.com') - it('should have ts errors and not need to await the promise when actual is browser', async () => { - // @ts-expect-error - const expectVoid: void = expect(browser).toHaveUrl('https://example.com') - // @ts-expect-error - const expectNotVoid: void = 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()) - it('should have ts errors when actual is an element', async () => { - // @ts-expect-error - await expect(element).toHaveUrl('https://example.com') - }) + // @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')) - it('should have ts errors when actual is an ChainableElement', async () => { - // @ts-expect-error - await expect(chainableElement).toHaveUrl('https://example.com') + // @ts-expect-error + await expect(browser).toHaveUrl(6) + //// @ts-expect-error TODO dprevost can we make the below fail? + // await expect(browser).toHaveUrl(expect.objectContaining({})) + }) + + 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') + }) }) - it('should support stringContaining', async () => { - const expectVoid1: Promise = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) + describe('toHaveTitle', () => { + it('should be supported correctly', async () => { + expectPromiseVoid = expect(browser).toHaveTitle('https://example.com') + expectPromiseVoid = expect(browser).not.toHaveTitle('https://example.com') - // @ts-expect-error - const expectVoid2: void = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) + // 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 type assertions', () => { + describe('element', () => { describe('toBeDisabled', () => { - it('should not have ts errors and be able to await the promise for element', async () => { - // expect no ts errors - const expectIsPromiseVoid: Promise = expect(element).toBeDisabled() - await expectIsPromiseVoid + 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() - const expectNotIsPromiseVoid: Promise = expect(element).not.toBeDisabled() - await expectNotIsPromiseVoid + // @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')]) - it('should not have ts errors and be able to await the promise for chainable', async () => { - // expect no ts errors - const expectIsPromiseVoid: Promise = expect(chainableElement).toBeDisabled() - await expectIsPromiseVoid + expectPromiseVoid = expect(elementArray).not.toHaveText('text') - const expectNotIsPromiseVoid: Promise = expect(chainableElement).not.toBeDisabled() - await expectNotIsPromiseVoid + // @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 typing to void for element', async () => { + it('should have ts errors when actual is not an element', async () => { // @ts-expect-error - const expectToBeIsVoid: void = expect(element).toBeDisabled() + await expect(browser).toHaveText('text') // @ts-expect-error - const expectNotToBeIsVoid: void = expect(element).not.toBeDisabled() + 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 typing to void for chainable', async () => { + it('should have ts errors when actual is string or Promise', async () => { + // @ts-expect-error + await expect('text').toHaveText('text') // @ts-expect-error - const expectToBeIsVoid: void = expect(chainableElement).toBeDisabled() + await expect('text').not.toHaveText('text') // @ts-expect-error - const expectNotToBeIsVoid: void = expect(chainableElement).not.toBeDisabled() + await expect(Promise.resolve('text')).toHaveText('text') + // @ts-expect-error + await expect(Promise.resolve('text')).toHaveText('text') }) }) - describe('toMatchSnapshot', () => { + 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) - it('should not have ts errors when typing to Promise for an element', async () => { - const expectPromise1: Promise = expect(element).toMatchSnapshot() - const expectPromise2: Promise = expect(element).toMatchSnapshot('test label') + // @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 not have ts errors when typing to Promise for a chainable', async () => { - const expectPromise1: Promise = expect(chainableElement).toMatchSnapshot() - const expectPromise2: Promise = expect(chainableElement).toMatchSnapshot('test label') + 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') }) + }) - // We need somehow to exclude the Jest types one for this to success - it('should have ts errors when typing to void for an element like', async () => { + 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 - const expectNotToBeVoid1: void = expect(element).toMatchSnapshot() + expectVoid = expect(chainableElement).toMatchSnapshot() //@ts-expect-error - const expectNotToBeVoid2: void = expect(chainableElement).toMatchSnapshot() + expectVoid = expect(chainableElement).not.toMatchSnapshot() }) - // TODO - conditional types check on T to have the below match void does not work - // it('should not have ts errors when typing to void for a string', async () => { - // const expectNotToBeVoid: void = expect('.findme').toMatchSnapshot() + // TODO - since we are overloading the `toMatchSnapshot` of jest.toMatchSnapshot, I wonder if we can achieve the below... + // it('should have ts errors when not an element or chainable', async () => { + // //@ts-expect-error + // await expect('.findme').toMatchSnapshot() // }) }) describe('toMatchInlineSnapshot', () => { - it('should not have ts errors when typing to Promise for an element', async () => { - const expectPromise1: Promise = expect(element).toMatchInlineSnapshot() - const expectPromise2: Promise = expect(element).toMatchInlineSnapshot('test snapshot') - const expectPromise3: Promise = expect(element).toMatchInlineSnapshot('test snapshot', 'test label') - }) + 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') - it('should not have ts errors when typing to Promise for a chainable', async () => { - const expectPromise1: Promise = expect(chainableElement).toMatchInlineSnapshot() - const expectPromise2: Promise = expect(chainableElement).toMatchInlineSnapshot('test snapshot') - const expectPromise3: Promise = 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') }) - // We need somehow to exclude the Jest types one for this to success - it('should have ts errors when typing to void for an element like', async () => { + 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 - const expectNotToBeVoid1: void = expect(element).toMatchInlineSnapshot() + expectPromiseVoid = expect(element).toMatchInlineSnapshot() //@ts-expect-error - const expectPromise2: void = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + expectVoid = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') }) + }) - // TODO - conditional types check on T to have the below match void does not work - // it('should not have ts errors when typing to void for a string', async () => { - // const expectNotToBeVoid: void = expect('.findme').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('toBe', () => { + 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() - it('should not have ts errors when typing to void when actual is boolean', async () => { - const expectToBeIsVoid: void = expect(true).toBe(true) - const expectNotToBeIsVoid: void = expect(true).not.toBe(true) - }) + // @ts-expect-error + expectPromiseVoid = expect('test').toBeCustom() + // @ts-expect-error + expectPromiseVoid = expect('test').not.toBeCustom() - it('should have ts errors when typing to Promise when actual is boolean', async () => { - //@ts-expect-error - const expectToBeIsNotPromiseVoid1: Promise = expect(true).toBe(true) + expectVoid = expect(1).toBeWithinRange(0, 2) + }) - //@ts-expect-error - const expectToBeIsNotPromiseVoid2: Promise = expect(true).not.toBe(true) + 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()) + }) }) - it('should expect void when actual is an awaited element/chainable', async () => { - const isClickableElement = await element.isClickable() - const expectPromiseVoid1: void = expect(isClickableElement).toBe(true) + describe('using `expect` module declaration', () => { - const isClickableChainable: boolean = await chainableElement.isClickable() - const expectPromiseVoid2: void = expect(isClickableChainable).toBe(true) + it('should support a simple matcher', async () => { + expectVoid = expect(5).toBeWithinRange(1, 10) - // @ts-expect-error - const expectPromiseVoid3: Promise = expect(isClickableElement).toBe(true) + // Or as an asymmetric matcher: + expectVoid = expect({ value: 5 }).toEqual({ + value: expect.toBeWithinRange(1, 10) + }) - // @ts-expect-error - const expectPromiseVoid4: Promise = expect(isClickableChainable).toBe(true) + // @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') + + // TODO how to make the below fails when the await is missing inf front of the expect from the asymmetric matcher? + // expectPromiseVoid = expect(chainableElement).toHaveCustomProperty( + // expect.toHaveCustomProperty(chainableElement) + // ) + + // @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) + + // TODO how to make the below fails when the await is missing inf front of the expect from the asymmetric matcher? + // expectPromiseVoid = expect(chainableElement).toHaveCustomProperty( + // expect.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) + ) + }) + + // TODO this is not supported in Wdio right now, maybe one day we can support it + // it('should support an async asymmetric matcher on a non async matcher', async () => { + // expectPromiseVoid = expect({ value: 5 }).toEqual({ + // value: expect.toHaveCustomProperty(chainableElement) + // }) + + // // @ts-expect-error + // expectVoid = expect({ value: 5 }).toEqual({ + // value: expect.toHaveCustomProperty(chainableElement) + // }) + + // }) }) }) - describe('string type assertions', () => { - it('should not have ts errors when typing to void', async () => { - // Expect no ts errors - const expectToBeIsVoid: void = expect('test').toBe('test') - }) + describe('toBe', () => { - it('should have ts errors when typing to Promise', async () => { + 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 - const expectToBeIsNotPromiseVoid: Promise = expect('test').toBe('test') + expectPromiseVoid = expect(true).not.toBe(true) }) - }) - describe('Promise<> type assertions', () => { - const booleanPromise: Promise = Promise.resolve(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) - it('should not have ts errors when typing to void', async () => { - // const expectToBeIsVoid: void = expect(booleanPromise).toBe(true) - const expectAwaitToBeIsVoid: void = expect(await booleanPromise).toBe(true) + //@ts-expect-error + expectPromiseVoid = expect(chainableElement).toBe(true) + //@ts-expect-error + expectPromiseVoid = expect(chainableElement).not.toBe(true) }) - it('should not have ts errors when resolves and rejects is typed to Promise', async () => { + it('should still expect void type when actual is a Promise since we do not overload them', async () => { + const promiseBoolean = Promise.resolve(true) - // @ts-expect-error - expect(booleanPromise).resolves.toBe(true) + expectVoid = expect(promiseBoolean).toBe(true) + expectVoid = expect(promiseBoolean).not.toBe(true) - // @ts-expect-error - expect(booleanPromise).rejects.toBe(true) + //@ts-expect-error + expectPromiseVoid = expect(promiseBoolean).toBe(true) + //@ts-expect-error + expectPromiseVoid = expect(promiseBoolean).toBe(true) }) - it('should have ts errors when typing to Promise', async () => { + 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 - const expectToBeIsNotPromiseVoid1: Promise = expect(booleanPromise).toBe(true) + expectPromiseVoid = expect('text').toBe(expect.stringContaining('text')) //@ts-expect-error - const expectToBeIsNotPromiseVoid2: Promise = expect(await booleanPromise).toBe(true) + expectPromiseVoid = expect('text').not.toBe(expect.stringContaining('text')) }) - - // it('should have ts errors when typing resolves and reject is typed to void', async () => { - // //@ts-expect-error - // const expectResolvesToBeIsVoid: void = expect(booleanPromise).resolves.toBe(true) - // //@ts-expect-error - // const expectRejectsToBeIsVoid: void = expect(booleanPromise).rejects.toBe(true) - // }) }) - describe('toBeElementsArrayOfSize', async () => { + describe('Promise type assertions', () => { + const booleanPromise: Promise = Promise.resolve(true) - it('should not have ts errors when typing to Promise', async () => { - const listItems = await chainableArray - const expectPromise: Promise = expect(listItems).toBeElementsArrayOfSize(5) - const expectPromise1: Promise = expect(listItems).toBeElementsArrayOfSize({ lte: 10 }) + it('should expect a Promise of type', async () => { + const expectPromiseBoolean1: ExpectWebdriverIO.MatchersAndInverse> = expect(booleanPromise) + const expectPromiseBoolean2: ExpectWebdriverIO.Matchers> = expect(booleanPromise).not }) - it('should have ts errors when typing to void', async () => { - const listItems = await chainableArray - // @ts-expect-error - const expectPromise: void = expect(listItems).toBeElementsArrayOfSize(5) - // @ts-expect-error - const expectPromise1: void = expect(listItems).toBeElementsArrayOfSize({ lte: 10 }) + it('should work with resolves & rejects correctly', async () => { + // TODO dprevost should we support this in Wdio since we do not even use it or document it? + // 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 = browser.mock('**/api/todo*') + const promiseNetworkMock = Promise.resolve(networkMock) it('should not have ts errors when typing to Promise', async () => { - const expectPromise1: Promise = expect(promiseNetworkMock).toBeRequested() - const expectPromise2: Promise = expect(promiseNetworkMock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) - const expectPromise3: Promise = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 - - const expectPromise4: Promise = expect(promiseNetworkMock).toBeRequestedWith({ - url: 'http://localhost:8080/api/todo', // [optional] string | function | custom matcher - method: 'POST', // [optional] string | array - statusCode: 200, // [optional] number | array - requestHeaders: { Authorization: 'foo' }, // [optional] object | function | custom matcher - responseHeaders: { Authorization: 'bar' }, // [optional] object | function | custom matcher - postData: { title: 'foo', description: 'bar' }, // [optional] object | function | custom matcher - response: { success: true }, // [optional] object | function | custom matcher + 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/todo', + method: 'POST', + statusCode: 200, + requestHeaders: { Authorization: 'foo' }, + responseHeaders: { Authorization: 'bar' }, + postData: { title: 'foo', description: 'bar' }, + response: { success: true }, + }) + + // TODO dprevost: Asymmetric matcher is not defined on the entire object in the .d.ts file, it is a bug? + // expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith(expect.objectContaining({ + // response: { success: true }, // [optional] object | function | custom matcher + // })) + + 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 - const expectPromise1: void = expect(promiseNetworkMock).toBeRequested() + expectVoid = expect(promiseNetworkMock).toBeRequested() // @ts-expect-error - const expectPromise2: void = expect(promiseNetworkMock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + expectVoid = expect(promiseNetworkMock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) // @ts-expect-error - const expectPromise3: void = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 + expectVoid = expect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 // @ts-expect-error - const expectPromise4: void = expect(promiseNetworkMock).toBeRequestedWith({ - url: 'http://localhost:8080/api/todo', // [optional] string | function | custom matcher - method: 'POST', // [optional] string | array - statusCode: 200, // [optional] number | array - requestHeaders: { Authorization: 'foo' }, // [optional] object | function | custom matcher - responseHeaders: { Authorization: 'bar' }, // [optional] object | function | custom matcher - postData: { title: 'foo', description: 'bar' }, // [optional] object | function | custom matcher - response: { success: true }, // [optional] object | function | custom matcher + 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/todo', + 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 }, + })) }) }) @@ -268,213 +640,198 @@ describe('type assertions', () => { expect.unimplementedFunction() }) - it('should support stringContaining, anything', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const expectAny1: any = expect.stringContaining('WebdriverIO') - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const expectAny2: any = expect.anything() + 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')) + + // TODO dprevost: Should we support these? + // 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('Expect Soft Assertions', async () => { - const expectString: string = await $('h1').getText() - const expectPromise: Promise = $('h1').getText() + describe('Soft Assertions', async () => { + const actualString: string = 'Test Page' + const actualPromiseString: Promise = Promise.resolve('Test Page') describe('expect.soft', () => { - it('should not have ts error and not need to be awaited/be a promise if actual is non-promise type', async () => { - const expectWdioMatcher: jasmine.Matchers = expect.soft(expectString) - const expectVoid: void = expect.soft(expectString).toBe('Test Page') - }) - - it('should not have ts error and need to be awaited/be a promise if actual is a promise type', async () => { - const expectWdioMatcher: jasmine.Matchers> = expect.soft(expectPromise) - const expectVoid: Promise = expect.soft(expectPromise).toBe('Test Page') + 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')) - await expect.soft(expectPromise).toBe('Test Page') - }) - - it('should have ts error when using await and actual is non-promise type', async () => { // @ts-expect-error - const expectWdioMatcher: WdioMatchers, string> = expect.soft(expectString) - + expectPromiseVoid = expect.soft(actualString).toBe('Test Page') // @ts-expect-error - const expectVoid: Promise = expect.soft(expectString).toBe('Test Page') - }) - - it('should not have ts error and need to be awaited/be a promise if actual is a promise type', async () => { - // @ts-expect-error - const expectWdioMatcher: WdioMatchers> = expect.soft(expectPromise) + expectPromiseVoid = expect.soft(actualString).not.toBe('Test Page') // @ts-expect-error - const expectVoid: void = expect.soft(expectPromise).toBe('Test Page') + expectPromiseVoid = expect.soft(actualString).not.toBe(expect.stringContaining('Test Page')) }) - it('should support chainable element', async () => { - const expectElement: jasmine.Matchers = expect.soft(element) - const expectElementChainable: jasmine.Matchers = expect.soft(chainableElement) + 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 - const expectElement2: WdioMatchers, WebdriverIO.Element> = expect.soft(element) + expectVoid = expect.soft(actualPromiseString).toBe('Test Page') // @ts-expect-error - const expectElementChainable2: WdioMatchers, typeof chainableElement> = expect.soft(chainableElement) + 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 with wdio Matchers', async () => { - const expectPromise1: Promise = expect.soft(element).toBeDisplayed() - const expectPromise2: Promise = expect.soft(chainableElement).toBeDisplayed() + it('should support chainable element', async () => { + const expectElement: WdioCustomMatchers = expect.soft(element) + const expectElementChainable: WdioCustomMatchers = expect.soft(chainableElement) // @ts-expect-error - const expectPromise3: void = expect.soft(element).toBeDisplayed() + const expectElement2: WdioCustomMatchers, WebdriverIO.Element> = expect.soft(element) // @ts-expect-error - const expectPromise4: void = expect.soft(chainableElement).toBeDisplayed() + const expectElementChainable2: WdioCustomMatchers, 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() - }) - - describe('not', async () => { - it('should support not with chainable', async () => { - const expectPromise1: Promise = expect.soft(element).not.toBeDisplayed() - const expectPromise2: Promise = expect.soft(chainableElement).not.toBeDisplayed() - - // @ts-expect-error - const expectPromise3: void = expect.soft(element).not.toBeDisplayed() - // @ts-expect-error - const expectPromise4: void = expect.soft(chainableElement).not.toBeDisplayed() - - await expect.soft(element).not.toBeDisplayed() - await expect.soft(chainableElement).not.toBeDisplayed() - }) - - it('should support not with non-promise', async () => { - const expectVoid1: void = expect.soft(expectString).not.toBe('Test Page') - - // @ts-expect-error - const expectVoid2: Promise = expect.soft(expectString).not.toBe('Test Page') - }) + await expect.soft(chainableArray).toBeDisplayed() - it('should support not with promise', async () => { - const expectPromise1: Promise = expect.soft(expectPromise).not.toBe('Test Page') - - // @ts-expect-error - const expectPromise2: void = expect.soft(expectPromise).not.toBe('Test Page') - - await expect.soft(expectPromise).not.toBe('Test Page') - }) - }) - }) - - describe('expect.getSoftFailures', () => { - it('should be of type `SoftFailure`', async () => { - const expectSoftFailure1: ExpectWebdriverIO.SoftFailure[] = expect.getSoftFailures() + 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 - const expectSoftFailure2: void = expect.getSoftFailures() - }) - }) - - describe('expect.assertSoftFailures', () => { - it('should be of type void', async () => { - const expectVoid1: void = expect.assertSoftFailures() - + expectVoid = expect.soft(element).toBeDisplayed() // @ts-expect-error - const expectVoid2: Promise = expect.assertSoftFailures() - }) - }) - - describe('expect.clearSoftFailures', () => { - it('should be of type void', async () => { - const expectVoid1: void = expect.clearSoftFailures() + expectVoid = expect.soft(chainableElement).toBeDisplayed() + // @ts-expect-error + expectVoid = expect.soft(chainableArray).toBeDisplayed() // @ts-expect-error - const expectVoid2: Promise = expect.clearSoftFailures() + expectVoid = expect.soft(element).not.toBeDisplayed() + // @ts-expect-error + expectVoid = expect.soft(chainableElement).not.toBeDisplayed() + // @ts-expect-error + expectVoid = expect.soft(chainableArray).not.toBeDisplayed() }) - }) - }) - describe('ExpectAsync Soft Assertions', async () => { - const expectString: string = await $('h1').getText() - const expectPromise: Promise = $('h1').getText() - - describe('expectAsync.soft', () => { - it('should not have ts error and not need to be awaited/be a promise if actual is non-promise type', async () => { - const expectWdioMatcher: jasmine.Matchers = expectAsync.soft(expectString) - const expectVoid: void = expectAsync.soft(expectString).toBe('Test Page') + it('should have ts (or lint) errors when actual is a chainable not awaited', async () => { + // TODO dprevost: see if an eslint rule could help us here to detect missing await when not using wdio matchers + // expectPromiseVoid = expect.soft(chainableElement.getText()).toEqual('Basketball Shoes') + // expectPromiseVoid = expect.soft(chainableElement.getText()).toMatch(/€\d+/) }) - it('should not have ts error and need to be awaited/be a promise if actual is a promise type', async () => { - const expectWdioMatcher: jasmine.Matchers> = expectAsync.soft(expectPromise) - const expectVoid: Promise = expectAsync.soft(expectPromise).toBeResolvedTo('Test Page') - - await expectAsync.soft(expectPromise).toBeResolvedTo('Test Page') - }) + 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) + ) - it('should have ts error when using await and actual is non-promise type', async () => { // @ts-expect-error - const expectWdioMatcher: WdioMatchers, string> = expect.soft(expectString) - + expectVoid = expect.soft(chainableElement).toHaveCustomProperty('text') // @ts-expect-error - const expectVoid: Promise = expect.soft(expectString).toBe('Test Page') - }) - - it('should not have ts error and need to be awaited/be a promise if actual is a promise type', async () => { - // @ts-expect-error - const expectWdioMatcher: WdioMatchers> = expect.soft(expectPromise) + expectVoid = expect.soft(chainableElement).toHaveCustomProperty(expect.stringContaining('text')) // @ts-expect-error - const expectVoid: void = expect.soft(expectPromise).toBe('Test Page') - }) + expectVoid = expect.soft(chainableElement).not.toHaveCustomProperty(expect.not.stringContaining('text')) + // @ts-expect-error + expectVoid = expect.soft(chainableElement).toHaveCustomProperty( + expect.toHaveCustomProperty(chainableElement) + ) - it('should support chainable element', async () => { - const expectElement: jasmine.Matchers = expect.soft(element) - const expectElementChainable: jasmine.Matchers = expect.soft(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 - const expectElement2: WdioMatchers, WebdriverIO.Element> = expect.soft(element) + expectVoid = expect.soft(chainableElement).toHaveCustomProperty('text') // @ts-expect-error - const expectElementChainable2: WdioMatchers, typeof chainableElement> = expect.soft(chainableElement) - }) - - it('should support chainable element with wdio Matchers', async () => { - const expectPromise1: Promise = expect.soft(element).toBeDisplayed() - const expectPromise2: Promise = expect.soft(chainableElement).toBeDisplayed() - + expectVoid = expect.soft(chainableElement).toHaveCustomProperty(expect.stringContaining('text')) // @ts-expect-error - const expectPromise3: void = expect.soft(element).toBeDisplayed() + expectVoid = expect.soft(chainableElement).not.toHaveCustomProperty(expect.not.stringContaining('text')) // @ts-expect-error - const expectPromise4: void = expect.soft(chainableElement).toBeDisplayed() - - await expect.soft(element).toBeDisplayed() - await expect.soft(chainableElement).toBeDisplayed() + expectVoid = expect.soft(chainableElement).toHaveCustomProperty( + expect.toHaveCustomProperty(chainableElement) + ) }) - describe('not', async () => { - it('should support not with chainable', async () => { - const expectPromise1: Promise = expect.soft(element).not.toBeDisplayed() - const expectPromise2: Promise = expect.soft(chainableElement).not.toBeDisplayed() - - // @ts-expect-error - const expectPromise3: void = expect.soft(element).not.toBeDisplayed() - // @ts-expect-error - const expectPromise4: void = expect.soft(chainableElement).not.toBeDisplayed() + 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) + ) - await expect.soft(element).not.toBeDisplayed() - await expect.soft(chainableElement).not.toBeDisplayed() - }) - - it('should support not with non-promise', async () => { - const expectVoid1: void = expect.soft(expectString).not.toBe('Test Page') - - // @ts-expect-error - const expectVoid2: Promise = expect.soft(expectString).not.toBe('Test Page') - }) - - it('should support not with promise', async () => { - const expectPromise1: Promise = expect.soft(expectPromise).not.toBe('Test Page') + // @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) + ) - // @ts-expect-error - const expectPromise2: void = expect.soft(expectPromise).not.toBe('Test Page') + 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) + ) - await expect.soft(expectPromise).not.toBe('Test Page') - }) + // @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) + ) }) }) @@ -483,60 +840,63 @@ describe('type assertions', () => { const expectSoftFailure1: ExpectWebdriverIO.SoftFailure[] = expect.getSoftFailures() // @ts-expect-error - const expectSoftFailure2: void = expect.getSoftFailures() + expectVoid = expect.getSoftFailures() }) }) describe('expect.assertSoftFailures', () => { it('should be of type void', async () => { - const expectVoid1: void = expect.assertSoftFailures() + expectVoid = expect.assertSoftFailures() // @ts-expect-error - const expectVoid2: Promise = expect.assertSoftFailures() + expectPromiseVoid = expect.assertSoftFailures() }) }) describe('expect.clearSoftFailures', () => { it('should be of type void', async () => { - const expectVoid1: void = expect.clearSoftFailures() + expectVoid = expect.clearSoftFailures() // @ts-expect-error - const expectVoid2: Promise = expect.clearSoftFailures() + expectPromiseVoid = expect.clearSoftFailures() }) }) }) + }) - describe('Jasmine', async () => { - const string = 'Test Page' - const promiseString = Promise.resolve('Test Page') - - it('should correctly support expect.toBe', async () => { - const expectNonPromiseMatched1: jasmine.Matchers = expect(string) - const expectVoid1: void = expect(string).toBe(string) - - // @ts-expect-error - const expectNonPromiseMatched2: jasmine.Matchers> = expect(string) - // @ts-expect-error - const expectVoid2: Promise = expect(string).toBe(string) - - // @ts-expect-error - expect(string).unimplementedFunction() - }) - - it('should correctly support expect.toBeResolvedTo', async () => { - const expectNonPromiseMatched1: jasmine.AsyncMatchers = expectAsync(promiseString) - const expectVoid1: PromiseLike = expectAsync(promiseString).toBeResolvedTo('Test Page') - - // @ts-expect-error - const expectNonPromiseMatched2: jasmine.AsyncMatchers, unknown> = expectAsync(promiseString) - // @ts-expect-error - const expectVoid2: void = expectAsync(promiseString).toBeResolvedTo('Test Page') + 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))) + }) + }) - await expectAsync(promiseString).toBeResolvedTo('Test Page') + describe('Jasmine Matchers', () => { + let expectPromiseLikeVoid: PromiseLike + it('should support expectAsync correctly for non wdio types', async () => { + expectPromiseLikeVoid = expectAsync(Promise.resolve('test')).toBeResolved() + expectPromiseLikeVoid = expectAsync(Promise.resolve('test')).toBeRejected() + expectPromiseLikeVoid = expectAsync(Promise.resolve('test')).toBeRejectedWith('test error') + expectPromiseLikeVoid = expectAsync(Promise.resolve('test')).toBeRejectedWithError('test error') - // @ts-expect-error - expectAsync(promiseString).unimplementedFunction() - }) + // @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')).toBeRejectedWith('test error') + // @ts-expect-error + expectVoid = expectAsync(Promise.resolve('test')).toBeRejectedWithError('test error') }) }) }) diff --git a/test-types/mocha/tsconfig.json b/test-types/mocha/tsconfig.json index 718b4f9c8..963fc6caa 100644 --- a/test-types/mocha/tsconfig.json +++ b/test-types/mocha/tsconfig.json @@ -6,10 +6,11 @@ "module": "node18", "skipLibCheck": true, "types": [ + "../../types/standalone-global.d.ts", "@types/mocha", "expect", "webdriverio", - "../../types/standalone-global.d.ts", + // "@wdio/globals/types", // TODO to review adding the global wdio types interfere with local types ] } diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 509b65cd6..e18d150ac 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -544,14 +544,16 @@ declare namespace ExpectWebdriverIO { message(): string } - // TODO dprevost: what is this, I'm unable to find it in the codebase, was a function before, seems to override something from Jasmine in the past? - // const matchers: Map< - // string, - // ( - // actual: unknown, - // ...expected: unknown[] - // ) => Promise - // > + /** + * Used by the wdio main project to configure the matchers in the runner when using Jasmine. + */ + const matchers: Map< + string, + ( + actual: unknown, + ...expected: unknown[] + ) => Promise + > interface AssertionHookParams { /** From 832028f5333d51163c70f3ccfef3d6043a741625 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Tue, 1 Jul 2025 14:04:20 -0400 Subject: [PATCH 48/99] Add Jasmine case + force the global expect also for Jasmine --- test-types/jasmine/tsconfig.json | 2 +- test-types/mocha/tsconfig.json | 2 +- types/{standalone-global.d.ts => expect-global.d.ts} | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) rename types/{standalone-global.d.ts => expect-global.d.ts} (58%) diff --git a/test-types/jasmine/tsconfig.json b/test-types/jasmine/tsconfig.json index 06c6f4314..8bf61792c 100644 --- a/test-types/jasmine/tsconfig.json +++ b/test-types/jasmine/tsconfig.json @@ -6,7 +6,7 @@ "module": "node18", "skipLibCheck": true, "types": [ - "../../types/standalone-global.d.ts", + "../../types/expect-global.d.ts", "@types/jasmine", ] } diff --git a/test-types/mocha/tsconfig.json b/test-types/mocha/tsconfig.json index 963fc6caa..9cbe9c425 100644 --- a/test-types/mocha/tsconfig.json +++ b/test-types/mocha/tsconfig.json @@ -6,7 +6,7 @@ "module": "node18", "skipLibCheck": true, "types": [ - "../../types/standalone-global.d.ts", + "../../types/expect-global.d.ts", "@types/mocha", "expect", "webdriverio", diff --git a/types/standalone-global.d.ts b/types/expect-global.d.ts similarity index 58% rename from types/standalone-global.d.ts rename to types/expect-global.d.ts index b8f9eb535..f2370583e 100644 --- a/types/standalone-global.d.ts +++ b/types/expect-global.d.ts @@ -1,8 +1,9 @@ /// /** - * Global declaration file for WebdriverIO's Expect library when not pair with another expect library like Jest. or Jasmine. - * One example is mocha without the chai expect library. + * 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 + * // TODO verify if this is also needed/forced when used with Jest! */ //// @ts-expect-error: IDE might flags this one but just does be concerned by it. This way the `tsc:root-types` can pass! From 1ebc16c035eadd1291c934b7e0a7778f86b9cf0e Mon Sep 17 00:00:00 2001 From: David Prevost Date: Tue, 1 Jul 2025 14:31:18 -0400 Subject: [PATCH 49/99] Review jasmine usage in the project --- test-types/jasmine/types-jasmine.test.ts | 19 ++++-- tsconfig.types.json | 11 ++- types/jasmine-soft-extend.d.ts | 86 ------------------------ 3 files changed, 17 insertions(+), 99 deletions(-) delete mode 100644 types/jasmine-soft-extend.d.ts diff --git a/test-types/jasmine/types-jasmine.test.ts b/test-types/jasmine/types-jasmine.test.ts index 9c5521445..6b9b9ba7a 100644 --- a/test-types/jasmine/types-jasmine.test.ts +++ b/test-types/jasmine/types-jasmine.test.ts @@ -881,22 +881,29 @@ describe('type assertions', () => { }) }) - describe('Jasmine Matchers', () => { + 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(expect.stringContaining('test error')) + expectPromiseLikeVoid = expectAsync(Promise.resolve('test')).not.toBeResolvedTo(expect.not.stringContaining('test error')) expectPromiseLikeVoid = expectAsync(Promise.resolve('test')).toBeRejected() - expectPromiseLikeVoid = expectAsync(Promise.resolve('test')).toBeRejectedWith('test error') - expectPromiseLikeVoid = expectAsync(Promise.resolve('test')).toBeRejectedWithError('test error') + 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')).toBeRejectedWith('test error') - // @ts-expect-error - expectVoid = expectAsync(Promise.resolve('test')).toBeRejectedWithError('test error') + expectVoid = expectAsync(Promise.resolve('test')).toBeResolved() }) + it('jasmine special asymmetric matcher', async () => { + // TODO dprevost: Is this valid since expect is from WebdriverIO and we force it to be `expectAsync` in the main project? + expect({}).toEqual(jasmine.any(Object)) + expect(12).toEqual(jasmine.any(Number)) + }) + }) }) diff --git a/tsconfig.types.json b/tsconfig.types.json index f6a0362c5..8ab0f0185 100644 --- a/tsconfig.types.json +++ b/tsconfig.types.json @@ -23,11 +23,8 @@ "noEmit": true, }, - "include": ["*.d.ts", - "./types/*.d.ts"], - "exclude": [ - "**/urlpattern-polyfill/**", - "jasmine.d.ts", // TODO dprevost: to remove - "types/jasmine-soft-extend.d.ts", // TODO dprevost: to remove - ] + "include": [ + "*.d.ts", + "./types/*.d.ts" + ], } \ No newline at end of file diff --git a/types/jasmine-soft-extend.d.ts b/types/jasmine-soft-extend.d.ts deleted file mode 100644 index 37ed6724c..000000000 --- a/types/jasmine-soft-extend.d.ts +++ /dev/null @@ -1,86 +0,0 @@ -/// - -declare global { - - // TODO dprevost might need to override the Array too (and more?) - function expect(actual: T): jasmine.Matchers - - // function expectAsync(actual: T | PromiseLike): jasmine.AsyncMatchers - namespace expect { - - // TODO should we use expectAsync here instead? - /** Wdio soft assertion */ - /** - * 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 - */ - function soft(actual: T): jasmine.Matchers - // soft(actual: T): T extends PromiseLike ? Matchers, T> : Matchers - - /** - * Get all current soft assertion failures - */ - function getSoftFailures(testId?: string): ExpectWebdriverIO.SoftFailure[] - - /** - * Manually assert all soft failures (throws an error if any failures exist) - */ - function assertSoftFailures(testId?: string): void - - /** - * Clear all current soft assertion failures - */ - function clearSoftFailures(testId?: string): void - - /** Expect Asymmetric Matchers */ - // function any(sample: unknown): AsyncMatcher - // function anything(): AsyncMatcher - // function arrayContaining(sample: Array): AsyncMatcher - // function arrayOf(sample: unknown): AsyncMatcher - // function closeTo(sample: number, precision?: number): AsyncMatcher - // function objectContaining(sample: Record): AsyncMatcher - // function stringContaining(sample: string): AsyncMatcher - // function stringMatching(sample: string | RegExp): AsyncMatcher - } - - namespace expectAsync { - - // TODO should we use expectAsync here instead? - /** Wdio soft assertion */ - /** - * 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 - */ - function soft(actual: T): jasmine.Matchers - // soft(actual: T): T extends PromiseLike ? Matchers, T> : Matchers - - /** - * Get all current soft assertion failures - */ - function getSoftFailures(testId?: string): ExpectWebdriverIO.SoftFailure[] - - /** - * Manually assert all soft failures (throws an error if any failures exist) - */ - function assertSoftFailures(testId?: string): void - - /** - * Clear all current soft assertion failures - */ - function clearSoftFailures(testId?: string): void - - /** Expect Asymmetric Matchers */ - // function any(sample: unknown): AsyncMatcher - // function anything(): AsyncMatcher - // function arrayContaining(sample: Array): AsyncMatcher - // function arrayOf(sample: unknown): AsyncMatcher - // function closeTo(sample: number, precision?: number): AsyncMatcher - // function objectContaining(sample: Record): AsyncMatcher - // function stringContaining(sample: string): AsyncMatcher - // function stringMatching(sample: string | RegExp): AsyncMatcher - } -} - -export {} \ No newline at end of file From fb7480937af1b2f9fdbc06d601320dfab29d56d3 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Tue, 1 Jul 2025 17:35:49 -0400 Subject: [PATCH 50/99] Add support for both `@jest/global` and `@types/jest` + add doc - Have a separate type check for `@jest/global` and `@types/jest` - Add back `jasmine.d.ts` for global `expect` - Add doc on supported configuration and their particularity --- docs/Framework.md | 102 ++ jasmine.d.ts | 1 + package-lock.json | 1265 ++++++++++++++++- package.json | 6 +- test-types/jasmine/tsconfig.json | 2 +- test-types/jasmine/types-jasmine.test.ts | 4 - .../customMatchers-module-expect.d.ts | 0 ...mMatchers-namespace-expectwebdriverio.d.ts | 0 test-types/jest-@jest_global/tsconfig.json | 13 + .../jest-@jest_global/types-jest.test.ts | 994 +++++++++++++ .../customMatchers-module-expect.d.ts | 26 + ...mMatchers-namespace-expectwebdriverio.d.ts | 14 + .../{jest => jest-@types_jest}/tsconfig.json | 1 - .../types-jest.test.ts | 0 test-types/mocha/tsconfig.json | 6 +- test-types/mocha/types-mocha.test.ts | 2 - types/expect-global.d.ts | 1 - types/expect-webdriverio.d.ts | 9 + 18 files changed, 2393 insertions(+), 53 deletions(-) create mode 100644 docs/Framework.md create mode 100644 jasmine.d.ts rename test-types/{jest => jest-@jest_global}/customMatchers/customMatchers-module-expect.d.ts (100%) rename test-types/{jest => jest-@jest_global}/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts (100%) create mode 100644 test-types/jest-@jest_global/tsconfig.json create mode 100644 test-types/jest-@jest_global/types-jest.test.ts create mode 100644 test-types/jest-@types_jest/customMatchers/customMatchers-module-expect.d.ts create mode 100644 test-types/jest-@types_jest/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts rename test-types/{jest => jest-@types_jest}/tsconfig.json (73%) rename test-types/{jest => jest-@types_jest}/types-jest.test.ts (100%) diff --git a/docs/Framework.md b/docs/Framework.md new file mode 100644 index 000000000..35b5c56d5 --- /dev/null +++ b/docs/Framework.md @@ -0,0 +1,102 @@ +# Expect-WebDriverIO Framework + +Expect-WebDriverIO is inspired by [`expect`](https://www.npmjs.com/package/expect) but also extending it. Therefore we can exploit usually everything provided by the API of expect with some WebDriverIO touch. + - Note: Yes, this is a package of Jest but it is usable without Jest. + +## Compatibility + +We can pair `expect-webdriver` with Jest, mocha, Jasmine. + - When an `expect` is defined globally, we usually overwrite it with the one of `expect-webdriverio` to have our defined assertions works out of the box. + +### Jest +We can use `expect-webdriver` with Jest with either the `@jest/global` (preferred) or the `@types/jest` (have global imports support) + - Note: Jest maintainer does not support `@types/jest`. In case this library gets out of date or has problems, support might be dropped. + +In each case, when used outside of [WDIO Testrunner](https://webdriver.io/docs/clioptions), types are required to be added in your `tsconfig.ts` + - Note: With Jest the matcher `toMatchSnapshot` and `toMatchInlineSnapshot` were overloaded. To resolved correctly the types `expect-webdriverio/jest` must be last. + +#### @jest/global +When paired with Jest and the `@jest/global`, we should use imports specifically + +```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 () => { + const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser + await expect(browser).toHaveUrl('https://example.com') + }) +}) +``` + +Expected `tsconfig.ts`: +```json + "types": [ + "@jest/globals", + "expect-webdriverio/jest", + ], +``` + + +#### @type/jest +When paired with Jest and the `@types/jest`, no imports are required. Global one are already defined correctly + +```ts +describe('My tests', async () => { + + it('should verify my browser to have the expected url', async () => { + const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser + await expect(browser).toHaveUrl('https://example.com') + }) +}) +``` + +Expected `tsconfig.ts`: +```json + "types": [ + "@types/jest", + "expect-webdriverio/jest", + ], +``` + +### Mocha +When paired with mocha, it can be used without (standalone) or with `chai` (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 () => { + const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser + await expect(browser).toHaveUrl('https://example.com') + }) +}) +``` + +Expected `tsconfig.ts`: +```json + "types": [ + "@types/mocha", + "expect-webdriverio", + ] +``` + +### Chai +TODO + +### Jasmine +When paired with Jasmine, it must also be used with `@wdio/jasmine-framework` from [webdriverio](https://github.com/webdriverio/webdriverio) since multiple configuration must be done prior to be runnable. For example, we actually force the `expect` being used to be the `expectAsync` instance so the promises resolved correctly. + +Expected `tsconfig.ts`: + - Note `expect-webdriverio/jasmine` must be before `@types/jasmine` to use the correct `expect` type of WebDriverIO globally +```json + "types": [ + "expect-webdriverio/jasmine", + "@types/jasmine", + ] +``` + diff --git a/jasmine.d.ts b/jasmine.d.ts new file mode 100644 index 000000000..7bf57efc3 --- /dev/null +++ b/jasmine.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 369c8a4b9..9d182157c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "lodash.isequal": "^4.5.0" }, "devDependencies": { + "@jest/globals": "^30.0.0", "@types/debug": "^4.1.12", "@types/jasmine": "^5.1.8", "@types/jest": "^30.0.0", @@ -83,6 +84,143 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/compat-data": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.7.tgz", + "integrity": "sha512-xgu/ySj2mTiUFmdE9yCMfBxLp4DHd5DwmbbD05YAuICfodYT3VvRxbrh81LGQ/8UpSdtMdfKMn3KouYDX59DGQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.7.tgz", + "integrity": "sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.27.7", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.7", + "@babel/types": "^7.27.7", + "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, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "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, + "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, + "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, + "bin": { + "semver": "bin/semver.js" + } + }, + "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, + "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, + "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, + "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", @@ -101,13 +239,35 @@ "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, + "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, + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/parser": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", - "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.7.tgz", + "integrity": "sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==", "dev": true, "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.27.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -116,10 +276,273 @@ "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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.27.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.7.tgz", + "integrity": "sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.5", + "@babel/parser": "^7.27.7", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/types": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", - "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.7.tgz", + "integrity": "sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -1195,6 +1618,111 @@ "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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 + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -1213,10 +1741,38 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/expect-utils": { + "node_modules/@jest/environment": { "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==", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.2.tgz", + "integrity": "sha512-hRLhZRJNxBiOhxIKSq2UkrlhMt3/zVFQOAi5lvS8T9I03+kxsbflwHJEF+eXEYXCrRGRhHwECT7CDk6DyngsRA==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "30.0.2", + "@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.3", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.3.tgz", + "integrity": "sha512-73BVLqfCeWjYWPEQoYjiRZ4xuQRhQZU0WdgvbyXGRHItKQqg5e6mt2y1kVhzLSuZpmUnccZHbGynoaL7IcLU3A==", + "dev": true, + "dependencies": { + "expect": "30.0.3", + "jest-snapshot": "30.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.3.tgz", + "integrity": "sha512-SMtBvf2sfX2agcT0dA9pXwcUrKvOSDqBY4e4iRfT+Hya33XzV35YVg+98YQFErVGA/VR1Gto5Y2+A6G9LSQ3Yg==", "dependencies": { "@jest/get-type": "30.0.1" }, @@ -1224,6 +1780,23 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@jest/fake-timers": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.2.tgz", + "integrity": "sha512-jfx0Xg7l0gmphTY9UKm5RtH12BlLYj/2Plj6wXjVW5Era4FZKfXeIvwC67WX+4q8UCFxYS20IgnMcFBcEU0DtA==", + "dev": true, + "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", @@ -1232,6 +1805,21 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@jest/globals": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.0.3.tgz", + "integrity": "sha512-fIduqNyYpMeeSr5iEAiMn15KxCzvrmxl7X7VwLDRGj7t5CoHtbF+7K3EvKk32mOUIJ4kIvFRlaixClMH2h/Vaw==", + "dev": true, + "dependencies": { + "@jest/environment": "30.0.2", + "@jest/expect": "30.0.3", + "@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", @@ -1258,6 +1846,109 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/snapshot-utils": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.1.tgz", + "integrity": "sha512-6Dpv7vdtoRiISEFwYF8/c7LIvqXD7xDXtLPNzC2xqAfBznKip0MQM+rkseKwUPUpv2PJ7KW/YsnwWXrIL2xF+A==", + "dev": true, + "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, + "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, + "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.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.2.tgz", + "integrity": "sha512-kJIuhLMTxRF7sc0gPzPtCDib/V9KwW3I2U25b+lYCYMVqHHSrcZopS8J8H+znx9yixuFv+Iozl8raLt/4MoxrA==", + "dev": true, + "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, + "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, + "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", @@ -1644,6 +2335,18 @@ "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, + "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", @@ -1973,6 +2676,24 @@ "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, + "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, + "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", @@ -2489,6 +3210,12 @@ "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 + }, "node_modules/@vitest/coverage-v8": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", @@ -3295,6 +4022,19 @@ "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, + "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", @@ -3459,22 +4199,121 @@ "devOptional": true, "license": "MIT" }, - "node_modules/async-retry": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", - "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "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, + "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, + "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, + "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, + "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": "MIT", "dependencies": { - "retry": "0.13.1" + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" } }, - "node_modules/b4a": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", - "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", - "devOptional": true, - "license": "Apache-2.0" + "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, + "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", @@ -3659,6 +4498,15 @@ "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, + "dependencies": { + "node-int64": "^0.4.0" + } + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -3805,6 +4653,15 @@ "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, + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001707", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", @@ -5128,13 +5985,13 @@ "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.3", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.3.tgz", + "integrity": "sha512-HXg6NvK35/cSYZCUKAtmlgCFyqKM4frEPbzrav5hRqb0GMz0E0lS5hfzYjSaiaE5ysnp/qI2aeZkeyeIAOeXzQ==", "dependencies": { - "@jest/expect-utils": "30.0.2", + "@jest/expect-utils": "30.0.3", "@jest/get-type": "30.0.1", - "jest-matcher-utils": "30.0.2", + "jest-matcher-utils": "30.0.3", "jest-message-util": "30.0.2", "jest-mock": "30.0.2", "jest-util": "30.0.2" @@ -5577,6 +6434,15 @@ "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, + "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", @@ -5711,6 +6577,12 @@ "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 + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -5776,6 +6648,15 @@ "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, + "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", @@ -5799,6 +6680,15 @@ "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, + "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", @@ -6194,6 +7084,17 @@ "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, + "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", @@ -6453,6 +7354,22 @@ "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, + "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", @@ -6514,9 +7431,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.3", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.3.tgz", + "integrity": "sha512-Q1TAV0cUcBTic57SVnk/mug0/ASyAqtSIOkr7RAlxx97llRYsM74+E8N5WdGJUlwCKwgxPAkVjKh653h1+HA9A==", "dependencies": { "@jest/diff-sequences": "30.0.1", "@jest/get-type": "30.0.1", @@ -6539,9 +7456,9 @@ } }, "node_modules/jest-diff/node_modules/@sinclair/typebox": { - "version": "0.34.35", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.35.tgz", - "integrity": "sha512-C6ypdODf2VZkgRT6sFM8E1F8vR+HcffniX0Kp8MsU8PIfrlXbNCBz0jzj17GjdmjTx1OtZzdH8+iALL21UjF5A==" + "version": "0.34.37", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.37.tgz", + "integrity": "sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==" }, "node_modules/jest-diff/node_modules/ansi-styles": { "version": "4.3.0", @@ -6607,14 +7524,38 @@ "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, + "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.3", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.3.tgz", + "integrity": "sha512-hMpVFGFOhYmIIRGJ0HgM9htC5qUiJ00famcc9sRFchJJiLZbbVKrAztcgE6VnXLRxA3XZ0bvNA7hQWh3oHXo/A==", "dependencies": { "@jest/get-type": "30.0.1", "chalk": "^4.1.2", - "jest-diff": "30.0.2", + "jest-diff": "30.0.3", "pretty-format": "30.0.2" }, "engines": { @@ -6801,6 +7742,113 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-snapshot": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.3.tgz", + "integrity": "sha512-F05JCohd3OA1N9+5aEPXA6I0qOfZDGIx0zTq5Z4yMBg2i1p5ELfBusjYAWwTkC12c7dHcbyth4QAfQbS7cRjow==", + "dev": true, + "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.3", + "@jest/get-type": "30.0.1", + "@jest/snapshot-utils": "30.0.1", + "@jest/transform": "30.0.2", + "@jest/types": "30.0.1", + "babel-preset-current-node-syntax": "^1.1.0", + "chalk": "^4.1.2", + "expect": "30.0.3", + "graceful-fs": "^4.2.11", + "jest-diff": "30.0.3", + "jest-matcher-utils": "30.0.3", + "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, + "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 + }, + "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, + "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, + "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, + "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, + "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", @@ -6857,6 +7905,37 @@ "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, + "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, + "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", @@ -6935,6 +8014,18 @@ "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, + "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", @@ -7302,6 +8393,15 @@ "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, + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", @@ -7557,6 +8657,12 @@ "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 + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -7889,6 +8995,15 @@ "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, + "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", @@ -8022,6 +9137,15 @@ "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, + "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", @@ -8119,6 +9243,15 @@ "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, + "engines": { + "node": ">= 6" + } + }, "node_modules/pkg-types": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz", @@ -9308,6 +10441,21 @@ "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, + "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", @@ -9525,6 +10673,12 @@ "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 + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -9570,6 +10724,15 @@ "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, + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "4.38.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.38.0.tgz", @@ -10028,6 +11191,15 @@ "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, + "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", @@ -10917,6 +12089,19 @@ "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, + "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", @@ -10948,6 +12133,12 @@ "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 + }, "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 92110aefa..26ece300e 100644 --- a/package.json +++ b/package.json @@ -52,9 +52,10 @@ "test:unit": "vitest --run", "test:types": "npm run ts && npm run tsc:root-types", "ts": "run-s ts:*", - "ts:jest": "cd test-types/jest && tsc --project ./tsconfig.json", + "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": "echo 'TODO dprevost to bring back' && exit 0 && cd test-types/jasmine && tsc --project ./tsconfig.json", + "ts:jasmine": "cd test-types/jasmine && tsc --project ./tsconfig.json", "watch": "npm run compile -- --watch", "prepare": "husky install" }, @@ -71,6 +72,7 @@ "@types/lodash.isequal": "^4.5.8", "@types/mocha": "^10.0.10", "@types/node": "^24.0.3", + "@jest/globals": "^30.0.0", "@vitest/coverage-v8": "^3.2.4", "@wdio/eslint": "^0.1.1", "@wdio/types": "^9.15.0", diff --git a/test-types/jasmine/tsconfig.json b/test-types/jasmine/tsconfig.json index 8bf61792c..320b43a82 100644 --- a/test-types/jasmine/tsconfig.json +++ b/test-types/jasmine/tsconfig.json @@ -6,7 +6,7 @@ "module": "node18", "skipLibCheck": true, "types": [ - "../../types/expect-global.d.ts", + "../../jasmine.d.ts", // Needs to be before "@types/jasmine" to ensure globals are the one of expect-webdriverio "@types/jasmine", ] } diff --git a/test-types/jasmine/types-jasmine.test.ts b/test-types/jasmine/types-jasmine.test.ts index 6b9b9ba7a..d5283b115 100644 --- a/test-types/jasmine/types-jasmine.test.ts +++ b/test-types/jasmine/types-jasmine.test.ts @@ -1,8 +1,4 @@ /* 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 diff --git a/test-types/jest/customMatchers/customMatchers-module-expect.d.ts b/test-types/jest-@jest_global/customMatchers/customMatchers-module-expect.d.ts similarity index 100% rename from test-types/jest/customMatchers/customMatchers-module-expect.d.ts rename to test-types/jest-@jest_global/customMatchers/customMatchers-module-expect.d.ts diff --git a/test-types/jest/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts b/test-types/jest-@jest_global/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts similarity index 100% rename from test-types/jest/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts rename to test-types/jest-@jest_global/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts diff --git a/test-types/jest-@jest_global/tsconfig.json b/test-types/jest-@jest_global/tsconfig.json new file mode 100644 index 000000000..693f1e852 --- /dev/null +++ b/test-types/jest-@jest_global/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "noEmit": true, + "noImplicitAny": true, + "target": "es2022", + "module": "node18", + "skipLibCheck": true, + "types": [ + "@jest/globals", + "../../jest.d.ts", + ], + } +} 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..5b9fc5668 --- /dev/null +++ b/test-types/jest-@jest_global/types-jest.test.ts @@ -0,0 +1,994 @@ +/* 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 () => { + // TODO dprevost: using @wdio/globals/types overlap with the local types/expect-webdriverio.d.ts, find how to work with this + // const chainableElement: ChainablePromiseElement = $('findMe') + // const chainableArray: ChainablePromiseArray = $$('ul>li') + + // const element: WebdriverIO.Element = await chainableElement?.getElement() + // const elementArray: WebdriverIO.ElementArray = await chainableArray?.getElements() + + 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) + //// @ts-expect-error TODO dprevost can we make the below fail? + // await expect(browser).toHaveUrl(expect.objectContaining({})) + }) + + 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() + }) + + // TODO - since we are overloading the `toMatchSnapshot` of jest.toMatchSnapshot, I wonder if we can achieve the below... + // it('should have ts errors when not an element or chainable', async () => { + // //@ts-expect-error + // await expect('.findme').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') + + // TODO how to make the below fails when the await is missing inf front of the expect from the asymmetric matcher? + // expectPromiseVoid = expect(chainableElement).toHaveCustomProperty( + // expect.toHaveCustomProperty(chainableElement) + // ) + + // @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) + + // TODO how to make the below fails when the await is missing inf front of the expect from the asymmetric matcher? + // expectPromiseVoid = expect(chainableElement).toHaveCustomProperty( + // expect.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) + ) + }) + + // TODO this is not supported in Wdio right now, maybe one day we can support it + // it('should support an async asymmetric matcher on a non async matcher', async () => { + // expectPromiseVoid = expect({ value: 5 }).toEqual({ + // value: expect.toHaveCustomProperty(chainableElement) + // }) + + // // @ts-expect-error + // expectVoid = expect({ value: 5 }).toEqual({ + // value: 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: ExpectWebdriverIO.MatchersAndInverse> = 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 () => { + // TODO dprevost should we bring back the support for this? + // 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 = browser.mock('**/api/todo*') + 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/todo', + method: 'POST', + statusCode: 200, + requestHeaders: { Authorization: 'foo' }, + responseHeaders: { Authorization: 'bar' }, + postData: { title: 'foo', description: 'bar' }, + response: { success: true }, + }) + + // TODO dprevost: Asymmetric matcher is not defined on the entire object in the .d.ts file, it is a bug? + // expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith(expect.objectContaining({ + // response: { success: true }, // [optional] object | function | custom matcher + // })) + + 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/todo', + 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')) + + //@ts-expect-error + expect.not.anything() + //@ts-expect-error + expect.not.any(Function) + //@ts-expect-error + expect.not.any(Number) + //@ts-expect-error + expect.not.any(Boolean) + //@ts-expect-error + expect.not.any(String) + //@ts-expect-error + expect.not.any(Symbol) + //@ts-expect-error + expect.not.any(Date) + //@ts-expect-error + 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: WdioCustomMatchers = expect.soft(element) + const expectElementChainable: WdioCustomMatchers = expect.soft(chainableElement) + + // @ts-expect-error + const expectElement2: WdioCustomMatchers, WebdriverIO.Element> = expect.soft(element) + // @ts-expect-error + const expectElementChainable2: WdioCustomMatchers, 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 have ts (or lint) errors when actual is a chainable not awaited', async () => { + // TODO dprevost: see if an eslint rule could help us here to detect missing await when not using wdio matchers + // expectPromiseVoid = expect.soft(chainableElement.getText()).toEqual('Basketball Shoes') + // expectPromiseVoid = expect.soft(chainableElement.getText()).toMatch(/€\d+/) + }) + + 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/tsconfig.json b/test-types/jest-@types_jest/tsconfig.json similarity index 73% rename from test-types/jest/tsconfig.json rename to test-types/jest-@types_jest/tsconfig.json index 4de10105e..69ed6c969 100644 --- a/test-types/jest/tsconfig.json +++ b/test-types/jest-@types_jest/tsconfig.json @@ -8,7 +8,6 @@ "types": [ "@types/jest", "../../jest.d.ts", // Needed to be after @types/jest to override the toMatchSnapshot typing - // "@wdio/globals/types", // This types interfere with the local types see how to workaround this ], } } diff --git a/test-types/jest/types-jest.test.ts b/test-types/jest-@types_jest/types-jest.test.ts similarity index 100% rename from test-types/jest/types-jest.test.ts rename to test-types/jest-@types_jest/types-jest.test.ts diff --git a/test-types/mocha/tsconfig.json b/test-types/mocha/tsconfig.json index 9cbe9c425..4a66cfff4 100644 --- a/test-types/mocha/tsconfig.json +++ b/test-types/mocha/tsconfig.json @@ -6,12 +6,8 @@ "module": "node18", "skipLibCheck": true, "types": [ - "../../types/expect-global.d.ts", "@types/mocha", - "expect", - "webdriverio", - - // "@wdio/globals/types", // TODO to review adding the global wdio types interfere with local types + "../../types/expect-global.d.ts", ] } } diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index d6d7c7ae4..12cd71b76 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -1,6 +1,4 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -/// - import type { ChainablePromiseElement, ChainablePromiseArray } from 'webdriverio' describe('type assertions', () => { diff --git a/types/expect-global.d.ts b/types/expect-global.d.ts index f2370583e..235bfe3d2 100644 --- a/types/expect-global.d.ts +++ b/types/expect-global.d.ts @@ -3,7 +3,6 @@ /** * 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 - * // TODO verify if this is also needed/forced when used with Jest! */ //// @ts-expect-error: IDE might flags this one but just does be concerned by it. This way the `tsc:root-types` can pass! diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index e18d150ac..5e15454fd 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -447,6 +447,15 @@ type WdioAsymmetricMatcher = ExpectWebdriverIO.PartialMatcher & { } 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 From c44baa75b8247cdd49b9ba1d4ee43a8221917255 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Tue, 1 Jul 2025 18:04:40 -0400 Subject: [PATCH 51/99] Review defined types in package.json --- docs/Framework.md | 2 +- package.json | 6 +++--- tsconfig.json | 2 +- types/expect-global.d.ts | 2 +- types/expect-webdriverio.d.ts | 4 +--- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/Framework.md b/docs/Framework.md index 35b5c56d5..ad6f49445 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -16,7 +16,7 @@ In each case, when used outside of [WDIO Testrunner](https://webdriver.io/docs/c - Note: With Jest the matcher `toMatchSnapshot` and `toMatchInlineSnapshot` were overloaded. To resolved correctly the types `expect-webdriverio/jest` must be last. #### @jest/global -When paired with Jest and the `@jest/global`, we should use imports specifically +When paired with Jest and the `@jest/global`, we should `import` the `expect` keyword from `expect-webdriverio` ```ts import { expect } from 'expect-webdriverio' diff --git a/package.json b/package.json index 26ece300e..4e66809d6 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,9 +33,9 @@ "types": "./jest.d.ts" } ], - "./types": "./types/jest-global.d.ts" + "./types": "./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" diff --git a/tsconfig.json b/tsconfig.json index c57148314..c515e5d15 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { /** - * Let's keep aligned with the WebdriverIO tsconfig.json. + * Let's aligned with the WebdriverIO tsconfig.json. * @see https://github.com/webdriverio/webdriverio/blob/main/tsconfig.json#L5 * TODO: dprevost, why using moduleResolution node16 in the above link? */ diff --git a/types/expect-global.d.ts b/types/expect-global.d.ts index 235bfe3d2..b546de019 100644 --- a/types/expect-global.d.ts +++ b/types/expect-global.d.ts @@ -12,4 +12,4 @@ 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 5e15454fd..d8c65d772 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -35,15 +35,13 @@ type WdioOnlyPromiseLike = ElementPromise | ElementArrayPromise | ChainablePromi */ type WdioOnlyMaybePromiseLike = ElementPromise | ElementArrayPromise | ChainablePromiseElement | ChainablePromiseArray | WebdriverIO.Browser | WebdriverIO.Element | WebdriverIO.ElementArray -// TODO dprevost - check if custom matchers (https://webdriver.io/docs/custommatchers/) will still work aka webdriverio/expect-webdriverio#1408 - /** * 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 user in conjunctions with the Type of the `actual` parameter of the `expect` + * 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 From a3aa5bfb2b2e0f7d9978d9b1f6066ab5eef7ac62 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Tue, 1 Jul 2025 19:06:17 -0400 Subject: [PATCH 52/99] Review doc and fix some type issues --- docs/Framework.md | 49 +++++++++++++--------- test-types/jest-@jest_global/tsconfig.json | 4 -- types/expect-webdriverio.d.ts | 19 +++++---- 3 files changed, 39 insertions(+), 33 deletions(-) diff --git a/docs/Framework.md b/docs/Framework.md index ad6f49445..9a1e8577c 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -12,10 +12,10 @@ We can pair `expect-webdriver` with Jest, mocha, Jasmine. We can use `expect-webdriver` with Jest with either the `@jest/global` (preferred) or the `@types/jest` (have global imports support) - Note: Jest maintainer does not support `@types/jest`. In case this library gets out of date or has problems, support might be dropped. -In each case, when used outside of [WDIO Testrunner](https://webdriver.io/docs/clioptions), types are required to be added in your `tsconfig.ts` +In each case, when used **outside of [WDIO Testrunner](https://webdriver.io/docs/clioptions)**, types are required to be added in your `tsconfig.ts` - Note: With Jest the matcher `toMatchSnapshot` and `toMatchInlineSnapshot` were overloaded. To resolved correctly the types `expect-webdriverio/jest` must be last. -#### @jest/global +#### With `@jest/global` When paired with Jest and the `@jest/global`, we should `import` the `expect` keyword from `expect-webdriverio` ```ts @@ -31,16 +31,17 @@ describe('My tests', async () => { }) ``` -Expected `tsconfig.ts`: +No `types` is expected in `tsconfig.ts` +Optionally, to not need `import { expect } from 'expect-webdriverio'` you can use the bellow ```json - "types": [ - "@jest/globals", - "expect-webdriverio/jest", - ], -``` - - -#### @type/jest +{ + "compilerOptions": { + "types": ["expect-webdriverio/types"] + } +} +``` + +#### With `@type/jest` When paired with Jest and the `@types/jest`, no imports are required. Global one are already defined correctly ```ts @@ -53,12 +54,16 @@ describe('My tests', async () => { }) ``` -Expected `tsconfig.ts`: +Expected in `tsconfig.ts`: ```json +{ + "compilerOptions": { "types": [ - "@types/jest", - "expect-webdriverio/jest", + "@types/jest", + "expect-webdriverio/jest", // Must be after for overloaded matcher `toMatchSnapshot` and `toMatchInlineSnapshot` ], + } +} ``` ### Mocha @@ -77,12 +82,16 @@ describe('My tests', async () => { }) ``` -Expected `tsconfig.ts`: +Expected in `tsconfig.ts`: ```json +{ + "compilerOptions": { "types": [ - "@types/mocha", - "expect-webdriverio", - ] + "@types/mocha", + "expect-webdriverio", + ], + } +} ``` ### Chai @@ -91,11 +100,11 @@ TODO ### Jasmine When paired with Jasmine, it must also be used with `@wdio/jasmine-framework` from [webdriverio](https://github.com/webdriverio/webdriverio) since multiple configuration must be done prior to be runnable. For example, we actually force the `expect` being used to be the `expectAsync` instance so the promises resolved correctly. -Expected `tsconfig.ts`: +Expected in `tsconfig.ts`: - Note `expect-webdriverio/jasmine` must be before `@types/jasmine` to use the correct `expect` type of WebDriverIO globally ```json "types": [ - "expect-webdriverio/jasmine", + "expect-webdriverio/jasmine", // Must be before for the global to apply correctly "@types/jasmine", ] ``` diff --git a/test-types/jest-@jest_global/tsconfig.json b/test-types/jest-@jest_global/tsconfig.json index 693f1e852..06f3ef3c7 100644 --- a/test-types/jest-@jest_global/tsconfig.json +++ b/test-types/jest-@jest_global/tsconfig.json @@ -5,9 +5,5 @@ "target": "es2022", "module": "node18", "skipLibCheck": true, - "types": [ - "@jest/globals", - "../../jest.d.ts", - ], } } diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index d8c65d772..43301d0c6 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -192,37 +192,37 @@ interface WdioElementOrArrayMatchers { /** * `WebdriverIO.Element` -> `isClickable` */ - toBeClickable: FnWhenElementOrArrayLike Promise> + toBeClickable: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `!isEnabled` */ - toBeDisabled: FnWhenElementOrArrayLike Promise> + toBeDisabled: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isDisplayedInViewport` */ - toBeDisplayedInViewport: FnWhenElementOrArrayLike Promise> + toBeDisplayedInViewport: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isEnabled` */ - toBeEnabled: FnWhenElementOrArrayLike Promise> + toBeEnabled: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isFocused` */ - toBeFocused: FnWhenElementOrArrayLike Promise> + toBeFocused: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isSelected` */ - toBeSelected: FnWhenElementOrArrayLike Promise> + toBeSelected: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isSelected` */ - toBeChecked: FnWhenElementOrArrayLike Promise> + toBeChecked: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `$$('./*').length` @@ -303,7 +303,7 @@ interface WdioElementOrArrayMatchers { * Element's computed label equals the computed label provided */ toHaveComputedLabel: FnWhenElementOrArrayLike| Array, + computedLabel: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, options?: ExpectWebdriverIO.StringOptions ) => Promise> @@ -312,7 +312,7 @@ interface WdioElementOrArrayMatchers { * Element's computed role equals the computed role provided */ toHaveComputedRole: FnWhenElementOrArrayLike| Array, + computedRole: string | RegExp | ExpectWebdriverIO.PartialMatcher | Array, options?: ExpectWebdriverIO.StringOptions ) => Promise> @@ -721,6 +721,7 @@ declare namespace ExpectWebdriverIO { * Some properties are omitted for the type check to work correctly. */ // TODO dprevost: verify if we do breaking changes on this PartialMatcher, since before it was the AsymmetricMatcher interface used everywhere. + // TODO dprevost: verify if we should restrict to possible asymmetric matchers used! type PartialMatcher = Omit, 'sample' | 'inverse' | '$$typeof'> } From 93d0bdd4f6601b60fcc11f98eec0e9a0d6fd7ce3 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Tue, 1 Jul 2025 19:10:40 -0400 Subject: [PATCH 53/99] Doc review --- docs/Framework.md | 56 +++++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/docs/Framework.md b/docs/Framework.md index 9a1e8577c..4d808a45a 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -5,18 +5,18 @@ Expect-WebDriverIO is inspired by [`expect`](https://www.npmjs.com/package/expec ## Compatibility -We can pair `expect-webdriver` with Jest, mocha, Jasmine. - - When an `expect` is defined globally, we usually overwrite it with the one of `expect-webdriverio` to have our defined assertions works out of the box. +We can pair `expect-webdriverio` with [Jest](https://jestjs.io/), [Mocha](https://mochajs.org/), [Jasmine](https://jasmine.github.io/). + - When an `expect` is defined globally, we usually overwrite it with the one of `expect-webdriverio` to have our defined assertions work out of the box. ### Jest -We can use `expect-webdriver` with Jest with either the `@jest/global` (preferred) or the `@types/jest` (have global imports support) - - Note: Jest maintainer does not support `@types/jest`. In case this library gets out of date or has problems, support might be dropped. +We can use `expect-webdriverio` with [Jest](https://jestjs.io/) with either the [`@jest/globals`](https://www.npmjs.com/package/@jest/globals) (preferred) or the [`@types/jest`](https://www.npmjs.com/package/@types/jest) (has global imports support) + - Note: Jest maintainers do not support `@types/jest`. In case this library gets out of date or has problems, support might be dropped. -In each case, when used **outside of [WDIO Testrunner](https://webdriver.io/docs/clioptions)**, types are required to be added in your `tsconfig.ts` - - Note: With Jest the matcher `toMatchSnapshot` and `toMatchInlineSnapshot` were overloaded. To resolved correctly the types `expect-webdriverio/jest` must be last. +In each case, when used **outside of [WDIO Testrunner](https://webdriver.io/docs/clioptions)**, types are required to be added in your `tsconfig.json` + - Note: With Jest the matchers `toMatchSnapshot` and `toMatchInlineSnapshot` were overloaded. To resolve correctly the types `expect-webdriverio/jest` must be last. -#### With `@jest/global` -When paired with Jest and the `@jest/global`, we should `import` the `expect` keyword from `expect-webdriverio` +#### With `@jest/globals` +When paired with [Jest](https://jestjs.io/) and the [`@jest/globals`](https://www.npmjs.com/package/@jest/globals), we should `import` the `expect` keyword from `expect-webdriverio` ```ts import { expect } from 'expect-webdriverio' @@ -31,8 +31,8 @@ describe('My tests', async () => { }) ``` -No `types` is expected in `tsconfig.ts` -Optionally, to not need `import { expect } from 'expect-webdriverio'` you can use the bellow +No `types` is expected in `tsconfig.json` +Optionally, to not need `import { expect } from 'expect-webdriverio'` you can use the below ```json { "compilerOptions": { @@ -41,8 +41,8 @@ Optionally, to not need `import { expect } from 'expect-webdriverio'` you can us } ``` -#### With `@type/jest` -When paired with Jest and the `@types/jest`, no imports are required. Global one are already defined correctly +#### With `@types/jest` +When paired with [Jest](https://jestjs.io/) and the [`@types/jest`](https://www.npmjs.com/package/@types/jest), no imports are required. Global ones are already defined correctly ```ts describe('My tests', async () => { @@ -54,22 +54,22 @@ describe('My tests', async () => { }) ``` -Expected in `tsconfig.ts`: +Expected in `tsconfig.json`: ```json { "compilerOptions": { "types": [ "@types/jest", - "expect-webdriverio/jest", // Must be after for overloaded matcher `toMatchSnapshot` and `toMatchInlineSnapshot` - ], + "expect-webdriverio/jest" // Must be after for overloaded matchers `toMatchSnapshot` and `toMatchInlineSnapshot` + ] } } ``` ### Mocha -When paired with mocha, it can be used without (standalone) or with `chai` (or any other assertion Library) +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 +#### Standalone No import is required, everything is set globally ```ts @@ -82,30 +82,34 @@ describe('My tests', async () => { }) ``` -Expected in `tsconfig.ts`: +Expected in `tsconfig.json`: ```json { "compilerOptions": { "types": [ "@types/mocha", - "expect-webdriverio", - ], + "expect-webdriverio/types" + ] } } ``` -### Chai -TODO +#### Chai +TODO - Integration with [Chai](https://www.chaijs.com/) assertion library ### Jasmine -When paired with Jasmine, it must also be used with `@wdio/jasmine-framework` from [webdriverio](https://github.com/webdriverio/webdriverio) since multiple configuration must be done prior to be runnable. For example, we actually force the `expect` being used to be the `expectAsync` instance so the promises resolved correctly. +When paired with [Jasmine](https://jasmine.github.io/), it must also be used with [`@wdio/jasmine-framework`](https://www.npmjs.com/package/@wdio/jasmine-framework) from [webdriverio](https://github.com/webdriverio/webdriverio) since multiple configurations must be done prior to being runnable. For example, we actually force the `expect` being used to be the `expectAsync` instance so the promises resolve correctly. -Expected in `tsconfig.ts`: - - Note `expect-webdriverio/jasmine` must be before `@types/jasmine` to use the correct `expect` type of WebDriverIO globally +Expected in `tsconfig.json`: + - Note: `expect-webdriverio/jasmine` must be before `@types/jasmine` to use the correct `expect` type of WebDriverIO globally ```json +{ + "compilerOptions": { "types": [ "expect-webdriverio/jasmine", // Must be before for the global to apply correctly - "@types/jasmine", + "@types/jasmine" ] + } +} ``` From 9d2324d2ee8b383a3080dec30b5608540384a8a6 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Thu, 3 Jul 2025 09:19:23 -0400 Subject: [PATCH 54/99] Test and document Jasmine typing --- .npmrc | 0 check-node-version.js | 0 docs/{Extend.md => CustomMatchers.md} | 2 +- docs/Examples.md | 2 +- docs/Framework.md | 66 +- docs/Types.md | 21 +- jasmine.d.ts | 20 +- package.json | 3 +- .../customMatchers-module-expect.d.ts | 29 + ...mMatchers-namespace-expectwebdriverio.d.ts | 14 + test-types/jasmine-async/tsconfig.json | 13 + .../jasmine-async/types-jasmine.test.ts | 885 ++++++++++++++++++ test-types/jasmine/tsconfig.json | 2 +- test-types/jasmine/types-jasmine.test.ts | 775 ++++++++------- types/expect-webdriverio.d.ts | 92 +- 15 files changed, 1475 insertions(+), 449 deletions(-) create mode 100644 .npmrc create mode 100644 check-node-version.js rename docs/{Extend.md => CustomMatchers.md} (90%) create mode 100644 test-types/jasmine-async/customMatchers/customMatchers-module-expect.d.ts create mode 100644 test-types/jasmine-async/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts create mode 100644 test-types/jasmine-async/tsconfig.json create mode 100644 test-types/jasmine-async/types-jasmine.test.ts diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..e69de29bb diff --git a/check-node-version.js b/check-node-version.js new file mode 100644 index 000000000..e69de29bb diff --git a/docs/Extend.md b/docs/CustomMatchers.md similarity index 90% rename from docs/Extend.md rename to docs/CustomMatchers.md index 6975ed5a5..fb4b3a9fd 100644 --- a/docs/Extend.md +++ b/docs/CustomMatchers.md @@ -2,7 +2,7 @@ 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 +- Jasmine see [custom matchers](https://jasmine.github.io/tutorials/custom_matchers) doc - Everyone else see Jest's [expect.extend](https://jestjs.io/docs/en/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 index 4d808a45a..2a377d19f 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -98,18 +98,74 @@ Expected in `tsconfig.json`: TODO - Integration with [Chai](https://www.chaijs.com/) assertion library ### Jasmine -When paired with [Jasmine](https://jasmine.github.io/), it must also be used with [`@wdio/jasmine-framework`](https://www.npmjs.com/package/@wdio/jasmine-framework) from [webdriverio](https://github.com/webdriverio/webdriverio) since multiple configurations must be done prior to being runnable. For example, we actually force the `expect` being used to be the `expectAsync` instance so the promises resolve correctly. +When paired with [Jasmine](https://jasmine.github.io/), [`@wdio/jasmine-framework`](https://www.npmjs.com/package/@wdio/jasmine-framework) is also required to have it configured correctly as it needs to force the `expect` to be `expectAsync` and also to register the wdio matchers with `addAsyncMatcher` since `expect-webdriverio` only supports the jest style `expect.extend` version. + +The types `expect-webdriverio/jasmine` is still offers but subject to removal or to be moved into `@wdio/jasmine-framework`. The usage of `expectAsync` is also subject to future removal. + +#### Jasmine `expectAsync` +Since the above types augment the `AsyncMatcher` of `Jasmine` then with this library alone it look like the below even though it is not runnable since the matcher are not registered + +```ts +describe('My tests', async () => { + + it('should verify my browser to have the expected url', async () => { + const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser + await expectAsync(browser).toHaveUrl('https://example.com') + }) +}) +``` + +Expected in `tsconfig.json`: +```json +{ + "compilerOptions": { + "types": [ + "@types/jasmine", + "expect-webdriverio/jasmine" + ] + } +} +``` + +#### `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/types"` +import { expect as wdioExpect } from 'expect-webdriverio' + +describe('My tests', async () => { + + it('should verify my browser to have the expected url', async () => { + const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser + await wdioExpect(browser).toHaveUrl('https://example.com') + }) +}) +``` Expected in `tsconfig.json`: - - Note: `expect-webdriverio/jasmine` must be before `@types/jasmine` to use the correct `expect` type of WebDriverIO globally ```json { "compilerOptions": { "types": [ - "expect-webdriverio/jasmine", // Must be before for the global to apply correctly - "@types/jasmine" - ] + "@types/jasmine", + "expect-webdriverio/types", // Force expect to be the 'expect-webdriverio', to comment and use the import above if it conflict with Jasmine + ] } } ``` +#### Asymmetric matcher +Asymmetric matcher has limited support, even though `jasmine.stringContaining` has not error it is potential not working even with `@wdio/jasmine-framework`, but the below should: + +```ts +describe('My tests', async () => { + + it('should verify my browser to have the expected url', async () => { + const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser + await expectAsync(browser).toHaveUrl(wdioExpect.stringContaining('WebdriverIO')) + }) +}) +``` + + diff --git a/docs/Types.md b/docs/Types.md index ffc12287f..b7e93e2c8 100644 --- a/docs/Types.md +++ b/docs/Types.md @@ -1,10 +1,13 @@ +# 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 +- `"expect-webdriverio/jest"` for Jest +- `"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) @@ -19,3 +22,15 @@ It's required to create `jsconfig.json` in project root and refer to the type de ] } ``` + +## Jasmine special case +Jasmine is different from Jest or the standard `expect` definition since it supports promises using `expectAsync` which make 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`, since it replaces `jasmine.expect` with `jasmine.expectAsync`, then matchers are usable on the keyword `expect`, but still typing on `expect` directly from Jasmine namespace is not supported as of today! \ No newline at end of file diff --git a/jasmine.d.ts b/jasmine.d.ts index 7bf57efc3..74b18c760 100644 --- a/jasmine.d.ts +++ b/jasmine.d.ts @@ -1 +1,19 @@ -/// \ No newline at end of file +/// + +/** + * Utility type that wraps non-Promise types in a Promise for Jasmine async matchers. + * If U is already a Promise, PromiseLike, or Chainable, return U as-is. + * Otherwise, wrap U in a Promise. + */ +type EnsurePromise = U extends Promise | PromiseLike | WdioPromiseLike ? U : Promise + +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, which will be wrapped in a Promise if it's not already one + * Both T,U must stay named as they are to override the default `AsyncMatchers` type from Jasmine. + */ + interface AsyncMatchers | void> extends ExpectWebdriverIO.Matchers, T> {} +} \ No newline at end of file diff --git a/package.json b/package.json index 4e66809d6..a3aead5cf 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "types": "./jest.d.ts" } ], - "./types": "./types/expect-global.d.ts" + "./types": "./types/expect-global.d.ts", + "./expect-global": "./types/expect-global.d.ts" }, "types": "./types/expect-webdriverio.d.ts", "typeScriptVersion": "3.8.3", 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.test.ts b/test-types/jasmine-async/types-jasmine.test.ts new file mode 100644 index 000000000..0164eb026 --- /dev/null +++ b/test-types/jasmine-async/types-jasmine.test.ts @@ -0,0 +1,885 @@ +/* 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) + //// @ts-expect-error TODO dprevost can we make the below fail? + // await expectAsync(browser).toHaveUrl(wdioExpect.objectContaining({})) + }) + + 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 () => { + expectVoid = expectAsync(element).toMatchSnapshot() + expectVoid = expectAsync(element).toMatchSnapshot('test label') + expectVoid = expectAsync(element).not.toMatchSnapshot('test label') + + expectPromiseVoid = expectAsync(chainableElement).toMatchSnapshot() + expectPromiseVoid = expectAsync(chainableElement).toMatchSnapshot('test label') + expectPromiseVoid = expectAsync(chainableElement).not.toMatchSnapshot('test label') + }) + + // TODO - since we are overloading the `toMatchSnapshot` of jest.toMatchSnapshot, I wonder if we can achieve the below... + // it('should have ts errors when not an element or chainable', async () => { + // //@ts-expect-error + // await expectAsync('.findme').toMatchSnapshot() + // }) + }) + + describe('toMatchInlineSnapshot', () => { + + it('should be correctly supported', async () => { + expectVoid = expectAsync(element).toMatchInlineSnapshot() + expectVoid = expectAsync(element).toMatchInlineSnapshot('test snapshot') + expectVoid = 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) + + // TODO dprevost this one seems to be a problem, it should be a promise!!!!! + // Or as an asymmetric matcher: + expectVoid = expect({ value: 5 }).toEqual({ + value: wdioExpect.toBeWithinRange(1, 10) + }) + + // @ts-expect-error + expectVoid = expectAsync(5).toBeWithinRange(1, '10') + // @ts-expect-error + expectPromiseVoid = 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') + + // TODO how to make the below fails when the await is missing inf front of the expect from the asymmetric matcher? + // expectPromiseVoid = expectAsync(chainableElement).toHaveCustomProperty( + // wdioExpect.toHaveCustomProperty(chainableElement) + // ) + + // @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) + + // TODO how to make the below fails when the await is missing inf front of the expect from the asymmetric matcher? + // expectPromiseVoid = expectAsync(chainableElement).toHaveCustomProperty( + // wdioExpect.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) + ) + }) + + // TODO this is not supported in Wdio right now, maybe one day we can support it + // it('should support an async asymmetric matcher on a non async matcher', async () => { + // expectPromiseVoid = expectAsync({ value: 5 }).toEqual({ + // value: wdioExpect.toHaveCustomProperty(chainableElement) + // }) + + // // @ts-expect-error + // expectVoid = expectAsync({ value: 5 }).toEqual({ + // value: 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) + + //@ts-expect-error + expectPromiseVoid = expectAsync(true).toBe(true) + //@ts-expect-error + expectPromiseVoid = expectAsync(true).not.toBe(true) + }) + + // // TODO dprevost: Is this a valid use case? Should we support it? + // 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) + + // //@ts-expect-error + // expectPromiseVoid = expectAsync(chainableElement).toBe(true) + // //@ts-expect-error + // expectPromiseVoid = expectAsync(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) + + expectPromiseUnknown = expectAsync(promiseBoolean).toBe(true) + expectPromiseUnknown = expectAsync(promiseBoolean).not.toBe(true) + + //@ts-expect-error + expectPromiseVoid = expectAsync(promiseBoolean).toBe(true) + //@ts-expect-error + 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')) + + //@ts-expect-error + expectPromiseVoid = expectAsync('text').toBe(true) + //@ts-expect-error + expectPromiseVoid = expectAsync('text').not.toBe(true) + //@ts-expect-error + expectPromiseVoid = expectAsync('text').toBe(wdioExpect.stringContaining('text')) + //@ts-expect-error + 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: ExpectWebdriverIO.MatchersAndInverse> = expectAsync(booleanPromise) + const expectPromiseBoolean2: ExpectWebdriverIO.Matchers> = expectAsync(booleanPromise).not + }) + + it('should work with resolves & rejects correctly', async () => { + // TODO dprevost should we support this in Wdio since we do not even use it or document it? + // expectPromiseVoid = expectAsync(booleanPromise).resolves.toBe(true) + // expectPromiseVoid = expectAsync(booleanPromise).rejects.toBe(true) + + //@ts-expect-error + expectVoid = expectAsync(booleanPromise).resolves.toBe(true) + //@ts-expect-error + expectVoid = expectAsync(booleanPromise).rejects.toBe(true) + + }) + + it('should not support chainable and expect PromiseVoid with toBe', async () => { + //@ts-expect-error + expectPromiseVoid = expectAsync(chainableElement).toBe(true) + //@ts-expect-error + 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/todo', + method: 'POST', + statusCode: 200, + requestHeaders: { Authorization: 'foo' }, + responseHeaders: { Authorization: 'bar' }, + postData: { title: 'foo', description: 'bar' }, + response: { success: true }, + }) + + // TODO dprevost: Asymmetric matcher is not defined on the entire object in the .d.ts file, it is a bug? + // expectPromiseVoid = expectAsync(promiseNetworkMock).toBeRequestedWith(wdioExpect.objectContaining({ + // response: { success: true }, // [optional] object | function | custom matcher + // })) + + 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/todo', + 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')) + + // TODO dprevost: Should we support these? + // wdioExpect.not.anything() + // wdioExpect.not.any(Function) + // wdioExpect.not.any(Number) + // wdioExpect.not.any(Boolean) + // wdioExpect.not.any(String) + // wdioExpect.not.any(Symbol) + // wdioExpect.not.any(Date) + // wdioExpect.not.any(Error) + }) + + 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 have ts (or lint) errors when actual is a chainable not awaited', async () => { + // TODO dprevost: see if an eslint rule could help us here to detect missing await when not using wdio matchers + // expectPromiseVoid = wdioExpect.soft(chainableElement.getText()).toEqual('Basketball Shoes') + // expectPromiseVoid = wdioExpect.soft(chainableElement.getText()).toMatch(/€\d+/) + }) + + 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 () => { + // TODO dprevost: Is this valid since expect is from WebdriverIO and we force it to be `expectAsync` in the main project? + expectAsync({}).toEqual(jasmine.any(Object)) + expectAsync(12).toEqual(jasmine.any(Number)) + }) + + }) +}) diff --git a/test-types/jasmine/tsconfig.json b/test-types/jasmine/tsconfig.json index 320b43a82..90080327e 100644 --- a/test-types/jasmine/tsconfig.json +++ b/test-types/jasmine/tsconfig.json @@ -6,8 +6,8 @@ "module": "node18", "skipLibCheck": true, "types": [ - "../../jasmine.d.ts", // Needs to be before "@types/jasmine" to ensure globals are the one of expect-webdriverio "@types/jasmine", + "../../jasmine.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 index d5283b115..bd5bf7fbc 100644 --- a/test-types/jasmine/types-jasmine.test.ts +++ b/test-types/jasmine/types-jasmine.test.ts @@ -1,4 +1,7 @@ /* 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 @@ -17,67 +20,67 @@ describe('type assertions', () => { describe('toHaveUrl', () => { it('should be supported correctly', async () => { - expectPromiseVoid = expect(browser).toHaveUrl('https://example.com') - expectPromiseVoid = expect(browser).not.toHaveUrl('https://example.com') + expectPromiseVoid = wdioExpect(browser).toHaveUrl('https://example.com') + expectPromiseVoid = wdioExpect(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()) + 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 = expect(browser).toHaveUrl('https://example.com') + expectVoid = wdioExpect(browser).toHaveUrl('https://example.com') // @ts-expect-error - expectVoid = expect(browser).not.toHaveUrl('https://example.com') + expectVoid = wdioExpect(browser).not.toHaveUrl('https://example.com') // @ts-expect-error - expectVoid = expect(browser).toHaveUrl(expect.stringContaining('WebdriverIO')) + expectVoid = wdioExpect(browser).toHaveUrl(wdioExpect.stringContaining('WebdriverIO')) // @ts-expect-error - await expect(browser).toHaveUrl(6) + await wdioExpect(browser).toHaveUrl(6) //// @ts-expect-error TODO dprevost can we make the below fail? - // await expect(browser).toHaveUrl(expect.objectContaining({})) + // await wdioExpect(browser).toHaveUrl(wdioExpect.objectContaining({})) }) it('should have ts errors when actual is not a Browser element', async () => { // @ts-expect-error - await expect(element).toHaveUrl('https://example.com') + await wdioExpect(element).toHaveUrl('https://example.com') // @ts-expect-error - await expect(element).not.toHaveUrl('https://example.com') + await wdioExpect(element).not.toHaveUrl('https://example.com') // @ts-expect-error - await expect(true).toHaveUrl('https://example.com') + await wdioExpect(true).toHaveUrl('https://example.com') // @ts-expect-error - await expect(true).not.toHaveUrl('https://example.com') + await wdioExpect(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') + expectPromiseVoid = wdioExpect(browser).toHaveTitle('https://example.com') + expectPromiseVoid = wdioExpect(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()) + expectPromiseVoid = wdioExpect(browser).toHaveTitle(wdioExpect.stringContaining('WebdriverIO')) + expectPromiseVoid = wdioExpect(browser).toHaveTitle(wdioExpect.any(String)) + expectPromiseVoid = wdioExpect(browser).toHaveTitle(wdioExpect.anything()) // @ts-expect-error - expectVoid = expect(browser).toHaveTitle('https://example.com') + expectVoid = wdioExpect(browser).toHaveTitle('https://example.com') // @ts-expect-error - expectVoid = expect(browser).not.toHaveTitle('https://example.com') + expectVoid = wdioExpect(browser).not.toHaveTitle('https://example.com') // @ts-expect-error - expectVoid = expect(browser).toHaveTitle(expect.stringContaining('WebdriverIO')) + expectVoid = wdioExpect(browser).toHaveTitle(wdioExpect.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') + await wdioExpect(element).toHaveTitle('https://example.com') // @ts-expect-error - await expect(element).not.toHaveTitle('https://example.com') + await wdioExpect(element).not.toHaveTitle('https://example.com') // @ts-expect-error - await expect(true).toHaveTitle('https://example.com') + await wdioExpect(true).toHaveTitle('https://example.com') // @ts-expect-error - await expect(true).not.toHaveTitle('https://example.com') + await wdioExpect(true).not.toHaveTitle('https://example.com') }) }) }) @@ -87,250 +90,250 @@ describe('type assertions', () => { describe('toBeDisabled', () => { it('should be supported correctly', async () => { // Element - expectPromiseVoid = expect(element).toBeDisabled() - expectPromiseVoid = expect(element).not.toBeDisabled() + expectPromiseVoid = wdioExpect(element).toBeDisabled() + expectPromiseVoid = wdioExpect(element).not.toBeDisabled() // Element array - expectPromiseVoid = expect(elementArray).toBeDisabled() - expectPromiseVoid = expect(elementArray).not.toBeDisabled() + expectPromiseVoid = wdioExpect(elementArray).toBeDisabled() + expectPromiseVoid = wdioExpect(elementArray).not.toBeDisabled() // Chainable element - expectPromiseVoid = expect(chainableElement).toBeDisabled() - expectPromiseVoid = expect(chainableElement).not.toBeDisabled() + expectPromiseVoid = wdioExpect(chainableElement).toBeDisabled() + expectPromiseVoid = wdioExpect(chainableElement).not.toBeDisabled() // Chainable element array - expectPromiseVoid = expect(chainableArray).toBeDisabled() - expectPromiseVoid = expect(chainableArray).not.toBeDisabled() + expectPromiseVoid = wdioExpect(chainableArray).toBeDisabled() + expectPromiseVoid = wdioExpect(chainableArray).not.toBeDisabled() // @ts-expect-error - expectVoid = expect(element).toBeDisabled() + expectVoid = wdioExpect(element).toBeDisabled() // @ts-expect-error - expectVoid = expect(element).not.toBeDisabled() + expectVoid = wdioExpect(element).not.toBeDisabled() }) it('should have ts errors when actual is not an element', async () => { // @ts-expect-error - await expect(browser).toBeDisabled() + await wdioExpect(browser).toBeDisabled() // @ts-expect-error - await expect(browser).not.toBeDisabled() + await wdioExpect(browser).not.toBeDisabled() // @ts-expect-error - await expect(true).toBeDisabled() + await wdioExpect(true).toBeDisabled() // @ts-expect-error - await expect(true).not.toBeDisabled() + await wdioExpect(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( + 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 = expect(element).not.toHaveText('text') + expectPromiseVoid = wdioExpect(element).not.toHaveText('text') // @ts-expect-error - expectVoid = expect(element).toHaveText('text') + expectVoid = wdioExpect(element).toHaveText('text') // @ts-expect-error - await expect(element).toHaveText(6) + await wdioExpect(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 = 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 = expect(chainableElement).not.toHaveText('text') + expectPromiseVoid = wdioExpect(chainableElement).not.toHaveText('text') // @ts-expect-error - expectVoid = expect(chainableElement).toHaveText('text') + expectVoid = wdioExpect(chainableElement).toHaveText('text') // @ts-expect-error - await expect(chainableElement).toHaveText(6) + await wdioExpect(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 = 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 = expect(elementArray).not.toHaveText('text') + expectPromiseVoid = wdioExpect(elementArray).not.toHaveText('text') // @ts-expect-error - expectVoid = expect(elementArray).toHaveText('text') + expectVoid = wdioExpect(elementArray).toHaveText('text') // @ts-expect-error - await expect(elementArray).toHaveText(6) + await wdioExpect(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 = 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 = expect(chainableArray).not.toHaveText('text') + expectPromiseVoid = wdioExpect(chainableArray).not.toHaveText('text') // @ts-expect-error - expectVoid = expect(chainableArray).toHaveText('text') + expectVoid = wdioExpect(chainableArray).toHaveText('text') // @ts-expect-error - await expect(chainableArray).toHaveText(6) + await wdioExpect(chainableArray).toHaveText(6) // @ts-expect-error - await expect(browser).toHaveText('text') + await wdioExpect(browser).toHaveText('text') }) it('should have ts errors when actual is not an element', async () => { // @ts-expect-error - await expect(browser).toHaveText('text') + await wdioExpect(browser).toHaveText('text') // @ts-expect-error - await expect(browser).not.toHaveText('text') + await wdioExpect(browser).not.toHaveText('text') // @ts-expect-error - await expect(true).toHaveText('text') + await wdioExpect(true).toHaveText('text') // @ts-expect-error - await expect(true).toHaveText('text') + await wdioExpect(true).toHaveText('text') }) it('should have ts errors when actual is string or Promise', async () => { // @ts-expect-error - await expect('text').toHaveText('text') + await wdioExpect('text').toHaveText('text') // @ts-expect-error - await expect('text').not.toHaveText('text') + await wdioExpect('text').not.toHaveText('text') // @ts-expect-error - await expect(Promise.resolve('text')).toHaveText('text') + await wdioExpect(Promise.resolve('text')).toHaveText('text') // @ts-expect-error - await expect(Promise.resolve('text')).toHaveText('text') + await wdioExpect(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 = 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 = 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' }) + 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 = expect(element).toHaveHeight(100) + expectVoid = wdioExpect(element).toHaveHeight(100) // @ts-expect-error - expectVoid = expect(element).not.toHaveHeight(100) + expectVoid = wdioExpect(element).not.toHaveHeight(100) // @ts-expect-error - expectVoid = expect(element).toHaveHeight({ width: 100, height: 200 }) + expectVoid = wdioExpect(element).toHaveHeight({ width: 100, height: 200 }) // @ts-expect-error - expectVoid = expect(element).not.toHaveHeight({ width: 100, height: 200 }) + expectVoid = wdioExpect(element).not.toHaveHeight({ width: 100, height: 200 }) // @ts-expect-error - await expect(browser).toHaveHeight(100) + await wdioExpect(browser).toHaveHeight(100) }) it('should have ts errors when actual is string or Promise', async () => { // @ts-expect-error - await expect('text').toHaveText('text') + await wdioExpect('text').toHaveText('text') // @ts-expect-error - await expect('text').not.toHaveText('text') + await wdioExpect('text').not.toHaveText('text') // @ts-expect-error - await expect(Promise.resolve('text')).toHaveText('text') + await wdioExpect(Promise.resolve('text')).toHaveText('text') // @ts-expect-error - await expect(Promise.resolve('text')).toHaveText('text') + await wdioExpect(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') + expectVoid = wdioExpect(element).toMatchSnapshot() + expectVoid = wdioExpect(element).toMatchSnapshot('test label') + expectVoid = wdioExpect(element).not.toMatchSnapshot('test label') - expectPromiseVoid = expect(chainableElement).toMatchSnapshot() - expectPromiseVoid = expect(chainableElement).toMatchSnapshot('test label') - expectPromiseVoid = expect(chainableElement).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 = expect(element).toMatchSnapshot() + expectPromiseVoid = wdioExpect(element).toMatchSnapshot() //@ts-expect-error - expectPromiseVoid = expect(element).not.toMatchSnapshot() + expectPromiseVoid = wdioExpect(element).not.toMatchSnapshot() //@ts-expect-error - expectVoid = expect(chainableElement).toMatchSnapshot() + expectVoid = wdioExpect(chainableElement).toMatchSnapshot() //@ts-expect-error - expectVoid = expect(chainableElement).not.toMatchSnapshot() + expectVoid = wdioExpect(chainableElement).not.toMatchSnapshot() }) // TODO - since we are overloading the `toMatchSnapshot` of jest.toMatchSnapshot, I wonder if we can achieve the below... // it('should have ts errors when not an element or chainable', async () => { // //@ts-expect-error - // await expect('.findme').toMatchSnapshot() + // await wdioExpect('.findme').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') + expectVoid = wdioExpect(element).toMatchInlineSnapshot() + expectVoid = wdioExpect(element).toMatchInlineSnapshot('test snapshot') + expectVoid = wdioExpect(element).toMatchInlineSnapshot('test snapshot', 'test label') - expectPromiseVoid = expect(chainableElement).toMatchInlineSnapshot() - expectPromiseVoid = expect(chainableElement).toMatchInlineSnapshot('test snapshot') - expectPromiseVoid = expect(chainableElement).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 = expect(element).toMatchInlineSnapshot() + expectPromiseVoid = wdioExpect(element).toMatchInlineSnapshot() //@ts-expect-error - expectVoid = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + expectVoid = wdioExpect(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 = wdioExpect(element.getCSSProperty('test')).toMatchInlineSnapshot() + expectPromiseVoid = wdioExpect(element.getCSSProperty('test')).toMatchInlineSnapshot('test snapshot') + expectPromiseVoid = wdioExpect(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') + 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 = expect(element).toMatchInlineSnapshot() + expectPromiseVoid = wdioExpect(element).toMatchInlineSnapshot() //@ts-expect-error - expectVoid = expect(chainableElement).toMatchInlineSnapshot('test snapshot', 'test label') + expectVoid = wdioExpect(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 }) + expectPromiseVoid = wdioExpect(chainableArray).toBeElementsArrayOfSize(5) + expectPromiseVoid = wdioExpect(chainableArray).toBeElementsArrayOfSize({ lte: 10 }) // @ts-expect-error - expectVoid = expect(chainableArray).toBeElementsArrayOfSize(5) + expectVoid = wdioExpect(chainableArray).toBeElementsArrayOfSize(5) // @ts-expect-error - expectVoid = expect(chainableArray).toBeElementsArrayOfSize({ lte: 10 }) + expectVoid = wdioExpect(chainableArray).toBeElementsArrayOfSize({ lte: 10 }) }) it('should not work when actual is not chainableArray', async () => { // @ts-expect-error - await expect(chainableElement).toBeElementsArrayOfSize(5) + await wdioExpect(chainableElement).toBeElementsArrayOfSize(5) // @ts-expect-error - await expect(chainableElement).toBeElementsArrayOfSize({ lte: 10 }) + await wdioExpect(chainableElement).toBeElementsArrayOfSize({ lte: 10 }) // @ts-expect-error - await expect(true).toBeElementsArrayOfSize(5) + await wdioExpect(true).toBeElementsArrayOfSize(5) // @ts-expect-error - await expect(true).toBeElementsArrayOfSize({ lte: 10 }) + await wdioExpect(true).toBeElementsArrayOfSize({ lte: 10 }) }) }) }) @@ -338,133 +341,133 @@ describe('type assertions', () => { 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() + expectVoid = wdioExpect('test').toBeCustom() + expectVoid = wdioExpect('test').not.toBeCustom() // @ts-expect-error - expectPromiseVoid = expect('test').toBeCustom() + expectPromiseVoid = wdioExpect('test').toBeCustom() // @ts-expect-error - expectPromiseVoid = expect('test').not.toBeCustom() + expectPromiseVoid = wdioExpect('test').not.toBeCustom() - expectVoid = expect(1).toBeWithinRange(0, 2) + expectVoid = wdioExpect(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')) + expectPromiseVoid = wdioExpect(chainableElement).toBeCustomPromise() + expectPromiseVoid = wdioExpect(chainableElement).toBeCustomPromise(wdioExpect.stringContaining('test')) + expectPromiseVoid = wdioExpect(chainableElement).not.toBeCustomPromise(wdioExpect.not.stringContaining('test')) // @ts-expect-error - expect('test').toBeCustomPromise() + wdioExpect('test').toBeCustomPromise() // @ts-expect-error - expectVoid = expect(chainableElement).toBeCustomPromise() + expectVoid = wdioExpect(chainableElement).toBeCustomPromise() // @ts-expect-error - expectVoid = expect(chainableElement).toBeCustomPromise(expect.stringContaining('test')) + expectVoid = wdioExpect(chainableElement).toBeCustomPromise(wdioExpect.stringContaining('test')) // @ts-expect-error - expectVoid = expect(chainableElement).not.toBeCustomPromise(expect.stringContaining('test')) + expectVoid = wdioExpect(chainableElement).not.toBeCustomPromise(wdioExpect.stringContaining('test')) // @ts-expect-error - expect(chainableElement).toBeCustomPromise(expect.stringContaining(6)) + wdioExpect(chainableElement).toBeCustomPromise(wdioExpect.stringContaining(6)) }) it('should support custom asymmetric matcher', async () => { - const expectString1 : ExpectWebdriverIO.PartialMatcher = expect.toBeCustom() - const expectString2 : ExpectWebdriverIO.PartialMatcher = expect.not.toBeCustom() + const expectString1 : ExpectWebdriverIO.PartialMatcher = wdioExpect.toBeCustom() + const expectString2 : ExpectWebdriverIO.PartialMatcher = wdioExpect.not.toBeCustom() - expectPromiseVoid = expect(chainableElement).toBeCustomPromise(expect.toBeCustom()) + expectPromiseVoid = wdioExpect(chainableElement).toBeCustomPromise(wdioExpect.toBeCustom()) // @ts-expect-error - expectPromiseVoid = expect.toBeCustom() + expectPromiseVoid = wdioExpect.toBeCustom() // @ts-expect-error - expectPromiseVoid = expect.not.toBeCustom() + expectPromiseVoid = wdioExpect.not.toBeCustom() //@ts-expect-error - expectVoid = expect(chainableElement).toBeCustomPromise(expect.toBeCustom()) + expectVoid = wdioExpect(chainableElement).toBeCustomPromise(wdioExpect.toBeCustom()) }) }) describe('using `expect` module declaration', () => { it('should support a simple matcher', async () => { - expectVoid = expect(5).toBeWithinRange(1, 10) + expectVoid = wdioExpect(5).toBeWithinRange(1, 10) // Or as an asymmetric matcher: - expectVoid = expect({ value: 5 }).toEqual({ - value: expect.toBeWithinRange(1, 10) + expectVoid = wdioExpect({ value: 5 }).toEqual({ + value: wdioExpect.toBeWithinRange(1, 10) }) // @ts-expect-error - expectVoid = expect(5).toBeWithinRange(1, '10') + expectVoid = wdioExpect(5).toBeWithinRange(1, '10') // @ts-expect-error - expectPromiseVoid = expect(5).toBeWithinRange('1') + expectPromiseVoid = wdioExpect(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')) + 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 = expect(chainableElement).toHaveSimpleCustomProperty( - expect.toHaveSimpleCustomProperty('string') + expectPromiseVoid = wdioExpect(chainableElement).toHaveSimpleCustomProperty( + wdioExpect.toHaveSimpleCustomProperty('string') ) - const expectString1:string = expect.toHaveSimpleCustomProperty('string') - const expectString2:string = expect.not.toHaveSimpleCustomProperty('string') + const expectString1:string = wdioExpect.toHaveSimpleCustomProperty('string') + const expectString2:string = wdioExpect.not.toHaveSimpleCustomProperty('string') // TODO how to make the below fails when the await is missing inf front of the expect from the asymmetric matcher? - // expectPromiseVoid = expect(chainableElement).toHaveCustomProperty( - // expect.toHaveCustomProperty(chainableElement) + // expectPromiseVoid = wdioExpect(chainableElement).toHaveCustomProperty( + // wdioExpect.toHaveCustomProperty(chainableElement) // ) // @ts-expect-error - expectVoid = expect.toHaveSimpleCustomProperty(chainableElement) + expectVoid = wdioExpect.toHaveSimpleCustomProperty(chainableElement) // @ts-expect-error - expectVoid = expect.not.toHaveSimpleCustomProperty(chainableElement) + expectVoid = wdioExpect.not.toHaveSimpleCustomProperty(chainableElement) // @ts-expect-error - expectVoid = expect.toHaveSimpleCustomProperty(chainableElement) + expectVoid = wdioExpect.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')) + 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 = expect(chainableElement).toHaveCustomProperty( - await expect.toHaveCustomProperty(chainableElement) + expectPromiseVoid = wdioExpect(chainableElement).toHaveCustomProperty( + await wdioExpect.toHaveCustomProperty(chainableElement) ) - const expectPromiseWdioElement1: Promise> = expect.toHaveCustomProperty(chainableElement) - const expectPromiseWdioElement2: Promise> = expect.not.toHaveCustomProperty(chainableElement) + const expectPromiseWdioElement1: Promise> = wdioExpect.toHaveCustomProperty(chainableElement) + const expectPromiseWdioElement2: Promise> = wdioExpect.not.toHaveCustomProperty(chainableElement) // TODO how to make the below fails when the await is missing inf front of the expect from the asymmetric matcher? - // expectPromiseVoid = expect(chainableElement).toHaveCustomProperty( - // expect.toHaveCustomProperty(chainableElement) + // expectPromiseVoid = wdioExpect(chainableElement).toHaveCustomProperty( + // wdioExpect.toHaveCustomProperty(chainableElement) // ) // @ts-expect-error - expectVoid = expect.toHaveCustomProperty(chainableElement) + expectVoid = wdioExpect.toHaveCustomProperty(chainableElement) // @ts-expect-error - expectVoid = expect.not.toHaveCustomProperty(chainableElement) + expectVoid = wdioExpect.not.toHaveCustomProperty(chainableElement) // @ts-expect-error - expectVoid = expect.toHaveCustomProperty(chainableElement) + expectVoid = wdioExpect.toHaveCustomProperty(chainableElement) // @ts-expect-error - expect.toHaveCustomProperty('test') + wdioExpect.toHaveCustomProperty('test') - await expect(chainableElement).toHaveCustomProperty( - await expect.toHaveCustomProperty(chainableElement) + await wdioExpect(chainableElement).toHaveCustomProperty( + await wdioExpect.toHaveCustomProperty(chainableElement) ) }) // TODO this is not supported in Wdio right now, maybe one day we can support it // it('should support an async asymmetric matcher on a non async matcher', async () => { - // expectPromiseVoid = expect({ value: 5 }).toEqual({ - // value: expect.toHaveCustomProperty(chainableElement) + // expectPromiseVoid = wdioExpect({ value: 5 }).toEqual({ + // value: wdioExpect.toHaveCustomProperty(chainableElement) // }) // // @ts-expect-error - // expectVoid = expect({ value: 5 }).toEqual({ - // value: expect.toHaveCustomProperty(chainableElement) + // expectVoid = wdioExpect({ value: 5 }).toEqual({ + // value: wdioExpect.toHaveCustomProperty(chainableElement) // }) // }) @@ -474,51 +477,51 @@ describe('type assertions', () => { describe('toBe', () => { it('should expect void type when actual is a boolean', async () => { - expectVoid = expect(true).toBe(true) - expectVoid = expect(true).not.toBe(true) + expectVoid = wdioExpect(true).toBe(true) + expectVoid = wdioExpect(true).not.toBe(true) //@ts-expect-error - expectPromiseVoid = expect(true).toBe(true) + expectPromiseVoid = wdioExpect(true).toBe(true) //@ts-expect-error - expectPromiseVoid = expect(true).not.toBe(true) + expectPromiseVoid = wdioExpect(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) + expectVoid = wdioExpect(chainableElement).toBe(true) + expectVoid = wdioExpect(chainableElement).not.toBe(true) //@ts-expect-error - expectPromiseVoid = expect(chainableElement).toBe(true) + expectPromiseVoid = wdioExpect(chainableElement).toBe(true) //@ts-expect-error - expectPromiseVoid = expect(chainableElement).not.toBe(true) + expectPromiseVoid = 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) - expectVoid = expect(promiseBoolean).toBe(true) - expectVoid = expect(promiseBoolean).not.toBe(true) + expectVoid = wdioExpect(promiseBoolean).toBe(true) + expectVoid = wdioExpect(promiseBoolean).not.toBe(true) //@ts-expect-error - expectPromiseVoid = expect(promiseBoolean).toBe(true) + expectPromiseVoid = wdioExpect(promiseBoolean).toBe(true) //@ts-expect-error - expectPromiseVoid = expect(promiseBoolean).toBe(true) + expectPromiseVoid = wdioExpect(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')) + expectVoid = wdioExpect('text').toBe(true) + expectVoid = wdioExpect('text').not.toBe(true) + expectVoid = wdioExpect('text').toBe(wdioExpect.stringContaining('text')) + expectVoid = wdioExpect('text').not.toBe(wdioExpect.stringContaining('text')) //@ts-expect-error - expectPromiseVoid = expect('text').toBe(true) + expectPromiseVoid = wdioExpect('text').toBe(true) //@ts-expect-error - expectPromiseVoid = expect('text').not.toBe(true) + expectPromiseVoid = wdioExpect('text').not.toBe(true) //@ts-expect-error - expectPromiseVoid = expect('text').toBe(expect.stringContaining('text')) + expectPromiseVoid = wdioExpect('text').toBe(wdioExpect.stringContaining('text')) //@ts-expect-error - expectPromiseVoid = expect('text').not.toBe(expect.stringContaining('text')) + expectPromiseVoid = wdioExpect('text').not.toBe(wdioExpect.stringContaining('text')) }) }) @@ -526,27 +529,27 @@ describe('type assertions', () => { const booleanPromise: Promise = Promise.resolve(true) it('should expect a Promise of type', async () => { - const expectPromiseBoolean1: ExpectWebdriverIO.MatchersAndInverse> = expect(booleanPromise) - const expectPromiseBoolean2: ExpectWebdriverIO.Matchers> = expect(booleanPromise).not + const expectPromiseBoolean1: ExpectWebdriverIO.MatchersAndInverse> = wdioExpect(booleanPromise) + const expectPromiseBoolean2: ExpectWebdriverIO.Matchers> = wdioExpect(booleanPromise).not }) it('should work with resolves & rejects correctly', async () => { // TODO dprevost should we support this in Wdio since we do not even use it or document it? - // expectPromiseVoid = expect(booleanPromise).resolves.toBe(true) - // expectPromiseVoid = expect(booleanPromise).rejects.toBe(true) + // expectPromiseVoid = wdioExpect(booleanPromise).resolves.toBe(true) + // expectPromiseVoid = wdioExpect(booleanPromise).rejects.toBe(true) //@ts-expect-error - expectVoid = expect(booleanPromise).resolves.toBe(true) + expectVoid = wdioExpect(booleanPromise).resolves.toBe(true) //@ts-expect-error - expectVoid = expect(booleanPromise).rejects.toBe(true) + expectVoid = wdioExpect(booleanPromise).rejects.toBe(true) }) it('should not support chainable and expect PromiseVoid with toBe', async () => { //@ts-expect-error - expectPromiseVoid = expect(chainableElement).toBe(true) + expectPromiseVoid = wdioExpect(chainableElement).toBe(true) //@ts-expect-error - expectPromiseVoid = expect(chainableElement).not.toBe(true) + expectPromiseVoid = wdioExpect(chainableElement).not.toBe(true) }) }) @@ -554,15 +557,15 @@ describe('type assertions', () => { 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 = wdioExpect(promiseNetworkMock).toBeRequested() + expectPromiseVoid = wdioExpect(promiseNetworkMock).toBeRequestedTimes(2) + expectPromiseVoid = wdioExpect(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 = wdioExpect(promiseNetworkMock).not.toBeRequested() + expectPromiseVoid = wdioExpect(promiseNetworkMock).not.toBeRequestedTimes(2) + expectPromiseVoid = wdioExpect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) - expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith({ + expectPromiseVoid = wdioExpect(promiseNetworkMock).toBeRequestedWith({ url: 'http://localhost:8080/api/todo', method: 'POST', statusCode: 200, @@ -573,47 +576,47 @@ describe('type assertions', () => { }) // TODO dprevost: Asymmetric matcher is not defined on the entire object in the .d.ts file, it is a bug? - // expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith(expect.objectContaining({ + // expectPromiseVoid = wdioExpect(promiseNetworkMock).toBeRequestedWith(wdioExpect.objectContaining({ // response: { success: true }, // [optional] object | function | custom matcher // })) - expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith({ - url: expect.stringContaining('test'), + expectPromiseVoid = wdioExpect(promiseNetworkMock).toBeRequestedWith({ + url: wdioExpect.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 }), + requestHeaders: wdioExpect.objectContaining({ Authorization: 'foo' }), + responseHeaders: wdioExpect.objectContaining({ Authorization: 'bar' }), + postData: wdioExpect.objectContaining({ title: 'foo', description: 'bar' }), + response: wdioExpect.objectContaining({ success: true }), }) - expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith({ - url: expect.stringMatching(/.*\/api\/.*/i), + expectPromiseVoid = wdioExpect(promiseNetworkMock).toBeRequestedWith({ + url: wdioExpect.stringMatching(/.*\/api\/.*/i), method: ['POST', 'PUT'], statusCode: [401, 403], requestHeaders: headers => headers.Authorization.startsWith('Bearer '), - postData: expect.objectContaining({ released: true, title: expect.stringContaining('foobar') }), + 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 = expect(promiseNetworkMock).toBeRequested() + expectVoid = wdioExpect(promiseNetworkMock).toBeRequested() // @ts-expect-error - expectVoid = expect(promiseNetworkMock).toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + expectVoid = wdioExpect(promiseNetworkMock).toBeRequestedTimes(2) // await wdioExpect(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 + expectVoid = wdioExpect(promiseNetworkMock).toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 // @ts-expect-error - expectVoid = expect(promiseNetworkMock).not.toBeRequested() + expectVoid = wdioExpect(promiseNetworkMock).not.toBeRequested() // @ts-expect-error - expectVoid = expect(promiseNetworkMock).not.toBeRequestedTimes(2) // await expect(mock).toBeRequestedTimes({ eq: 2 }) + expectVoid = wdioExpect(promiseNetworkMock).not.toBeRequestedTimes(2) // await wdioExpect(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 + expectVoid = wdioExpect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) // request called at least 5 times but less than 11 // @ts-expect-error - expectVoid = expect(promiseNetworkMock).toBeRequestedWith({ + expectVoid = wdioExpect(promiseNetworkMock).toBeRequestedWith({ url: 'http://localhost:8080/api/todo', method: 'POST', statusCode: 200, @@ -624,237 +627,237 @@ describe('type assertions', () => { }) // @ts-expect-error - expectVoid = expect(promiseNetworkMock).toBeRequestedWith(expect.objectContaining({ + expectVoid = wdioExpect(promiseNetworkMock).toBeRequestedWith(wdioExpect.objectContaining({ response: { success: true }, })) }) }) describe('Expect', () => { - it('should have ts errors when using a non existing expect.function', async () => { + it('should have ts errors when using a non existing wdioExpect.function', async () => { // @ts-expect-error - expect.unimplementedFunction() + wdioExpect.unimplementedFunction() }) it('should support stringContaining, anything and more', async () => { - expect.stringContaining('WebdriverIO') - expect.stringMatching(/WebdriverIO/) - expect.arrayContaining(['WebdriverIO', 'Test']) - expect.objectContaining({ name: 'WebdriverIO' }) + wdioExpect.stringContaining('WebdriverIO') + wdioExpect.stringMatching(/WebdriverIO/) + wdioExpect.arrayContaining(['WebdriverIO', 'Test']) + wdioExpect.objectContaining({ name: 'WebdriverIO' }) // Was not there but works! - expect.closeTo(5, 10) - expect.arrayContaining(['WebdriverIO', 'Test']) + wdioExpect.closeTo(5, 10) + wdioExpect.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')) + 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')) // TODO dprevost: Should we support these? - // 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) + // wdioExpect.not.anything() + // wdioExpect.not.any(Function) + // wdioExpect.not.any(Number) + // wdioExpect.not.any(Boolean) + // wdioExpect.not.any(String) + // wdioExpect.not.any(Symbol) + // wdioExpect.not.any(Date) + // wdioExpect.not.any(Error) }) describe('Soft Assertions', async () => { const actualString: string = 'Test Page' const actualPromiseString: Promise = Promise.resolve('Test Page') - describe('expect.soft', () => { + describe('wdioExpect.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')) + 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 = expect.soft(actualString).toBe('Test Page') + expectPromiseVoid = wdioExpect.soft(actualString).toBe('Test Page') // @ts-expect-error - expectPromiseVoid = expect.soft(actualString).not.toBe('Test Page') + expectPromiseVoid = wdioExpect.soft(actualString).not.toBe('Test Page') // @ts-expect-error - expectPromiseVoid = expect.soft(actualString).not.toBe(expect.stringContaining('Test Page')) + 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> = 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')) + 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 = expect.soft(actualPromiseString).toBe('Test Page') + expectVoid = wdioExpect.soft(actualPromiseString).toBe('Test Page') // @ts-expect-error - expectVoid = expect.soft(actualPromiseString).not.toBe('Test Page') + expectVoid = wdioExpect.soft(actualPromiseString).not.toBe('Test Page') // @ts-expect-error - expectVoid = expect.soft(actualPromiseString).not.toBe(expect.stringContaining('Test Page')) + expectVoid = wdioExpect.soft(actualPromiseString).not.toBe(wdioExpect.stringContaining('Test Page')) }) it('should support chainable element', async () => { - const expectElement: WdioCustomMatchers = expect.soft(element) - const expectElementChainable: WdioCustomMatchers = expect.soft(chainableElement) + const expectElement: WdioCustomMatchers = wdioExpect.soft(element) + const expectElementChainable: WdioCustomMatchers = wdioExpect.soft(chainableElement) // @ts-expect-error - const expectElement2: WdioCustomMatchers, WebdriverIO.Element> = expect.soft(element) + const expectElement2: WdioCustomMatchers, WebdriverIO.Element> = wdioExpect.soft(element) // @ts-expect-error - const expectElementChainable2: WdioCustomMatchers, typeof chainableElement> = expect.soft(chainableElement) + const expectElementChainable2: WdioCustomMatchers, typeof chainableElement> = wdioExpect.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 = 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 = 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() + 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 = expect.soft(element).toBeDisplayed() + expectVoid = wdioExpect.soft(element).toBeDisplayed() // @ts-expect-error - expectVoid = expect.soft(chainableElement).toBeDisplayed() + expectVoid = wdioExpect.soft(chainableElement).toBeDisplayed() // @ts-expect-error - expectVoid = expect.soft(chainableArray).toBeDisplayed() + expectVoid = wdioExpect.soft(chainableArray).toBeDisplayed() // @ts-expect-error - expectVoid = expect.soft(element).not.toBeDisplayed() + expectVoid = wdioExpect.soft(element).not.toBeDisplayed() // @ts-expect-error - expectVoid = expect.soft(chainableElement).not.toBeDisplayed() + expectVoid = wdioExpect.soft(chainableElement).not.toBeDisplayed() // @ts-expect-error - expectVoid = expect.soft(chainableArray).not.toBeDisplayed() + expectVoid = wdioExpect.soft(chainableArray).not.toBeDisplayed() }) it('should have ts (or lint) errors when actual is a chainable not awaited', async () => { // TODO dprevost: see if an eslint rule could help us here to detect missing await when not using wdio matchers - // expectPromiseVoid = expect.soft(chainableElement.getText()).toEqual('Basketball Shoes') - // expectPromiseVoid = expect.soft(chainableElement.getText()).toMatch(/€\d+/) + // expectPromiseVoid = wdioExpect.soft(chainableElement.getText()).toEqual('Basketball Shoes') + // expectPromiseVoid = wdioExpect.soft(chainableElement.getText()).toMatch(/€\d+/) }) 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) + 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 = expect.soft(chainableElement).toHaveCustomProperty('text') + expectVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty('text') // @ts-expect-error - expectVoid = expect.soft(chainableElement).toHaveCustomProperty(expect.stringContaining('text')) + expectVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty(wdioExpect.stringContaining('text')) // @ts-expect-error - expectVoid = expect.soft(chainableElement).not.toHaveCustomProperty(expect.not.stringContaining('text')) + expectVoid = wdioExpect.soft(chainableElement).not.toHaveCustomProperty(wdioExpect.not.stringContaining('text')) // @ts-expect-error - expectVoid = expect.soft(chainableElement).toHaveCustomProperty( - expect.toHaveCustomProperty(chainableElement) + expectVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty( + wdioExpect.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) + 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 = expect.soft(chainableElement).toHaveCustomProperty('text') + expectVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty('text') // @ts-expect-error - expectVoid = expect.soft(chainableElement).toHaveCustomProperty(expect.stringContaining('text')) + expectVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty(wdioExpect.stringContaining('text')) // @ts-expect-error - expectVoid = expect.soft(chainableElement).not.toHaveCustomProperty(expect.not.stringContaining('text')) + expectVoid = wdioExpect.soft(chainableElement).not.toHaveCustomProperty(wdioExpect.not.stringContaining('text')) // @ts-expect-error - expectVoid = expect.soft(chainableElement).toHaveCustomProperty( - expect.toHaveCustomProperty(chainableElement) + expectVoid = wdioExpect.soft(chainableElement).toHaveCustomProperty( + wdioExpect.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) + 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 = expect.soft(chainableElement).toBeCustomPromise('text') + expectVoid = wdioExpect.soft(chainableElement).toBeCustomPromise('text') // @ts-expect-error - expectVoid = expect.soft(chainableElement).toBeCustomPromise(expect.stringContaining('text')) + expectVoid = wdioExpect.soft(chainableElement).toBeCustomPromise(wdioExpect.stringContaining('text')) // @ts-expect-error - expectVoid = expect.soft(chainableElement).not.toBeCustomPromise(expect.not.stringContaining('text')) + expectVoid = wdioExpect.soft(chainableElement).not.toBeCustomPromise(wdioExpect.not.stringContaining('text')) // @ts-expect-error - expectVoid = expect.soft(chainableElement).toBeCustomPromise( - expect.toBeCustomPromise(chainableElement) + expectVoid = wdioExpect.soft(chainableElement).toBeCustomPromise( + wdioExpect.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) + 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 = expect.soft(chainableElement).toBeCustomPromise('text') + expectVoid = wdioExpect.soft(chainableElement).toBeCustomPromise('text') // @ts-expect-error - expectVoid = expect.soft(chainableElement).toBeCustomPromise(expect.stringContaining('text')) + expectVoid = wdioExpect.soft(chainableElement).toBeCustomPromise(wdioExpect.stringContaining('text')) // @ts-expect-error - expectVoid = expect.soft(chainableElement).not.toBeCustomPromise(expect.not.stringContaining('text')) + expectVoid = wdioExpect.soft(chainableElement).not.toBeCustomPromise(wdioExpect.not.stringContaining('text')) // @ts-expect-error - expectVoid = expect.soft(chainableElement).toBeCustomPromise( - expect.toBeCustomPromise(chainableElement) + expectVoid = wdioExpect.soft(chainableElement).toBeCustomPromise( + wdioExpect.toBeCustomPromise(chainableElement) ) }) }) - describe('expect.getSoftFailures', () => { + describe('wdioExpect.getSoftFailures', () => { it('should be of type `SoftFailure`', async () => { - const expectSoftFailure1: ExpectWebdriverIO.SoftFailure[] = expect.getSoftFailures() + const expectSoftFailure1: ExpectWebdriverIO.SoftFailure[] = wdioExpect.getSoftFailures() // @ts-expect-error - expectVoid = expect.getSoftFailures() + expectVoid = wdioExpect.getSoftFailures() }) }) - describe('expect.assertSoftFailures', () => { + describe('wdioExpect.assertSoftFailures', () => { it('should be of type void', async () => { - expectVoid = expect.assertSoftFailures() + expectVoid = wdioExpect.assertSoftFailures() // @ts-expect-error - expectPromiseVoid = expect.assertSoftFailures() + expectPromiseVoid = wdioExpect.assertSoftFailures() }) }) - describe('expect.clearSoftFailures', () => { + describe('wdioExpect.clearSoftFailures', () => { it('should be of type void', async () => { - expectVoid = expect.clearSoftFailures() + expectVoid = wdioExpect.clearSoftFailures() // @ts-expect-error - expectPromiseVoid = expect.clearSoftFailures() + expectPromiseVoid = wdioExpect.clearSoftFailures() }) }) }) @@ -867,39 +870,27 @@ describe('type assertions', () => { 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))) + 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(expect.stringContaining('test error')) - expectPromiseLikeVoid = expectAsync(Promise.resolve('test')).not.toBeResolvedTo(expect.not.stringContaining('test error')) + 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 () => { - // TODO dprevost: Is this valid since expect is from WebdriverIO and we force it to be `expectAsync` in the main project? - expect({}).toEqual(jasmine.any(Object)) - expect(12).toEqual(jasmine.any(Number)) - }) - }) }) diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 43301d0c6..7a30eea4f 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -52,30 +52,34 @@ 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 FnWhenMock = ActualT extends MockPromise ? 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{ +interface WdioBrowserMatchers<_R, ActualT>{ /** * `WebdriverIO.Browser` -> `getUrl` */ - toHaveUrl: FnWhenBrowser, options?: ExpectWebdriverIO.StringOptions) => Promise> + toHaveUrl: FnWhenBrowser, options?: ExpectWebdriverIO.StringOptions) => Promise> /** * `WebdriverIO.Browser` -> `getTitle` */ - toHaveTitle: FnWhenBrowser, options?: ExpectWebdriverIO.StringOptions) => Promise> + toHaveTitle: FnWhenBrowser, options?: ExpectWebdriverIO.StringOptions) => Promise> /** * `WebdriverIO.Browser` -> `execute` */ - toHaveClipboardText: FnWhenBrowser, options?: ExpectWebdriverIO.StringOptions) => Promise> + toHaveClipboardText: FnWhenBrowser, options?: ExpectWebdriverIO.StringOptions) => Promise> } /** @@ -83,20 +87,20 @@ interface WdioBrowserMatchers{ * 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 { +interface WdioNetworkMatchers<_R, ActualT> { /** * Check that `WebdriverIO.Mock` was called */ - toBeRequested: FnWhenMock Promise> + toBeRequested: FnWhenMock Promise> /** * Check that `WebdriverIO.Mock` was called N times */ - toBeRequestedTimes: FnWhenMock Promise> + toBeRequestedTimes: FnWhenMock Promise> /** * Check that `WebdriverIO.Mock` was called with the specific parameters */ - toBeRequestedWith: FnWhenMock Promise> + toBeRequestedWith: FnWhenMock Promise> } /** @@ -104,27 +108,27 @@ interface WdioNetworkMatchers { * 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 { +interface WdioElementOrArrayMatchers<_R, ActualT = unknown> { // ===== $ or $$ ===== /** * `WebdriverIO.Element` -> `isDisplayed` */ - toBeDisplayed: FnWhenElementOrArrayLike Promise> + toBeDisplayed: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isExisting` */ - toExist: FnWhenElementOrArrayLike Promise> + toExist: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isExisting` */ - toBePresent: FnWhenElementOrArrayLike Promise> + toBePresent: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isExisting` */ - toBeExisting: FnWhenElementOrArrayLike Promise> + toBeExisting: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `getAttribute` @@ -132,7 +136,7 @@ interface WdioElementOrArrayMatchers { toHaveAttribute: FnWhenElementOrArrayLike, options?: ExpectWebdriverIO.StringOptions) - => Promise> + => Promise> /** * `WebdriverIO.Element` -> `getAttribute` @@ -140,7 +144,7 @@ interface WdioElementOrArrayMatchers { toHaveAttr: FnWhenElementOrArrayLike, options?: ExpectWebdriverIO.StringOptions - ) => Promise> + ) => Promise> /** * `WebdriverIO.Element` -> `getAttribute` class @@ -149,7 +153,7 @@ interface WdioElementOrArrayMatchers { toHaveClass: FnWhenElementOrArrayLike, options?: ExpectWebdriverIO.StringOptions - ) => Promise> + ) => Promise> /** * `WebdriverIO.Element` -> `getAttribute` class @@ -170,7 +174,7 @@ interface WdioElementOrArrayMatchers { toHaveElementClass: FnWhenElementOrArrayLike | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions - ) => Promise> + ) => Promise> /** * `WebdriverIO.Element` -> `getProperty` @@ -179,7 +183,7 @@ interface WdioElementOrArrayMatchers { property: string | RegExp | ExpectWebdriverIO.PartialMatcher, value?: unknown, options?: ExpectWebdriverIO.StringOptions - ) => Promise> + ) => Promise> /** * `WebdriverIO.Element` -> `getProperty` value @@ -187,42 +191,42 @@ interface WdioElementOrArrayMatchers { toHaveValue: FnWhenElementOrArrayLike, options?: ExpectWebdriverIO.StringOptions - ) => Promise> + ) => Promise> /** * `WebdriverIO.Element` -> `isClickable` */ - toBeClickable: FnWhenElementOrArrayLike Promise> + toBeClickable: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `!isEnabled` */ - toBeDisabled: FnWhenElementOrArrayLike Promise> + toBeDisabled: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isDisplayedInViewport` */ - toBeDisplayedInViewport: FnWhenElementOrArrayLike Promise> + toBeDisplayedInViewport: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isEnabled` */ - toBeEnabled: FnWhenElementOrArrayLike Promise> + toBeEnabled: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isFocused` */ - toBeFocused: FnWhenElementOrArrayLike Promise> + toBeFocused: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isSelected` */ - toBeSelected: FnWhenElementOrArrayLike Promise> + toBeSelected: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isSelected` */ - toBeChecked: FnWhenElementOrArrayLike Promise> + toBeChecked: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `$$('./*').length` @@ -231,7 +235,7 @@ interface WdioElementOrArrayMatchers { toHaveChildren: FnWhenElementOrArrayLike Promise> + ) => Promise> /** * `WebdriverIO.Element` -> `getAttribute` href @@ -239,7 +243,7 @@ interface WdioElementOrArrayMatchers { toHaveHref: FnWhenElementOrArrayLike, options?: ExpectWebdriverIO.StringOptions - ) => Promise> + ) => Promise> /** * `WebdriverIO.Element` -> `getAttribute` href @@ -247,7 +251,7 @@ interface WdioElementOrArrayMatchers { toHaveLink: FnWhenElementOrArrayLike, options?: ExpectWebdriverIO.StringOptions - ) => Promise> + ) => Promise> /** * `WebdriverIO.Element` -> `getProperty` value @@ -255,7 +259,7 @@ interface WdioElementOrArrayMatchers { toHaveId: FnWhenElementOrArrayLike, options?: ExpectWebdriverIO.StringOptions - ) => Promise> + ) => Promise> /** * `WebdriverIO.Element` -> `getSize` value @@ -263,7 +267,7 @@ interface WdioElementOrArrayMatchers { toHaveSize: FnWhenElementOrArrayLike Promise> + ) => Promise> /** * `WebdriverIO.Element` -> `getText` @@ -287,7 +291,7 @@ interface WdioElementOrArrayMatchers { toHaveText: FnWhenElementOrArrayLike | Array>, options?: ExpectWebdriverIO.StringOptions - ) => Promise> + ) => Promise> /** * `WebdriverIO.Element` -> `getHTML` @@ -296,7 +300,7 @@ interface WdioElementOrArrayMatchers { toHaveHTML: FnWhenElementOrArrayLike | Array, options?: ExpectWebdriverIO.StringOptions - ) => Promise> + ) => Promise> /** * `WebdriverIO.Element` -> `getComputedLabel` @@ -305,7 +309,7 @@ interface WdioElementOrArrayMatchers { toHaveComputedLabel: FnWhenElementOrArrayLike | Array, options?: ExpectWebdriverIO.StringOptions - ) => Promise> + ) => Promise> /** * `WebdriverIO.Element` -> `getComputedRole` @@ -314,13 +318,13 @@ interface WdioElementOrArrayMatchers { toHaveComputedRole: FnWhenElementOrArrayLike | Array, options?: ExpectWebdriverIO.StringOptions - ) => Promise> + ) => Promise> /** * `WebdriverIO.Element` -> `getSize('width')` * Element's width equals the width provided */ - toHaveWidth: FnWhenElementOrArrayLike Promise> + toHaveWidth: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `getSize('height')` or `getSize()` @@ -338,12 +342,12 @@ interface WdioElementOrArrayMatchers { toHaveHeight: FnWhenElementOrArrayLike Promise> + ) => Promise> /** * `WebdriverIO.Element` -> `getAttribute("style")` */ - toHaveStyle: FnWhenElementOrArrayLike Promise> + toHaveStyle: FnWhenElementOrArrayLike Promise> } /** @@ -351,7 +355,7 @@ interface WdioElementOrArrayMatchers { * 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 { +interface WdioElementArrayOnlyMatchers<_R, ActualT = unknown> { // ===== $$ only ===== /** * `WebdriverIO.ElementArray` -> `$$('...').length` @@ -360,7 +364,7 @@ interface WdioElementArrayOnlyMatchers { toBeElementsArrayOfSize: FnWhenElementArrayLike Promise & Promise> + ) => Promise & Promise> } /** @@ -373,18 +377,18 @@ interface WdioElementArrayOnlyMatchers { * * TODO dprevost: Review for better typings... */ -interface WdioJestOverloadedMatchers { +interface WdioJestOverloadedMatchers<_R, ActualT> { /** * snapshot matcher * @param label optional snapshot label */ - toMatchSnapshot(label?: string): ActualT extends WdioPromiseLike ? Promise : R; + toMatchSnapshot(label?: string): ActualT extends WdioPromiseLike ? Promise : R; /** * 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 : R; + toMatchInlineSnapshot(snapshot?: string, label?: string): ActualT extends WdioPromiseLike ? Promise : R; } /** From 9fd54b51088d4a15cd8892613948aaeb01237a3f Mon Sep 17 00:00:00 2001 From: David Prevost Date: Thu, 3 Jul 2025 09:44:15 -0400 Subject: [PATCH 55/99] Fix impact of supporting Jasmine typing --- docs/Framework.md | 8 ++++---- jest.d.ts | 4 ++-- test-types/jasmine/types-jasmine.test.ts | 8 ++++---- test-types/mocha/types-mocha.test.ts | 15 +++++++++++---- types/expect-webdriverio.d.ts | 6 +++--- 5 files changed, 24 insertions(+), 17 deletions(-) diff --git a/docs/Framework.md b/docs/Framework.md index 2a377d19f..8ca8af0bd 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -36,7 +36,7 @@ Optionally, to not need `import { expect } from 'expect-webdriverio'` you can us ```json { "compilerOptions": { - "types": ["expect-webdriverio/types"] + "types": ["expect-webdriverio/expect-global"] } } ``` @@ -88,7 +88,7 @@ Expected in `tsconfig.json`: "compilerOptions": { "types": [ "@types/mocha", - "expect-webdriverio/types" + "expect-webdriverio/expect-global" ] } } @@ -131,7 +131,7 @@ Expected in `tsconfig.json`: 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/types"` +// 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 () => { @@ -149,7 +149,7 @@ Expected in `tsconfig.json`: "compilerOptions": { "types": [ "@types/jasmine", - "expect-webdriverio/types", // Force expect to be the 'expect-webdriverio', to comment and use the import above if it conflict with Jasmine + "expect-webdriverio/expect-global", // Force expect to be the 'expect-webdriverio', to comment and use the import above if it conflict with Jasmine ] } } diff --git a/jest.d.ts b/jest.d.ts index e9177927f..cf96fe737 100644 --- a/jest.d.ts +++ b/jest.d.ts @@ -21,14 +21,14 @@ declare namespace jest { * snapshot matcher * @param label optional snapshot label */ - toMatchSnapshot(label?: string): T extends WdioPromiseLike ? Promise : R; + 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 : R; + toMatchInlineSnapshot(snapshot?: string, label?: string): T extends WdioPromiseLike ? Promise : void; } interface Expect extends ExpectWebdriverIO.Expect {} diff --git a/test-types/jasmine/types-jasmine.test.ts b/test-types/jasmine/types-jasmine.test.ts index bd5bf7fbc..45f0e9e64 100644 --- a/test-types/jasmine/types-jasmine.test.ts +++ b/test-types/jasmine/types-jasmine.test.ts @@ -712,13 +712,13 @@ describe('type assertions', () => { }) it('should support chainable element', async () => { - const expectElement: WdioCustomMatchers = wdioExpect.soft(element) - const expectElementChainable: WdioCustomMatchers = wdioExpect.soft(chainableElement) + const expectElement: ExpectWebdriverIO.MatchersAndInverse = wdioExpect.soft(element) + const expectElementChainable: ExpectWebdriverIO.MatchersAndInverse = wdioExpect.soft(chainableElement) // @ts-expect-error - const expectElement2: WdioCustomMatchers, WebdriverIO.Element> = wdioExpect.soft(element) + const expectElement2: ExpectWebdriverIO.MatchersAndInverse, WebdriverIO.Element> = wdioExpect.soft(element) // @ts-expect-error - const expectElementChainable2: WdioCustomMatchers, typeof chainableElement> = wdioExpect.soft(chainableElement) + const expectElementChainable2: ExpectWebdriverIO.MatchersAndInverse, typeof chainableElement> = wdioExpect.soft(chainableElement) }) it('should support chainable element with wdio Matchers', async () => { diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index 12cd71b76..77f14d728 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -305,10 +305,17 @@ describe('type assertions', () => { 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() }) }) @@ -711,13 +718,13 @@ describe('type assertions', () => { }) it('should support chainable element', async () => { - const expectElement: WdioCustomMatchers = expect.soft(element) - const expectElementChainable: WdioCustomMatchers = expect.soft(chainableElement) + const expectElement: ExpectWebdriverIO.MatchersAndInverse = expect.soft(element) + const expectElementChainable: ExpectWebdriverIO.MatchersAndInverse = expect.soft(chainableElement) // @ts-expect-error - const expectElement2: WdioCustomMatchers, WebdriverIO.Element> = expect.soft(element) + const expectElement2: ExpectWebdriverIO.MatchersAndInverse, WebdriverIO.Element> = expect.soft(element) // @ts-expect-error - const expectElementChainable2: WdioCustomMatchers, typeof chainableElement> = expect.soft(chainableElement) + const expectElementChainable2: ExpectWebdriverIO.MatchersAndInverse, typeof chainableElement> = expect.soft(chainableElement) }) it('should support chainable element with wdio Matchers', async () => { diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 7a30eea4f..7566cdd13 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -56,7 +56,7 @@ type FnWhenElementOrArrayLike = ActualT extends ElementOrArrayLike 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` + * 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 @@ -382,13 +382,13 @@ interface WdioJestOverloadedMatchers<_R, ActualT> { * snapshot matcher * @param label optional snapshot label */ - toMatchSnapshot(label?: string): ActualT extends WdioPromiseLike ? Promise : R; + 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 : R; + toMatchInlineSnapshot(snapshot?: string, label?: string): ActualT extends WdioPromiseLike ? Promise : void; } /** From 3145c40f3c890e1563d9655e896e365c4bcd47dd Mon Sep 17 00:00:00 2001 From: David Prevost Date: Thu, 3 Jul 2025 21:49:02 -0400 Subject: [PATCH 56/99] Use latest Jest with exportable Inverse --- package-lock.json | 342 ++++++++++++++++++++++------------ package.json | 6 +- types/expect-webdriverio.d.ts | 13 +- 3 files changed, 225 insertions(+), 136 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9d182157c..6ff7298ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,12 @@ "license": "MIT", "dependencies": { "@vitest/snapshot": "^3.2.4", - "expect": "^30.0.0", - "jest-matcher-utils": "^30.0.0", + "expect": "^30.0.4", + "jest-matcher-utils": "^30.0.4", "lodash.isequal": "^4.5.0" }, "devDependencies": { - "@jest/globals": "^30.0.0", + "@jest/globals": "^30.0.4", "@types/debug": "^4.1.12", "@types/jasmine": "^5.1.8", "@types/jest": "^30.0.0", @@ -85,30 +85,32 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.7.tgz", - "integrity": "sha512-xgu/ySj2mTiUFmdE9yCMfBxLp4DHd5DwmbbD05YAuICfodYT3VvRxbrh81LGQ/8UpSdtMdfKMn3KouYDX59DGQ==", + "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.27.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.7.tgz", - "integrity": "sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==", + "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.27.5", + "@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.27.7", + "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.27.7", - "@babel/types": "^7.27.7", + "@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", @@ -128,20 +130,22 @@ "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.27.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", - "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "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.27.5", - "@babel/types": "^7.27.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@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": { @@ -153,6 +157,7 @@ "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", @@ -169,6 +174,7 @@ "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" } @@ -178,15 +184,27 @@ "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" @@ -200,6 +218,7 @@ "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", @@ -217,6 +236,7 @@ "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" } @@ -244,6 +264,7 @@ "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" } @@ -253,6 +274,7 @@ "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" @@ -262,12 +284,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.7.tgz", - "integrity": "sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==", + "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.27.7" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -281,6 +304,7 @@ "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" }, @@ -293,6 +317,7 @@ "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" }, @@ -305,6 +330,7 @@ "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" }, @@ -317,6 +343,7 @@ "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" }, @@ -332,6 +359,7 @@ "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" }, @@ -347,6 +375,7 @@ "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" }, @@ -359,6 +388,7 @@ "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" }, @@ -371,6 +401,7 @@ "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" }, @@ -386,6 +417,7 @@ "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" }, @@ -398,6 +430,7 @@ "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" }, @@ -410,6 +443,7 @@ "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" }, @@ -422,6 +456,7 @@ "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" }, @@ -434,6 +469,7 @@ "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" }, @@ -446,6 +482,7 @@ "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" }, @@ -458,6 +495,7 @@ "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" }, @@ -473,6 +511,7 @@ "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" }, @@ -488,6 +527,7 @@ "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" }, @@ -503,6 +543,7 @@ "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", @@ -513,37 +554,30 @@ } }, "node_modules/@babel/traverse": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.7.tgz", - "integrity": "sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==", + "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.27.5", - "@babel/parser": "^7.27.7", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", - "@babel/types": "^7.27.7", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/types": "^7.28.0", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.7.tgz", - "integrity": "sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==", + "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.27.1", "@babel/helper-validator-identifier": "^7.27.1" @@ -1623,6 +1657,7 @@ "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", @@ -1639,6 +1674,7 @@ "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" } @@ -1648,6 +1684,7 @@ "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" @@ -1661,6 +1698,7 @@ "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" @@ -1674,6 +1712,7 @@ "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" }, @@ -1686,6 +1725,7 @@ "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" }, @@ -1701,6 +1741,7 @@ "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" }, @@ -1713,6 +1754,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1721,7 +1763,8 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", @@ -1737,17 +1780,19 @@ "version": "30.0.1", "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "license": "MIT", "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/environment": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.2.tgz", - "integrity": "sha512-hRLhZRJNxBiOhxIKSq2UkrlhMt3/zVFQOAi5lvS8T9I03+kxsbflwHJEF+eXEYXCrRGRhHwECT7CDk6DyngsRA==", + "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.2", + "@jest/fake-timers": "30.0.4", "@jest/types": "30.0.1", "@types/node": "*", "jest-mock": "30.0.2" @@ -1757,22 +1802,24 @@ } }, "node_modules/@jest/expect": { - "version": "30.0.3", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.3.tgz", - "integrity": "sha512-73BVLqfCeWjYWPEQoYjiRZ4xuQRhQZU0WdgvbyXGRHItKQqg5e6mt2y1kVhzLSuZpmUnccZHbGynoaL7IcLU3A==", + "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.3", - "jest-snapshot": "30.0.3" + "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.3", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.3.tgz", - "integrity": "sha512-SMtBvf2sfX2agcT0dA9pXwcUrKvOSDqBY4e4iRfT+Hya33XzV35YVg+98YQFErVGA/VR1Gto5Y2+A6G9LSQ3Yg==", + "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" }, @@ -1781,10 +1828,11 @@ } }, "node_modules/@jest/fake-timers": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.2.tgz", - "integrity": "sha512-jfx0Xg7l0gmphTY9UKm5RtH12BlLYj/2Plj6wXjVW5Era4FZKfXeIvwC67WX+4q8UCFxYS20IgnMcFBcEU0DtA==", + "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", @@ -1801,18 +1849,20 @@ "version": "30.0.1", "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz", "integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==", + "license": "MIT", "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/globals": { - "version": "30.0.3", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.0.3.tgz", - "integrity": "sha512-fIduqNyYpMeeSr5iEAiMn15KxCzvrmxl7X7VwLDRGj7t5CoHtbF+7K3EvKk32mOUIJ4kIvFRlaixClMH2h/Vaw==", + "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.2", - "@jest/expect": "30.0.3", + "@jest/environment": "30.0.4", + "@jest/expect": "30.0.4", "@jest/types": "30.0.1", "jest-mock": "30.0.2" }, @@ -1847,10 +1897,11 @@ } }, "node_modules/@jest/snapshot-utils": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.1.tgz", - "integrity": "sha512-6Dpv7vdtoRiISEFwYF8/c7LIvqXD7xDXtLPNzC2xqAfBznKip0MQM+rkseKwUPUpv2PJ7KW/YsnwWXrIL2xF+A==", + "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", @@ -1866,6 +1917,7 @@ "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" }, @@ -1881,6 +1933,7 @@ "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" @@ -1893,10 +1946,11 @@ } }, "node_modules/@jest/transform": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.2.tgz", - "integrity": "sha512-kJIuhLMTxRF7sc0gPzPtCDib/V9KwW3I2U25b+lYCYMVqHHSrcZopS8J8H+znx9yixuFv+Iozl8raLt/4MoxrA==", + "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", @@ -1923,6 +1977,7 @@ "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" }, @@ -1938,6 +1993,7 @@ "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" @@ -2012,18 +2068,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": { @@ -2036,16 +2088,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", @@ -2053,9 +2095,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": { @@ -2340,6 +2382,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" }, @@ -2681,6 +2724,7 @@ "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" } @@ -2690,6 +2734,7 @@ "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" } @@ -3214,7 +3259,8 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/@vitest/coverage-v8": { "version": "3.2.4", @@ -4027,6 +4073,7 @@ "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" @@ -4221,6 +4268,7 @@ "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", @@ -4237,6 +4285,7 @@ "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" @@ -4248,6 +4297,7 @@ "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", @@ -4268,6 +4318,7 @@ "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" }, @@ -4280,6 +4331,7 @@ "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", @@ -4294,6 +4346,7 @@ "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", @@ -4503,6 +4556,7 @@ "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" } @@ -4658,6 +4712,7 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -5985,13 +6040,14 @@ "license": "ISC" }, "node_modules/expect": { - "version": "30.0.3", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.3.tgz", - "integrity": "sha512-HXg6NvK35/cSYZCUKAtmlgCFyqKM4frEPbzrav5hRqb0GMz0E0lS5hfzYjSaiaE5ysnp/qI2aeZkeyeIAOeXzQ==", + "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.3", + "@jest/expect-utils": "30.0.4", "@jest/get-type": "30.0.1", - "jest-matcher-utils": "30.0.3", + "jest-matcher-utils": "30.0.4", "jest-message-util": "30.0.2", "jest-mock": "30.0.2", "jest-util": "30.0.2" @@ -6439,6 +6495,7 @@ "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" } @@ -6581,7 +6638,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", @@ -6653,6 +6711,7 @@ "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" } @@ -6685,6 +6744,7 @@ "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" } @@ -7090,6 +7150,7 @@ "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" @@ -7359,6 +7420,7 @@ "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", @@ -7431,9 +7493,10 @@ } }, "node_modules/jest-diff": { - "version": "30.0.3", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.3.tgz", - "integrity": "sha512-Q1TAV0cUcBTic57SVnk/mug0/ASyAqtSIOkr7RAlxx97llRYsM74+E8N5WdGJUlwCKwgxPAkVjKh653h1+HA9A==", + "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", "@jest/get-type": "30.0.1", @@ -7448,6 +7511,7 @@ "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" }, @@ -7458,12 +7522,14 @@ "node_modules/jest-diff/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==" + "integrity": "sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==", + "license": "MIT" }, "node_modules/jest-diff/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" }, @@ -7478,6 +7544,7 @@ "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" @@ -7493,6 +7560,7 @@ "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", @@ -7506,6 +7574,7 @@ "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" }, @@ -7529,6 +7598,7 @@ "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": "*", @@ -7549,13 +7619,14 @@ } }, "node_modules/jest-matcher-utils": { - "version": "30.0.3", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.3.tgz", - "integrity": "sha512-hMpVFGFOhYmIIRGJ0HgM9htC5qUiJ00famcc9sRFchJJiLZbbVKrAztcgE6VnXLRxA3XZ0bvNA7hQWh3oHXo/A==", + "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.3", + "jest-diff": "30.0.4", "pretty-format": "30.0.2" }, "engines": { @@ -7566,6 +7637,7 @@ "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" }, @@ -7574,9 +7646,10 @@ } }, "node_modules/jest-matcher-utils/node_modules/@sinclair/typebox": { - "version": "0.34.35", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.35.tgz", - "integrity": "sha512-C6ypdODf2VZkgRT6sFM8E1F8vR+HcffniX0Kp8MsU8PIfrlXbNCBz0jzj17GjdmjTx1OtZzdH8+iALL21UjF5A==" + "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": { "version": "4.3.0", @@ -7613,6 +7686,7 @@ "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", @@ -7626,6 +7700,7 @@ "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" }, @@ -7743,27 +7818,28 @@ } }, "node_modules/jest-snapshot": { - "version": "30.0.3", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.3.tgz", - "integrity": "sha512-F05JCohd3OA1N9+5aEPXA6I0qOfZDGIx0zTq5Z4yMBg2i1p5ELfBusjYAWwTkC12c7dHcbyth4QAfQbS7cRjow==", + "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.3", + "@jest/expect-utils": "30.0.4", "@jest/get-type": "30.0.1", - "@jest/snapshot-utils": "30.0.1", - "@jest/transform": "30.0.2", + "@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.3", + "expect": "30.0.4", "graceful-fs": "^4.2.11", - "jest-diff": "30.0.3", - "jest-matcher-utils": "30.0.3", + "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", @@ -7779,6 +7855,7 @@ "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" }, @@ -7790,13 +7867,15 @@ "version": "0.34.37", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.37.tgz", "integrity": "sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==", - "dev": true + "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" }, @@ -7812,6 +7891,7 @@ "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" @@ -7828,6 +7908,7 @@ "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", @@ -7842,6 +7923,7 @@ "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" }, @@ -7910,6 +7992,7 @@ "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", @@ -7926,6 +8009,7 @@ "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" }, @@ -8019,6 +8103,7 @@ "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" }, @@ -8398,6 +8483,7 @@ "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" } @@ -8661,7 +8747,8 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/node-releases": { "version": "2.0.19", @@ -9000,6 +9087,7 @@ "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" } @@ -9142,6 +9230,7 @@ "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" } @@ -9248,6 +9337,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" } @@ -10446,6 +10536,7 @@ "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" }, @@ -10677,7 +10768,8 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -10729,6 +10821,7 @@ "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" } @@ -11196,6 +11289,7 @@ "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" } @@ -12094,6 +12188,7 @@ "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" @@ -12137,7 +12232,8 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/yargs": { "version": "17.7.2", diff --git a/package.json b/package.json index a3aead5cf..1b198e92f 100644 --- a/package.json +++ b/package.json @@ -62,8 +62,8 @@ }, "dependencies": { "@vitest/snapshot": "^3.2.4", - "expect": "^30.0.0", - "jest-matcher-utils": "^30.0.0", + "expect": "^30.0.4", + "jest-matcher-utils": "^30.0.4", "lodash.isequal": "^4.5.0" }, "devDependencies": { @@ -73,7 +73,7 @@ "@types/lodash.isequal": "^4.5.8", "@types/mocha": "^10.0.10", "@types/node": "^24.0.3", - "@jest/globals": "^30.0.0", + "@jest/globals": "^30.0.4", "@vitest/coverage-v8": "^3.2.4", "@wdio/eslint": "^0.1.1", "@wdio/types": "^9.15.0", diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 7566cdd13..63cefdf7c 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -15,6 +15,7 @@ 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 // TODO dprevost: a suggestion would be to move any code outside of the namespace to separate types.ts file, so that we can import the types. @@ -474,7 +475,7 @@ declare namespace ExpectWebdriverIO { * `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, Inverse>, WdioExpect { + 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. @@ -492,21 +493,13 @@ declare namespace ExpectWebdriverIO { interface Matchers, T> extends WdioMatchers {} - // To remove when exportable from 'expect'. See https://github.com/jestjs/jest/pull/15704 (already merged) - interface Inverse { - /** - * Inverse next matcher. If you know how to test something, `.not` lets you test its opposite. - */ - not: Matchers; - } - interface AsymmetricMatchers extends WdioAsymmetricMatchers {} /** * End of block overloading types from the expect library. */ - type MatchersAndInverse, ActualT> = ExpectWebdriverIO.Matchers & Inverse> + type MatchersAndInverse, ActualT> = ExpectWebdriverIO.Matchers & ExpectLibInverse> interface SnapshotServiceArgs { updateState?: SnapshotUpdateState From 3e4504c9c22ec27ea9491477a44ec986c7c7635b Mon Sep 17 00:00:00 2001 From: David Prevost Date: Thu, 3 Jul 2025 22:20:07 -0400 Subject: [PATCH 57/99] Fix Jasmine left over problem --- jasmine.d.ts | 16 +++++++++-- package.json | 6 +++++ .../jasmine-async/types-jasmine.test.ts | 27 +++++++++---------- 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/jasmine.d.ts b/jasmine.d.ts index 74b18c760..26014e335 100644 --- a/jasmine.d.ts +++ b/jasmine.d.ts @@ -5,7 +5,7 @@ * If U is already a Promise, PromiseLike, or Chainable, return U as-is. * Otherwise, wrap U in a Promise. */ -type EnsurePromise = U extends Promise | PromiseLike | WdioPromiseLike ? U : Promise +type EnsurePromise> = U extends Promise | PromiseLike | WdioPromiseLike ? U : Promise declare namespace jasmine { @@ -15,5 +15,17 @@ declare namespace jasmine { * U is the type of the expected value, which will be wrapped in a Promise if it's not already one * Both T,U must stay named as they are to override the default `AsyncMatchers` type from Jasmine. */ - interface AsyncMatchers | void> 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/package.json b/package.json index 1b198e92f..f397fc30a 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,11 @@ "types": "./jest.d.ts" } ], + "./jasmine": [ + { + "types": "./jasmine.d.ts" + } + ], "./types": "./types/expect-global.d.ts", "./expect-global": "./types/expect-global.d.ts" }, @@ -57,6 +62,7 @@ "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" }, diff --git a/test-types/jasmine-async/types-jasmine.test.ts b/test-types/jasmine-async/types-jasmine.test.ts index 0164eb026..ac818229f 100644 --- a/test-types/jasmine-async/types-jasmine.test.ts +++ b/test-types/jasmine-async/types-jasmine.test.ts @@ -255,9 +255,9 @@ describe('type assertions', () => { describe('toMatchSnapshot', () => { it('should be supported correctly', async () => { - expectVoid = expectAsync(element).toMatchSnapshot() - expectVoid = expectAsync(element).toMatchSnapshot('test label') - expectVoid = expectAsync(element).not.toMatchSnapshot('test label') + 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') @@ -274,9 +274,9 @@ describe('type assertions', () => { describe('toMatchInlineSnapshot', () => { it('should be correctly supported', async () => { - expectVoid = expectAsync(element).toMatchInlineSnapshot() - expectVoid = expectAsync(element).toMatchInlineSnapshot('test snapshot') - expectVoid = expectAsync(element).toMatchInlineSnapshot('test snapshot', 'test label') + 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') @@ -453,6 +453,7 @@ describe('type assertions', () => { describe('toBe', () => { it('should expect void type when actual is a boolean', async () => { + // TODO dprevost we migth need to be a Promise here because of the expectAsync of wdio expectVoid = expect(true).toBe(true) expectVoid = expect(true).not.toBe(true) @@ -506,20 +507,16 @@ describe('type assertions', () => { const booleanPromise: Promise = Promise.resolve(true) it('should expect a Promise of type', async () => { - const expectPromiseBoolean1: ExpectWebdriverIO.MatchersAndInverse> = expectAsync(booleanPromise) - const expectPromiseBoolean2: ExpectWebdriverIO.Matchers> = expectAsync(booleanPromise).not + 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 () => { - // TODO dprevost should we support this in Wdio since we do not even use it or document it? - // expectPromiseVoid = expectAsync(booleanPromise).resolves.toBe(true) - // expectPromiseVoid = expectAsync(booleanPromise).rejects.toBe(true) - //@ts-expect-error - expectVoid = expectAsync(booleanPromise).resolves.toBe(true) + expectAsync(booleanPromise).resolves.toBe(true) //@ts-expect-error - expectVoid = expectAsync(booleanPromise).rejects.toBe(true) - + expectAsync(booleanPromise).rejects.toBe(true) }) it('should not support chainable and expect PromiseVoid with toBe', async () => { From 69057ca97a8282e51e947eb3de578e8f8f4ba367 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Thu, 3 Jul 2025 23:09:13 -0400 Subject: [PATCH 58/99] Review some TODOs --- .../jest-@jest_global/types-jest.test.ts | 41 ++++++------------- .../jest-@types_jest/types-jest.test.ts | 10 ++--- test-types/mocha/types-mocha.test.ts | 9 +--- types/expect-webdriverio.d.ts | 8 +--- 4 files changed, 20 insertions(+), 48 deletions(-) diff --git a/test-types/jest-@jest_global/types-jest.test.ts b/test-types/jest-@jest_global/types-jest.test.ts index 5b9fc5668..1890f5b67 100644 --- a/test-types/jest-@jest_global/types-jest.test.ts +++ b/test-types/jest-@jest_global/types-jest.test.ts @@ -46,8 +46,6 @@ describe('type assertions', async () => { // @ts-expect-error await expect(browser).toHaveUrl(6) - //// @ts-expect-error TODO dprevost can we make the below fail? - // await expect(browser).toHaveUrl(expect.objectContaining({})) }) it('should have ts errors when actual is not a Browser element', async () => { @@ -538,7 +536,7 @@ describe('type assertions', async () => { it('should expect a Promise of type', async () => { const expectPromiseBoolean1: ExpectWebdriverIO.MatchersAndInverse> = expect(booleanPromise) - const expectPromiseBoolean2: jest.Matchers> = expect(booleanPromise).not + const expectPromiseBoolean2: ExpectWebdriverIO.Matchers> = expect(booleanPromise).not // @ts-expect-error const expectPromiseBoolean3: jest.JestMatchers = expect(booleanPromise) @@ -589,11 +587,6 @@ describe('type assertions', async () => { response: { success: true }, }) - // TODO dprevost: Asymmetric matcher is not defined on the entire object in the .d.ts file, it is a bug? - // expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith(expect.objectContaining({ - // response: { success: true }, // [optional] object | function | custom matcher - // })) - expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith({ url: expect.stringContaining('test'), method: 'POST', @@ -681,22 +674,14 @@ describe('type assertions', async () => { expect.not.arrayContaining(['WebdriverIO', 'Test']) expect.not.arrayOf(expect.stringContaining('WebdriverIO')) - //@ts-expect-error - expect.not.anything() - //@ts-expect-error - expect.not.any(Function) - //@ts-expect-error - expect.not.any(Number) - //@ts-expect-error - expect.not.any(Boolean) - //@ts-expect-error - expect.not.any(String) - //@ts-expect-error - expect.not.any(Symbol) - //@ts-expect-error - expect.not.any(Date) - //@ts-expect-error - expect.not.any(Error) + // 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 () => { @@ -733,13 +718,13 @@ describe('type assertions', async () => { }) it('should support chainable element', async () => { - const expectElement: WdioCustomMatchers = expect.soft(element) - const expectElementChainable: WdioCustomMatchers = expect.soft(chainableElement) + const expectElement: ExpectWebdriverIO.MatchersAndInverse = expect.soft(element) + const expectElementChainable: ExpectWebdriverIO.MatchersAndInverse = expect.soft(chainableElement) // @ts-expect-error - const expectElement2: WdioCustomMatchers, WebdriverIO.Element> = expect.soft(element) + const expectElement2: ExpectWebdriverIO.MatchersAndInverse, WebdriverIO.Element> = expect.soft(element) // @ts-expect-error - const expectElementChainable2: WdioCustomMatchers, typeof chainableElement> = expect.soft(chainableElement) + const expectElementChainable2: ExpectWebdriverIO.MatchersAndInverse, typeof chainableElement> = expect.soft(chainableElement) }) it('should support chainable element with wdio Matchers', async () => { diff --git a/test-types/jest-@types_jest/types-jest.test.ts b/test-types/jest-@types_jest/types-jest.test.ts index 78e1ea608..5a7efd04b 100644 --- a/test-types/jest-@types_jest/types-jest.test.ts +++ b/test-types/jest-@types_jest/types-jest.test.ts @@ -43,8 +43,6 @@ describe('type assertions', async () => { // @ts-expect-error await expect(browser).toHaveUrl(6) - //// @ts-expect-error TODO dprevost can we make the below fail? - // await expect(browser).toHaveUrl(expect.objectContaining({})) }) it('should have ts errors when actual is not a Browser element', async () => { @@ -720,13 +718,13 @@ describe('type assertions', async () => { }) it('should support chainable element', async () => { - const expectElement: WdioCustomMatchers = expect.soft(element) - const expectElementChainable: WdioCustomMatchers = expect.soft(chainableElement) + const expectElement: ExpectWebdriverIO.MatchersAndInverse = expect.soft(element) + const expectElementChainable: ExpectWebdriverIO.MatchersAndInverse = expect.soft(chainableElement) // @ts-expect-error - const expectElement2: WdioCustomMatchers, WebdriverIO.Element> = expect.soft(element) + const expectElement2: ExpectWebdriverIO.MatchersAndInverse, WebdriverIO.Element> = expect.soft(element) // @ts-expect-error - const expectElementChainable2: WdioCustomMatchers, typeof chainableElement> = expect.soft(chainableElement) + const expectElementChainable2: ExpectWebdriverIO.MatchersAndInverse, typeof chainableElement> = expect.soft(chainableElement) }) it('should support chainable element with wdio Matchers', async () => { diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index 77f14d728..2a3f332b2 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -37,8 +37,6 @@ describe('type assertions', () => { // @ts-expect-error await expect(browser).toHaveUrl(6) - //// @ts-expect-error TODO dprevost can we make the below fail? - // await expect(browser).toHaveUrl(expect.objectContaining({})) }) it('should have ts errors when actual is not a Browser element', async () => { @@ -581,11 +579,6 @@ describe('type assertions', () => { response: { success: true }, }) - // TODO dprevost: Asymmetric matcher is not defined on the entire object in the .d.ts file, it is a bug? - // expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith(expect.objectContaining({ - // response: { success: true }, // [optional] object | function | custom matcher - // })) - expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith({ url: expect.stringContaining('test'), method: 'POST', @@ -673,7 +666,7 @@ describe('type assertions', () => { expect.not.arrayContaining(['WebdriverIO', 'Test']) expect.not.arrayOf(expect.stringContaining('WebdriverIO')) - // TODO dprevost: Should we support these? + // TODO dprevost to review // expect.not.anything() // expect.not.any(Function) // expect.not.any(Number) diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 63cefdf7c..1923df0f3 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -15,7 +15,7 @@ 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 ExpectLibInverse = import('expect').Inverse // TODO dprevost: a suggestion would be to move any code outside of the namespace to separate types.ts file, so that we can import the types. @@ -375,8 +375,6 @@ interface WdioElementArrayOnlyMatchers<_R, ActualT = unknown> { * ⚠️ 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 - * - * TODO dprevost: Review for better typings... */ interface WdioJestOverloadedMatchers<_R, ActualT> { /** @@ -475,7 +473,7 @@ declare namespace ExpectWebdriverIO { * `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 { + 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. @@ -717,8 +715,6 @@ declare namespace ExpectWebdriverIO { * Allow to partially matches value. Same as asymmetric matcher in jest. * Some properties are omitted for the type check to work correctly. */ - // TODO dprevost: verify if we do breaking changes on this PartialMatcher, since before it was the AsymmetricMatcher interface used everywhere. - // TODO dprevost: verify if we should restrict to possible asymmetric matchers used! type PartialMatcher = Omit, 'sample' | 'inverse' | '$$typeof'> } From a97198d1f755bb53d05ff62e94105e85c13496d7 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Fri, 4 Jul 2025 17:10:07 -0400 Subject: [PATCH 59/99] Simplify Jasmine augmentation --- jasmine.d.ts | 14 +++++--------- test-types/jasmine-async/types-jasmine.test.ts | 11 ----------- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/jasmine.d.ts b/jasmine.d.ts index 26014e335..d13a1f2f2 100644 --- a/jasmine.d.ts +++ b/jasmine.d.ts @@ -1,21 +1,17 @@ /// -/** - * Utility type that wraps non-Promise types in a Promise for Jasmine async matchers. - * If U is already a Promise, PromiseLike, or Chainable, return U as-is. - * Otherwise, wrap U in a Promise. - */ -type EnsurePromise> = U extends Promise | PromiseLike | WdioPromiseLike ? U : Promise - 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, which will be wrapped in a Promise if it's not already one + * 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) */ - interface AsyncMatchers> extends Omit, T>, 'toMatchSnapshot' | 'toMatchInlineSnapshot'> { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface AsyncMatchers extends Omit, T>, 'toMatchSnapshot' | 'toMatchInlineSnapshot'> { /** * snapshot matcher * @param label optional snapshot label diff --git a/test-types/jasmine-async/types-jasmine.test.ts b/test-types/jasmine-async/types-jasmine.test.ts index ac818229f..42d41c898 100644 --- a/test-types/jasmine-async/types-jasmine.test.ts +++ b/test-types/jasmine-async/types-jasmine.test.ts @@ -453,13 +453,10 @@ describe('type assertions', () => { describe('toBe', () => { it('should expect void type when actual is a boolean', async () => { - // TODO dprevost we migth need to be a Promise here because of the expectAsync of wdio expectVoid = expect(true).toBe(true) expectVoid = expect(true).not.toBe(true) - //@ts-expect-error expectPromiseVoid = expectAsync(true).toBe(true) - //@ts-expect-error expectPromiseVoid = expectAsync(true).not.toBe(true) }) @@ -480,9 +477,7 @@ describe('type assertions', () => { expectPromiseUnknown = expectAsync(promiseBoolean).toBe(true) expectPromiseUnknown = expectAsync(promiseBoolean).not.toBe(true) - //@ts-expect-error expectPromiseVoid = expectAsync(promiseBoolean).toBe(true) - //@ts-expect-error expectPromiseVoid = expectAsync(promiseBoolean).toBe(true) }) @@ -492,13 +487,9 @@ describe('type assertions', () => { expectPromiseUnknown = expectAsync('text').toBe(wdioExpect.stringContaining('text')) expectPromiseUnknown = expectAsync('text').not.toBe(wdioExpect.stringContaining('text')) - //@ts-expect-error expectPromiseVoid = expectAsync('text').toBe(true) - //@ts-expect-error expectPromiseVoid = expectAsync('text').not.toBe(true) - //@ts-expect-error expectPromiseVoid = expectAsync('text').toBe(wdioExpect.stringContaining('text')) - //@ts-expect-error expectPromiseVoid = expectAsync('text').not.toBe(wdioExpect.stringContaining('text')) }) }) @@ -520,9 +511,7 @@ describe('type assertions', () => { }) it('should not support chainable and expect PromiseVoid with toBe', async () => { - //@ts-expect-error expectPromiseVoid = expectAsync(chainableElement).toBe(true) - //@ts-expect-error expectPromiseVoid = expectAsync(chainableElement).not.toBe(true) }) }) From 44827550fc1653864d8d4759305247b0d185fd98 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Fri, 4 Jul 2025 17:18:14 -0400 Subject: [PATCH 60/99] Add jest types check to the ts command --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f397fc30a..c88d4345a 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "test:lint": "eslint .", "test:unit": "vitest --run", "test:types": "npm run ts && npm run tsc:root-types", - "ts": "run-s ts:*", + "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", From 19a326392a76603637f26e82fc57e4dbcdfcd8cf Mon Sep 17 00:00:00 2001 From: David Prevost Date: Fri, 4 Jul 2025 17:31:37 -0400 Subject: [PATCH 61/99] Review doc by the AI --- docs/CustomMatchers.md | 4 ++-- docs/Framework.md | 10 +++++----- docs/Types.md | 10 +++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/CustomMatchers.md b/docs/CustomMatchers.md index fb4b3a9fd..06766f335 100644 --- a/docs/CustomMatchers.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/tutorials/custom_matchers) 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/en/expect#expectextendmatchers) Custom matchers should be added in wdio `before` hook diff --git a/docs/Framework.md b/docs/Framework.md index 8ca8af0bd..4bf564bab 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -10,9 +10,9 @@ We can pair `expect-webdriverio` with [Jest](https://jestjs.io/), [Mocha](https: ### Jest We can use `expect-webdriverio` with [Jest](https://jestjs.io/) with either the [`@jest/globals`](https://www.npmjs.com/package/@jest/globals) (preferred) or the [`@types/jest`](https://www.npmjs.com/package/@types/jest) (has global imports support) - - Note: Jest maintainers do not support `@types/jest`. In case this library gets out of date or has problems, support might be dropped. + - Note: Jest maintainers do not support [`@types/jest`](https://www.npmjs.com/package/@types/jest). In case this library gets out of date or has problems, support might be dropped. -In each case, when used **outside of [WDIO Testrunner](https://webdriver.io/docs/clioptions)**, types are required to be added in your `tsconfig.json` +In each case, when used **outside of [WDIO Testrunner](https://webdriver.io/docs/clioptions)**, types are required to be added in your [`tsconfig.json`](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) - Note: With Jest the matchers `toMatchSnapshot` and `toMatchInlineSnapshot` were overloaded. To resolve correctly the types `expect-webdriverio/jest` must be last. #### With `@jest/globals` @@ -95,7 +95,7 @@ Expected in `tsconfig.json`: ``` #### Chai -TODO - Integration with [Chai](https://www.chaijs.com/) assertion library +TODO - Integration with [Chai](https://www.chaijs.com/) assertion library. See [`@types/chai`](https://www.npmjs.com/package/@types/chai) for type definitions. ### Jasmine When paired with [Jasmine](https://jasmine.github.io/), [`@wdio/jasmine-framework`](https://www.npmjs.com/package/@wdio/jasmine-framework) is also required to have it configured correctly as it needs to force the `expect` to be `expectAsync` and also to register the wdio matchers with `addAsyncMatcher` since `expect-webdriverio` only supports the jest style `expect.extend` version. @@ -103,7 +103,7 @@ When paired with [Jasmine](https://jasmine.github.io/), [`@wdio/jasmine-framewor The types `expect-webdriverio/jasmine` is still offers but subject to removal or to be moved into `@wdio/jasmine-framework`. The usage of `expectAsync` is also subject to future removal. #### Jasmine `expectAsync` -Since the above types augment the `AsyncMatcher` of `Jasmine` then with this library alone it look like the below even though it is not runnable since the matcher are not registered +Since the above types augment the `AsyncMatcher` of `Jasmine` then with this library alone it looks like the below even though it is not runnable since the matchers are not registered ```ts describe('My tests', async () => { @@ -156,7 +156,7 @@ Expected in `tsconfig.json`: ``` #### Asymmetric matcher -Asymmetric matcher has limited support, even though `jasmine.stringContaining` has not error it is potential not working even with `@wdio/jasmine-framework`, but the below should: +Asymmetric matcher has limited support, even though `jasmine.stringContaining` has no error it is potentially not working even with `@wdio/jasmine-framework`, but the below should: ```ts describe('My tests', async () => { diff --git a/docs/Types.md b/docs/Types.md index b7e93e2c8..de1f486ab 100644 --- a/docs/Types.md +++ b/docs/Types.md @@ -4,14 +4,14 @@ 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 Jasmine/Jest users. -- `"expect-webdriverio/jasmine"` for Jasmine -- `"expect-webdriverio/jest"` for Jest +- `"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 { @@ -24,7 +24,7 @@ It's required to create `jsconfig.json` in project root and refer to the type de ``` ## Jasmine special case -Jasmine is different from Jest or the standard `expect` definition since it supports promises using `expectAsync` which make it quite challenging. +[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`. @@ -33,4 +33,4 @@ Support of `expectAsync` keyword is subject to change and may be dropped in the ### 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`, since it replaces `jasmine.expect` with `jasmine.expectAsync`, then matchers are usable on the keyword `expect`, but still typing on `expect` directly from Jasmine namespace is not supported as of today! \ No newline at end of file +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 From bcaba9b39ff9f4d332839e1fec96d7a7fef0f86f Mon Sep 17 00:00:00 2001 From: David Prevost Date: Fri, 4 Jul 2025 17:43:18 -0400 Subject: [PATCH 62/99] Review dead link with AI --- README.md | 2 +- docs/API.md | 4 ++-- docs/CustomMatchers.md | 2 +- src/jasmineUtils.ts | 2 +- src/utils.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) 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..d5eb00fab 100644 --- a/docs/API.md +++ b/docs/API.md @@ -667,7 +667,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 +858,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/CustomMatchers.md b/docs/CustomMatchers.md index 06766f335..8fa9b00ef 100644 --- a/docs/CustomMatchers.md +++ b/docs/CustomMatchers.md @@ -3,7 +3,7 @@ Similar to how `expect-webdriverio` extends Jasmine/Jest matchers it's possible to add custom matchers. - [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/en/expect#expectextendmatchers) +- 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/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/utils.ts b/src/utils.ts index 15aafa086..2ddd8a380 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -36,7 +36,7 @@ function isStringContainingMatcher(expected: unknown): expected is WdioAsymmetri /** * 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 ( From cfc0af3e4b64c9882717fa4e4d7a55e9b351047b Mon Sep 17 00:00:00 2001 From: David Prevost Date: Fri, 4 Jul 2025 18:39:13 -0400 Subject: [PATCH 63/99] Add details with Chai --- docs/Framework.md | 3 ++- package-lock.json | 2 +- package.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/Framework.md b/docs/Framework.md index 4bf564bab..8f016a73f 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -95,7 +95,8 @@ Expected in `tsconfig.json`: ``` #### Chai -TODO - Integration with [Chai](https://www.chaijs.com/) assertion library. See [`@types/chai`](https://www.npmjs.com/package/@types/chai) for type definitions. +`expect-webdriverio` can coexist with [Chai](https://www.chaijs.com/) assertion library, by importing both library 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 have it configured correctly as it needs to force the `expect` to be `expectAsync` and also to register the wdio matchers with `addAsyncMatcher` since `expect-webdriverio` only supports the jest style `expect.extend` version. diff --git a/package-lock.json b/package-lock.json index 6ff7298ff..25bab34a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,7 @@ "webdriverio": "^9.15.0" }, "engines": { - "node": ">=18 || >=20 || >=22" + "node": ">=20 || >=22" }, "peerDependencies": { "@wdio/globals": "^9.0.0", diff --git a/package.json b/package.json index c88d4345a..dff806b40 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "types": "./types/expect-webdriverio.d.ts", "typeScriptVersion": "3.8.3", "engines": { - "node": ">=18 || >=20 || >=22" + "node": ">=20 || >=22" }, "scripts": { "build": "run-s clean compile", From df5f93dffa5b7bab1592d0c3c569c97616c602b2 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Fri, 4 Jul 2025 18:48:18 -0400 Subject: [PATCH 64/99] Remove out of scope TODOs --- .../jasmine-async/types-jasmine.test.ts | 43 +---------------- test-types/jasmine/types-jasmine.test.ts | 42 ----------------- .../jest-@jest_global/types-jest.test.ts | 42 ----------------- .../jest-@types_jest/types-jest.test.ts | 47 ------------------- test-types/mocha/types-mocha.test.ts | 35 -------------- 5 files changed, 1 insertion(+), 208 deletions(-) diff --git a/test-types/jasmine-async/types-jasmine.test.ts b/test-types/jasmine-async/types-jasmine.test.ts index 42d41c898..0ed1d110b 100644 --- a/test-types/jasmine-async/types-jasmine.test.ts +++ b/test-types/jasmine-async/types-jasmine.test.ts @@ -263,12 +263,6 @@ describe('type assertions', () => { expectPromiseVoid = expectAsync(chainableElement).toMatchSnapshot('test label') expectPromiseVoid = expectAsync(chainableElement).not.toMatchSnapshot('test label') }) - - // TODO - since we are overloading the `toMatchSnapshot` of jest.toMatchSnapshot, I wonder if we can achieve the below... - // it('should have ts errors when not an element or chainable', async () => { - // //@ts-expect-error - // await expectAsync('.findme').toMatchSnapshot() - // }) }) describe('toMatchInlineSnapshot', () => { @@ -390,11 +384,6 @@ describe('type assertions', () => { const expectString1:string = wdioExpect.toHaveSimpleCustomProperty('string') const expectString2:string = wdioExpect.not.toHaveSimpleCustomProperty('string') - // TODO how to make the below fails when the await is missing inf front of the expect from the asymmetric matcher? - // expectPromiseVoid = expectAsync(chainableElement).toHaveCustomProperty( - // wdioExpect.toHaveCustomProperty(chainableElement) - // ) - // @ts-expect-error expectVoid = wdioExpect.toHaveSimpleCustomProperty(chainableElement) // @ts-expect-error @@ -416,11 +405,6 @@ describe('type assertions', () => { const expectPromiseWdioElement1: Promise> = wdioExpect.toHaveCustomProperty(chainableElement) const expectPromiseWdioElement2: Promise> = wdioExpect.not.toHaveCustomProperty(chainableElement) - // TODO how to make the below fails when the await is missing inf front of the expect from the asymmetric matcher? - // expectPromiseVoid = expectAsync(chainableElement).toHaveCustomProperty( - // wdioExpect.toHaveCustomProperty(chainableElement) - // ) - // @ts-expect-error expectVoid = wdioExpect.toHaveCustomProperty(chainableElement) // @ts-expect-error @@ -435,19 +419,6 @@ describe('type assertions', () => { await wdioExpect.toHaveCustomProperty(chainableElement) ) }) - - // TODO this is not supported in Wdio right now, maybe one day we can support it - // it('should support an async asymmetric matcher on a non async matcher', async () => { - // expectPromiseVoid = expectAsync({ value: 5 }).toEqual({ - // value: wdioExpect.toHaveCustomProperty(chainableElement) - // }) - - // // @ts-expect-error - // expectVoid = expectAsync({ value: 5 }).toEqual({ - // value: wdioExpect.toHaveCustomProperty(chainableElement) - // }) - - // }) }) }) @@ -538,11 +509,6 @@ describe('type assertions', () => { response: { success: true }, }) - // TODO dprevost: Asymmetric matcher is not defined on the entire object in the .d.ts file, it is a bug? - // expectPromiseVoid = expectAsync(promiseNetworkMock).toBeRequestedWith(wdioExpect.objectContaining({ - // response: { success: true }, // [optional] object | function | custom matcher - // })) - expectPromiseVoid = expectAsync(promiseNetworkMock).toBeRequestedWith({ url: wdioExpect.stringContaining('test'), method: 'POST', @@ -713,13 +679,6 @@ describe('type assertions', () => { // @ts-expect-error expectVoid = wdioExpect.soft(chainableArray).not.toBeDisplayed() }) - - it('should have ts (or lint) errors when actual is a chainable not awaited', async () => { - // TODO dprevost: see if an eslint rule could help us here to detect missing await when not using wdio matchers - // expectPromiseVoid = wdioExpect.soft(chainableElement.getText()).toEqual('Basketball Shoes') - // expectPromiseVoid = wdioExpect.soft(chainableElement.getText()).toMatch(/€\d+/) - }) - 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')) @@ -862,7 +821,7 @@ describe('type assertions', () => { expectVoid = expectAsync(Promise.resolve('test')).toBeResolved() }) it('jasmine special asymmetric matcher', async () => { - // TODO dprevost: Is this valid since expect is from WebdriverIO and we force it to be `expectAsync` in the main project? + // 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/types-jasmine.test.ts b/test-types/jasmine/types-jasmine.test.ts index 45f0e9e64..f67800999 100644 --- a/test-types/jasmine/types-jasmine.test.ts +++ b/test-types/jasmine/types-jasmine.test.ts @@ -38,8 +38,6 @@ describe('type assertions', () => { // @ts-expect-error await wdioExpect(browser).toHaveUrl(6) - //// @ts-expect-error TODO dprevost can we make the below fail? - // await wdioExpect(browser).toHaveUrl(wdioExpect.objectContaining({})) }) it('should have ts errors when actual is not a Browser element', async () => { @@ -272,12 +270,6 @@ describe('type assertions', () => { //@ts-expect-error expectVoid = wdioExpect(chainableElement).not.toMatchSnapshot() }) - - // TODO - since we are overloading the `toMatchSnapshot` of jest.toMatchSnapshot, I wonder if we can achieve the below... - // it('should have ts errors when not an element or chainable', async () => { - // //@ts-expect-error - // await wdioExpect('.findme').toMatchSnapshot() - // }) }) describe('toMatchInlineSnapshot', () => { @@ -413,11 +405,6 @@ describe('type assertions', () => { const expectString1:string = wdioExpect.toHaveSimpleCustomProperty('string') const expectString2:string = wdioExpect.not.toHaveSimpleCustomProperty('string') - // TODO how to make the below fails when the await is missing inf front of the expect from the asymmetric matcher? - // expectPromiseVoid = wdioExpect(chainableElement).toHaveCustomProperty( - // wdioExpect.toHaveCustomProperty(chainableElement) - // ) - // @ts-expect-error expectVoid = wdioExpect.toHaveSimpleCustomProperty(chainableElement) // @ts-expect-error @@ -439,11 +426,6 @@ describe('type assertions', () => { const expectPromiseWdioElement1: Promise> = wdioExpect.toHaveCustomProperty(chainableElement) const expectPromiseWdioElement2: Promise> = wdioExpect.not.toHaveCustomProperty(chainableElement) - // TODO how to make the below fails when the await is missing inf front of the expect from the asymmetric matcher? - // expectPromiseVoid = wdioExpect(chainableElement).toHaveCustomProperty( - // wdioExpect.toHaveCustomProperty(chainableElement) - // ) - // @ts-expect-error expectVoid = wdioExpect.toHaveCustomProperty(chainableElement) // @ts-expect-error @@ -458,19 +440,6 @@ describe('type assertions', () => { await wdioExpect.toHaveCustomProperty(chainableElement) ) }) - - // TODO this is not supported in Wdio right now, maybe one day we can support it - // it('should support an async asymmetric matcher on a non async matcher', async () => { - // expectPromiseVoid = wdioExpect({ value: 5 }).toEqual({ - // value: wdioExpect.toHaveCustomProperty(chainableElement) - // }) - - // // @ts-expect-error - // expectVoid = wdioExpect({ value: 5 }).toEqual({ - // value: wdioExpect.toHaveCustomProperty(chainableElement) - // }) - - // }) }) }) @@ -575,11 +544,6 @@ describe('type assertions', () => { response: { success: true }, }) - // TODO dprevost: Asymmetric matcher is not defined on the entire object in the .d.ts file, it is a bug? - // expectPromiseVoid = wdioExpect(promiseNetworkMock).toBeRequestedWith(wdioExpect.objectContaining({ - // response: { success: true }, // [optional] object | function | custom matcher - // })) - expectPromiseVoid = wdioExpect(promiseNetworkMock).toBeRequestedWith({ url: wdioExpect.stringContaining('test'), method: 'POST', @@ -751,12 +715,6 @@ describe('type assertions', () => { expectVoid = wdioExpect.soft(chainableArray).not.toBeDisplayed() }) - it('should have ts (or lint) errors when actual is a chainable not awaited', async () => { - // TODO dprevost: see if an eslint rule could help us here to detect missing await when not using wdio matchers - // expectPromiseVoid = wdioExpect.soft(chainableElement.getText()).toEqual('Basketball Shoes') - // expectPromiseVoid = wdioExpect.soft(chainableElement.getText()).toMatch(/€\d+/) - }) - 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')) diff --git a/test-types/jest-@jest_global/types-jest.test.ts b/test-types/jest-@jest_global/types-jest.test.ts index 1890f5b67..ef0f7ca39 100644 --- a/test-types/jest-@jest_global/types-jest.test.ts +++ b/test-types/jest-@jest_global/types-jest.test.ts @@ -4,13 +4,6 @@ import { expect } from 'expect-webdriverio' import { describe, it, expect as jestExpect } from '@jest/globals' describe('type assertions', async () => { - // TODO dprevost: using @wdio/globals/types overlap with the local types/expect-webdriverio.d.ts, find how to work with this - // const chainableElement: ChainablePromiseElement = $('findMe') - // const chainableArray: ChainablePromiseArray = $$('ul>li') - - // const element: WebdriverIO.Element = await chainableElement?.getElement() - // const elementArray: WebdriverIO.ElementArray = await chainableArray?.getElements() - const chainableElement = {} as unknown as ChainablePromiseElement const chainableArray = {} as ChainablePromiseArray @@ -278,12 +271,6 @@ describe('type assertions', async () => { //@ts-expect-error expectVoid = expect(chainableElement).not.toMatchSnapshot() }) - - // TODO - since we are overloading the `toMatchSnapshot` of jest.toMatchSnapshot, I wonder if we can achieve the below... - // it('should have ts errors when not an element or chainable', async () => { - // //@ts-expect-error - // await expect('.findme').toMatchSnapshot() - // }) }) describe('toMatchInlineSnapshot', () => { @@ -419,11 +406,6 @@ describe('type assertions', async () => { const expectString1:string = expect.toHaveSimpleCustomProperty('string') const expectString2:string = expect.not.toHaveSimpleCustomProperty('string') - // TODO how to make the below fails when the await is missing inf front of the expect from the asymmetric matcher? - // expectPromiseVoid = expect(chainableElement).toHaveCustomProperty( - // expect.toHaveCustomProperty(chainableElement) - // ) - // @ts-expect-error expectVoid = expect.toHaveSimpleCustomProperty(chainableElement) // @ts-expect-error @@ -445,11 +427,6 @@ describe('type assertions', async () => { const expectPromiseWdioElement1: Promise> = expect.toHaveCustomProperty(chainableElement) const expectPromiseWdioElement2: Promise> = expect.not.toHaveCustomProperty(chainableElement) - // TODO how to make the below fails when the await is missing inf front of the expect from the asymmetric matcher? - // expectPromiseVoid = expect(chainableElement).toHaveCustomProperty( - // expect.toHaveCustomProperty(chainableElement) - // ) - // @ts-expect-error expectVoid = expect.toHaveCustomProperty(chainableElement) // @ts-expect-error @@ -464,19 +441,6 @@ describe('type assertions', async () => { await expect.toHaveCustomProperty(chainableElement) ) }) - - // TODO this is not supported in Wdio right now, maybe one day we can support it - // it('should support an async asymmetric matcher on a non async matcher', async () => { - // expectPromiseVoid = expect({ value: 5 }).toEqual({ - // value: expect.toHaveCustomProperty(chainableElement) - // }) - - // // @ts-expect-error - // expectVoid = expect({ value: 5 }).toEqual({ - // value: expect.toHaveCustomProperty(chainableElement) - // }) - - // }) }) }) @@ -757,12 +721,6 @@ describe('type assertions', async () => { expectVoid = expect.soft(chainableArray).not.toBeDisplayed() }) - it('should have ts (or lint) errors when actual is a chainable not awaited', async () => { - // TODO dprevost: see if an eslint rule could help us here to detect missing await when not using wdio matchers - // expectPromiseVoid = expect.soft(chainableElement.getText()).toEqual('Basketball Shoes') - // expectPromiseVoid = expect.soft(chainableElement.getText()).toMatch(/€\d+/) - }) - 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')) diff --git a/test-types/jest-@types_jest/types-jest.test.ts b/test-types/jest-@types_jest/types-jest.test.ts index 5a7efd04b..01fa574f4 100644 --- a/test-types/jest-@types_jest/types-jest.test.ts +++ b/test-types/jest-@types_jest/types-jest.test.ts @@ -1,13 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ describe('type assertions', async () => { - // TODO dprevost: using @wdio/globals/types overlap with the local types/expect-webdriverio.d.ts, find how to work with this - // const chainableElement: ChainablePromiseElement = $('findMe') - // const chainableArray: ChainablePromiseArray = $$('ul>li') - - // const element: WebdriverIO.Element = await chainableElement?.getElement() - // const elementArray: WebdriverIO.ElementArray = await chainableArray?.getElements() - const chainableElement = {} as unknown as ChainablePromiseElement const chainableArray = {} as ChainablePromiseArray @@ -275,12 +268,6 @@ describe('type assertions', async () => { //@ts-expect-error expectVoid = expect(chainableElement).not.toMatchSnapshot() }) - - // TODO - since we are overloading the `toMatchSnapshot` of jest.toMatchSnapshot, I wonder if we can achieve the below... - // it('should have ts errors when not an element or chainable', async () => { - // //@ts-expect-error - // await expect('.findme').toMatchSnapshot() - // }) }) describe('toMatchInlineSnapshot', () => { @@ -416,11 +403,6 @@ describe('type assertions', async () => { const expectString1:string = expect.toHaveSimpleCustomProperty('string') const expectString2:string = expect.not.toHaveSimpleCustomProperty('string') - // TODO how to make the below fails when the await is missing inf front of the expect from the asymmetric matcher? - // expectPromiseVoid = expect(chainableElement).toHaveCustomProperty( - // expect.toHaveCustomProperty(chainableElement) - // ) - // @ts-expect-error expectVoid = expect.toHaveSimpleCustomProperty(chainableElement) // @ts-expect-error @@ -442,11 +424,6 @@ describe('type assertions', async () => { const expectPromiseWdioElement1: Promise> = expect.toHaveCustomProperty(chainableElement) const expectPromiseWdioElement2: Promise> = expect.not.toHaveCustomProperty(chainableElement) - // TODO how to make the below fails when the await is missing inf front of the expect from the asymmetric matcher? - // expectPromiseVoid = expect(chainableElement).toHaveCustomProperty( - // expect.toHaveCustomProperty(chainableElement) - // ) - // @ts-expect-error expectVoid = expect.toHaveCustomProperty(chainableElement) // @ts-expect-error @@ -461,19 +438,6 @@ describe('type assertions', async () => { await expect.toHaveCustomProperty(chainableElement) ) }) - - // TODO this is not supported in Wdio right now, maybe one day we can support it - // it('should support an async asymmetric matcher on a non async matcher', async () => { - // expectPromiseVoid = expect({ value: 5 }).toEqual({ - // value: expect.toHaveCustomProperty(chainableElement) - // }) - - // // @ts-expect-error - // expectVoid = expect({ value: 5 }).toEqual({ - // value: expect.toHaveCustomProperty(chainableElement) - // }) - - // }) }) }) @@ -583,11 +547,6 @@ describe('type assertions', async () => { response: { success: true }, }) - // TODO dprevost: Asymmetric matcher is not defined on the entire object in the .d.ts file, it is a bug? - // expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith(expect.objectContaining({ - // response: { success: true }, // [optional] object | function | custom matcher - // })) - expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith({ url: expect.stringContaining('test'), method: 'POST', @@ -757,12 +716,6 @@ describe('type assertions', async () => { expectVoid = expect.soft(chainableArray).not.toBeDisplayed() }) - it('should have ts (or lint) errors when actual is a chainable not awaited', async () => { - // TODO dprevost: see if an eslint rule could help us here to detect missing await when not using wdio matchers - // expectPromiseVoid = expect.soft(chainableElement.getText()).toEqual('Basketball Shoes') - // expectPromiseVoid = expect.soft(chainableElement.getText()).toMatch(/€\d+/) - }) - 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')) diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index 2a3f332b2..4e9e4d9f3 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -269,12 +269,6 @@ describe('type assertions', () => { //@ts-expect-error expectVoid = expect(chainableElement).not.toMatchSnapshot() }) - - // TODO - since we are overloading the `toMatchSnapshot` of jest.toMatchSnapshot, I wonder if we can achieve the below... - // it('should have ts errors when not an element or chainable', async () => { - // //@ts-expect-error - // await expect('.findme').toMatchSnapshot() - // }) }) describe('toMatchInlineSnapshot', () => { @@ -417,11 +411,6 @@ describe('type assertions', () => { const expectString1:string = expect.toHaveSimpleCustomProperty('string') const expectString2:string = expect.not.toHaveSimpleCustomProperty('string') - // TODO how to make the below fails when the await is missing inf front of the expect from the asymmetric matcher? - // expectPromiseVoid = expect(chainableElement).toHaveCustomProperty( - // expect.toHaveCustomProperty(chainableElement) - // ) - // @ts-expect-error expectVoid = expect.toHaveSimpleCustomProperty(chainableElement) // @ts-expect-error @@ -443,11 +432,6 @@ describe('type assertions', () => { const expectPromiseWdioElement1: Promise> = expect.toHaveCustomProperty(chainableElement) const expectPromiseWdioElement2: Promise> = expect.not.toHaveCustomProperty(chainableElement) - // TODO how to make the below fails when the await is missing inf front of the expect from the asymmetric matcher? - // expectPromiseVoid = expect(chainableElement).toHaveCustomProperty( - // expect.toHaveCustomProperty(chainableElement) - // ) - // @ts-expect-error expectVoid = expect.toHaveCustomProperty(chainableElement) // @ts-expect-error @@ -462,19 +446,6 @@ describe('type assertions', () => { await expect.toHaveCustomProperty(chainableElement) ) }) - - // TODO this is not supported in Wdio right now, maybe one day we can support it - // it('should support an async asymmetric matcher on a non async matcher', async () => { - // expectPromiseVoid = expect({ value: 5 }).toEqual({ - // value: expect.toHaveCustomProperty(chainableElement) - // }) - - // // @ts-expect-error - // expectVoid = expect({ value: 5 }).toEqual({ - // value: expect.toHaveCustomProperty(chainableElement) - // }) - - // }) }) }) @@ -750,12 +721,6 @@ describe('type assertions', () => { expectVoid = expect.soft(chainableArray).not.toBeDisplayed() }) - it('should have ts (or lint) errors when actual is a chainable not awaited', async () => { - // TODO dprevost: see if an eslint rule could help us here to detect missing await when not using wdio matchers - // expectPromiseVoid = expect.soft(chainableElement.getText()).toEqual('Basketball Shoes') - // expectPromiseVoid = expect.soft(chainableElement.getText()).toMatch(/€\d+/) - }) - 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')) From acaaeb3d3e41a82cd6b570818f89972d38338b10 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Fri, 4 Jul 2025 19:21:35 -0400 Subject: [PATCH 65/99] Add back promise matchers! --- src/softExpect.ts | 4 ++- test-types/jasmine/types-jasmine.test.ts | 18 ++++------- .../jest-@jest_global/types-jest.test.ts | 30 +++++++------------ test-types/mocha/types-mocha.test.ts | 23 +++++++------- types/expect-webdriverio.d.ts | 19 ++++++++++-- 5 files changed, 49 insertions(+), 45 deletions(-) diff --git a/src/softExpect.ts b/src/softExpect.ts index 4aa2dfbb9..4f984b396 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,6 +87,7 @@ const createSoftMatcher = ( expectChain = expectChain.rejects } + // TODO ddprevost might need to review this await since not all expect requires to be awaited return await ((expectChain as unknown) as Record Promise>)[matcherName](...args) } catch (error) { diff --git a/test-types/jasmine/types-jasmine.test.ts b/test-types/jasmine/types-jasmine.test.ts index f67800999..9544badd1 100644 --- a/test-types/jasmine/types-jasmine.test.ts +++ b/test-types/jasmine/types-jasmine.test.ts @@ -468,13 +468,13 @@ describe('type assertions', () => { it('should still expect void type when actual is a Promise since we do not overload them', async () => { const promiseBoolean = Promise.resolve(true) - expectVoid = wdioExpect(promiseBoolean).toBe(true) - expectVoid = wdioExpect(promiseBoolean).not.toBe(true) + expectPromiseVoid = wdioExpect(promiseBoolean).toBe(true) + expectPromiseVoid = wdioExpect(promiseBoolean).not.toBe(true) //@ts-expect-error - expectPromiseVoid = wdioExpect(promiseBoolean).toBe(true) + expectVoid = wdioExpect(promiseBoolean).toBe(true) //@ts-expect-error - expectPromiseVoid = wdioExpect(promiseBoolean).toBe(true) + expectVoid = wdioExpect(promiseBoolean).toBe(true) }) it('should work with string', async () => { @@ -497,15 +497,9 @@ describe('type assertions', () => { describe('Promise type assertions', () => { const booleanPromise: Promise = Promise.resolve(true) - it('should expect a Promise of type', async () => { - const expectPromiseBoolean1: ExpectWebdriverIO.MatchersAndInverse> = wdioExpect(booleanPromise) - const expectPromiseBoolean2: ExpectWebdriverIO.Matchers> = wdioExpect(booleanPromise).not - }) - it('should work with resolves & rejects correctly', async () => { - // TODO dprevost should we support this in Wdio since we do not even use it or document it? - // expectPromiseVoid = wdioExpect(booleanPromise).resolves.toBe(true) - // expectPromiseVoid = wdioExpect(booleanPromise).rejects.toBe(true) + expectPromiseVoid = wdioExpect(booleanPromise).resolves.toBe(true) + expectPromiseVoid = wdioExpect(booleanPromise).rejects.toBe(true) //@ts-expect-error expectVoid = wdioExpect(booleanPromise).resolves.toBe(true) diff --git a/test-types/jest-@jest_global/types-jest.test.ts b/test-types/jest-@jest_global/types-jest.test.ts index ef0f7ca39..14dfcd67a 100644 --- a/test-types/jest-@jest_global/types-jest.test.ts +++ b/test-types/jest-@jest_global/types-jest.test.ts @@ -469,13 +469,14 @@ describe('type assertions', async () => { 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) + // TODO dprevost check if this one need to stay a void or can be a Promise + // 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 () => { @@ -498,20 +499,9 @@ describe('type assertions', async () => { describe('Promise type assertions', () => { const booleanPromise: Promise = Promise.resolve(true) - it('should expect a Promise of type', async () => { - const expectPromiseBoolean1: ExpectWebdriverIO.MatchersAndInverse> = expect(booleanPromise) - const expectPromiseBoolean2: ExpectWebdriverIO.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 () => { - // TODO dprevost should we bring back the support for this? - // expectPromiseVoid = expect(booleanPromise).resolves.toBe(true) - // expectPromiseVoid = expect(booleanPromise).rejects.toBe(true) + expectPromiseVoid = expect(booleanPromise).resolves.toBe(true) + expectPromiseVoid = expect(booleanPromise).rejects.toBe(true) //@ts-expect-error expectVoid = expect(booleanPromise).resolves.toBe(true) diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index 4e9e4d9f3..a8b99d0e2 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -474,13 +474,13 @@ describe('type assertions', () => { 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) + expectPromiseVoid = expect(promiseBoolean).toBe(true) + expectPromiseVoid = expect(promiseBoolean).not.toBe(true) //@ts-expect-error - expectPromiseVoid = expect(promiseBoolean).toBe(true) + expectVoid = expect(promiseBoolean).toBe(true) //@ts-expect-error - expectPromiseVoid = expect(promiseBoolean).toBe(true) + expectVoid = expect(promiseBoolean).toBe(true) }) it('should work with string', async () => { @@ -503,21 +503,24 @@ describe('type assertions', () => { describe('Promise type assertions', () => { const booleanPromise: Promise = Promise.resolve(true) - it('should expect a Promise of type', async () => { - const expectPromiseBoolean1: ExpectWebdriverIO.MatchersAndInverse> = expect(booleanPromise) - const expectPromiseBoolean2: ExpectWebdriverIO.Matchers> = expect(booleanPromise).not + it('should have expect return Matchers with a Promise', async () => { + const expectPromiseBoolean1: ExpectWebdriverIO.Matchers, Promise> & ExpectLibInverse, Promise>> & ExpectWebdriverIO.PromiseMatchers> = expect(booleanPromise) + const expectPromiseBoolean2: ExpectWebdriverIO.Matchers, Promise> = expect(booleanPromise).not }) it('should work with resolves & rejects correctly', async () => { - // TODO dprevost should we support this in Wdio since we do not even use it or document it? - // expectPromiseVoid = expect(booleanPromise).resolves.toBe(true) - // expectPromiseVoid = expect(booleanPromise).rejects.toBe(true) + 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) + //@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 () => { diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 1923df0f3..8f50b1296 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -409,7 +409,7 @@ interface WdioCustomExpect { * 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): T extends PromiseLike ? ExpectWebdriverIO.MatchersAndInverse, T> : ExpectWebdriverIO.MatchersAndInverse; + soft(actual: T): T extends PromiseLike ? ExpectWebdriverIO.MatchersAndInverse, T> & ExpectWebdriverIO.PromiseMatchers : ExpectWebdriverIO.MatchersAndInverse; /** * Get all current soft assertion failures @@ -486,7 +486,7 @@ declare namespace ExpectWebdriverIO { * * @param actual The value to apply matchers against. */ - (actual: T): ExpectWebdriverIO.MatchersAndInverse; + (actual: T): T extends PromiseLike ? ExpectWebdriverIO.MatchersAndInverse, T> & ExpectWebdriverIO.PromiseMatchers : ExpectWebdriverIO.MatchersAndInverse; } interface Matchers, T> extends WdioMatchers {} @@ -499,6 +499,21 @@ declare namespace ExpectWebdriverIO { 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 From 3cebbe1e372701f64b872cefbe76ebb4f262c22f Mon Sep 17 00:00:00 2001 From: David Prevost Date: Fri, 4 Jul 2025 19:30:58 -0400 Subject: [PATCH 66/99] Review more TODOs --- test-types/jasmine-async/types-jasmine.test.ts | 6 ++---- test-types/jasmine/types-jasmine.test.ts | 4 ++-- test-types/jest-@jest_global/types-jest.test.ts | 5 ++--- test-types/jest-@types_jest/types-jest.test.ts | 5 ++--- test-types/mocha/types-mocha.test.ts | 4 ++-- 5 files changed, 10 insertions(+), 14 deletions(-) diff --git a/test-types/jasmine-async/types-jasmine.test.ts b/test-types/jasmine-async/types-jasmine.test.ts index 0ed1d110b..619bde284 100644 --- a/test-types/jasmine-async/types-jasmine.test.ts +++ b/test-types/jasmine-async/types-jasmine.test.ts @@ -37,8 +37,6 @@ describe('type assertions', () => { // @ts-expect-error await expectAsync(browser).toHaveUrl(6) - //// @ts-expect-error TODO dprevost can we make the below fail? - // await expectAsync(browser).toHaveUrl(wdioExpect.objectContaining({})) }) it('should have ts errors when actual is not a Browser element', async () => { @@ -500,7 +498,7 @@ describe('type assertions', () => { expectPromiseVoid = expectAsync(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) expectPromiseVoid = expectAsync(promiseNetworkMock).toBeRequestedWith({ - url: 'http://localhost:8080/api/todo', + url: 'http://localhost:8080/api', method: 'POST', statusCode: 200, requestHeaders: { Authorization: 'foo' }, @@ -546,7 +544,7 @@ describe('type assertions', () => { // @ts-expect-error expectVoid = expectAsync(promiseNetworkMock).toBeRequestedWith({ - url: 'http://localhost:8080/api/todo', + url: 'http://localhost:8080/api', method: 'POST', statusCode: 200, requestHeaders: { Authorization: 'foo' }, diff --git a/test-types/jasmine/types-jasmine.test.ts b/test-types/jasmine/types-jasmine.test.ts index 9544badd1..59a742ea7 100644 --- a/test-types/jasmine/types-jasmine.test.ts +++ b/test-types/jasmine/types-jasmine.test.ts @@ -529,7 +529,7 @@ describe('type assertions', () => { expectPromiseVoid = wdioExpect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) expectPromiseVoid = wdioExpect(promiseNetworkMock).toBeRequestedWith({ - url: 'http://localhost:8080/api/todo', + url: 'http://localhost:8080/api', method: 'POST', statusCode: 200, requestHeaders: { Authorization: 'foo' }, @@ -575,7 +575,7 @@ describe('type assertions', () => { // @ts-expect-error expectVoid = wdioExpect(promiseNetworkMock).toBeRequestedWith({ - url: 'http://localhost:8080/api/todo', + url: 'http://localhost:8080/api', method: 'POST', statusCode: 200, requestHeaders: { Authorization: 'foo' }, diff --git a/test-types/jest-@jest_global/types-jest.test.ts b/test-types/jest-@jest_global/types-jest.test.ts index 14dfcd67a..733e18dbb 100644 --- a/test-types/jest-@jest_global/types-jest.test.ts +++ b/test-types/jest-@jest_global/types-jest.test.ts @@ -519,7 +519,6 @@ describe('type assertions', async () => { }) describe('Network Matchers', () => { - // const promiseNetworkMock = browser.mock('**/api/todo*') const promiseNetworkMock = Promise.resolve(networkMock) it('should not have ts errors when typing to Promise', async () => { @@ -532,7 +531,7 @@ describe('type assertions', async () => { expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith({ - url: 'http://localhost:8080/api/todo', + url: 'http://localhost:8080/api', method: 'POST', statusCode: 200, requestHeaders: { Authorization: 'foo' }, @@ -578,7 +577,7 @@ describe('type assertions', async () => { // @ts-expect-error expectVoid = expect(promiseNetworkMock).toBeRequestedWith({ - url: 'http://localhost:8080/api/todo', + url: 'http://localhost:8080/api', method: 'POST', statusCode: 200, requestHeaders: { Authorization: 'foo' }, diff --git a/test-types/jest-@types_jest/types-jest.test.ts b/test-types/jest-@types_jest/types-jest.test.ts index 01fa574f4..445b7758a 100644 --- a/test-types/jest-@types_jest/types-jest.test.ts +++ b/test-types/jest-@types_jest/types-jest.test.ts @@ -525,7 +525,6 @@ describe('type assertions', async () => { }) describe('Network Matchers', () => { - // const promiseNetworkMock = browser.mock('**/api/todo*') const promiseNetworkMock = Promise.resolve(networkMock) it('should not have ts errors when typing to Promise', async () => { @@ -538,7 +537,7 @@ describe('type assertions', async () => { expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith({ - url: 'http://localhost:8080/api/todo', + url: 'http://localhost:8080/api', method: 'POST', statusCode: 200, requestHeaders: { Authorization: 'foo' }, @@ -584,7 +583,7 @@ describe('type assertions', async () => { // @ts-expect-error expectVoid = expect(promiseNetworkMock).toBeRequestedWith({ - url: 'http://localhost:8080/api/todo', + url: 'http://localhost:8080/api', method: 'POST', statusCode: 200, requestHeaders: { Authorization: 'foo' }, diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index a8b99d0e2..2f9e78f18 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -544,7 +544,7 @@ describe('type assertions', () => { expectPromiseVoid = expect(promiseNetworkMock).not.toBeRequestedTimes({ gte: 5, lte: 10 }) expectPromiseVoid = expect(promiseNetworkMock).toBeRequestedWith({ - url: 'http://localhost:8080/api/todo', + url: 'http://localhost:8080/api', method: 'POST', statusCode: 200, requestHeaders: { Authorization: 'foo' }, @@ -590,7 +590,7 @@ describe('type assertions', () => { // @ts-expect-error expectVoid = expect(promiseNetworkMock).toBeRequestedWith({ - url: 'http://localhost:8080/api/todo', + url: 'http://localhost:8080/api', method: 'POST', statusCode: 200, requestHeaders: { Authorization: 'foo' }, From 8eacdfa593e4334a4028b191ce6a76b4d7029115 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sat, 5 Jul 2025 12:06:34 -0400 Subject: [PATCH 67/99] Add finding on augmentation of `@jest/globals` --- docs/Framework.md | 47 ++++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/docs/Framework.md b/docs/Framework.md index 8f016a73f..ce3d84d3a 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -1,22 +1,22 @@ # Expect-WebDriverIO Framework -Expect-WebDriverIO is inspired by [`expect`](https://www.npmjs.com/package/expect) but also extending it. Therefore we can exploit usually everything provided by the API of expect with some WebDriverIO touch. +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. - Note: 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/), [Jasmine](https://jasmine.github.io/). - - When an `expect` is defined globally, we usually overwrite it with the one of `expect-webdriverio` to have our defined assertions work out of the box. +We can pair `expect-webdriverio` with [Jest](https://jestjs.io/), [Mocha](https://mochajs.org/), and [Jasmine](https://jasmine.github.io/). + - When `expect` is defined globally, we usually overwrite it with the one from `expect-webdriverio` to have our defined assertions work out of the box. ### Jest -We can use `expect-webdriverio` with [Jest](https://jestjs.io/) with either the [`@jest/globals`](https://www.npmjs.com/package/@jest/globals) (preferred) or the [`@types/jest`](https://www.npmjs.com/package/@types/jest) (has global imports support) +We can use `expect-webdriverio` with [Jest](https://jestjs.io/) using either [`@jest/globals`](https://www.npmjs.com/package/@jest/globals) (preferred) or [`@types/jest`](https://www.npmjs.com/package/@types/jest) (has global imports support). - Note: Jest maintainers do not support [`@types/jest`](https://www.npmjs.com/package/@types/jest). In case this library gets out of date or has problems, support might be dropped. -In each case, when used **outside of [WDIO Testrunner](https://webdriver.io/docs/clioptions)**, types are required to be added in your [`tsconfig.json`](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) - - Note: With Jest the matchers `toMatchSnapshot` and `toMatchInlineSnapshot` were overloaded. To resolve correctly the types `expect-webdriverio/jest` must be last. +In each case, 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). + - 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 with [Jest](https://jestjs.io/) and the [`@jest/globals`](https://www.npmjs.com/package/@jest/globals), we should `import` the `expect` keyword from `expect-webdriverio` +When paired with [Jest](https://jestjs.io/) and [`@jest/globals`](https://www.npmjs.com/package/@jest/globals), we should `import` the `expect` function from `expect-webdriverio`. ```ts import { expect } from 'expect-webdriverio' @@ -31,18 +31,23 @@ describe('My tests', async () => { }) ``` -No `types` is expected in `tsconfig.json` -Optionally, to not need `import { expect } from 'expect-webdriverio'` you can use the below +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's `expect` +Multiple attempts were made to augment `@jest/globals` to support `expect-webdriverio` matchers directly on Jest's `expect`. 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 paired with [Jest](https://jestjs.io/) and the [`@types/jest`](https://www.npmjs.com/package/@types/jest), no imports are required. Global ones are already defined correctly +When paired with [Jest](https://jestjs.io/) and [`@types/jest`](https://www.npmjs.com/package/@types/jest), no imports are required. Global types are already defined correctly. ```ts describe('My tests', async () => { @@ -67,10 +72,10 @@ Expected in `tsconfig.json`: ``` ### 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) +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 +No import is required; everything is set globally. ```ts describe('My tests', async () => { @@ -95,16 +100,16 @@ Expected in `tsconfig.json`: ``` #### Chai -`expect-webdriverio` can coexist with [Chai](https://www.chaijs.com/) assertion library, by importing both library explicitly. -See also this [documentation](https://webdriver.io/docs/assertion/#migrating-from-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 have it configured correctly as it needs to force the `expect` to be `expectAsync` and also to register the wdio matchers with `addAsyncMatcher` since `expect-webdriverio` only supports the jest style `expect.extend` version. +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` is still offers but subject to removal or to be moved into `@wdio/jasmine-framework`. The usage of `expectAsync` is also subject to future removal. +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` -Since the above types augment the `AsyncMatcher` of `Jasmine` then with this library alone it looks like the below even though it is not runnable since the matchers are not registered +Since the above types augment the `AsyncMatcher` of Jasmine, with this library alone it looks like the following, even though it is not runnable since the matchers are not registered: ```ts describe('My tests', async () => { @@ -129,7 +134,7 @@ Expected in `tsconfig.json`: ``` #### `expect` of `expect-webdriverio` -It is preferable to use the `expect` from `expect-webdriverio` to guarantee future compatibility +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"` @@ -150,14 +155,14 @@ Expected in `tsconfig.json`: "compilerOptions": { "types": [ "@types/jasmine", - "expect-webdriverio/expect-global", // Force expect to be the 'expect-webdriverio', to comment and use the import above if it conflict with Jasmine + "expect-webdriverio/expect-global", // Force expect to be the 'expect-webdriverio'; comment out and use the import above if it conflicts with Jasmine ] } } ``` #### Asymmetric matcher -Asymmetric matcher has limited support, even though `jasmine.stringContaining` has no error it is potentially not working even with `@wdio/jasmine-framework`, but the below should: +Asymmetric matchers have limited support. Even though `jasmine.stringContaining` has no error, it is potentially not working even with `@wdio/jasmine-framework`, but the below should work: ```ts describe('My tests', async () => { From 60ccb412ed0eea84a22bc426f323fbeccbedc8e0 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sat, 5 Jul 2025 13:23:37 -0400 Subject: [PATCH 68/99] Update doc following finding requiring registering matcher with Jest --- .npmrc | 0 check-node-version.js | 0 docs/Framework.md | 25 +++++++++++++++++++++---- 3 files changed, 21 insertions(+), 4 deletions(-) delete mode 100644 .npmrc delete mode 100644 check-node-version.js diff --git a/.npmrc b/.npmrc deleted file mode 100644 index e69de29bb..000000000 diff --git a/check-node-version.js b/check-node-version.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/Framework.md b/docs/Framework.md index ce3d84d3a..f50f970fd 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -9,8 +9,8 @@ We can pair `expect-webdriverio` with [Jest](https://jestjs.io/), [Mocha](https: - When `expect` is defined globally, we usually overwrite it with the one from `expect-webdriverio` to have our defined assertions work out of the box. ### Jest -We can use `expect-webdriverio` with [Jest](https://jestjs.io/) using either [`@jest/globals`](https://www.npmjs.com/package/@jest/globals) (preferred) or [`@types/jest`](https://www.npmjs.com/package/@types/jest) (has global imports support). - - Note: Jest maintainers do not support [`@types/jest`](https://www.npmjs.com/package/@types/jest). In case this library gets out of date or has problems, support might be dropped. +We can use `expect-webdriverio` with [Jest](https://jestjs.io/) using either [`@jest/globals`](https://www.npmjs.com/package/@jest/globals) (preferred) or [`@types/jest`](https://www.npmjs.com/package/@types/jest) (which has global imports 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. In each case, 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). - Note: With Jest, the matchers `toMatchSnapshot` and `toMatchInlineSnapshot` are overloaded. To resolve the types correctly, `expect-webdriverio/jest` must be last. @@ -47,8 +47,25 @@ This [Jest issue](https://github.com/jestjs/jest/issues/12424) seems to target t #### With `@types/jest` -When paired with [Jest](https://jestjs.io/) and [`@types/jest`](https://www.npmjs.com/package/@types/jest), no imports are required. Global types are already defined correctly. +When also paired with [`@types/jest`](https://www.npmjs.com/package/@types/jest), no imports are required. Global types are already defined correctly and you can simply use Jest's `expect` directly. +Optional: If you are NOT using WDIO Testrunner, it might be required to correctly register the WDIO matchers on Jest's `expect` as shown below: +```ts +import { matchers } from "expect-webdriverio"; + +// Import and extend Jest's expect with WebdriverIO matchers +beforeAll(async () => { + // Convert the Map to a plain object and extend Jest's expect + const matchersObject: Record = {}; + matchers.forEach((matcher, name) => { + matchersObject[name] = matcher; + }); + + expect.extend(matchersObject); +}); +``` + +As shown below, no imports are required and we can use WDIO matchers directly on Jest's `expect`: ```ts describe('My tests', async () => { @@ -65,7 +82,7 @@ Expected in `tsconfig.json`: "compilerOptions": { "types": [ "@types/jest", - "expect-webdriverio/jest" // Must be after for overloaded matchers `toMatchSnapshot` and `toMatchInlineSnapshot` + "expect-webdriverio/jest" // Must be last for overloaded matchers `toMatchSnapshot` and `toMatchInlineSnapshot` ] } } From 4f159d2136063931c70897b32b9128b52f8f83ce Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sun, 6 Jul 2025 09:13:00 -0400 Subject: [PATCH 69/99] Align `AssertionResult` and `matchers` type with expect library --- docs/Framework.md | 35 +++++++++++++++-------------------- src/softExpect.ts | 2 +- src/utils.ts | 2 +- types/expect-webdriverio.d.ts | 28 ++++++++++++++++------------ 4 files changed, 33 insertions(+), 34 deletions(-) diff --git a/docs/Framework.md b/docs/Framework.md index f50f970fd..b4f81246c 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -5,18 +5,19 @@ Expect-WebDriverIO is inspired by [`expect`](https://www.npmjs.com/package/expec ## Compatibility -We can pair `expect-webdriverio` with [Jest](https://jestjs.io/), [Mocha](https://mochajs.org/), and [Jasmine](https://jasmine.github.io/). - - When `expect` is defined globally, we usually overwrite it with the one from `expect-webdriverio` to have our defined assertions work out of the box. +We can pair `expect-webdriverio` with [Jest](https://jestjs.io/), [Mocha](https://mochajs.org/), and even [Jasmine](https://jasmine.github.io/). + +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). ### Jest -We can use `expect-webdriverio` with [Jest](https://jestjs.io/) using either [`@jest/globals`](https://www.npmjs.com/package/@jest/globals) (preferred) or [`@types/jest`](https://www.npmjs.com/package/@types/jest) (which has global imports support). +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. - -In each case, 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). - 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 with [Jest](https://jestjs.io/) and [`@jest/globals`](https://www.npmjs.com/package/@jest/globals), we should `import` the `expect` function from `expect-webdriverio`. +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' @@ -40,28 +41,22 @@ Optionally, to avoid needing `import { expect } from 'expect-webdriverio'`, you } } ``` -##### Augmenting Jest's `expect` -Multiple attempts were made to augment `@jest/globals` to support `expect-webdriverio` matchers directly on Jest's `expect`. 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. +##### 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 types are already defined correctly and you can simply use Jest's `expect` directly. +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. -Optional: If you are NOT using WDIO Testrunner, it might be required to correctly register the WDIO matchers on Jest's `expect` as shown below: +If you are NOT using WDIO Testrunner, it may be required to correctly register the WDIO matchers on Jest's `expect` as shown below: ```ts +import { expect } from "@jest/globals"; import { matchers } from "expect-webdriverio"; -// Import and extend Jest's expect with WebdriverIO matchers beforeAll(async () => { - // Convert the Map to a plain object and extend Jest's expect - const matchersObject: Record = {}; - matchers.forEach((matcher, name) => { - matchersObject[name] = matcher; - }); - - expect.extend(matchersObject); + expect.extend(matchers); }); ``` @@ -178,8 +173,8 @@ Expected in `tsconfig.json`: } ``` -#### Asymmetric matcher -Asymmetric matchers have limited support. Even though `jasmine.stringContaining` has no error, it is potentially not working even with `@wdio/jasmine-framework`, but the below should work: +#### Asymmetric matchers +Asymmetric matchers have limited support. Even though `jasmine.stringContaining` has no error, it potentially does not work even with `@wdio/jasmine-framework`, but the example below should work: ```ts describe('My tests', async () => { diff --git a/src/softExpect.ts b/src/softExpect.ts index 4f984b396..41963dc9f 100644 --- a/src/softExpect.ts +++ b/src/softExpect.ts @@ -88,7 +88,7 @@ const createSoftMatcher = ( } // TODO ddprevost might need to review this await since not all expect requires to be awaited - 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/utils.ts b/src/utils.ts index 2ddd8a380..5227e8f4d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -91,7 +91,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() diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 8f50b1296..2c350f5da 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -16,9 +16,19 @@ 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 // TODO dprevost: a suggestion would be to move any code outside of the namespace to separate types.ts file, so that we can import the types. +// 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. */ @@ -556,21 +566,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 /** - * Used by the wdio main project to configure the matchers in the runner when using Jasmine. + * 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< - string, - ( - actual: unknown, - ...expected: unknown[] - ) => Promise - > + const matchers: Record interface AssertionHookParams { /** From e5843b24889465f748df634d4078b294284638dd Mon Sep 17 00:00:00 2001 From: David Prevost Date: Mon, 7 Jul 2025 07:32:12 -0400 Subject: [PATCH 70/99] Add documentation on required configuration --- docs/Framework.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/Framework.md b/docs/Framework.md index b4f81246c..86c13afd8 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -9,7 +9,7 @@ We can pair `expect-webdriverio` with [Jest](https://jestjs.io/), [Mocha](https: 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). +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). @@ -50,15 +50,26 @@ This [Jest issue](https://github.com/jestjs/jest/issues/12424) seems to target t #### 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, it may be required to correctly register the WDIO matchers on Jest's `expect` as shown below: +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); + expect.extend(matchers as Record); }); ``` +For the soft assertion, it needs to expose the `createSoftExpect` first. + As shown below, no imports are required and we can use WDIO matchers directly on Jest's `expect`: ```ts From 506d528962ae517df8be37dfd92b98743f4ce623 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Mon, 7 Jul 2025 09:25:23 -0400 Subject: [PATCH 71/99] Document more findings around soft assertions Doc soft limitation --- docs/Framework.md | 27 +++++++++++++++++++++++++-- src/softExpect.ts | 1 - test/softAssertions.test.ts | 16 +++++++++++++++- types/expect-webdriverio.d.ts | 1 + 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/docs/Framework.md b/docs/Framework.md index 86c13afd8..3d16e45a0 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -68,10 +68,33 @@ beforeAll(async () => { expect.extend(matchers as Record); }); ``` -For the soft assertion, it needs to expose the `createSoftExpect` first. +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), + }); +}); +``` -As shown below, no imports are required and we can use WDIO matchers directly on Jest's `expect`: +Then as shown below, no imports are required and we can use WDIO matchers directly on Jest's `expect`: ```ts describe('My tests', async () => { diff --git a/src/softExpect.ts b/src/softExpect.ts index 41963dc9f..31edd5402 100644 --- a/src/softExpect.ts +++ b/src/softExpect.ts @@ -87,7 +87,6 @@ const createSoftMatcher = ( expectChain = expectChain.rejects } - // TODO ddprevost might need to review this await since not all expect requires to be awaited return await ((expectChain as unknown) as Record ExpectWebdriverIO.AsyncAssertionResult>)[matcherName](...args) } catch (error) { diff --git a/test/softAssertions.test.ts b/test/softAssertions.test.ts index bef7e9603..898072240 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,20 @@ describe('Soft Assertions', () => { // Should be no failures now expect(expectWdio.getSoftFailures().length).toBe(0) }) + + // TODO: Soft are currently not supporting basic matchers like toBe or toEqual.To fix one day! + 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/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 2c350f5da..6ac990379 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -418,6 +418,7 @@ 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 is always returns a Promise. */ soft(actual: T): T extends PromiseLike ? ExpectWebdriverIO.MatchersAndInverse, T> & ExpectWebdriverIO.PromiseMatchers : ExpectWebdriverIO.MatchersAndInverse; From 8fa83e1216ab1d01310c9083bec105b9c41f75db Mon Sep 17 00:00:00 2001 From: David Prevost Date: Tue, 8 Jul 2025 07:28:54 -0400 Subject: [PATCH 72/99] Add known limitations for soft assertions --- docs/API.md | 6 ++++++ types/expect-webdriverio.d.ts | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/API.md b/docs/API.md index d5eb00fab..fd23834f3 100644 --- a/docs/API.md +++ b/docs/API.md @@ -111,6 +111,12 @@ 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 + +Soft assertion currently works only on custom wdio matchers and not on the basic ones like `toBe` or `toEqual` +Jasmine, even with `@wdio/jasmine-framework`, is not auto-configure to use it. + + ## 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. diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 6ac990379..f4d6138d6 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -418,7 +418,7 @@ 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 is always returns a Promise. + * 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; From 538bbcb34925921c4f73412ac6ad22e25dd887f0 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Tue, 8 Jul 2025 08:04:41 -0400 Subject: [PATCH 73/99] Clarify usage of `expectAsync` --- docs/Framework.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/Framework.md b/docs/Framework.md index 3d16e45a0..fa355bce8 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -155,7 +155,8 @@ When paired with [Jasmine](https://jasmine.github.io/), [`@wdio/jasmine-framewor 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` -Since the above types augment the `AsyncMatcher` of Jasmine, with this library alone it looks like the following, even though it is not runnable since the matchers are not registered: +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`. + - Note: Without `@wdio/jasmine-framework`, matchers will need to be registered manually. ```ts describe('My tests', async () => { From 34408ffa434be7cc9d3b7c81b36a6abe2f69cce5 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Tue, 8 Jul 2025 20:13:36 -0400 Subject: [PATCH 74/99] Processing more TODOs - `parent > test 2` snapshot does not exists --- src/utils.ts | 1 - ...ne.test.ts => types-jasmine_async.test.ts} | 29 ++++--------------- test/snapshot.test.ts | 11 ------- test/softAssertions.test.ts | 5 +++- tsconfig.json | 3 +- types/expect-webdriverio.d.ts | 2 -- 6 files changed, 11 insertions(+), 40 deletions(-) rename test-types/jasmine-async/{types-jasmine.test.ts => types-jasmine_async.test.ts} (97%) diff --git a/src/utils.ts b/src/utils.ts index 5227e8f4d..a7650f23b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -21,7 +21,6 @@ export function isAsymmetricMatcher(expected: unknown): expected is WdioAsymmetr typeof expected === 'object' && typeof expected === 'object' && expected && - // TODO dprevost will this one still work? '$$typeof' in expected && 'asymmetricMatch' in expected && expected.$$typeof === asymmetricMatcher && diff --git a/test-types/jasmine-async/types-jasmine.test.ts b/test-types/jasmine-async/types-jasmine_async.test.ts similarity index 97% rename from test-types/jasmine-async/types-jasmine.test.ts rename to test-types/jasmine-async/types-jasmine_async.test.ts index 619bde284..373757537 100644 --- a/test-types/jasmine-async/types-jasmine.test.ts +++ b/test-types/jasmine-async/types-jasmine_async.test.ts @@ -358,16 +358,15 @@ describe('type assertions', () => { it('should support a simple matcher', async () => { expectPromiseVoid = expectAsync(5).toBeWithinRange(1, 10) - // TODO dprevost this one seems to be a problem, it should be a promise!!!!! // Or as an asymmetric matcher: - expectVoid = expect({ value: 5 }).toEqual({ + expectPromiseVoid = expectAsync({ value: 5 }).toEqual({ value: wdioExpect.toBeWithinRange(1, 10) }) // @ts-expect-error expectVoid = expectAsync(5).toBeWithinRange(1, '10') // @ts-expect-error - expectPromiseVoid = expectAsync(5).toBeWithinRange('1') + expectAsync(5).toBeWithinRange('1') }) it('should support a simple custom matcher with a chainable element matcher with promise', async () => { @@ -429,16 +428,10 @@ describe('type assertions', () => { expectPromiseVoid = expectAsync(true).not.toBe(true) }) - // // TODO dprevost: Is this a valid use case? Should we support it? - // 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) - - // //@ts-expect-error - // expectPromiseVoid = expectAsync(chainableElement).toBe(true) - // //@ts-expect-error - // expectPromiseVoid = expectAsync(chainableElement).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 still expect void type when actual is a Promise since we do not overload them', async () => { const promiseBoolean = Promise.resolve(true) @@ -593,16 +586,6 @@ describe('type assertions', () => { wdioExpect.not.closeTo(5, 10) wdioExpect.not.arrayContaining(['WebdriverIO', 'Test']) wdioExpect.not.arrayOf(wdioExpect.stringContaining('WebdriverIO')) - - // TODO dprevost: Should we support these? - // wdioExpect.not.anything() - // wdioExpect.not.any(Function) - // wdioExpect.not.any(Number) - // wdioExpect.not.any(Boolean) - // wdioExpect.not.any(String) - // wdioExpect.not.any(Symbol) - // wdioExpect.not.any(Date) - // wdioExpect.not.any(Error) }) describe('Soft Assertions', async () => { diff --git a/test/snapshot.test.ts b/test/snapshot.test.ts index b757bb16b..bfe61a4c5 100644 --- a/test/snapshot.test.ts +++ b/test/snapshot.test.ts @@ -13,17 +13,6 @@ const service = SnapshotService.initiate({ resolveSnapshotPath: (path, extension) => path + extension }) -// TODO dprevost the below is missing in the snapshot.test.ts.snap file -// exports[`parent > test 2`] = ` -// { -// "deep": { -// "nested": { -// "object": "value", -// }, -// }, -// } -// `; - test('supports snapshot testing', async () => { await service.beforeTest({ title: 'test', diff --git a/test/softAssertions.test.ts b/test/softAssertions.test.ts index 898072240..04734de9d 100644 --- a/test/softAssertions.test.ts +++ b/test/softAssertions.test.ts @@ -107,7 +107,10 @@ describe('Soft Assertions', () => { expect(expectWdio.getSoftFailures().length).toBe(0) }) - // TODO: Soft are currently not supporting basic matchers like toBe or toEqual.To fix one day! + /** + * 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') diff --git a/tsconfig.json b/tsconfig.json index c515e5d15..e3870dc8b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,6 @@ /** * Let's aligned with the WebdriverIO tsconfig.json. * @see https://github.com/webdriverio/webdriverio/blob/main/tsconfig.json#L5 - * TODO: dprevost, why using moduleResolution node16 in the above link? */ "compilerOptions": { "outDir": "./lib/", @@ -15,7 +14,7 @@ "skipLibCheck": true, "strictPropertyInitialization": true, "strictNullChecks": true, - "moduleResolution": "node", // node is actually node10, this should be reviewed but node16 as in WebdriverIO main project does not work. + "moduleResolution": "node", // To review since this is equivalent to node10 "allowSyntheticDefaultImports": true, "types": [ "node", diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index f4d6138d6..69c0fe797 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -21,8 +21,6 @@ type ExpectLibAsyncExpectationResult = import('expect').AsyncExpectationResult type ExpectLibExpectationResult = import('expect').ExpectationResult type ExpectLibMatcherContext = import('expect').MatcherContext -// TODO dprevost: a suggestion would be to move any code outside of the namespace to separate types.ts file, so that we can import the types. - // 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 From 10c1d521dd69ff5727363ee29a66d9c593c6d706 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sat, 19 Jul 2025 11:20:34 -0400 Subject: [PATCH 75/99] Rebase + clean example + Add note on augmentation limitations --- docs/Framework.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/Framework.md b/docs/Framework.md index fa355bce8..57732b869 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -26,7 +26,6 @@ import { describe, it, expect as jestExpect } from '@jest/globals' describe('My tests', async () => { it('should verify my browser to have the expected url', async () => { - const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser await expect(browser).toHaveUrl('https://example.com') }) }) @@ -69,7 +68,7 @@ beforeAll(async () => { }); ``` -For the soft assertion, the `createSoftExpect` is currently not correctly exposed but the below works: +[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"; @@ -99,7 +98,6 @@ Then as shown below, no imports are required and we can use WDIO matchers direct describe('My tests', async () => { it('should verify my browser to have the expected url', async () => { - const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser await expect(browser).toHaveUrl('https://example.com') }) }) @@ -127,7 +125,6 @@ No import is required; everything is set globally. describe('My tests', async () => { it('should verify my browser to have the expected url', async () => { - const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser await expect(browser).toHaveUrl('https://example.com') }) }) @@ -162,7 +159,6 @@ When not using `@wdio/globals/types` or having `@types/jasmine` before it, the J describe('My tests', async () => { it('should verify my browser to have the expected url', async () => { - const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser await expectAsync(browser).toHaveUrl('https://example.com') }) }) @@ -190,7 +186,6 @@ import { expect as wdioExpect } from 'expect-webdriverio' describe('My tests', async () => { it('should verify my browser to have the expected url', async () => { - const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser await wdioExpect(browser).toHaveUrl('https://example.com') }) }) @@ -215,10 +210,12 @@ Asymmetric matchers have limited support. Even though `jasmine.stringContaining` describe('My tests', async () => { it('should verify my browser to have the expected url', async () => { - const browser: WebdriverIO.Browser = {} as unknown as WebdriverIO.Browser await expectAsync(browser).toHaveUrl(wdioExpect.stringContaining('WebdriverIO')) }) }) ``` +### Jest & Jasmine Augmentation Notes +When already using Jest or Jasmine globally, then using `import { expect } from 'expect-webdriverio'` is the most compatible approach even though augmentation exists. +It would be recommended to build your project on the above instead of augmentation to ensure future compatibility while sorting out augmentation limitations. See [this issue](https://github.com/webdriverio/expect-webdriverio/issues/1893) for more information. From f42f159c5c5e962b01e15acf4ef03be911cee63b Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sat, 19 Jul 2025 11:29:28 -0400 Subject: [PATCH 76/99] Document better limitation of soft assertion with Jasmine --- docs/API.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/API.md b/docs/API.md index fd23834f3..76309a988 100644 --- a/docs/API.md +++ b/docs/API.md @@ -113,9 +113,8 @@ This is useful if you want full control over when soft assertions are verified o ### Known limitations -Soft assertion currently works only on custom wdio matchers and not on the basic ones like `toBe` or `toEqual` -Jasmine, even with `@wdio/jasmine-framework`, is not auto-configure to use it. - +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 From 551de1900f4e2131b3aceb8940e3c92857fbf32c Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sat, 19 Jul 2025 11:48:09 -0400 Subject: [PATCH 77/99] Add cucumber to doc + fix some errors --- docs/Framework.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/Framework.md b/docs/Framework.md index 57732b869..d535e7801 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -5,7 +5,7 @@ Expect-WebDriverIO is inspired by [`expect`](https://www.npmjs.com/package/expec ## Compatibility -We can pair `expect-webdriverio` with [Jest](https://jestjs.io/), [Mocha](https://mochajs.org/), and even [Jasmine](https://jasmine.github.io/). +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. @@ -189,7 +189,7 @@ describe('My tests', async () => { await wdioExpect(browser).toHaveUrl('https://example.com') }) }) -``` + Expected in `tsconfig.json`: ```json @@ -203,8 +203,9 @@ Expected in `tsconfig.json`: } ``` -#### Asymmetric matchers -Asymmetric matchers have limited support. Even though `jasmine.stringContaining` has no error, it potentially does not work even with `@wdio/jasmine-framework`, but the example below should work: + +#### 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 () => { @@ -215,7 +216,12 @@ describe('My tests', async () => { }) ``` + ### Jest & Jasmine Augmentation Notes -When already using Jest or Jasmine globally, then using `import { expect } from 'expect-webdriverio'` is the most compatible approach even though augmentation exists. -It would be recommended to build your project on the above instead of augmentation to ensure future compatibility while sorting out augmentation limitations. See [this issue](https://github.com/webdriverio/expect-webdriverio/issues/1893) for more information. +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 From d3b00b19251ea08e424dfa4a0daa5739cc4ea1ee Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sat, 19 Jul 2025 11:50:07 -0400 Subject: [PATCH 78/99] Simplify node constraints --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dff806b40..79a4dff82 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "types": "./types/expect-webdriverio.d.ts", "typeScriptVersion": "3.8.3", "engines": { - "node": ">=20 || >=22" + "node": ">=20" }, "scripts": { "build": "run-s clean compile", From d1be84d955deb0e4f5f9386c7aba6150e23a4305 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sat, 19 Jul 2025 20:33:51 -0400 Subject: [PATCH 79/99] Have `expect(Promise).toBeDefined()` be void and not a Promise --- .../jasmine-async/types-jasmine_async.test.ts | 2 +- test-types/jasmine/types-jasmine.test.ts | 15 ++++++++------- test-types/jest-@jest_global/types-jest.test.ts | 15 +++++++-------- test-types/mocha/types-mocha.test.ts | 14 ++++++++------ types/expect-webdriverio.d.ts | 2 +- 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/test-types/jasmine-async/types-jasmine_async.test.ts b/test-types/jasmine-async/types-jasmine_async.test.ts index 373757537..01f9611e4 100644 --- a/test-types/jasmine-async/types-jasmine_async.test.ts +++ b/test-types/jasmine-async/types-jasmine_async.test.ts @@ -433,7 +433,7 @@ describe('type assertions', () => { expectPromiseVoid = expectAsync(chainableElement).not.toBe(true) }) - it('should still expect void type when actual is a Promise since we do not overload them', async () => { + 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) diff --git a/test-types/jasmine/types-jasmine.test.ts b/test-types/jasmine/types-jasmine.test.ts index 59a742ea7..c4f2f23d0 100644 --- a/test-types/jasmine/types-jasmine.test.ts +++ b/test-types/jasmine/types-jasmine.test.ts @@ -468,13 +468,14 @@ describe('type assertions', () => { 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) + // TODO dprevost verify which typing apply here, is it the expectLib or the force expectAsync of wdio/jasmine-framework? + // 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 () => { diff --git a/test-types/jest-@jest_global/types-jest.test.ts b/test-types/jest-@jest_global/types-jest.test.ts index 733e18dbb..193856f87 100644 --- a/test-types/jest-@jest_global/types-jest.test.ts +++ b/test-types/jest-@jest_global/types-jest.test.ts @@ -469,14 +469,13 @@ describe('type assertions', async () => { it('should still expect void type when actual is a Promise since we do not overload them', async () => { const promiseBoolean = Promise.resolve(true) - // TODO dprevost check if this one need to stay a void or can be a Promise - // 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) + 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 () => { diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index 2f9e78f18..9c18afdca 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -474,13 +474,13 @@ describe('type assertions', () => { it('should still expect void type when actual is a Promise since we do not overload them', async () => { const promiseBoolean = Promise.resolve(true) - expectPromiseVoid = expect(promiseBoolean).toBe(true) - expectPromiseVoid = expect(promiseBoolean).not.toBe(true) + expectVoid = expect(promiseBoolean).toBeDefined() + expectVoid = expect(promiseBoolean).not.toBeDefined() //@ts-expect-error - expectVoid = expect(promiseBoolean).toBe(true) + expectPromiseVoid = expect(promiseBoolean).toBeDefined() //@ts-expect-error - expectVoid = expect(promiseBoolean).toBe(true) + expectPromiseVoid = expect(promiseBoolean).toBeDefined() }) it('should work with string', async () => { @@ -504,13 +504,15 @@ describe('type assertions', () => { const booleanPromise: Promise = Promise.resolve(true) it('should have expect return Matchers with a Promise', async () => { - const expectPromiseBoolean1: ExpectWebdriverIO.Matchers, Promise> & ExpectLibInverse, Promise>> & ExpectWebdriverIO.PromiseMatchers> = expect(booleanPromise) - const expectPromiseBoolean2: ExpectWebdriverIO.Matchers, Promise> = expect(booleanPromise).not + 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) diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 69c0fe797..ed472f60e 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -495,7 +495,7 @@ declare namespace ExpectWebdriverIO { * * @param actual The value to apply matchers against. */ - (actual: T): T extends PromiseLike ? ExpectWebdriverIO.MatchersAndInverse, T> & ExpectWebdriverIO.PromiseMatchers : ExpectWebdriverIO.MatchersAndInverse; + (actual: T): T extends PromiseLike ? ExpectWebdriverIO.MatchersAndInverse & ExpectWebdriverIO.PromiseMatchers : ExpectWebdriverIO.MatchersAndInverse; } interface Matchers, T> extends WdioMatchers {} From b1795b1e71414f4033c3fa2e5b707d90b40367bd Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sun, 20 Jul 2025 09:01:22 -0400 Subject: [PATCH 80/99] Even jest does not support `expect.not.anything()` --- test-types/jasmine/types-jasmine.test.ts | 10 ---------- test-types/mocha/types-mocha.test.ts | 10 ---------- 2 files changed, 20 deletions(-) diff --git a/test-types/jasmine/types-jasmine.test.ts b/test-types/jasmine/types-jasmine.test.ts index c4f2f23d0..ab63f1684 100644 --- a/test-types/jasmine/types-jasmine.test.ts +++ b/test-types/jasmine/types-jasmine.test.ts @@ -625,16 +625,6 @@ describe('type assertions', () => { wdioExpect.not.closeTo(5, 10) wdioExpect.not.arrayContaining(['WebdriverIO', 'Test']) wdioExpect.not.arrayOf(wdioExpect.stringContaining('WebdriverIO')) - - // TODO dprevost: Should we support these? - // wdioExpect.not.anything() - // wdioExpect.not.any(Function) - // wdioExpect.not.any(Number) - // wdioExpect.not.any(Boolean) - // wdioExpect.not.any(String) - // wdioExpect.not.any(Symbol) - // wdioExpect.not.any(Date) - // wdioExpect.not.any(Error) }) describe('Soft Assertions', async () => { diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index 9c18afdca..4d5ebd886 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -641,16 +641,6 @@ describe('type assertions', () => { expect.not.closeTo(5, 10) expect.not.arrayContaining(['WebdriverIO', 'Test']) expect.not.arrayOf(expect.stringContaining('WebdriverIO')) - - // TODO dprevost to review - // 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 () => { From cebf40f3a16ca9ead87673f83aff4f106875ca57 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sun, 20 Jul 2025 13:40:01 -0400 Subject: [PATCH 81/99] Force expect under Jasmine to always be async --- expect-global-jasmine-async.d.ts | 23 ++ .../customMatchers-module-expect.d.ts | 4 +- ...mMatchers-namespace-expectwebdriverio.d.ts | 2 +- test-types/jasmine/tsconfig.json | 3 +- test-types/jasmine/types-jasmine.test.ts | 228 +++++++++--------- 5 files changed, 138 insertions(+), 122 deletions(-) create mode 100644 expect-global-jasmine-async.d.ts diff --git a/expect-global-jasmine-async.d.ts b/expect-global-jasmine-async.d.ts new file mode 100644 index 000000000..904eb2767 --- /dev/null +++ b/expect-global-jasmine-async.d.ts @@ -0,0 +1,23 @@ +/// + +/** + * 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/test-types/jasmine/customMatchers/customMatchers-module-expect.d.ts b/test-types/jasmine/customMatchers/customMatchers-module-expect.d.ts index 750d6e1ff..125fb77c7 100644 --- a/test-types/jasmine/customMatchers/customMatchers-module-expect.d.ts +++ b/test-types/jasmine/customMatchers/customMatchers-module-expect.d.ts @@ -13,7 +13,7 @@ declare module 'expect' { interface Matchers { toBeWithinRange(floor: number, ceiling: number): R - toHaveSimpleCustomProperty(string: string | ExpectWebdriverIO.PartialMatcher): Promise + 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 ? @@ -21,6 +21,6 @@ declare module 'expect' { // Needed for the custom asymmetric matcher defined above to be typed correctly Promise>) // Using `never` blocks the call on non-element types - => Promise : never; + => 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 index 7a833bd87..89423c9d3 100644 --- a/test-types/jasmine/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts +++ b/test-types/jasmine/customMatchers/customMatchers-namespace-expectwebdriverio.d.ts @@ -9,6 +9,6 @@ declare namespace ExpectWebdriverIO { } interface Matchers { toBeCustom(): R; - toBeCustomPromise: T extends ChainablePromiseElement ? (expected?: string | ExpectWebdriverIO.PartialMatcher | Promise>) => Promise : never; + 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 90080327e..b0352ed4b 100644 --- a/test-types/jasmine/tsconfig.json +++ b/test-types/jasmine/tsconfig.json @@ -7,7 +7,8 @@ "skipLibCheck": true, "types": [ "@types/jasmine", - "../../jasmine.d.ts" + "../../jasmine.d.ts", + "../../expect-global-jasmine-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 index ab63f1684..7b9749f77 100644 --- a/test-types/jasmine/types-jasmine.test.ts +++ b/test-types/jasmine/types-jasmine.test.ts @@ -333,15 +333,15 @@ describe('type assertions', () => { describe('Custom matchers', () => { describe('using `ExpectWebdriverIO` namespace augmentation', () => { it('should supported correctly a non-promise custom matcher', async () => { - expectVoid = wdioExpect('test').toBeCustom() - expectVoid = wdioExpect('test').not.toBeCustom() + expectPromiseVoid = wdioExpect('test').toBeCustom() + expectPromiseVoid = wdioExpect('test').not.toBeCustom() // @ts-expect-error - expectPromiseVoid = wdioExpect('test').toBeCustom() + expectVoid = wdioExpect('test').toBeCustom() // @ts-expect-error - expectPromiseVoid = wdioExpect('test').not.toBeCustom() + expectVoid = wdioExpect('test').not.toBeCustom() - expectVoid = wdioExpect(1).toBeWithinRange(0, 2) + expectPromiseVoid = wdioExpect(1).toBeWithinRange(0, 2) }) it('should supported correctly a promise custom matcher with only chainableElement as actual', async () => { @@ -380,10 +380,10 @@ describe('type assertions', () => { describe('using `expect` module declaration', () => { it('should support a simple matcher', async () => { - expectVoid = wdioExpect(5).toBeWithinRange(1, 10) + expectPromiseVoid = wdioExpect(5).toBeWithinRange(1, 10) // Or as an asymmetric matcher: - expectVoid = wdioExpect({ value: 5 }).toEqual({ + expectPromiseVoid = wdioExpect({ value: 5 }).toEqual({ value: wdioExpect.toBeWithinRange(1, 10) }) @@ -394,6 +394,7 @@ describe('type assertions', () => { }) 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')) @@ -446,75 +447,64 @@ describe('type assertions', () => { describe('toBe', () => { it('should expect void type when actual is a boolean', async () => { - expectVoid = wdioExpect(true).toBe(true) - expectVoid = wdioExpect(true).not.toBe(true) + expectPromiseVoid = wdioExpect(true).toBe(true) + expectPromiseVoid = wdioExpect(true).not.toBe(true) //@ts-expect-error - expectPromiseVoid = wdioExpect(true).toBe(true) + expectVoid = wdioExpect(true).toBe(true) //@ts-expect-error - expectPromiseVoid = wdioExpect(true).not.toBe(true) + 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 () => { - expectVoid = wdioExpect(chainableElement).toBe(true) - expectVoid = wdioExpect(chainableElement).not.toBe(true) + expectPromiseVoid = wdioExpect(chainableElement).toBe(true) + expectPromiseVoid = wdioExpect(chainableElement).not.toBe(true) //@ts-expect-error - expectPromiseVoid = wdioExpect(chainableElement).toBe(true) + expectVoid = wdioExpect(chainableElement).toBe(true) //@ts-expect-error - expectPromiseVoid = wdioExpect(chainableElement).not.toBe(true) + 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) - // TODO dprevost verify which typing apply here, is it the expectLib or the force expectAsync of wdio/jasmine-framework? - // expectPromiseVoid = wdioExpect(promiseBoolean).toBe(true) - // expectPromiseVoid = wdioExpect(promiseBoolean).not.toBe(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) + //@ts-expect-error + expectVoid = wdioExpect(promiseBoolean).toBe(true) + //@ts-expect-error + expectVoid = wdioExpect(promiseBoolean).toBe(true) }) it('should work with string', async () => { - expectVoid = wdioExpect('text').toBe(true) - expectVoid = wdioExpect('text').not.toBe(true) - expectVoid = wdioExpect('text').toBe(wdioExpect.stringContaining('text')) - expectVoid = wdioExpect('text').not.toBe(wdioExpect.stringContaining('text')) + 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 - expectPromiseVoid = wdioExpect('text').toBe(true) + expectVoid = wdioExpect('text').toBe(true) //@ts-expect-error - expectPromiseVoid = wdioExpect('text').not.toBe(true) + expectVoid = wdioExpect('text').not.toBe(true) //@ts-expect-error - expectPromiseVoid = wdioExpect('text').toBe(wdioExpect.stringContaining('text')) + expectVoid = wdioExpect('text').toBe(wdioExpect.stringContaining('text')) //@ts-expect-error - expectPromiseVoid = wdioExpect('text').not.toBe(wdioExpect.stringContaining('text')) + expectVoid = wdioExpect('text').not.toBe(wdioExpect.stringContaining('text')) }) }) describe('Promise type assertions', () => { const booleanPromise: Promise = Promise.resolve(true) - it('should work with resolves & rejects correctly', async () => { - expectPromiseVoid = wdioExpect(booleanPromise).resolves.toBe(true) - expectPromiseVoid = wdioExpect(booleanPromise).rejects.toBe(true) - + it('should not compile', async () => { //@ts-expect-error - expectVoid = wdioExpect(booleanPromise).resolves.toBe(true) + wdioExpect(booleanPromise).resolves.toBe(true) //@ts-expect-error - expectVoid = wdioExpect(booleanPromise).rejects.toBe(true) - + wdioExpect(booleanPromise).rejects.toBe(true) }) - it('should not support chainable and expect PromiseVoid with toBe', async () => { - //@ts-expect-error - expectPromiseVoid = wdioExpect(chainableElement).toBe(true) - //@ts-expect-error - expectPromiseVoid = wdioExpect(chainableElement).not.toBe(true) - }) }) describe('Network Matchers', () => { @@ -700,81 +690,83 @@ describe('type assertions', () => { 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) - ) - }) + // 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', () => { From 415d3ba5f12b66b3807b19fa633bec33cfc762b1 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sun, 20 Jul 2025 21:29:01 -0400 Subject: [PATCH 82/99] Finalize jasmine support with typing for wdio expect being asyncrhonous --- .npmignore | 1 + docs/Framework.md | 36 ++++++++++++++++++- ...ne-async.d.ts => jasmine-expect-async.d.ts | 0 package-lock.json | 2 +- 4 files changed, 37 insertions(+), 2 deletions(-) rename expect-global-jasmine-async.d.ts => jasmine-expect-async.d.ts (100%) diff --git a/.npmignore b/.npmignore index e4d6a736f..edadd577e 100644 --- a/.npmignore +++ b/.npmignore @@ -3,6 +3,7 @@ !lib/**/*.js !types/**/*.d.ts !jasmine.d.ts +!jasmine-expect-async.d.ts !jest.d.ts !LICENSE !package.json diff --git a/docs/Framework.md b/docs/Framework.md index d535e7801..326a3b5d3 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -160,6 +160,8 @@ 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) }) }) ``` @@ -176,6 +178,35 @@ Expected in `tsconfig.json`: } ``` +#### 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-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. @@ -187,6 +218,9 @@ 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) }) }) @@ -197,7 +231,7 @@ Expected in `tsconfig.json`: "compilerOptions": { "types": [ "@types/jasmine", - "expect-webdriverio/expect-global", // Force expect to be the 'expect-webdriverio'; comment out and use the import above if it conflicts with Jasmine + // "expect-webdriverio/expect-global", // Optional to have the global ambient expect the one of wdio ] } } diff --git a/expect-global-jasmine-async.d.ts b/jasmine-expect-async.d.ts similarity index 100% rename from expect-global-jasmine-async.d.ts rename to jasmine-expect-async.d.ts diff --git a/package-lock.json b/package-lock.json index 25bab34a0..556154842 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,7 @@ "webdriverio": "^9.15.0" }, "engines": { - "node": ">=20 || >=22" + "node": ">=20" }, "peerDependencies": { "@wdio/globals": "^9.0.0", From 07fe3a3d109c0997a78fcc8303efa15c803b9036 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Mon, 21 Jul 2025 07:23:30 -0400 Subject: [PATCH 83/99] Revert matchers type breaking changes + better jasmine sync `d.ts` name --- .npmignore | 2 +- docs/Framework.md | 2 +- jasmine-expect-async.d.ts => jasmine-wdio-expect-async.d.ts | 0 src/index.ts | 4 ++-- src/types.ts | 2 ++ test-types/jasmine/tsconfig.json | 2 +- types/expect-webdriverio.d.ts | 2 +- 7 files changed, 8 insertions(+), 6 deletions(-) rename jasmine-expect-async.d.ts => jasmine-wdio-expect-async.d.ts (100%) diff --git a/.npmignore b/.npmignore index edadd577e..3e5c84af7 100644 --- a/.npmignore +++ b/.npmignore @@ -3,7 +3,7 @@ !lib/**/*.js !types/**/*.d.ts !jasmine.d.ts -!jasmine-expect-async.d.ts +!jasmine-wdio-expect-async.d.ts !jest.d.ts !LICENSE !package.json diff --git a/docs/Framework.md b/docs/Framework.md index 326a3b5d3..d13a5c7d1 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -201,7 +201,7 @@ Expected in `tsconfig.json`: "@wdio/globals/types", "@wdio/jasmine-framework", "@types/jasmine", - "expect-webdriverio/jasmine-expect-async", // Force expect to return Promises + "expect-webdriverio/jasmine-wdio-expect-async", // Force expect to return Promises ] } } diff --git a/jasmine-expect-async.d.ts b/jasmine-wdio-expect-async.d.ts similarity index 100% rename from jasmine-expect-async.d.ts rename to jasmine-wdio-expect-async.d.ts diff --git a/src/index.ts b/src/index.ts index 8da64d398..c88bd5c10 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +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/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/test-types/jasmine/tsconfig.json b/test-types/jasmine/tsconfig.json index b0352ed4b..3b199e0ef 100644 --- a/test-types/jasmine/tsconfig.json +++ b/test-types/jasmine/tsconfig.json @@ -8,7 +8,7 @@ "types": [ "@types/jasmine", "../../jasmine.d.ts", - "../../expect-global-jasmine-async.d.ts" + "../../jasmine-wdio-expect-async.d.ts" ] } } \ No newline at end of file diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index ed472f60e..fe6af3626 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -573,7 +573,7 @@ declare namespace ExpectWebdriverIO { * Equivalent as `MatchersObject` from the expect library. * @see https://github.com/jestjs/jest/blob/fd3d6cf9fe416b549a74b6577e5e1ea1130e3659/packages/expect/src/types.ts#L43C13-L43C27 */ - const matchers: Record + const matchers: Map interface AssertionHookParams { /** From 58a82447e91d7b0dfa6ff6b6cebb042f638e6c06 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Mon, 21 Jul 2025 20:47:52 -0400 Subject: [PATCH 84/99] Have `InverseAsymmetricMatchers` for easier typing --- jasmine-wdio-expect-async.d.ts | 2 +- types/expect-webdriverio.d.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/jasmine-wdio-expect-async.d.ts b/jasmine-wdio-expect-async.d.ts index 904eb2767..62b2a2c9f 100644 --- a/jasmine-wdio-expect-async.d.ts +++ b/jasmine-wdio-expect-async.d.ts @@ -5,7 +5,7 @@ */ declare namespace ExpectWebdriverIO { - interface Expect extends ExpectWebdriverIO.AsymmetricMatchers, ExpectLibInverse>, WdioExpect { + 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. diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index fe6af3626..429c12c32 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -482,7 +482,7 @@ declare namespace ExpectWebdriverIO { * `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 { + 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. @@ -502,6 +502,8 @@ declare namespace ExpectWebdriverIO { interface AsymmetricMatchers extends WdioAsymmetricMatchers {} + interface InverseAsymmetricMatchers extends Omit {} + /** * End of block overloading types from the expect library. */ From 1d11d0abfdc5d29113a48b4927df5531fc40639a Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Mon, 21 Jul 2025 18:10:52 -0700 Subject: [PATCH 85/99] Update docs/Framework.md --- docs/Framework.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/Framework.md b/docs/Framework.md index d13a5c7d1..2048d02cf 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -24,7 +24,6 @@ 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') }) From eefed070679589013e932f708aa79e993653aabf Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Mon, 21 Jul 2025 18:10:59 -0700 Subject: [PATCH 86/99] Update docs/Framework.md --- docs/Framework.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/Framework.md b/docs/Framework.md index 2048d02cf..d0e7d3c84 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -1,7 +1,6 @@ # 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. - - Note: Yes, this is a package of Jest but it is usable without Jest. +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 From 446b182901fd39b9a2f90e1556c69035e9773beb Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Mon, 21 Jul 2025 18:11:08 -0700 Subject: [PATCH 87/99] Update docs/Framework.md --- docs/Framework.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Framework.md b/docs/Framework.md index d0e7d3c84..7cf0e78e0 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -31,6 +31,7 @@ describe('My tests', async () => { No `types` are expected in `tsconfig.json`. Optionally, to avoid needing `import { expect } from 'expect-webdriverio'`, you can use the following: + ```json { "compilerOptions": { From 4f32ad2225cd4317d7a4ac043f09d2bc48deaae2 Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Mon, 21 Jul 2025 18:11:15 -0700 Subject: [PATCH 88/99] Update docs/Framework.md --- docs/Framework.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/Framework.md b/docs/Framework.md index 7cf0e78e0..4b7caae8b 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -44,7 +44,6 @@ Multiple attempts were made to augment `@jest/globals` to support `expect-webdri 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. From 66cd08acfa040a429a994d2e72c47b30416c13af Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Mon, 21 Jul 2025 18:11:22 -0700 Subject: [PATCH 89/99] Update docs/Framework.md --- docs/Framework.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/Framework.md b/docs/Framework.md index 4b7caae8b..4ed5427a5 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -94,7 +94,6 @@ beforeAll(async () => { 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') }) From e242f3bb779ef860d58ba7f5cb7c1bdb4080151a Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Mon, 21 Jul 2025 18:11:34 -0700 Subject: [PATCH 90/99] Update docs/Framework.md --- docs/Framework.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Framework.md b/docs/Framework.md index 4ed5427a5..f1902886b 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -32,6 +32,7 @@ describe('My tests', async () => { No `types` are expected in `tsconfig.json`. Optionally, to avoid needing `import { expect } from 'expect-webdriverio'`, you can use the following: + ```json { "compilerOptions": { From 888f8ffac69e273869da2c5c6028fa7b2e6c8d72 Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Mon, 21 Jul 2025 18:11:43 -0700 Subject: [PATCH 91/99] Update docs/Framework.md --- docs/Framework.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/Framework.md b/docs/Framework.md index f1902886b..918d828db 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -181,7 +181,6 @@ When the global ambiant is the `expect` of wdio but forced to be `expectAsync` u ```ts describe('My tests', async () => { - it('should verify my browser to have the expected url', async () => { await expect(browser).toHaveUrl('https://example.com') From 64e9198709caaad5d29ccdb8b52fe3352f649e0b Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Mon, 21 Jul 2025 18:11:50 -0700 Subject: [PATCH 92/99] Update docs/Framework.md --- docs/Framework.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Framework.md b/docs/Framework.md index 918d828db..996485ae7 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -199,7 +199,7 @@ Expected in `tsconfig.json`: "@wdio/jasmine-framework", "@types/jasmine", "expect-webdriverio/jasmine-wdio-expect-async", // Force expect to return Promises - ] + ] } } ``` From 79516ea6eef0f55a180ea6c8fa123ad030367578 Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Mon, 21 Jul 2025 18:11:56 -0700 Subject: [PATCH 93/99] Update docs/Framework.md --- docs/Framework.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/Framework.md b/docs/Framework.md index 996485ae7..880010551 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -212,7 +212,6 @@ It is preferable to use the `expect` from `expect-webdriverio` to guarantee futu 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') From 4092e002c0338cd805edb907bd8ea2a15bdcd86a Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Mon, 21 Jul 2025 18:12:03 -0700 Subject: [PATCH 94/99] Update docs/Framework.md --- docs/Framework.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/Framework.md b/docs/Framework.md index 880010551..017acf242 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -239,7 +239,6 @@ Asymmetric matchers have limited support. Even though `jasmine.stringContaining` ```ts describe('My tests', async () => { - it('should verify my browser to have the expected url', async () => { await expectAsync(browser).toHaveUrl(wdioExpect.stringContaining('WebdriverIO')) }) From e12557cab1eface69afe31779f31a15c6a6af848 Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Mon, 21 Jul 2025 18:12:10 -0700 Subject: [PATCH 95/99] Update jasmine-wdio-expect-async.d.ts --- jasmine-wdio-expect-async.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/jasmine-wdio-expect-async.d.ts b/jasmine-wdio-expect-async.d.ts index 62b2a2c9f..b9caa3d58 100644 --- a/jasmine-wdio-expect-async.d.ts +++ b/jasmine-wdio-expect-async.d.ts @@ -4,7 +4,6 @@ * 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. From f716df605dbef278e596140bb3215cb12214c6d9 Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Mon, 21 Jul 2025 18:12:19 -0700 Subject: [PATCH 96/99] Update docs/Framework.md --- docs/Framework.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/Framework.md b/docs/Framework.md index 017acf242..d9f081353 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -121,7 +121,6 @@ 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') }) From dc6486e2aec3df14acaed1e34900fbf9e5d3d997 Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Mon, 21 Jul 2025 18:12:28 -0700 Subject: [PATCH 97/99] Update docs/Framework.md --- docs/Framework.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/Framework.md b/docs/Framework.md index d9f081353..8e403ac83 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -149,8 +149,7 @@ When paired with [Jasmine](https://jasmine.github.io/), [`@wdio/jasmine-framewor 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`. - - Note: Without `@wdio/jasmine-framework`, matchers will need to be registered manually. +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 () => { From cc9aafcad76b91391bc39c03cb9ff61fe9a91d86 Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Mon, 21 Jul 2025 18:12:37 -0700 Subject: [PATCH 98/99] Update docs/Framework.md --- docs/Framework.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/Framework.md b/docs/Framework.md index 8e403ac83..0ad36d9c9 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -156,7 +156,6 @@ 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) }) }) From 743d98f629136c18377651d879d475bde507f6e3 Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Mon, 21 Jul 2025 18:12:45 -0700 Subject: [PATCH 99/99] Update docs/Framework.md --- docs/Framework.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/Framework.md b/docs/Framework.md index 0ad36d9c9..17f075826 100644 --- a/docs/Framework.md +++ b/docs/Framework.md @@ -153,7 +153,6 @@ When not using `@wdio/globals/types` or having `@types/jasmine` before it, the J ```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)