diff --git a/jest.config.ts b/jest.config.ts deleted file mode 100644 index cc5850da..00000000 --- a/jest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default { - projects: [ - '/projects/auth0-angular', - '/projects/playground', - ], -}; diff --git a/package-lock.json b/package-lock.json index 690071bb..e71f17f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,8 @@ "zone.js": "~0.15.1" }, "devDependencies": { + "@analogjs/vite-plugin-angular": "^1.0.0", + "@analogjs/vitest-angular": "^1.0.0", "@angular-devkit/build-angular": "^20.3.35", "@angular-eslint/builder": "^20.7.0", "@angular-eslint/eslint-plugin": "^20.7.0", @@ -31,10 +33,10 @@ "@cypress/schematic": "^2.5.2", "@types/jasmine": "^3.10.18", "@types/jasminewd2": "~2.0.13", - "@types/jest": "^30.0.0", "@types/node": "^22.19.17", "@typescript-eslint/eslint-plugin": "^8.68.0", "@typescript-eslint/parser": "^8.59.0", + "@vitest/coverage-v8": "^3.0.0", "concurrently": "^6.2.0", "cors": "^2.8.6", "cross-fetch": "^4.1.0", @@ -45,9 +47,6 @@ "eslint-plugin-prefer-arrow": "^1.2.3", "express-jwt": "^8.5.1", "husky": "^4.3.8", - "jest": "^30.0.0", - "jest-environment-jsdom": "^30.0.0", - "jest-preset-angular": "^17.0.0", "jwks-rsa": "^3.2.2", "moment": "^2.30.1", "ng-packagr": "^20.3.2", @@ -57,7 +56,8 @@ "protractor": "~7.0.0", "ts-node": "^10.9.2", "typedoc": "^0.28.19", - "typescript": "~5.8.3" + "typescript": "~5.8.3", + "vitest": "^3.0.0" } }, "node_modules/@algolia/abtesting": { @@ -268,6 +268,47 @@ "node": ">=6.0.0" } }, + "node_modules/@analogjs/vite-plugin-angular": { + "version": "1.22.5", + "integrity": "sha512-N1BQD6HQSp2Imbb1fThymskWFSLq0ZF+d2fe3DgErwlBFf6SzRp++iFltddQc3wIzenTXE+5brS4fAPXO8UT9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "ts-morph": "^21.0.0", + "vfile": "^6.0.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/brandonroberts" + }, + "peerDependencies": { + "@angular-devkit/build-angular": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0", + "@angular/build": "^18.0.0 || ^19.0.0 || ^20.0.0" + }, + "peerDependenciesMeta": { + "@angular-devkit/build-angular": { + "optional": true + }, + "@angular/build": { + "optional": true + } + } + }, + "node_modules/@analogjs/vitest-angular": { + "version": "1.22.5", + "integrity": "sha512-lYwa9f6LFClW80sPhCydTOQvKnpWPnyChFhYdgMRpUZuKYo9102PndOLZkqR5K8WlKSilbP2293whaG1vZBMyA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/brandonroberts" + }, + "peerDependencies": { + "@analogjs/vite-plugin-angular": "*", + "@angular-devkit/architect": ">=0.1500.0 < 0.2100.0", + "vitest": "^1.3.1 || ^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, "node_modules/@angular-devkit/architect": { "version": "0.2003.35", "integrity": "sha512-fuiccQamG0SBfuKRSA8uAoALI4VprZAWZbwwOGmpMaYrxoN9E4CgicR5hQUEEsp9IzMtuRvFpJPorudv2xzGMw==", @@ -909,6 +950,7 @@ "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", "dev": true, "license": "MIT", + "optional": true, "peer": true, "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", @@ -926,6 +968,7 @@ "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", "dev": true, "license": "MIT", + "optional": true, "peer": true, "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", @@ -943,6 +986,7 @@ "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", "dev": true, "license": "MIT", + "optional": true, "peer": true, "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" @@ -953,6 +997,7 @@ "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", "dev": true, "license": "MIT", + "optional": true, "peer": true }, "node_modules/@auth0/auth0-auth-js": { @@ -1538,6 +1583,8 @@ "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1550,6 +1597,8 @@ "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1562,6 +1611,8 @@ "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, @@ -1574,6 +1625,8 @@ "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -1619,6 +1672,8 @@ "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -1631,6 +1686,8 @@ "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1643,6 +1700,8 @@ "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1658,6 +1717,8 @@ "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -1670,6 +1731,8 @@ "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1682,6 +1745,8 @@ "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -1694,6 +1759,8 @@ "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1706,6 +1773,8 @@ "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1718,6 +1787,8 @@ "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1730,6 +1801,8 @@ "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -1745,6 +1818,8 @@ "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -1760,6 +1835,8 @@ "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -2825,13 +2902,16 @@ "version": "0.2.3", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@bramus/specificity": { "version": "2.4.2", "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", "dev": true, "license": "MIT", + "optional": true, "peer": true, "dependencies": { "css-tree": "^3.0.0" @@ -2877,6 +2957,7 @@ } ], "license": "MIT-0", + "optional": true, "peer": true, "engines": { "node": ">=20.19.0" @@ -2897,6 +2978,7 @@ } ], "license": "MIT", + "optional": true, "peer": true, "engines": { "node": ">=20.19.0" @@ -2921,6 +3003,7 @@ } ], "license": "MIT", + "optional": true, "peer": true, "dependencies": { "@csstools/color-helpers": "^6.1.1", @@ -2949,6 +3032,7 @@ } ], "license": "MIT", + "optional": true, "peer": true, "engines": { "node": ">=20.19.0" @@ -2972,6 +3056,7 @@ } ], "license": "MIT-0", + "optional": true, "peer": true, "peerDependencies": { "css-tree": "^3.2.1" @@ -2997,6 +3082,7 @@ } ], "license": "MIT", + "optional": true, "peer": true, "engines": { "node": ">=20.19.0" @@ -3079,6 +3165,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" @@ -3090,6 +3177,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -3100,6 +3188,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -4207,6 +4296,8 @@ "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", @@ -4223,6 +4314,8 @@ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "sprintf-js": "~1.0.2" } @@ -4232,6 +4325,8 @@ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -4245,6 +4340,8 @@ "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -4258,6 +4355,8 @@ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -4270,6 +4369,8 @@ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "p-try": "^2.0.0" }, @@ -4285,6 +4386,8 @@ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -4297,6 +4400,8 @@ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -4315,6 +4420,8 @@ "integrity": "sha512-v3bhyxUh9Hgmo5p6hAOXe14/R3ZxZDOsvHleh4B07z3m/x4/ngPUXEm9XwK4sF4u+f+P2ORb0Ge+MgpaqRMVDA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/types": "30.4.1", "@types/node": "*", @@ -4332,6 +4439,8 @@ "integrity": "sha512-TZJA6cPJUFxoWhxaLo8t0VX/MZX2wPWr0uIDvLSHIvN4gu9h02vSzqI2kBADG1ExqQlC+cY09xKMSreivvrChQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/console": "30.4.1", "@jest/pattern": "30.4.0", @@ -4379,6 +4488,8 @@ "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } @@ -4388,6 +4499,8 @@ "integrity": "sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/fake-timers": "30.4.1", "@jest/types": "30.4.1", @@ -4403,6 +4516,8 @@ "integrity": "sha512-dSlKrqug3siYNHVnjwIldShY12wAH3spwRltO/+8VOjg0X+xEq7vOs3DbBs4LRKsu7OH+NUb9kuZUNBF9Ho3TA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/environment": "30.4.1", "@jest/fake-timers": "30.4.1", @@ -4430,6 +4545,8 @@ "integrity": "sha512-ginrj6TMgh2GshLUGCjO94Ptx9HhdZA/I6A9iUfyeLKFtdAjnKzHDgzgP9HYQgbxM1lbXScQ2eUBz2lGeVDPWA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "expect": "30.4.1", "jest-snapshot": "30.4.1" @@ -4443,6 +4560,8 @@ "integrity": "sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/get-type": "30.1.0" }, @@ -4455,6 +4574,8 @@ "integrity": "sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/types": "30.4.1", "@sinonjs/fake-timers": "^15.4.0", @@ -4472,6 +4593,8 @@ "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } @@ -4481,6 +4604,8 @@ "integrity": "sha512-ZbuY4cmXC8DkxYjfvT2DbcHWL2T6vmsMhXCDcmTB2T0y0gaezBI77ufq5ZAIdcRkYZ7NEQEDg1xFeKbxUJ5v5Q==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/environment": "30.4.1", "@jest/expect": "30.4.1", @@ -4496,6 +4621,8 @@ "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@types/node": "*", "jest-regex-util": "30.4.0" @@ -4509,6 +4636,8 @@ "integrity": "sha512-/SnkPCzEQpUaBH81kjdEdDdo2WZl5hxw+BmLDGWjRkm8o7XlhjwsU36cqwe5PGBE5WYpBvDzRSdXx9rbGuJtNA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@jest/console": "30.4.1", @@ -4550,13 +4679,17 @@ "version": "1.0.2", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@jest/reporters/node_modules/brace-expansion": { "version": "2.1.4", "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -4567,6 +4700,8 @@ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -4586,13 +4721,17 @@ "version": "10.4.3", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "license": "ISC" + "license": "ISC", + "optional": true, + "peer": true }, "node_modules/@jest/reporters/node_modules/minimatch": { "version": "9.0.9", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "brace-expansion": "^2.0.2" }, @@ -4608,6 +4747,8 @@ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, "license": "BlueOak-1.0.0", + "optional": true, + "peer": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -4624,6 +4765,8 @@ "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@sinclair/typebox": "^0.34.0" }, @@ -4636,6 +4779,8 @@ "integrity": "sha512-ObY4ljvQ95mt6iwKtVLetR/4yXiAgl3H4nJxhztr0MTjrN97TwDYrnCp/kF60Ec9HdhkWTHSu+Hg05aXfngpOA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/types": "30.4.1", "chalk": "^4.1.2", @@ -4651,6 +4796,8 @@ "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "callsites": "^3.1.0", @@ -4665,6 +4812,8 @@ "integrity": "sha512-/ZG7pgEiOmmWkN9TplKbOu4id2N5lh7FHwRwlkgBVAzGdRH+OkkQ8wX/kIxg4zmd3ZQvAL1RwL2yWsvNYYECTw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/console": "30.4.1", "@jest/types": "30.4.1", @@ -4680,6 +4829,8 @@ "integrity": "sha512-PeYE+4td5rKjoRPxztObrXU+H8hsjZfxKMXOcmrr34JerSyB/ROOxbbicz8B7A5j9R9VayDnVPvBmedqCsFCdw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/test-result": "30.4.1", "graceful-fs": "^4.2.11", @@ -4695,6 +4846,8 @@ "integrity": "sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/core": "^7.27.4", "@jest/types": "30.4.1", @@ -4719,13 +4872,17 @@ "version": "2.0.0", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@jest/types": { "version": "30.4.1", "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/pattern": "30.4.0", "@jest/schemas": "30.4.1", @@ -5774,6 +5931,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@tybys/wasm-util": "^0.10.3" }, @@ -6592,6 +6750,8 @@ "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": "^14.18.0 || >=16.0.0" }, @@ -7139,7 +7299,9 @@ "version": "0.34.52", "integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@sindresorhus/base62": { "version": "1.0.0", @@ -7170,6 +7332,8 @@ "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, "license": "BSD-3-Clause", + "optional": true, + "peer": true, "dependencies": { "type-detect": "4.0.8" } @@ -7179,6 +7343,8 @@ "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", "dev": true, "license": "BSD-3-Clause", + "optional": true, + "peer": true, "dependencies": { "@sinonjs/commons": "^3.0.1" } @@ -7195,6 +7361,48 @@ "node": ">=10" } }, + "node_modules/@ts-morph/common": { + "version": "0.22.0", + "integrity": "sha512-HqNBuV/oIlMKdkLshXd1zKBqNQCsuPEsgQOkfFQ/eUKjRlwndXW1AjN9LVkBEIukm00gGXSRmfkl0Wv5VXLnlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.3.2", + "minimatch": "^9.0.3", + "mkdirp": "^3.0.1", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@ts-morph/common/node_modules/balanced-match": { + "version": "1.0.2", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ts-morph/common/node_modules/brace-expansion": { + "version": "2.1.4", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@ts-morph/common/node_modules/minimatch": { + "version": "9.0.9", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.12", "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", @@ -7247,6 +7455,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -7256,6 +7465,8 @@ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -7269,6 +7480,8 @@ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/types": "^7.0.0" } @@ -7278,6 +7491,8 @@ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" @@ -7288,6 +7503,8 @@ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/types": "^7.28.2" } @@ -7323,6 +7540,16 @@ "@types/responselike": "^1.0.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", @@ -7342,6 +7569,12 @@ "@types/node": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "9.6.1", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", @@ -7432,13 +7665,17 @@ "version": "2.0.6", "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.3", "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@types/istanbul-lib-coverage": "*" } @@ -7448,6 +7685,8 @@ "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@types/istanbul-lib-report": "*" } @@ -7467,21 +7706,13 @@ "@types/jasmine": "*" } }, - "node_modules/@types/jest": { - "version": "30.0.0", - "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^30.0.0", - "pretty-format": "^30.0.0" - } - }, "node_modules/@types/jsdom": { "version": "21.1.7", "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", @@ -7655,13 +7886,17 @@ "version": "2.0.3", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@types/unist": { "version": "3.0.3", @@ -7683,6 +7918,8 @@ "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@types/yargs-parser": "*" } @@ -7691,7 +7928,9 @@ "version": "21.0.3", "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@types/yauzl": { "version": "2.10.3", @@ -7970,7 +8209,8 @@ "optional": true, "os": [ "android" - ] + ], + "peer": true }, "node_modules/@unrs/resolver-binding-android-arm64": { "version": "1.12.2", @@ -7983,7 +8223,8 @@ "optional": true, "os": [ "android" - ] + ], + "peer": true }, "node_modules/@unrs/resolver-binding-darwin-arm64": { "version": "1.12.2", @@ -7996,7 +8237,8 @@ "optional": true, "os": [ "darwin" - ] + ], + "peer": true }, "node_modules/@unrs/resolver-binding-darwin-x64": { "version": "1.12.2", @@ -8009,7 +8251,8 @@ "optional": true, "os": [ "darwin" - ] + ], + "peer": true }, "node_modules/@unrs/resolver-binding-freebsd-x64": { "version": "1.12.2", @@ -8022,7 +8265,8 @@ "optional": true, "os": [ "freebsd" - ] + ], + "peer": true }, "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { "version": "1.12.2", @@ -8035,7 +8279,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { "version": "1.12.2", @@ -8048,7 +8293,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { "version": "1.12.2", @@ -8061,7 +8307,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@unrs/resolver-binding-linux-arm64-musl": { "version": "1.12.2", @@ -8074,7 +8321,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { "version": "1.12.2", @@ -8087,7 +8335,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@unrs/resolver-binding-linux-loong64-musl": { "version": "1.12.2", @@ -8100,7 +8349,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { "version": "1.12.2", @@ -8113,7 +8363,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { "version": "1.12.2", @@ -8126,7 +8377,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { "version": "1.12.2", @@ -8139,7 +8391,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { "version": "1.12.2", @@ -8152,7 +8405,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@unrs/resolver-binding-linux-x64-gnu": { "version": "1.12.2", @@ -8165,7 +8419,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@unrs/resolver-binding-linux-x64-musl": { "version": "1.12.2", @@ -8178,7 +8433,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@unrs/resolver-binding-openharmony-arm64": { "version": "1.12.2", @@ -8191,7 +8447,8 @@ "optional": true, "os": [ "openharmony" - ] + ], + "peer": true }, "node_modules/@unrs/resolver-binding-wasm32-wasi": { "version": "1.12.2", @@ -8202,6 +8459,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", @@ -8222,7 +8480,8 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { "version": "1.12.2", @@ -8235,7 +8494,8 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@unrs/resolver-binding-win32-x64-msvc": { "version": "1.12.2", @@ -8248,7 +8508,8 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@vitejs/plugin-basic-ssl": { "version": "2.1.0", @@ -8262,6 +8523,252 @@ "vite": "^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.7", + "integrity": "sha512-NEGWJS2XNu2PfRLQwOO3CTKj1tTETxNBdk454vDxVBhxJYhPaA/eS0nAI0c+1El1P7a60z8+i+ZrQoGESweGKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.7", + "vitest": "3.2.7" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-v8/node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/coverage-v8/node_modules/balanced-match": { + "version": "1.0.2", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/coverage-v8/node_modules/brace-expansion": { + "version": "2.1.4", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@vitest/coverage-v8/node_modules/glob": { + "version": "10.5.0", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vitest/coverage-v8/node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vitest/coverage-v8/node_modules/lru-cache": { + "version": "10.4.3", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitest/coverage-v8/node_modules/path-scurry": { + "version": "1.11.1", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vitest/coverage-v8/node_modules/test-exclude": { + "version": "7.0.2", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", @@ -8983,6 +9490,41 @@ "node": ">=0.8" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { + "version": "3.0.3", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/astral-regex": { "version": "2.0.0", "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", @@ -9112,6 +9654,8 @@ "integrity": "sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/transform": "30.4.1", "@types/babel__core": "^7.20.5", @@ -9149,6 +9693,8 @@ "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", "dev": true, "license": "BSD-3-Clause", + "optional": true, + "peer": true, "workspaces": [ "test/babel-8" ], @@ -9168,6 +9714,8 @@ "integrity": "sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@types/babel__core": "^7.20.5" }, @@ -9228,6 +9776,8 @@ "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", @@ -9254,6 +9804,8 @@ "integrity": "sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "babel-plugin-jest-hoist": "30.4.0", "babel-preset-current-node-syntax": "^1.2.0" @@ -9345,6 +9897,7 @@ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", "dev": true, "license": "MIT", + "optional": true, "peer": true, "dependencies": { "require-from-string": "^2.0.2" @@ -9576,23 +10129,13 @@ "node": ">= 4.5.0" } }, - "node_modules/bs-logger": { - "version": "0.2.6", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-json-stable-stringify": "2.x" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/bser": { "version": "2.1.1", "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", "dev": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "node-int64": "^0.4.0" } @@ -9675,6 +10218,15 @@ "node": ">=6.0.0" } }, + "node_modules/cac": { + "version": "6.7.14", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/cacache": { "version": "20.0.4", "integrity": "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==", @@ -9862,6 +10414,22 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/chai": { + "version": "5.3.3", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", @@ -9895,6 +10463,8 @@ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=10" } @@ -9905,6 +10475,15 @@ "dev": true, "license": "MIT" }, + "node_modules/check-error": { + "version": "2.1.3", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/check-more-types": { "version": "2.24.0", "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", @@ -9966,7 +10545,9 @@ "version": "2.2.1", "integrity": "sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/clean-stack": { "version": "2.2.0", @@ -10232,11 +10813,19 @@ "node": ">= 0.12.0" } }, + "node_modules/code-block-writer": { + "version": "12.0.0", + "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==", + "dev": true, + "license": "MIT" + }, "node_modules/collect-v8-coverage": { "version": "1.0.3", "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/color-convert": { "version": "2.0.1", @@ -10709,6 +11298,7 @@ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, "license": "MIT", + "optional": true, "peer": true, "dependencies": { "mdn-data": "2.27.1", @@ -10747,6 +11337,8 @@ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" @@ -10760,6 +11352,8 @@ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", @@ -10783,6 +11377,8 @@ } ], "license": "MIT-0", + "optional": true, + "peer": true, "engines": { "node": ">=18" } @@ -10802,6 +11398,8 @@ } ], "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=18" }, @@ -10825,6 +11423,8 @@ } ], "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@csstools/color-helpers": "^5.1.0", "@csstools/css-calc": "^2.1.4" @@ -10852,6 +11452,8 @@ } ], "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=18" }, @@ -10874,6 +11476,8 @@ } ], "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=18" } @@ -10882,7 +11486,9 @@ "version": "10.4.3", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "license": "ISC" + "license": "ISC", + "optional": true, + "peer": true }, "node_modules/cypress": { "version": "14.5.4", @@ -11102,6 +11708,7 @@ "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", "dev": true, "license": "MIT", + "optional": true, "peer": true, "dependencies": { "whatwg-mimetype": "^5.0.0", @@ -11214,7 +11821,9 @@ "version": "10.6.0", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/decompress-response": { "version": "6.0.0", @@ -11248,6 +11857,8 @@ "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, @@ -11257,6 +11868,15 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-equal": { "version": "1.0.1", "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", @@ -11274,6 +11894,8 @@ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -11450,6 +12072,8 @@ "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -11627,6 +12251,8 @@ "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=12" }, @@ -12438,6 +13064,8 @@ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, "license": "BSD-2-Clause", + "optional": true, + "peer": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -12599,6 +13227,8 @@ "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 0.8.0" } @@ -12608,6 +13238,8 @@ "integrity": "sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/expect-utils": "30.4.1", "@jest/get-type": "30.1.0", @@ -12620,6 +13252,15 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exponential-backoff": { "version": "3.1.3", "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", @@ -12856,6 +13497,8 @@ "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", "dev": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "bser": "2.1.1" } @@ -13360,6 +14003,8 @@ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=8.0.0" } @@ -13662,40 +14307,10 @@ "license": "MIT" }, "node_modules/handle-thing": { - "version": "2.0.1", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "dev": true, - "license": "MIT" - }, - "node_modules/handlebars": { - "version": "4.7.9", - "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/handlebars/node_modules/source-map": { - "version": "0.6.1", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "version": "2.0.1", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" }, "node_modules/har-schema": { "version": "2.0.0", @@ -13922,6 +14537,7 @@ "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", "dev": true, "license": "MIT", + "optional": true, "peer": true, "dependencies": { "@exodus/bytes": "^1.6.0" @@ -13935,6 +14551,7 @@ "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", "dev": true, "license": "MIT", + "optional": true, "peer": true, "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" @@ -14353,6 +14970,8 @@ "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" @@ -14372,6 +14991,8 @@ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -14385,6 +15006,8 @@ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -14397,6 +15020,8 @@ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "p-try": "^2.0.0" }, @@ -14412,6 +15037,8 @@ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -14424,6 +15051,8 @@ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "find-up": "^4.0.0" }, @@ -14732,6 +15361,8 @@ "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=6" } @@ -14941,7 +15572,9 @@ "version": "1.0.1", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/is-promise": { "version": "4.0.0", @@ -15307,6 +15940,8 @@ "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/core": "30.4.2", "@jest/types": "30.4.1", @@ -15333,6 +15968,8 @@ "integrity": "sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "execa": "^5.1.1", "jest-util": "30.4.1", @@ -15347,6 +15984,8 @@ "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -15370,6 +16009,8 @@ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=10" }, @@ -15382,6 +16023,8 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "engines": { "node": ">=10.17.0" } @@ -15390,13 +16033,17 @@ "version": "3.0.7", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, - "license": "ISC" + "license": "ISC", + "optional": true, + "peer": true }, "node_modules/jest-circus": { "version": "30.4.2", "integrity": "sha512-rvHH7VlY6LgbJXJTQ87GW62g1FntOtbhh0zT+v04kC+pgL6aBKyYINXxWukCpj3dcIBMw5/XUbtDS9dU9JTXeQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/environment": "30.4.1", "@jest/expect": "30.4.1", @@ -15428,6 +16075,8 @@ "integrity": "sha512-jfA2ocvVHMXS2QijrJ0d31ektP+d/W0T5RpcTX2Pq+3sVqHlsXVCM2+FmwpL+bdY8OfHpIg9xMxLF17Zg0U49Q==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/core": "30.4.2", "@jest/test-result": "30.4.1", @@ -15460,6 +16109,8 @@ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -15474,6 +16125,8 @@ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -15491,6 +16144,8 @@ "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -15509,6 +16164,8 @@ "integrity": "sha512-rNHAShJQqQwFNoL0hbf3BphSBOWnpOUAKvidLS/AjNVLPfoj5mSf4jQMfW3cYOs6hXeZC7nF7mDHaBnbxELOzg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/core": "^7.27.4", "@jest/get-type": "30.1.0", @@ -15558,13 +16215,17 @@ "version": "1.0.2", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/jest-config/node_modules/brace-expansion": { "version": "2.1.4", "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -15575,6 +16236,8 @@ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -15594,13 +16257,17 @@ "version": "10.4.3", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "license": "ISC" + "license": "ISC", + "optional": true, + "peer": true }, "node_modules/jest-config/node_modules/minimatch": { "version": "9.0.9", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "brace-expansion": "^2.0.2" }, @@ -15616,6 +16283,8 @@ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, "license": "BlueOak-1.0.0", + "optional": true, + "peer": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -15632,6 +16301,8 @@ "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/diff-sequences": "30.4.0", "@jest/get-type": "30.1.0", @@ -15647,6 +16318,8 @@ "integrity": "sha512-ZPMabUZCx5MpbZ2eBYSvZ0J8fvo3dR9oM+eeUpb3aKNQFuS2tu3Duw1TNlMoP8k3WQgKGJuhcMFvwcVuq6T7oA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "detect-newline": "^3.1.0" }, @@ -15659,6 +16332,8 @@ "integrity": "sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/get-type": "30.1.0", "@jest/types": "30.4.1", @@ -15675,6 +16350,8 @@ "integrity": "sha512-o3nfaN4zej7qgk2X0j8Jhq/S9nAVKs2xK3QeQxeHVvpkEPxaA1yxDGydR+iVI7zPy7Cp62Aq2h3Ja46QvfWHGA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/environment": "30.4.1", "@jest/environment-jsdom-abstract": "30.4.1", @@ -15697,6 +16374,8 @@ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" @@ -15710,6 +16389,8 @@ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "whatwg-encoding": "^3.1.1" }, @@ -15722,6 +16403,8 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -15761,6 +16444,8 @@ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "punycode": "^2.3.1" }, @@ -15773,6 +16458,8 @@ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, "license": "BSD-2-Clause", + "optional": true, + "peer": true, "engines": { "node": ">=12" } @@ -15782,6 +16469,8 @@ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=18" } @@ -15791,6 +16480,8 @@ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" @@ -15804,6 +16495,8 @@ "integrity": "sha512-4FZYVOk85hz2AyT6BbarKy9u37g6DbrDyCdFhsnDdXqyrueYQvB+0zO4f/kqLCRD0BsPRXPMNJeQwihKZV8naw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/environment": "30.4.1", "@jest/fake-timers": "30.4.1", @@ -15822,6 +16515,8 @@ "integrity": "sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/types": "30.4.1", "@types/node": "*", @@ -15846,6 +16541,8 @@ "integrity": "sha512-IpmyiioeHxiWDhesHnUFmOxcTzwCwKpgACgWajtAP+nYQXiY7DakTxB6Bx9JFiRMljr0AX1PvnQdaU1KFoz6NQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/get-type": "30.1.0", "pretty-format": "30.4.1" @@ -15859,6 +16556,8 @@ "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", @@ -15874,6 +16573,8 @@ "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@jest/types": "30.4.1", @@ -15895,6 +16596,8 @@ "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/types": "30.4.1", "@types/node": "*", @@ -15909,6 +16612,8 @@ "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=6" }, @@ -15921,39 +16626,13 @@ } } }, - "node_modules/jest-preset-angular": { - "version": "17.0.0", - "integrity": "sha512-2yAHkA1c5rSICGJVtLYYqPC5RDsvo4+i4CwWFHVXwv41cHNX7gCWeh074IuZ5mFw7Vwsr+i25EowCnGtKkxNWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment-jsdom-abstract": "^30.4.1", - "bs-logger": "^0.2.6", - "esbuild-wasm": ">=0.28.0", - "jest-util": "^30.4.1", - "pretty-format": "^30.4.1", - "ts-jest": "^29.4.11" - }, - "engines": { - "node": "^20.11.1 || >=22.0.0" - }, - "optionalDependencies": { - "esbuild": ">=0.28.0" - }, - "peerDependencies": { - "@angular/compiler-cli": ">=20.0.0 <23.0.0", - "@angular/core": ">=20.0.0 <23.0.0", - "@angular/platform-browser": ">=20.0.0 <23.0.0", - "jest": "^30.0.0", - "jsdom": ">=26.0.0", - "typescript": ">=5.8" - } - }, "node_modules/jest-regex-util": { "version": "30.4.0", "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } @@ -15963,6 +16642,8 @@ "integrity": "sha512-Zry8Yq/yJcNAZ7dJ5F2heic8AheXvbFZ7XI5V+h28nrYZ7Qoyy4dItq8OodjnYD270mvX+ZudmrNV9cysqhW5Q==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "chalk": "^4.1.2", "graceful-fs": "^4.2.11", @@ -15982,6 +16663,8 @@ "integrity": "sha512-gDiVh1I+GxYzz9oXlyw+1wv6VOYX1WYxMOfjsA3iGKePV2oxmbHhwxfkALxNxYy1ciw6APWwkW2zZONwP97aEQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "jest-regex-util": "30.4.0", "jest-snapshot": "30.4.1" @@ -15995,6 +16678,8 @@ "integrity": "sha512-2dw0PslVYXxffXGpLo+Ejad+KcI1Qkjn7f4X4619gf21oCUmL+SPfjqIa/losUem3yEOvfNZe/F1HWUcNpODcg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/console": "30.4.1", "@jest/environment": "30.4.1", @@ -16028,6 +16713,8 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", + "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -16037,6 +16724,8 @@ "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -16047,6 +16736,8 @@ "integrity": "sha512-3/5e8iPz2k/VLqlr8DgTftYyLUv8Su3FkCAO2/Od81UsUTpSxOrS6O5x5KkoQwyUjmpYyDJKeyAvg2T2nvpNkQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/environment": "30.4.1", "@jest/fake-timers": "30.4.1", @@ -16079,13 +16770,17 @@ "version": "1.0.2", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/jest-runtime/node_modules/brace-expansion": { "version": "2.1.4", "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -16096,6 +16791,8 @@ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -16115,13 +16812,17 @@ "version": "10.4.3", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "license": "ISC" + "license": "ISC", + "optional": true, + "peer": true }, "node_modules/jest-runtime/node_modules/minimatch": { "version": "9.0.9", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "brace-expansion": "^2.0.2" }, @@ -16137,6 +16838,8 @@ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, "license": "BlueOak-1.0.0", + "optional": true, + "peer": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -16153,6 +16856,8 @@ "integrity": "sha512-tEOkkfOMppUyeiHwjZswOQ3lcnoTnws/q5FnGIaeIh/jmoU0ZlgMYRR8sTlTj+nNGCoJ0RDq6SfxGxCsyMTPmw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/core": "^7.27.4", "@babel/generator": "^7.27.5", @@ -16185,6 +16890,8 @@ "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/types": "30.4.1", "@types/node": "*", @@ -16202,6 +16909,8 @@ "integrity": "sha512-PDWi4SOwLnwqNDfHZjOcsEFyZ4fc/2W2gVL3DEoyqnB6jCQMLRtfBong8s6omIw3lI0HWOus12xfnFmQtjW3fw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/get-type": "30.1.0", "@jest/types": "30.4.1", @@ -16219,6 +16928,8 @@ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=10" }, @@ -16231,6 +16942,8 @@ "integrity": "sha512-/l9UonmvCwjHH7d2h3iAwIloLc1H0S8mJZ/LNK3i86hqwPAz8otUJjP9MfYtz9Tt77Su5FD2xGjZn8d31IZHlw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/test-result": "30.4.1", "@jest/types": "30.4.1", @@ -16250,6 +16963,8 @@ "integrity": "sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", @@ -16326,6 +17041,7 @@ "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", "dev": true, "license": "MIT", + "optional": true, "peer": true, "dependencies": { "@asamuzakjp/css-color": "^5.1.11", @@ -16367,6 +17083,7 @@ "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", "dev": true, "license": "MIT", + "optional": true, "peer": true, "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" @@ -16399,6 +17116,7 @@ "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", "dev": true, "license": "BSD-2-Clause", + "optional": true, "peer": true, "engines": { "node": ">=20.19.0" @@ -16412,6 +17130,7 @@ "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "dev": true, "license": "BlueOak-1.0.0", + "optional": true, "peer": true, "engines": { "node": "20 || >=22" @@ -16422,6 +17141,7 @@ "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", "dev": true, "license": "MIT", + "optional": true, "peer": true, "dependencies": { "entities": "^8.0.0" @@ -16435,6 +17155,7 @@ "integrity": "sha512-aBiNayCfTQxuIJBm06M+xR14cYaYlDlSXZbgsnKzKNxDKUVq7KFwTjwBSsb7m9Y5xO8WfPnBc63WaYFMTGlvqw==", "dev": true, "license": "MIT", + "optional": true, "peer": true, "dependencies": { "tldts-core": "^7.4.11" @@ -16448,6 +17169,7 @@ "integrity": "sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg==", "dev": true, "license": "MIT", + "optional": true, "peer": true }, "node_modules/jsdom/node_modules/tough-cookie": { @@ -16455,6 +17177,7 @@ "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", "dev": true, "license": "BSD-3-Clause", + "optional": true, "peer": true, "dependencies": { "tldts": "^7.0.5" @@ -16468,6 +17191,7 @@ "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", "dev": true, "license": "MIT", + "optional": true, "peer": true, "engines": { "node": ">=20.18.1" @@ -16984,6 +17708,8 @@ "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=6" } @@ -17276,12 +18002,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", @@ -17500,6 +18220,12 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/loupe": { + "version": "3.2.1", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lowercase-keys": { "version": "2.0.0", "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", @@ -17561,6 +18287,17 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/magicast": { + "version": "0.3.5", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, "node_modules/make-dir": { "version": "4.0.0", "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", @@ -17619,6 +18356,8 @@ "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", "dev": true, "license": "BSD-3-Clause", + "optional": true, + "peer": true, "dependencies": { "tmpl": "1.0.5" } @@ -17664,6 +18403,7 @@ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "dev": true, "license": "CC0-1.0", + "optional": true, "peer": true }, "node_modules/mdurl": { @@ -18007,6 +18747,21 @@ "node": ">= 18" } }, + "node_modules/mkdirp": { + "version": "3.0.1", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/moment": { "version": "2.30.1", "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", @@ -18161,6 +18916,8 @@ "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "bin": { "napi-postinstall": "lib/cli.js" }, @@ -18898,7 +19655,9 @@ "version": "0.4.0", "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/node-releases": { "version": "2.0.51", @@ -19095,7 +19854,9 @@ "version": "2.2.24", "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/oauth-sign": { "version": "0.9.0", @@ -19848,6 +20609,8 @@ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "entities": "^6.0.0" }, @@ -19946,6 +20709,8 @@ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, "license": "BSD-2-Clause", + "optional": true, + "peer": true, "engines": { "node": ">=0.12" }, @@ -19988,6 +20753,12 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", @@ -20071,6 +20842,21 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/pend": { "version": "1.2.0", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", @@ -20136,6 +20922,8 @@ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 6" } @@ -20418,6 +21206,8 @@ "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jest/schemas": "30.4.1", "ansi-styles": "^5.2.0", @@ -20433,6 +21223,8 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=10" }, @@ -20975,7 +21767,9 @@ "url": "https://opencollective.com/fast-check" } ], - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/pvtsutils": { "version": "1.3.6", @@ -21086,14 +21880,18 @@ "version": "18.3.1", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/react-is-19": { "name": "react-is", "version": "19.2.8", "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/readable-stream": { "version": "2.3.8", @@ -21432,6 +22230,8 @@ "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "resolve-from": "^5.0.0" }, @@ -21444,6 +22244,8 @@ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -21678,7 +22480,9 @@ "version": "0.8.0", "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/run-applescript": { "version": "7.1.0", @@ -21928,6 +22732,8 @@ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "xmlchars": "^2.2.0" }, @@ -22413,6 +23219,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", @@ -22672,7 +23484,9 @@ "version": "1.0.3", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true, - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "optional": true, + "peer": true }, "node_modules/sshpk": { "version": "1.18.0", @@ -22716,6 +23530,8 @@ "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "escape-string-regexp": "^2.0.0" }, @@ -22728,10 +23544,18 @@ "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=8" } }, + "node_modules/stackback": { + "version": "0.0.2", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", @@ -22741,6 +23565,12 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.10.0", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/stdin-discarder": { "version": "0.2.2", "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", @@ -22786,6 +23616,8 @@ "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" @@ -22928,6 +23760,8 @@ "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -22953,6 +23787,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/supports-color": { "version": "8.1.1", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", @@ -22984,13 +23836,17 @@ "version": "3.2.4", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/synckit": { "version": "0.11.13", "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@pkgr/core": "^0.3.6" }, @@ -23142,6 +23998,8 @@ "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", @@ -23155,13 +24013,17 @@ "version": "1.0.2", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/test-exclude/node_modules/brace-expansion": { "version": "1.1.18", "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -23172,6 +24034,8 @@ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -23222,6 +24086,18 @@ "dev": true, "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.14", "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", @@ -23238,6 +24114,33 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tldts": { "version": "6.1.86", "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", @@ -23269,7 +24172,9 @@ "version": "1.0.5", "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true, - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "optional": true, + "peer": true }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -23325,6 +24230,7 @@ "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", "dev": true, "license": "MIT", + "optional": true, "peer": true, "dependencies": { "punycode": "^2.3.1" @@ -23370,80 +24276,14 @@ "typescript": ">=4.8.4" } }, - "node_modules/ts-jest": { - "version": "29.4.12", - "integrity": "sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==", + "node_modules/ts-morph": { + "version": "21.0.1", + "integrity": "sha512-dbDtVdEAncKctzrVZ+Nr7kHpHkv+0JDJb2MjjpBaj8bFeCkePU9rHfMklmhuLFnpeq/EJZk2IhStY6NzqgjOkg==", "dev": true, "license": "MIT", "dependencies": { - "bs-logger": "^0.2.6", - "fast-json-stable-stringify": "^2.1.0", - "handlebars": "^4.7.9", - "json5": "^2.2.3", - "lodash.memoize": "^4.1.2", - "make-error": "^1.3.6", - "semver": "^7.8.5", - "type-fest": "^4.41.0", - "yargs-parser": "^21.1.1" - }, - "bin": { - "ts-jest": "cli.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/transform": "^29.0.0 || ^30.0.0", - "@jest/types": "^29.0.0 || ^30.0.0", - "babel-jest": "^29.0.0 || ^30.0.0", - "jest": "^29.0.0 || ^30.0.0", - "jest-util": "^29.0.0 || ^30.0.0", - "typescript": ">=4.3 <7" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "@jest/transform": { - "optional": true - }, - "@jest/types": { - "optional": true - }, - "babel-jest": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jest-util": { - "optional": true - } - } - }, - "node_modules/ts-jest/node_modules/semver": { - "version": "7.8.5", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ts-jest/node_modules/type-fest": { - "version": "4.41.0", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "@ts-morph/common": "~0.22.0", + "code-block-writer": "^12.0.0" } }, "node_modules/ts-node": { @@ -23603,6 +24443,8 @@ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=4" } @@ -23797,19 +24639,6 @@ "dev": true, "license": "MIT" }, - "node_modules/uglify-js": { - "version": "3.19.3", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/unbox-primitive": { "version": "1.1.0", "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", @@ -23883,6 +24712,19 @@ "node": ">=4" } }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/universalify": { "version": "2.0.1", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", @@ -23907,6 +24749,8 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "napi-postinstall": "^0.3.4" }, @@ -24022,6 +24866,8 @@ "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", "dev": true, "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", @@ -24035,7 +24881,9 @@ "version": "2.0.0", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/validate-npm-package-name": { "version": "6.0.2", @@ -24075,6 +24923,34 @@ "dev": true, "license": "MIT" }, + "node_modules/vfile": { + "version": "6.0.3", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "7.3.6", "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", @@ -24149,6 +25025,34 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/es-module-lexer": { + "version": "1.7.0", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/vite/node_modules/tinyglobby": { "version": "0.2.17", "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", @@ -24165,11 +25069,85 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/vitest": { + "version": "3.2.7", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "xml-name-validator": "^5.0.0" }, @@ -24182,6 +25160,8 @@ "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", "dev": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "makeerror": "1.0.12" } @@ -24350,6 +25330,7 @@ "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", "dev": true, "license": "BSD-2-Clause", + "optional": true, "peer": true, "engines": { "node": ">=20" @@ -24975,6 +25956,8 @@ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "iconv-lite": "0.6.3" }, @@ -24987,6 +25970,8 @@ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -24999,6 +25984,7 @@ "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", "dev": true, "license": "MIT", + "optional": true, "peer": true, "engines": { "node": ">=20" @@ -25009,6 +25995,7 @@ "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", "dev": true, "license": "MIT", + "optional": true, "peer": true, "dependencies": { "@exodus/bytes": "^1.11.0", @@ -25024,6 +26011,7 @@ "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", "dev": true, "license": "MIT", + "optional": true, "peer": true, "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" @@ -25166,6 +26154,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wildcard": { "version": "2.0.1", "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", @@ -25181,12 +26185,6 @@ "node": ">=0.10.0" } }, - "node_modules/wordwrap": { - "version": "1.0.0", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, - "license": "MIT" - }, "node_modules/wrap-ansi": { "version": "6.2.0", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", @@ -25230,6 +26228,8 @@ "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", "dev": true, "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" @@ -25279,6 +26279,8 @@ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "engines": { "node": ">=18" } @@ -25309,7 +26311,9 @@ "version": "2.2.0", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/y18n": { "version": "5.0.8", @@ -25363,6 +26367,8 @@ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", + "optional": true, + "peer": true, "engines": { "node": ">=12" } diff --git a/package.json b/package.json index d66171b7..e95ce225 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "build": "ng build auth0-angular --configuration=production && npm run build:schematics", "build:schematics": "npm run build --prefix projects/auth0-angular", "postbuild": "cp README.md ./dist/auth0-angular/", - "test": "jest", - "test:ci": "jest --coverage --silent", + "test": "vitest", + "test:ci": "vitest run --coverage", "e2e": "ng e2e", "e2e:headless": "ng e2e --no-watch --headless", "e2e:ci": "concurrently --raw --kill-others --success first npm:start:local:oidc npm:e2e:headless", @@ -48,7 +48,9 @@ "@cypress/schematic": "^2.5.2", "@types/jasmine": "^3.10.18", "@types/jasminewd2": "~2.0.13", - "@types/jest": "^30.0.0", + "@analogjs/vite-plugin-angular": "^1.0.0", + "@analogjs/vitest-angular": "^1.0.0", + "@vitest/coverage-v8": "^3.0.0", "@types/node": "^22.19.17", "@typescript-eslint/eslint-plugin": "^8.68.0", "@typescript-eslint/parser": "^8.59.0", @@ -62,9 +64,7 @@ "eslint-plugin-prefer-arrow": "^1.2.3", "express-jwt": "^8.5.1", "husky": "^4.3.8", - "jest": "^30.0.0", - "jest-environment-jsdom": "^30.0.0", - "jest-preset-angular": "^17.0.0", + "vitest": "^3.0.0", "jwks-rsa": "^3.2.2", "moment": "^2.30.1", "ng-packagr": "^20.3.2", diff --git a/projects/auth0-angular/jest.config.ts b/projects/auth0-angular/jest.config.ts deleted file mode 100644 index 16795c68..00000000 --- a/projects/auth0-angular/jest.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createCjsPreset } from 'jest-preset-angular/presets/index.js'; -/* eslint-disable */ -export default { - displayName: 'auth0-angular', - ...createCjsPreset(), - setupFilesAfterEnv: ['/src/test-setup.ts'], - coverageDirectory: '../../coverage/auth0-angular', - snapshotSerializers: [ - 'jest-preset-angular/build/serializers/no-ng-attributes', - 'jest-preset-angular/build/serializers/ng-snapshot', - 'jest-preset-angular/build/serializers/html-comment', - ], -}; diff --git a/projects/auth0-angular/src/lib/abstract-navigator.spec.ts b/projects/auth0-angular/src/lib/abstract-navigator.spec.ts index 9394823d..bc1ef8a1 100644 --- a/projects/auth0-angular/src/lib/abstract-navigator.spec.ts +++ b/projects/auth0-angular/src/lib/abstract-navigator.spec.ts @@ -1,9 +1,25 @@ -import { TestBed, fakeAsync, tick } from '@angular/core/testing'; +// Each spec file runs in its own jsdom window; zone.js from test-setup.ts +// patched a different window, so it must be imported here too. +import 'zone.js'; +import 'zone.js/testing'; +import '@angular/compiler'; +import { TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { Location } from '@angular/common'; import { AbstractNavigator } from './abstract-navigator'; import { Component } from '@angular/core'; import { Router } from '@angular/router'; +import { + BrowserTestingModule, + platformBrowserTesting, +} from '@angular/platform-browser/testing'; + +// The forks pool loads setupFiles in a separate module-cache context from spec +// files, so initTestEnvironment called in test-setup.ts targets a different +// TestBed instance. Initialize the environment here in the spec's own context. +beforeAll(() => { + TestBed.initTestEnvironment(BrowserTestingModule, platformBrowserTesting()); +}); describe('RouteNavigator', () => { let navigator: AbstractNavigator; @@ -22,7 +38,7 @@ describe('RouteNavigator', () => { navigator = TestBed.inject(AbstractNavigator); const location = TestBed.inject(Location); - replaceStateSpy = jest.spyOn(location, 'replaceState'); + replaceStateSpy = vi.spyOn(location, 'replaceState'); }); it('should be created', () => { @@ -52,15 +68,14 @@ describe('RouteNavigator', () => { navigator = TestBed.inject(AbstractNavigator); const location = TestBed.inject(Location); - replaceStateSpy = jest.spyOn(location, 'replaceState'); + replaceStateSpy = vi.spyOn(location, 'replaceState'); }); - it('should use the router if available', fakeAsync(() => { + it('should use the router if available', async () => { const location = TestBed.inject(Location); - navigator.navigateByUrl('/test-route'); - tick(); + await navigator.navigateByUrl('/test-route'); expect(location.path()).toBe('/test-route'); - })); + }); it('should not use the window object to navigate', async () => { expect(replaceStateSpy).not.toHaveBeenCalled(); diff --git a/projects/auth0-angular/src/lib/auth.guard.spec.ts b/projects/auth0-angular/src/lib/auth.guard.spec.ts index 0a0f33cb..837438f7 100644 --- a/projects/auth0-angular/src/lib/auth.guard.spec.ts +++ b/projects/auth0-angular/src/lib/auth.guard.spec.ts @@ -1,6 +1,5 @@ import { of } from 'rxjs'; import { AuthGuard } from './auth.guard'; -import { expect } from '@jest/globals'; describe('AuthGuard', () => { let guard: AuthGuard; @@ -11,10 +10,10 @@ describe('AuthGuard', () => { it('should return true for a logged in user', () => { const authServiceMock: any = { isAuthenticated$: of(true), - loginWithRedirect: jest.fn(), + loginWithRedirect: vi.fn(), }; guard = new AuthGuard(authServiceMock); - const listener = jest.fn(); + const listener = vi.fn(); guard.canActivate(routeMock, routeStateMock).subscribe(listener); expect(authServiceMock.loginWithRedirect).not.toHaveBeenCalled(); expect(listener).toHaveBeenCalledWith(true); @@ -23,7 +22,7 @@ describe('AuthGuard', () => { it('should redirect a logged out user', () => { const authServiceMock: any = { isAuthenticated$: of(false), - loginWithRedirect: jest.fn(), + loginWithRedirect: vi.fn().mockReturnValue(of(undefined)), }; guard = new AuthGuard(authServiceMock); guard.canActivate(routeMock, routeStateMock).subscribe(); @@ -37,10 +36,10 @@ describe('AuthGuard', () => { it('should return true for a logged in user', () => { const authServiceMock: any = { isAuthenticated$: of(true), - loginWithRedirect: jest.fn(), + loginWithRedirect: vi.fn().mockReturnValue(of(undefined)), }; guard = new AuthGuard(authServiceMock); - const listener = jest.fn(); + const listener = vi.fn(); guard.canActivateChild(routeMock, routeStateMock).subscribe(listener); expect(authServiceMock.loginWithRedirect).not.toHaveBeenCalled(); expect(listener).toHaveBeenCalledWith(true); @@ -49,7 +48,7 @@ describe('AuthGuard', () => { it('should redirect a logged out user', () => { const authServiceMock: any = { isAuthenticated$: of(false), - loginWithRedirect: jest.fn(), + loginWithRedirect: vi.fn().mockReturnValue(of(undefined)), }; guard = new AuthGuard(authServiceMock); guard.canActivateChild(routeMock, routeStateMock).subscribe(); @@ -65,7 +64,7 @@ describe('AuthGuard', () => { isAuthenticated$: of(true), }; guard = new AuthGuard(authServiceMock); - const listener = jest.fn(); + const listener = vi.fn(); guard.canLoad(routeMock, []).subscribe(listener); expect(listener).toHaveBeenCalledWith(true); }); @@ -75,7 +74,7 @@ describe('AuthGuard', () => { isAuthenticated$: of(false), }; guard = new AuthGuard(authServiceMock); - const listener = jest.fn(); + const listener = vi.fn(); guard.canLoad(routeMock, []).subscribe(listener); expect(listener).toHaveBeenCalledWith(false); }); diff --git a/projects/auth0-angular/src/lib/auth.interceptor.spec.ts b/projects/auth0-angular/src/lib/auth.interceptor.spec.ts index 9b5b9934..29089540 100644 --- a/projects/auth0-angular/src/lib/auth.interceptor.spec.ts +++ b/projects/auth0-angular/src/lib/auth.interceptor.spec.ts @@ -1,5 +1,10 @@ +// Each spec file runs in its own jsdom window; zone.js from test-setup.ts +// patched a different window, so it must be imported here too. +import 'zone.js'; +import 'zone.js/testing'; +import '@angular/compiler'; import { AuthHttpInterceptor } from './auth.interceptor'; -import { TestBed, fakeAsync, flush } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { HttpClient, HTTP_INTERCEPTORS, @@ -23,9 +28,21 @@ import { Auth0Client } from '@auth0/auth0-spa-js'; import { Auth0ClientService } from './auth.client'; import { AuthState } from './auth.state'; import { AuthService } from './auth.service'; +import type { MockInstance } from 'vitest'; +import { + BrowserTestingModule, + platformBrowserTesting, +} from '@angular/platform-browser/testing'; // NOTE: Read Async testing: https://github.com/angular/angular/issues/25733#issuecomment-636154553 +// The forks pool loads setupFiles in a separate module-cache context from spec +// files, so initTestEnvironment called in test-setup.ts targets a different +// TestBed instance. Initialize the environment here in the spec's own context. +beforeAll(() => { + TestBed.initTestEnvironment(BrowserTestingModule, platformBrowserTesting()); +}); + const mockWindow = global as any; Object.defineProperty(mockWindow, 'crypto', { @@ -50,13 +67,8 @@ describe('The Auth HTTP Interceptor', () => { let authService: AuthService; let isLoading$: Subject; - const assertAuthorizedApiCallTo = async ( - url: string, - done: () => void, - method = 'get' - ) => { - httpClient.request(method, url).subscribe(done); - flush(); + const assertAuthorizedApiCallTo = async (url: string, method = 'get') => { + httpClient.request(method, url).subscribe(); await new Promise(process.nextTick); req = httpTestingController.expectOne(url); @@ -65,9 +77,8 @@ describe('The Auth HTTP Interceptor', () => { ); }; - const assertPassThruApiCallTo = async (url: string, done: () => void) => { - httpClient.get(url).subscribe(done); - flush(); + const assertPassThruApiCallTo = async (url: string) => { + httpClient.get(url).subscribe(); await new Promise(process.nextTick); req = httpTestingController.expectOne(url); expect(req.request.headers.get('Authorization')).toBeFalsy(); @@ -84,9 +95,9 @@ describe('The Auth HTTP Interceptor', () => { clientId: '', }); - jest - .spyOn(auth0Client, 'getTokenSilently') - .mockImplementation(() => Promise.resolve('access-token')); + vi.spyOn(auth0Client, 'getTokenSilently').mockImplementation(() => + Promise.resolve('access-token') + ); config = { httpInterceptor: { @@ -161,7 +172,7 @@ describe('The Auth HTTP Interceptor', () => { authState = TestBed.inject(AuthState); authService = TestBed.inject(AuthService); - jest.spyOn(authState, 'setError'); + vi.spyOn(authState, 'setError'); }); afterEach(() => { @@ -172,115 +183,90 @@ describe('The Auth HTTP Interceptor', () => { }); describe('When no httpInterceptor is configured', () => { - it('pass through and do not have access tokens attached', fakeAsync(async ( - done: () => void - ) => { + it('pass through and do not have access tokens attached', async () => { config.httpInterceptor = null as unknown as HttpInterceptorConfig; - await assertPassThruApiCallTo('https://my-api.com/api/public', done); - })); + await assertPassThruApiCallTo('https://my-api.com/api/public'); + }); }); describe('Requests that do not require authentication', () => { - it('pass through and do not have access tokens attached', fakeAsync(async ( - done: () => void - ) => { - await assertPassThruApiCallTo('https://my-api.com/api/public', done); - })); + it('pass through and do not have access tokens attached', async () => { + await assertPassThruApiCallTo('https://my-api.com/api/public'); + }); }); describe('Requests that are configured using a primitive', () => { - it('waits unil isLoading emits false', fakeAsync(async ( - done: () => void - ) => { + it('waits unil isLoading emits false', async () => { const method = 'GET'; const url = 'https://my-api.com/api/photos'; isLoading$.next(true); - httpClient.request(method, url).subscribe(done); - flush(); + httpClient.request(method, url).subscribe(); + await new Promise(process.nextTick); httpTestingController.expectNone(url); isLoading$.next(false); - flush(); + await new Promise(process.nextTick); httpTestingController.expectOne(url); - })); + }); - it('attach the access token when the configuration uri is a string', fakeAsync(async ( - done: () => void - ) => { + it('attach the access token when the configuration uri is a string', async () => { // Testing /api/photos (exact match) - await assertAuthorizedApiCallTo('https://my-api.com/api/photos', done); - })); + await assertAuthorizedApiCallTo('https://my-api.com/api/photos'); + }); - it('attach the access token when the configuration uri is a string with a wildcard', fakeAsync(async ( - done: () => void - ) => { + it('attach the access token when the configuration uri is a string with a wildcard', async () => { // Testing /api/people* (wildcard match) - await assertAuthorizedApiCallTo( - 'https://my-api.com/api/people/profile', - done - ); - })); + await assertAuthorizedApiCallTo('https://my-api.com/api/people/profile'); + }); - it('matches a full url to an API', fakeAsync(async (done: () => void) => { + it('matches a full url to an API', async () => { // Testing 'https://my-api.com/orders' (exact) - await assertAuthorizedApiCallTo('https://my-api.com/orders', done); - })); + await assertAuthorizedApiCallTo('https://my-api.com/orders'); + }); - it('matches a URL that contains a query string', fakeAsync(async ( - done: () => void - ) => { + it('matches a URL that contains a query string', async () => { await assertAuthorizedApiCallTo( - 'https://my-api.com/api/people?name=test', - done + 'https://my-api.com/api/people?name=test' ); - })); + }); - it('matches a URL that contains a hash fragment', fakeAsync(async ( - done: () => void - ) => { + it('matches a URL that contains a hash fragment', async () => { await assertAuthorizedApiCallTo( - 'https://my-api.com/api/people#hash-fragment', - done + 'https://my-api.com/api/people#hash-fragment' ); - })); + }); }); describe('Requests that are configured using a complex object', () => { - it('waits unil isLoading emits false', fakeAsync(async ( - done: () => void - ) => { + it('waits unil isLoading emits false', async () => { const method = 'GET'; const url = 'https://my-api.com/api/orders'; isLoading$.next(true); - httpClient.request(method, url).subscribe(done); - flush(); + httpClient.request(method, url).subscribe(); + await new Promise(process.nextTick); httpTestingController.expectNone(url); isLoading$.next(false); - flush(); + await new Promise(process.nextTick); httpTestingController.expectOne(url); - })); + }); - it('attach the access token when the uri is configured using a string', fakeAsync(async ( - done: () => void - ) => { + it('attach the access token when the uri is configured using a string', async () => { // Testing { uri: /api/orders } (exact match) - await assertAuthorizedApiCallTo('https://my-api.com/api/orders', done); - })); + await assertAuthorizedApiCallTo('https://my-api.com/api/orders'); + }); - it('pass through the route options to getTokenSilently, without additional properties', fakeAsync(async ( - done: () => void - ) => { + it('pass through the route options to getTokenSilently, without additional properties', async () => { // Testing { uri: /api/addresses } (exact match) - await assertAuthorizedApiCallTo('https://my-api.com/api/addresses', done); + await assertAuthorizedApiCallTo('https://my-api.com/api/addresses'); expect(auth0Client.getTokenSilently).toHaveBeenCalledWith({ authorizationParams: { @@ -288,157 +274,127 @@ describe('The Auth HTTP Interceptor', () => { scope: 'scope', }, }); - })); + }); - it('attach the access token when the configuration uri is a string with a wildcard', fakeAsync(async ( - done: () => void - ) => { + it('attach the access token when the configuration uri is a string with a wildcard', async () => { // Testing { uri: /api/calendar* } (wildcard match) - await assertAuthorizedApiCallTo( - 'https://my-api.com/api/calendar/events', - done - ); - })); + await assertAuthorizedApiCallTo('https://my-api.com/api/calendar/events'); + }); - it('attaches the access token when the HTTP method matches', fakeAsync(async ( - done: () => void - ) => { + it('attaches the access token when the HTTP method matches', async () => { // Testing { uri: /api/register } (wildcard match) await assertAuthorizedApiCallTo( 'https://my-api.com/api/register', - done, 'post' ); - })); + }); - it('does not attach the access token if the HTTP method does not match', fakeAsync(async ( - done: () => void - ) => { - await assertPassThruApiCallTo('https://my-api.com/api/public', done); - })); + it('does not attach the access token if the HTTP method does not match', async () => { + await assertPassThruApiCallTo('https://my-api.com/api/public'); + }); - it('does not execute HTTP call when not able to retrieve a token', fakeAsync(async ( - done: () => void - ) => { - ( - auth0Client.getTokenSilently as unknown as jest.SpyInstance - ).mockReturnValue(throwError({ error: 'login_required' })); + it('does not execute HTTP call when not able to retrieve a token', async () => { + (auth0Client.getTokenSilently as unknown as MockInstance).mockReturnValue( + throwError({ error: 'login_required' }) + ); httpClient.request('get', 'https://my-api.com/api/calendar').subscribe({ error: (err) => expect(err).toEqual({ error: 'login_required' }), }); - flush(); await new Promise(process.nextTick); httpTestingController.expectNone('https://my-api.com/api/calendar'); - })); + }); - it('does execute HTTP call when not able to retrieve a token but allowAnonymous is set to true', fakeAsync(async ( - done: () => void - ) => { - ( - auth0Client.getTokenSilently as unknown as jest.SpyInstance - ).mockReturnValue(throwError({ error: 'login_required' })); + it('does execute HTTP call when not able to retrieve a token but allowAnonymous is set to true', async () => { + (auth0Client.getTokenSilently as unknown as MockInstance).mockReturnValue( + throwError({ error: 'login_required' }) + ); - await assertPassThruApiCallTo('https://my-api.com/api/orders', done); - })); + await assertPassThruApiCallTo('https://my-api.com/api/orders'); + }); - it('does execute HTTP call when missing_refresh_token but allowAnonymous is set to true', fakeAsync(async ( - done: () => void - ) => { - ( - auth0Client.getTokenSilently as unknown as jest.SpyInstance - ).mockReturnValue(throwError({ error: 'missing_refresh_token' })); + it('does execute HTTP call when missing_refresh_token but allowAnonymous is set to true', async () => { + (auth0Client.getTokenSilently as unknown as MockInstance).mockReturnValue( + throwError({ error: 'missing_refresh_token' }) + ); - await assertPassThruApiCallTo('https://my-api.com/api/orders', done); - })); + await assertPassThruApiCallTo('https://my-api.com/api/orders'); + }); - it('emit error when not able to retrieve a token but allowAnonymous is set to false', fakeAsync(async ( - done: () => void - ) => { + it('emit error when not able to retrieve a token but allowAnonymous is set to false', async () => { ( - auth0Client.getTokenSilently as unknown as jest.SpyInstance + auth0Client.getTokenSilently as unknown as MockInstance ).mockRejectedValue({ error: 'login_required' }); httpClient.request('get', 'https://my-api.com/api/calendar').subscribe({ error: (err) => expect(err).toEqual({ error: 'login_required' }), }); - flush(); await new Promise(process.nextTick); httpTestingController.expectNone('https://my-api.com/api/calendar'); expect(authState.setError).toHaveBeenCalled(); - })); + }); - it('does not emit error when not able to retrieve a token but allowAnonymous is set to true', fakeAsync(async () => { + it('does not emit error when not able to retrieve a token but allowAnonymous is set to true', async () => { ( - auth0Client.getTokenSilently as unknown as jest.SpyInstance + auth0Client.getTokenSilently as unknown as MockInstance ).mockRejectedValue({ error: 'login_required' }); - await assertPassThruApiCallTo('https://my-api.com/api/orders', () => { - expect(authState.setError).not.toHaveBeenCalled(); - }); - })); + await assertPassThruApiCallTo('https://my-api.com/api/orders'); + expect(authState.setError).not.toHaveBeenCalled(); + }); - it('does not emit error when missing_refresh_token but allowAnonymous is set to true', fakeAsync(async () => { + it('does not emit error when missing_refresh_token but allowAnonymous is set to true', async () => { ( - auth0Client.getTokenSilently as unknown as jest.SpyInstance + auth0Client.getTokenSilently as unknown as MockInstance ).mockRejectedValue({ error: 'missing_refresh_token' }); - await assertPassThruApiCallTo('https://my-api.com/api/orders', () => { - expect(authState.setError).not.toHaveBeenCalled(); - }); - })); + await assertPassThruApiCallTo('https://my-api.com/api/orders'); + expect(authState.setError).not.toHaveBeenCalled(); + }); - it('does execute HTTP call when interaction_required but allowAnonymous is set to true', fakeAsync(async ( - done: () => void - ) => { - ( - auth0Client.getTokenSilently as unknown as jest.SpyInstance - ).mockReturnValue(throwError({ error: 'interaction_required' })); + it('does execute HTTP call when interaction_required but allowAnonymous is set to true', async () => { + (auth0Client.getTokenSilently as unknown as MockInstance).mockReturnValue( + throwError({ error: 'interaction_required' }) + ); - await assertPassThruApiCallTo('https://my-api.com/api/orders', done); - })); + await assertPassThruApiCallTo('https://my-api.com/api/orders'); + }); - it('does execute HTTP call when getTokenSilently returns undefined but allowAnonymous is set to true', fakeAsync(async ( - done: () => void - ) => { + it('does execute HTTP call when getTokenSilently returns undefined but allowAnonymous is set to true', async () => { ( - auth0Client.getTokenSilently as unknown as jest.SpyInstance + auth0Client.getTokenSilently as unknown as MockInstance ).mockResolvedValue(undefined); - await assertPassThruApiCallTo('https://my-api.com/api/orders', done); - })); + await assertPassThruApiCallTo('https://my-api.com/api/orders'); + }); - it('does not emit error when interaction_required but allowAnonymous is set to true', fakeAsync(async () => { + it('does not emit error when interaction_required but allowAnonymous is set to true', async () => { ( - auth0Client.getTokenSilently as unknown as jest.SpyInstance + auth0Client.getTokenSilently as unknown as MockInstance ).mockRejectedValue({ error: 'interaction_required' }); - await assertPassThruApiCallTo('https://my-api.com/api/orders', () => { - expect(authState.setError).not.toHaveBeenCalled(); - }); - })); + await assertPassThruApiCallTo('https://my-api.com/api/orders'); + expect(authState.setError).not.toHaveBeenCalled(); + }); - it('does not emit error when getTokenSilently returns undefined but allowAnonymous is set to true', fakeAsync(async () => { + it('does not emit error when getTokenSilently returns undefined but allowAnonymous is set to true', async () => { ( - auth0Client.getTokenSilently as unknown as jest.SpyInstance + auth0Client.getTokenSilently as unknown as MockInstance ).mockResolvedValue(undefined); - await assertPassThruApiCallTo('https://my-api.com/api/orders', () => { - expect(authState.setError).not.toHaveBeenCalled(); - }); - })); + await assertPassThruApiCallTo('https://my-api.com/api/orders'); + expect(authState.setError).not.toHaveBeenCalled(); + }); - it('attach the access token when tokenOptions includes detailedResponse: true', fakeAsync(async ( - done: () => void - ) => { + it('attach the access token when tokenOptions includes detailedResponse: true', async () => { // Mock getTokenSilently to return a detailed response object ( - auth0Client.getTokenSilently as unknown as jest.SpyInstance + auth0Client.getTokenSilently as unknown as MockInstance ).mockResolvedValue({ access_token: 'detailed-access-token', id_token: 'id-token', @@ -455,8 +411,7 @@ describe('The Auth HTTP Interceptor', () => { }, }); - httpClient.get('https://my-api.com/api/detailed').subscribe(done); - flush(); + httpClient.get('https://my-api.com/api/detailed').subscribe(); await new Promise(process.nextTick); req = httpTestingController.expectOne('https://my-api.com/api/detailed'); @@ -464,11 +419,9 @@ describe('The Auth HTTP Interceptor', () => { expect(req.request.headers.get('Authorization')).toBe( 'Bearer detailed-access-token' ); - })); + }); - it('stores only the access token string when detailedResponse is used', fakeAsync(async ( - done: () => void - ) => { + it('stores only the access token string when detailedResponse is used', async () => { const detailedResponse = { access_token: 'detailed-token-123', id_token: 'id-token-456', @@ -478,10 +431,10 @@ describe('The Auth HTTP Interceptor', () => { }; ( - auth0Client.getTokenSilently as unknown as jest.SpyInstance + auth0Client.getTokenSilently as unknown as MockInstance ).mockResolvedValue(detailedResponse); - jest.spyOn(authState, 'setAccessToken'); + vi.spyOn(authState, 'setAccessToken'); config.httpInterceptor!.allowedList!.push({ uri: 'https://my-api.com/api/detailed-store', @@ -490,8 +443,7 @@ describe('The Auth HTTP Interceptor', () => { }, }); - httpClient.get('https://my-api.com/api/detailed-store').subscribe(done); - flush(); + httpClient.get('https://my-api.com/api/detailed-store').subscribe(); await new Promise(process.nextTick); req = httpTestingController.expectOne( 'https://my-api.com/api/detailed-store' @@ -501,17 +453,15 @@ describe('The Auth HTTP Interceptor', () => { expect(authState.setAccessToken).toHaveBeenCalledWith( 'detailed-token-123' ); - })); + }); - it('handles string token response when detailedResponse is false', fakeAsync(async ( - done: () => void - ) => { + it('handles string token response when detailedResponse is false', async () => { // Mock getTokenSilently to return a plain string token (default behavior) ( - auth0Client.getTokenSilently as unknown as jest.SpyInstance + auth0Client.getTokenSilently as unknown as MockInstance ).mockResolvedValue('simple-access-token'); - jest.spyOn(authState, 'setAccessToken'); + vi.spyOn(authState, 'setAccessToken'); config.httpInterceptor!.allowedList!.push({ uri: 'https://my-api.com/api/simple', @@ -520,8 +470,7 @@ describe('The Auth HTTP Interceptor', () => { }, }); - httpClient.get('https://my-api.com/api/simple').subscribe(done); - flush(); + httpClient.get('https://my-api.com/api/simple').subscribe(); await new Promise(process.nextTick); req = httpTestingController.expectOne('https://my-api.com/api/simple'); @@ -532,49 +481,35 @@ describe('The Auth HTTP Interceptor', () => { expect(authState.setAccessToken).toHaveBeenCalledWith( 'simple-access-token' ); - })); + }); }); describe('Requests that are configured using an uri matcher', () => { - it('waits unil isLoading emits false', fakeAsync(async ( - done: () => void - ) => { + it('waits unil isLoading emits false', async () => { const method = 'GET'; const url = 'https://my-api.com/api/orders'; isLoading$.next(true); - httpClient.request(method, url).subscribe(done); - flush(); + httpClient.request(method, url).subscribe(); + await new Promise(process.nextTick); httpTestingController.expectNone(url); isLoading$.next(false); - flush(); + await new Promise(process.nextTick); httpTestingController.expectOne(url); - })); + }); - it('attach the access token when the matcher returns true', fakeAsync(async ( - done: () => void - ) => { + it('attach the access token when the matcher returns true', async () => { // Testing { uriMatcher: (uri) => uri.indexOf('/api/contact') !== -1 } - await assertAuthorizedApiCallTo( - 'https://my-api.com/api/contact', - done, - 'post' - ); - })); + await assertAuthorizedApiCallTo('https://my-api.com/api/contact', 'post'); + }); - it('pass through the route options to getTokenSilently, without additional properties', fakeAsync(async ( - done: () => void - ) => { + it('pass through the route options to getTokenSilently, without additional properties', async () => { // Testing { uriMatcher: (uri) => uri.indexOf('/api/contact') !== -1 } - await assertAuthorizedApiCallTo( - 'https://my-api.com/api/contact', - done, - 'post' - ); + await assertAuthorizedApiCallTo('https://my-api.com/api/contact', 'post'); expect(auth0Client.getTokenSilently).toHaveBeenCalledWith({ authorizationParams: { @@ -582,24 +517,16 @@ describe('The Auth HTTP Interceptor', () => { scope: 'scope', }, }); - })); + }); - it('does attach the access token when the HTTP method does match', fakeAsync(async ( - done: () => void - ) => { + it('does attach the access token when the HTTP method does match', async () => { // Testing { uriMatcher: (uri) => uri.indexOf('/api/contact') !== -1 } - await assertAuthorizedApiCallTo( - 'https://my-api.com/api/contact', - done, - 'post' - ); - })); + await assertAuthorizedApiCallTo('https://my-api.com/api/contact', 'post'); + }); - it('does not attach the access token when the HTTP method does not match', fakeAsync(async ( - done: () => void - ) => { + it('does not attach the access token when the HTTP method does not match', async () => { // Testing { uriMatcher: (uri) => uri.indexOf('/api/contact') !== -1 } - await assertPassThruApiCallTo('https://my-api.com/api/contact', done); - })); + await assertPassThruApiCallTo('https://my-api.com/api/contact'); + }); }); }); diff --git a/projects/auth0-angular/src/lib/auth.service.spec.ts b/projects/auth0-angular/src/lib/auth.service.spec.ts index 01c34383..2732b86a 100644 --- a/projects/auth0-angular/src/lib/auth.service.spec.ts +++ b/projects/auth0-angular/src/lib/auth.service.spec.ts @@ -1,4 +1,9 @@ -import { fakeAsync, TestBed } from '@angular/core/testing'; +// Each spec file runs in its own jsdom window; zone.js from test-setup.ts +// patched a different window, so it must be imported here too. +import 'zone.js'; +import 'zone.js/testing'; +import '@angular/compiler'; +import { TestBed } from '@angular/core/testing'; import { AuthService } from './auth.service'; import { Auth0ClientService } from './auth.client'; import { @@ -9,6 +14,7 @@ import { RefreshTokenMode, } from '@auth0/auth0-spa-js'; import { AbstractNavigator } from './abstract-navigator'; +import { Observable } from 'rxjs'; import { bufferCount, bufferTime, @@ -20,6 +26,41 @@ import { } from 'rxjs/operators'; import { AuthConfig, AuthConfigService } from './auth.config'; import { AuthState } from './auth.state'; +import type { MockInstance, Mock } from 'vitest'; +import { + BrowserTestingModule, + platformBrowserTesting, +} from '@angular/platform-browser/testing'; + +// RxJS 6 polyfill for firstValueFrom (added in RxJS 7). +// Rejects with EmptyError on empty completion to match RxJS 7 semantics. +const firstValueFrom = (obs: Observable): Promise => + new Promise((resolve, reject) => { + let hasValue = false; + obs.pipe(take(1)).subscribe({ + next: (value) => { + hasValue = true; + resolve(value); + }, + error: reject, + complete: () => { + if (!hasValue) { + reject( + Object.assign(new Error('No elements in sequence'), { + name: 'EmptyError', + }) + ); + } + }, + }); + }); + +// The forks pool loads setupFiles in a separate module-cache context from spec +// files, so initTestEnvironment called in test-setup.ts targets a different +// TestBed instance. Initialize the environment here in the spec's own context. +beforeAll(() => { + TestBed.initTestEnvironment(BrowserTestingModule, platformBrowserTesting()); +}); const mockWindow = global as any; @@ -35,11 +76,6 @@ Object.defineProperty(mockWindow, 'crypto', { writable: false, }); -const assignMock = jest.fn(); - -delete mockWindow.location; -mockWindow.location = { assign: assignMock }; - /** * Wraps service.isLoading$ so that assertions can be made * only when the SDK has finished loading. @@ -64,104 +100,102 @@ describe('AuthService', () => { clientId: '', }); - jest.spyOn(auth0Client, 'handleRedirectCallback').mockResolvedValue({ + vi.spyOn(auth0Client, 'handleRedirectCallback').mockResolvedValue({ appState: undefined, response_type: ResponseType.Code, } as any); - jest.spyOn(auth0Client, 'loginWithRedirect').mockResolvedValue(); - jest.spyOn(auth0Client, 'connectAccountWithRedirect').mockResolvedValue(); - jest.spyOn(auth0Client, 'loginWithPopup').mockResolvedValue(); - jest.spyOn(auth0Client, 'checkSession').mockResolvedValue(); - jest.spyOn(auth0Client, 'isAuthenticated').mockResolvedValue(false); - jest.spyOn(auth0Client, 'getUser').mockResolvedValue(undefined); - jest.spyOn(auth0Client, 'getIdTokenClaims').mockResolvedValue(undefined); - jest.spyOn(auth0Client, 'logout'); - jest - .spyOn(auth0Client, 'getTokenSilently') - .mockResolvedValue('__access_token__'); - - jest - .spyOn(auth0Client, 'getTokenWithPopup') - .mockResolvedValue('__access_token_from_popup__'); - - jest.spyOn(auth0Client, 'loginWithCustomTokenExchange').mockResolvedValue({ + vi.spyOn(auth0Client, 'loginWithRedirect').mockResolvedValue(); + vi.spyOn(auth0Client, 'connectAccountWithRedirect').mockResolvedValue(); + vi.spyOn(auth0Client, 'loginWithPopup').mockResolvedValue(); + vi.spyOn(auth0Client, 'checkSession').mockResolvedValue(); + vi.spyOn(auth0Client, 'isAuthenticated').mockResolvedValue(false); + vi.spyOn(auth0Client, 'getUser').mockResolvedValue(undefined); + vi.spyOn(auth0Client, 'getIdTokenClaims').mockResolvedValue(undefined); + vi.spyOn(auth0Client, 'logout'); + vi.spyOn(auth0Client, 'getTokenSilently').mockResolvedValue( + '__access_token__' + ); + + vi.spyOn(auth0Client, 'getTokenWithPopup').mockResolvedValue( + '__access_token_from_popup__' + ); + + vi.spyOn(auth0Client, 'loginWithCustomTokenExchange').mockResolvedValue({ access_token: '__exchanged_access_token__', id_token: '__exchanged_id_token__', token_type: 'Bearer', expires_in: 86400, }); - jest - .spyOn(auth0Client, 'getDpopNonce') - .mockResolvedValue('test-nonce-value'); - jest.spyOn(auth0Client, 'setDpopNonce').mockResolvedValue(undefined); - jest - .spyOn(auth0Client, 'generateDpopProof') - .mockResolvedValue('test-proof-jwt'); - jest.spyOn(auth0Client, 'createFetcher').mockReturnValue({ - fetch: jest.fn(), + vi.spyOn(auth0Client, 'getDpopNonce').mockResolvedValue('test-nonce-value'); + vi.spyOn(auth0Client, 'setDpopNonce').mockResolvedValue(undefined); + vi.spyOn(auth0Client, 'generateDpopProof').mockResolvedValue( + 'test-proof-jwt' + ); + vi.spyOn(auth0Client, 'createFetcher').mockReturnValue({ + fetch: vi.fn(), } as any); - jest - .spyOn(auth0Client.mfa, 'getAuthenticators') - .mockResolvedValue([ - { id: 'auth-1', authenticatorType: 'otp', active: true }, - ]); - jest.spyOn(auth0Client.mfa, 'enroll').mockResolvedValue({ + vi.spyOn(auth0Client.mfa, 'getAuthenticators').mockResolvedValue([ + { id: 'auth-1', authenticatorType: 'otp', active: true }, + ]); + vi.spyOn(auth0Client.mfa, 'enroll').mockResolvedValue({ authenticatorType: 'otp', secret: '__totp_secret__', barcodeUri: '__barcode_uri__', }); - jest.spyOn(auth0Client.mfa, 'challenge').mockResolvedValue({ + vi.spyOn(auth0Client.mfa, 'challenge').mockResolvedValue({ challengeType: 'otp', }); - jest - .spyOn(auth0Client.mfa, 'getEnrollmentFactors') - .mockResolvedValue([{ type: 'otp' }]); - jest.spyOn(auth0Client.mfa, 'verify').mockResolvedValue({ + vi.spyOn(auth0Client.mfa, 'getEnrollmentFactors').mockResolvedValue([ + { type: 'otp' }, + ]); + vi.spyOn(auth0Client.mfa, 'verify').mockResolvedValue({ access_token: '__mfa_access_token__', id_token: '__mfa_id_token__', token_type: 'Bearer', expires_in: 86400, }); - jest.spyOn(auth0Client.passkey, 'signup').mockResolvedValue({ + vi.spyOn(auth0Client.passkey, 'signup').mockResolvedValue({ access_token: '__passkey_access_token__', id_token: '__passkey_id_token__', token_type: 'Bearer', expires_in: 86400, }); - jest.spyOn(auth0Client.passkey, 'login').mockResolvedValue({ + vi.spyOn(auth0Client.passkey, 'login').mockResolvedValue({ access_token: '__passkey_access_token__', id_token: '__passkey_id_token__', token_type: 'Bearer', expires_in: 86400, }); - jest.spyOn(auth0Client.myAccount, 'getFactors').mockResolvedValue([]); - jest - .spyOn(auth0Client.myAccount, 'getAuthenticationMethods') - .mockResolvedValue([]); - jest - .spyOn(auth0Client.myAccount, 'getAuthenticationMethod') - .mockResolvedValue({ id: '__method_id__' } as any); - jest - .spyOn(auth0Client.myAccount, 'deleteAuthenticationMethod') - .mockResolvedValue(); - jest - .spyOn(auth0Client.myAccount, 'updateAuthenticationMethod') - .mockResolvedValue({ id: '__method_id__' } as any); - jest - .spyOn(auth0Client.myAccount, 'enrollmentChallenge') - .mockResolvedValue({ - id: '__challenge_id__', - location: 'https://example.auth0.com/enroll', - auth_session: '__auth_session__', - type: 'totp', - } as any); - jest - .spyOn(auth0Client.myAccount, 'enrollmentVerify') - .mockResolvedValue({ id: '__method_id__' } as any); + vi.spyOn(auth0Client.myAccount, 'getFactors').mockResolvedValue([]); + vi.spyOn( + auth0Client.myAccount, + 'getAuthenticationMethods' + ).mockResolvedValue([]); + vi.spyOn( + auth0Client.myAccount, + 'getAuthenticationMethod' + ).mockResolvedValue({ id: '__method_id__' } as any); + vi.spyOn( + auth0Client.myAccount, + 'deleteAuthenticationMethod' + ).mockResolvedValue(); + vi.spyOn( + auth0Client.myAccount, + 'updateAuthenticationMethod' + ).mockResolvedValue({ id: '__method_id__' } as any); + vi.spyOn(auth0Client.myAccount, 'enrollmentChallenge').mockResolvedValue({ + id: '__challenge_id__', + location: 'https://example.auth0.com/enroll', + auth_session: '__auth_session__', + type: 'totp', + } as any); + vi.spyOn(auth0Client.myAccount, 'enrollmentVerify').mockResolvedValue({ + id: '__method_id__', + } as any); window.history.replaceState(null, '', ''); @@ -183,9 +217,7 @@ describe('AuthService', () => { authState = TestBed.inject(AuthState); }); - afterEach(() => { - assignMock.mockClear(); - }); + afterEach(() => {}); describe('constructor', () => { it('should be created', () => { @@ -203,246 +235,205 @@ describe('AuthService', () => { expect(auth0Client.checkSession).toHaveBeenCalled(); }); - it('should set isLoading$ in the correct sequence', (done) => { - const values: boolean[] = []; + it('should set isLoading$ in the correct sequence', async () => { const service = createService(); - - service.isLoading$.subscribe((loading) => { - values.push(loading); - - if (!loading) { - expect(values).toEqual([true, false]); - done(); - } - }); + const values = await firstValueFrom( + service.isLoading$.pipe(bufferCount(2)) + ); + expect(values).toEqual([true, false]); }); - it('should not set isLoading when service destroyed before checkSession finished', (done) => { - ( - auth0Client.checkSession as unknown as jest.SpyInstance - ).mockImplementation( + it('should not set isLoading when service destroyed before checkSession finished', async () => { + (auth0Client.checkSession as unknown as MockInstance).mockImplementation( () => new Promise((resolve) => setTimeout(resolve, 5000)) ); const localService = createService(); - - localService.isLoading$.pipe(bufferTime(500)).subscribe((loading) => { - expect(loading.length).toEqual(1); - expect(loading).toEqual([true]); - done(); - }); - + const pending = firstValueFrom( + localService.isLoading$.pipe(bufferTime(500)) + ); localService.ngOnDestroy(); + const loading = await pending; + expect(loading.length).toEqual(1); + expect(loading).toEqual([true]); }); }); describe('The `isAuthenticated` observable', () => { - it('should return `false` when the client is not authenticated', (done) => { + it('should return `false` when the client is not authenticated', async () => { const service = createService(); - service.isAuthenticated$.subscribe((value) => { - expect(value).toBe(false); - done(); - }); + const value = await firstValueFrom(service.isAuthenticated$); + expect(value).toBe(false); }); - it('should return `true` when the client is authenticated', (done) => { + it('should return `true` when the client is authenticated', async () => { ( - auth0Client.isAuthenticated as unknown as jest.SpyInstance + auth0Client.isAuthenticated as unknown as MockInstance ).mockResolvedValue(true); const service = createService(); - - loaded(service).subscribe(() => { - service.isAuthenticated$.subscribe((value) => { - expect(value).toBe(true); - done(); - }); - }); + await firstValueFrom(loaded(service)); + const value = await firstValueFrom(service.isAuthenticated$); + expect(value).toBe(true); }); - it('should return true after successfully getting a new token', (done) => { + it('should return true after successfully getting a new token', async () => { ( - auth0Client.isAuthenticated as unknown as jest.SpyInstance + auth0Client.isAuthenticated as unknown as MockInstance ).mockResolvedValue(false); const service = createService(); - service.isAuthenticated$.pipe(bufferCount(2)).subscribe((values) => { - expect(values[0]).toBe(false); - expect(values[1]).toBe(true); - done(); - }); + const pending = firstValueFrom( + service.isAuthenticated$.pipe(bufferCount(2)) + ); // Add a small delay before triggering a new emit to the isAuthenticated$. // This ensures we can capture both emits using the above bufferCount(2) setTimeout(() => { ( - auth0Client.getTokenSilently as unknown as jest.SpyInstance + auth0Client.getTokenSilently as unknown as MockInstance ).mockResolvedValue({}); ( - auth0Client.isAuthenticated as unknown as jest.SpyInstance + auth0Client.isAuthenticated as unknown as MockInstance ).mockResolvedValue(true); service.getAccessTokenSilently().subscribe(); }, 0); + + const values = await pending; + expect(values[0]).toBe(false); + expect(values[1]).toBe(true); }); - it('should still return true when the token is expired', fakeAsync(( - done: any - ) => { + it('should still return true when the token is expired', async () => { authState.setIsLoading(false); ( - auth0Client.isAuthenticated as unknown as jest.SpyInstance + auth0Client.isAuthenticated as unknown as MockInstance ).mockResolvedValue(true); const service = createService(); - service.isAuthenticated$.pipe(take(1)).subscribe((value) => { - expect(value).toBe(true); - }); + const value1 = await firstValueFrom(service.isAuthenticated$); + expect(value1).toBe(true); // When the token is expired, auth0Client.isAuthenticated is resolving to false. // This is unexpected but known behavior in Auth0-SPA-JS, so we shouldnt rely on it apart from initially. // Once this is resolved, we should be able to rely on `auth0Client.isAuthenticated`, even when the Access Token is expired. ( - auth0Client.isAuthenticated as unknown as jest.SpyInstance + auth0Client.isAuthenticated as unknown as MockInstance ).mockResolvedValue(false); - service.isAuthenticated$.pipe(take(1)).subscribe((value) => { - expect(value).toBe(true); - }); - })); + // shareReplay(1) returns the cached value; the service does not re-query isAuthenticated + const value2 = await firstValueFrom(service.isAuthenticated$); + expect(value2).toBe(true); + }); }); describe('The `user` observable', () => { - it('should get the user if authenticated', (done) => { + it('should get the user if authenticated', async () => { const user = { name: 'Test User', }; ( - auth0Client.isAuthenticated as unknown as jest.SpyInstance + auth0Client.isAuthenticated as unknown as MockInstance ).mockResolvedValue(true); - (auth0Client.getUser as unknown as jest.SpyInstance).mockResolvedValue( - user - ); + (auth0Client.getUser as unknown as MockInstance).mockResolvedValue(user); const service = createService(); - service.user$.subscribe((value) => { - expect(value).toBe(user); - done(); - }); + const value = await firstValueFrom(service.user$); + expect(value).toBe(user); }); - it('should update the user after successfully getting a new token', (done) => { - const user = { - name: 'Test User', - }; - - const user2 = { - name: 'Another User', - }; + it('should update the user after successfully getting a new token', async () => { + const user = { name: 'Test User' }; + const user2 = { name: 'Another User' }; ( - auth0Client.isAuthenticated as unknown as jest.SpyInstance + auth0Client.isAuthenticated as unknown as MockInstance ).mockResolvedValue(true); - (auth0Client.getUser as unknown as jest.SpyInstance).mockResolvedValue( - user - ); + (auth0Client.getUser as unknown as MockInstance).mockResolvedValue(user); const service = createService(); - service.user$.pipe(bufferCount(2)).subscribe((values) => { - expect(values[0]).toBe(user); - expect(values[1]).toBe(user2); - done(); - }); + const pending = firstValueFrom(service.user$.pipe(bufferCount(2))); // Add a small delay before triggering a new emit to the user$. // This ensures we can capture both emits using the above bufferCount(2) setTimeout(() => { ( - auth0Client.getTokenSilently as unknown as jest.SpyInstance + auth0Client.getTokenSilently as unknown as MockInstance ).mockResolvedValue({}); - (auth0Client.getUser as unknown as jest.SpyInstance).mockResolvedValue( + (auth0Client.getUser as unknown as MockInstance).mockResolvedValue( user2 ); - service.getAccessTokenSilently().subscribe(); }, 0); + + const values = await pending; + expect(values[0]).toBe(user); + expect(values[1]).toBe(user2); }); - it('should return null when logged out', (done) => { - const user = { - name: 'Test User', - }; + it('should return null when logged out', async () => { + const user = { name: 'Test User' }; ( - auth0Client.isAuthenticated as unknown as jest.SpyInstance + auth0Client.isAuthenticated as unknown as MockInstance ).mockResolvedValue(true); - (auth0Client.getUser as unknown as jest.SpyInstance).mockResolvedValue( - user - ); + (auth0Client.getUser as unknown as MockInstance).mockResolvedValue(user); const service = createService(); - service.user$.pipe(bufferCount(2)).subscribe((values) => { - expect(values[0]).toBe(user); - expect(values[1]).toBe(null); - done(); - }); + const pending = firstValueFrom(service.user$.pipe(bufferCount(2))); service.isAuthenticated$.pipe(filter(Boolean)).subscribe(() => { ( - auth0Client.isAuthenticated as unknown as jest.SpyInstance + auth0Client.isAuthenticated as unknown as MockInstance ).mockResolvedValue(false); - service.logout({ - openUrl: false, - }); + service.logout({ openUrl: false }); }); + + const values = await pending; + expect(values[0]).toBe(user); + expect(values[1]).toBe(null); }); - it('should not emit when state is updated that doesnt change the user', (done) => { - const user = { - name: 'Test User', - }; + it('should not emit when state is updated that doesnt change the user', async () => { + const user = { name: 'Test User' }; ( - auth0Client.isAuthenticated as unknown as jest.SpyInstance + auth0Client.isAuthenticated as unknown as MockInstance ).mockResolvedValue(true); ( - auth0Client.getTokenSilently as unknown as jest.SpyInstance + auth0Client.getTokenSilently as unknown as MockInstance ).mockResolvedValue('AT1'); - (auth0Client.getUser as unknown as jest.SpyInstance).mockResolvedValue( - user - ); + (auth0Client.getUser as unknown as MockInstance).mockResolvedValue(user); let userEmissions = 0; const service = createService(); service.user$.subscribe(() => userEmissions++); - service - .getAccessTokenSilently() - .pipe( + await firstValueFrom( + service.getAccessTokenSilently().pipe( tap(() => ( - auth0Client.getTokenSilently as unknown as jest.SpyInstance + auth0Client.getTokenSilently as unknown as MockInstance ).mockResolvedValue('AT2') ), mergeMap(() => service.getAccessTokenSilently()), tap(() => ( - auth0Client.getTokenSilently as unknown as jest.SpyInstance + auth0Client.getTokenSilently as unknown as MockInstance ).mockResolvedValue('AT3') ), mergeMap(() => service.getAccessTokenSilently()), // Allow user emissions to come through delay(0) ) - .subscribe(() => { - expect(userEmissions).toBe(1); - done(); - }); + ); + expect(userEmissions).toBe(1); }); }); describe('The `idTokenClaims` observable', () => { - it('should get the ID token claims if authenticated', (done) => { + it('should get the ID token claims if authenticated', async () => { const claims: IdToken = { __raw: 'idToken', exp: 1602887231, @@ -451,20 +442,18 @@ describe('AuthService', () => { }; ( - auth0Client.isAuthenticated as unknown as jest.SpyInstance + auth0Client.isAuthenticated as unknown as MockInstance ).mockResolvedValue(true); ( - auth0Client.getIdTokenClaims as unknown as jest.SpyInstance + auth0Client.getIdTokenClaims as unknown as MockInstance ).mockResolvedValue(claims); const service = createService(); - service.idTokenClaims$.subscribe((value) => { - expect(value).toBe(claims); - done(); - }); + const value = await firstValueFrom(service.idTokenClaims$); + expect(value).toBe(claims); }); - it('should update the ID token claims if a new token is requested', (done) => { + it('should update the ID token claims if a new token is requested', async () => { const claims: IdToken = { __raw: 'idToken', exp: 1602887231, @@ -480,34 +469,35 @@ describe('AuthService', () => { }; ( - auth0Client.isAuthenticated as unknown as jest.SpyInstance + auth0Client.isAuthenticated as unknown as MockInstance ).mockResolvedValue(true); ( - auth0Client.getIdTokenClaims as unknown as jest.SpyInstance + auth0Client.getIdTokenClaims as unknown as MockInstance ).mockResolvedValue(claims); const service = createService(); - service.idTokenClaims$.pipe(bufferCount(2)).subscribe((values) => { - expect(values[0]).toBe(claims); - expect(values[1]).toBe(claims2); - done(); - }); + const pending = firstValueFrom( + service.idTokenClaims$.pipe(bufferCount(2)) + ); // Add a small delay before triggering a new emit to the idTokenClaims$. // This ensures we can capture both emits using the above bufferCount(2) setTimeout(() => { ( - auth0Client.getTokenSilently as unknown as jest.SpyInstance + auth0Client.getTokenSilently as unknown as MockInstance ).mockResolvedValue({}); ( - auth0Client.getIdTokenClaims as unknown as jest.SpyInstance + auth0Client.getIdTokenClaims as unknown as MockInstance ).mockResolvedValue(claims2); - service.getAccessTokenSilently().subscribe(); }, 0); + + const values = await pending; + expect(values[0]).toBe(claims); + expect(values[1]).toBe(claims2); }); - it('should return null when logged out', (done) => { + it('should return null when logged out', async () => { const claims: IdToken = { __raw: 'idToken', exp: 1602887231, @@ -516,27 +506,27 @@ describe('AuthService', () => { }; ( - auth0Client.isAuthenticated as unknown as jest.SpyInstance + auth0Client.isAuthenticated as unknown as MockInstance ).mockResolvedValue(true); ( - auth0Client.getIdTokenClaims as unknown as jest.SpyInstance + auth0Client.getIdTokenClaims as unknown as MockInstance ).mockResolvedValue(claims); const service = createService(); - service.idTokenClaims$.pipe(bufferCount(2)).subscribe((values) => { - expect(values[0]).toBe(claims); - expect(values[1]).toBe(null); - done(); - }); + const pending = firstValueFrom( + service.idTokenClaims$.pipe(bufferCount(2)) + ); service.isAuthenticated$.pipe(filter(Boolean)).subscribe(() => { ( - auth0Client.isAuthenticated as unknown as jest.SpyInstance + auth0Client.isAuthenticated as unknown as MockInstance ).mockResolvedValue(false); - service.logout({ - openUrl: false, - }); + service.logout({ openUrl: false }); }); + + const values = await pending; + expect(values[0]).toBe(claims); + expect(values[1]).toBe(null); }); }); @@ -547,7 +537,7 @@ describe('AuthService', () => { TestBed.resetTestingModule(); navigator = { - navigateByUrl: jest.fn().mockResolvedValue(true), + navigateByUrl: vi.fn().mockResolvedValue(true), } as any; TestBed.configureTestingModule({ @@ -571,171 +561,116 @@ describe('AuthService', () => { window.history.replaceState(null, '', '?code=123&state=456'); }); - it('should handle the callback when code and state are available', (done) => { + it('should handle the callback when code and state are available', async () => { mockWindow.location.search = '?code=123&state=456'; const localService = createService(); - - loaded(localService).subscribe(() => { - expect(auth0Client.handleRedirectCallback).toHaveBeenCalledTimes(1); - done(); - }); + await firstValueFrom(loaded(localService)); + expect(auth0Client.handleRedirectCallback).toHaveBeenCalledTimes(1); }); - it('should handle the callback when connect_code and state are available', (done) => { + it('should handle the callback when connect_code and state are available', async () => { mockWindow.location.search = '?connect_code=123&state=456'; const localService = createService(); - - loaded(localService).subscribe(() => { - expect(auth0Client.handleRedirectCallback).toHaveBeenCalledTimes(1); - done(); - }); + await firstValueFrom(loaded(localService)); + expect(auth0Client.handleRedirectCallback).toHaveBeenCalledTimes(1); }); - it('should not handle the callback when skipRedirectCallback is true', (done) => { + it('should not handle the callback when skipRedirectCallback is true', async () => { mockWindow.location.search = '?code=123&state=456'; authConfig.skipRedirectCallback = true; - const localService = createService(); - - loaded(localService).subscribe(() => { - expect(auth0Client.handleRedirectCallback).not.toHaveBeenCalled(); - done(); - }); + await firstValueFrom(loaded(localService)); + expect(auth0Client.handleRedirectCallback).not.toHaveBeenCalled(); }); - it('should redirect to the correct route', (done) => { + it('should redirect to the correct route', async () => { const localService = createService(); - - loaded(localService).subscribe(() => { - expect(navigator.navigateByUrl).toHaveBeenCalledWith('/'); - done(); - }); + await firstValueFrom(loaded(localService)); + expect(navigator.navigateByUrl).toHaveBeenCalledWith('/'); }); - it('should redirect to the route specified in appState', (done) => { + it('should redirect to the route specified in appState', async () => { ( - auth0Client.handleRedirectCallback as unknown as jest.SpyInstance + auth0Client.handleRedirectCallback as unknown as MockInstance ).mockResolvedValue({ - appState: { - target: '/test-route', - }, + appState: { target: '/test-route' }, }); - const localService = createService(); - - loaded(localService).subscribe(() => { - expect(navigator.navigateByUrl).toHaveBeenCalledWith('/test-route'); - done(); - }); + await firstValueFrom(loaded(localService)); + expect(navigator.navigateByUrl).toHaveBeenCalledWith('/test-route'); }); - it('should fallback to `/` when missing appState', (done) => { + it('should fallback to `/` when missing appState', async () => { ( - auth0Client.handleRedirectCallback as unknown as jest.SpyInstance + auth0Client.handleRedirectCallback as unknown as MockInstance ).mockResolvedValue({}); - const localService = createService(); - - loaded(localService).subscribe(() => { - expect(navigator.navigateByUrl).toHaveBeenCalledWith('/'); - done(); - }); + await firstValueFrom(loaded(localService)); + expect(navigator.navigateByUrl).toHaveBeenCalledWith('/'); }); - it('should fallback to `/` when handleRedirectCallback returns undefined', (done) => { + it('should fallback to `/` when handleRedirectCallback returns undefined', async () => { ( - auth0Client.handleRedirectCallback as unknown as jest.SpyInstance + auth0Client.handleRedirectCallback as unknown as MockInstance ).mockResolvedValue(undefined); - const localService = createService(); - - loaded(localService).subscribe(() => { - expect(navigator.navigateByUrl).toHaveBeenCalledWith('/'); - done(); - }); + await firstValueFrom(loaded(localService)); + expect(navigator.navigateByUrl).toHaveBeenCalledWith('/'); }); - it('should record the appState in the appState$ observable if it is present', (done) => { - const appState = { - myValue: 'State to Preserve', - }; - + it('should record the appState in the appState$ observable if it is present', async () => { + const appState = { myValue: 'State to Preserve' }; ( - auth0Client.handleRedirectCallback as unknown as jest.SpyInstance - ).mockResolvedValue({ - appState, - }); - + auth0Client.handleRedirectCallback as unknown as MockInstance + ).mockResolvedValue({ appState }); const localService = createService(); - - localService.appState$.subscribe((recievedState) => { - expect(recievedState).toEqual(appState); - done(); - }); + const recievedState = await firstValueFrom(localService.appState$); + expect(recievedState).toEqual(appState); }); - it('should record errors in the error$ observable', (done) => { + it('should record errors in the error$ observable', async () => { const errorObj = new Error('An error has occured'); - ( - auth0Client.handleRedirectCallback as unknown as jest.SpyInstance + auth0Client.handleRedirectCallback as unknown as MockInstance ).mockImplementation(() => { throw errorObj; }); - const localService = createService(); - - loaded(localService).subscribe(() => { - localService.error$.subscribe((err: Error) => { - expect(err).toBe(errorObj); - expect(navigator.navigateByUrl).toHaveBeenCalledWith('/'); - done(); - }); - }); + await firstValueFrom(loaded(localService)); + const err = await firstValueFrom(localService.error$); + expect(err).toBe(errorObj); + expect(navigator.navigateByUrl).toHaveBeenCalledWith('/'); }); - it('should redirect to errorPath when an error occured in handleRedirectCallback', (done) => { + it('should redirect to errorPath when an error occured in handleRedirectCallback', async () => { const errorObj = new Error('An error has occured'); - authConfig.errorPath = '/error'; ( - auth0Client.handleRedirectCallback as unknown as jest.SpyInstance + auth0Client.handleRedirectCallback as unknown as MockInstance ).mockImplementation(() => { throw errorObj; }); - const localService = createService(); - - loaded(localService).subscribe(() => { - expect(navigator.navigateByUrl).toHaveBeenCalledWith('/error'); - done(); - }); + await firstValueFrom(loaded(localService)); + expect(navigator.navigateByUrl).toHaveBeenCalledWith('/error'); }); - it('should process the callback when an error appears in the query string', (done) => { + it('should process the callback when an error appears in the query string', async () => { window.history.replaceState( null, '', `?error=${encodeURIComponent('This is an error')}&state=456` ); - const localService = createService(); - - loaded(localService).subscribe(() => { - expect(auth0Client.handleRedirectCallback).toHaveBeenCalled(); - done(); - }); + await firstValueFrom(loaded(localService)); + expect(auth0Client.handleRedirectCallback).toHaveBeenCalled(); }); - it('should not process the callback when query string is a sub string', (done) => { + it('should not process the callback when query string is a sub string', async () => { window.history.replaceState(null, '', '?abccode=123&xyzstate=456'); - const localService = createService(); - - loaded(localService).subscribe(() => { - expect(auth0Client.handleRedirectCallback).not.toHaveBeenCalled(); - done(); - }); + await firstValueFrom(loaded(localService)); + expect(auth0Client.handleRedirectCallback).not.toHaveBeenCalled(); }); }); @@ -780,51 +715,33 @@ describe('AuthService', () => { ); }); - it('should call `loginWithPopup`', (done) => { + it('should call `loginWithPopup`', async () => { const service = createService(); - loaded(service).subscribe(() => { - (auth0Client.isAuthenticated as unknown as jest.SpyInstance).mockReset(); - ( - auth0Client.isAuthenticated as unknown as jest.SpyInstance - ).mockResolvedValue(true); - - service.loginWithPopup(); - - service.isAuthenticated$.subscribe((authenticated) => { - if (authenticated) { - expect(auth0Client.loginWithPopup).toHaveBeenCalled(); - expect(auth0Client.isAuthenticated).toHaveBeenCalled(); - done(); - } - }); - }); + await firstValueFrom(loaded(service)); + (auth0Client.isAuthenticated as unknown as MockInstance).mockReset(); + (auth0Client.isAuthenticated as unknown as MockInstance).mockResolvedValue( + true + ); + service.loginWithPopup(); + await firstValueFrom(service.isAuthenticated$.pipe(filter(Boolean))); + expect(auth0Client.loginWithPopup).toHaveBeenCalled(); + expect(auth0Client.isAuthenticated).toHaveBeenCalled(); }); - it('should call `loginWithPopup` with options', (done) => { + it('should call `loginWithPopup` with options', async () => { // These objects are empty, as we just want to check that the // same object reference was passed through than any specific options. const options = {}; const config = {}; const service = createService(); - - loaded(service).subscribe(() => { - (auth0Client.isAuthenticated as unknown as jest.SpyInstance).mockReset(); - ( - auth0Client.isAuthenticated as unknown as jest.SpyInstance - ).mockResolvedValue(true); - - service.loginWithPopup(options, config); - - service.isAuthenticated$.subscribe((authenticated) => { - if (authenticated) { - expect(auth0Client.loginWithPopup).toHaveBeenCalledWith( - options, - config - ); - done(); - } - }); - }); + await firstValueFrom(loaded(service)); + (auth0Client.isAuthenticated as unknown as MockInstance).mockReset(); + (auth0Client.isAuthenticated as unknown as MockInstance).mockResolvedValue( + true + ); + service.loginWithPopup(options, config); + await firstValueFrom(service.isAuthenticated$.pipe(filter(Boolean))); + expect(auth0Client.loginWithPopup).toHaveBeenCalledWith(options, config); }); it('should call `logout`', () => { @@ -840,318 +757,250 @@ describe('AuthService', () => { expect(auth0Client.logout).toHaveBeenCalledWith(options); }); - it('should reset the authentication state when passing `localOnly` to logout', (done) => { + it('should reset the authentication state when passing `localOnly` to logout', async () => { const options = { openUrl: async () => { ( - auth0Client.isAuthenticated as unknown as jest.SpyInstance + auth0Client.isAuthenticated as unknown as MockInstance ).mockResolvedValue(false); }, }; - ( - auth0Client.isAuthenticated as unknown as jest.SpyInstance - ).mockResolvedValue(true); + (auth0Client.isAuthenticated as unknown as MockInstance).mockResolvedValue( + true + ); const service = createService(); + await firstValueFrom(loaded(service)); - loaded(service).subscribe(() => { - service.isAuthenticated$.pipe(bufferCount(2)).subscribe((values) => { - expect(values[0]).toBe(true); - expect(values[1]).toBe(false); - done(); - }); - - service.logout(options); - }); + const pending = firstValueFrom( + service.isAuthenticated$.pipe(bufferCount(2)) + ); + service.logout(options); + const values = await pending; + expect(values[0]).toBe(true); + expect(values[1]).toBe(false); }); describe('Online Access (Online Refresh Tokens)', () => { - it('clears isAuthenticated$ and user$ after logout with an Online Refresh Token', (done) => { + it('clears isAuthenticated$ and user$ after logout with an Online Refresh Token', async () => { authConfig.useRefreshTokens = true; authConfig.refreshTokenMode = RefreshTokenMode.Online; authConfig.useDpop = true; - const user = { - name: 'Test User', - }; + const user = { name: 'Test User' }; ( - auth0Client.isAuthenticated as unknown as jest.SpyInstance + auth0Client.isAuthenticated as unknown as MockInstance ).mockResolvedValue(true); - (auth0Client.getUser as unknown as jest.SpyInstance).mockResolvedValue( - user - ); + (auth0Client.getUser as unknown as MockInstance).mockResolvedValue(user); const service = createService(); - service.user$.pipe(bufferCount(2)).subscribe((values) => { - expect(values[0]).toBe(user); - expect(values[1]).toBe(null); - }); - - service.isAuthenticated$.pipe(bufferCount(2)).subscribe((values) => { - expect(values[0]).toBe(true); - expect(values[1]).toBe(false); - done(); - }); + const userPending = firstValueFrom(service.user$.pipe(bufferCount(2))); + const authPending = firstValueFrom( + service.isAuthenticated$.pipe(bufferCount(2)) + ); service.isAuthenticated$.pipe(filter(Boolean)).subscribe(() => { // Revoking the Online Refresh Token terminates the Auth0 session // server-side, so the underlying SDK reflects no user going forward. ( - auth0Client.isAuthenticated as unknown as jest.SpyInstance + auth0Client.isAuthenticated as unknown as MockInstance ).mockResolvedValue(false); - (auth0Client.getUser as unknown as jest.SpyInstance).mockResolvedValue( + (auth0Client.getUser as unknown as MockInstance).mockResolvedValue( undefined ); - - service.logout({ - openUrl: false, - }); + service.logout({ openUrl: false }); }); + + const userValues = await userPending; + expect(userValues[0]).toBe(user); + expect(userValues[1]).toBe(null); + + const authValues = await authPending; + expect(authValues[0]).toBe(true); + expect(authValues[1]).toBe(false); }); }); describe('getAccessTokenSilently', () => { - it('should call the underlying SDK', (done) => { + it('should call the underlying SDK', async () => { const service = createService(); - service.getAccessTokenSilently().subscribe((token) => { - expect(auth0Client.getTokenSilently).toHaveBeenCalled(); - done(); - }); + await firstValueFrom(service.getAccessTokenSilently()); + expect(auth0Client.getTokenSilently).toHaveBeenCalled(); }); - it('should call the underlying SDK and pass along the options', (done) => { + it('should call the underlying SDK and pass along the options', async () => { // Empty object here just to test the object reference const options = {}; const service = createService(); - - service.getAccessTokenSilently(options).subscribe((token) => { - expect(auth0Client.getTokenSilently).toHaveBeenCalledWith(options); - done(); - }); + await firstValueFrom(service.getAccessTokenSilently(options)); + expect(auth0Client.getTokenSilently).toHaveBeenCalledWith(options); }); - it('should get the full token response when detailedResponse is true', (done) => { + it('should get the full token response when detailedResponse is true', async () => { const tokenResponse = { access_token: '123', id_token: '456', expires_in: 2, }; ( - auth0Client.getTokenSilently as unknown as jest.SpyInstance + auth0Client.getTokenSilently as unknown as MockInstance ).mockResolvedValue(tokenResponse); const service = createService(); - service - .getAccessTokenSilently({ detailedResponse: true }) - .subscribe((token) => { - expect(token).toEqual(tokenResponse); - done(); - }); + const token = await firstValueFrom( + service.getAccessTokenSilently({ detailedResponse: true }) + ); + expect(token).toEqual(tokenResponse); }); - it('should null when nothing in cache', (done) => { + it('should null when nothing in cache', async () => { ( - auth0Client.getTokenSilently as unknown as jest.SpyInstance + auth0Client.getTokenSilently as unknown as MockInstance ).mockResolvedValue(null); const service = createService(); - service.getAccessTokenSilently().subscribe((token) => { - expect(token).toBeNull(); - done(); - }); + const token = await firstValueFrom(service.getAccessTokenSilently()); + expect(token).toBeNull(); }); - it('should record errors in the error$ observable', (done) => { + it('should record errors in the error$ observable', async () => { const errorObj = new Error('An error has occured'); - ( - auth0Client.getTokenSilently as unknown as jest.SpyInstance + auth0Client.getTokenSilently as unknown as MockInstance ).mockRejectedValue(errorObj); const service = createService(); - - service.getAccessTokenSilently().subscribe({ - error: () => {}, - }); - - service.error$.subscribe((err: Error) => { - expect(err).toBe(errorObj); - done(); - }); + service.getAccessTokenSilently().subscribe({ error: () => {} }); + const err = await firstValueFrom(service.error$); + expect(err).toBe(errorObj); }); - it('should bubble errors', (done) => { + it('should bubble errors', async () => { const errorObj = new Error('An error has occured'); - ( - auth0Client.getTokenSilently as unknown as jest.SpyInstance + auth0Client.getTokenSilently as unknown as MockInstance ).mockRejectedValue(errorObj); const service = createService(); - - service.getAccessTokenSilently().subscribe({ - error: (err: Error) => { - expect(err).toBe(errorObj); - done(); - }, - }); + await expect( + firstValueFrom(service.getAccessTokenSilently()) + ).rejects.toBe(errorObj); }); }); describe('getAccessTokenWithPopup', () => { - it('should call the underlying SDK', (done) => { + it('should call the underlying SDK', async () => { const service = createService(); - service.getAccessTokenWithPopup().subscribe((token) => { - expect(auth0Client.getTokenWithPopup).toHaveBeenCalled(); - done(); - }); + await firstValueFrom(service.getAccessTokenWithPopup()); + expect(auth0Client.getTokenWithPopup).toHaveBeenCalled(); }); - it('should call the underlying SDK and pass along the options', (done) => { + it('should call the underlying SDK and pass along the options', async () => { // Empty object just to test reference const options = {}; - const service = createService(); - service.getAccessTokenWithPopup(options).subscribe((token) => { - expect(auth0Client.getTokenWithPopup).toHaveBeenCalledWith(options); - done(); - }); + await firstValueFrom(service.getAccessTokenWithPopup(options)); + expect(auth0Client.getTokenWithPopup).toHaveBeenCalledWith(options); }); - it('should record errors in the error$ observable', (done) => { + it('should record errors in the error$ observable', async () => { const errorObj = new Error('An error has occured'); - ( - auth0Client.getTokenWithPopup as unknown as jest.SpyInstance + auth0Client.getTokenWithPopup as unknown as MockInstance ).mockRejectedValue(errorObj); - const service = createService(); - service.getAccessTokenWithPopup().subscribe({ - error: () => {}, - }); - - service.error$.subscribe((err: Error) => { - expect(err).toBe(errorObj); - done(); - }); + service.getAccessTokenWithPopup().subscribe({ error: () => {} }); + const err = await firstValueFrom(service.error$); + expect(err).toBe(errorObj); }); - it('should bubble errors', (done) => { + it('should bubble errors', async () => { const errorObj = new Error('An error has occured'); - ( - auth0Client.getTokenWithPopup as unknown as jest.SpyInstance + auth0Client.getTokenWithPopup as unknown as MockInstance ).mockRejectedValue(errorObj); - const service = createService(); - service.getAccessTokenWithPopup().subscribe({ - error: (err: Error) => { - expect(err).toBe(errorObj); - done(); - }, - }); + await expect( + firstValueFrom(service.getAccessTokenWithPopup()) + ).rejects.toBe(errorObj); }); }); describe('loginWithCustomTokenExchange', () => { - it('should call the underlying SDK', (done) => { + it('should call the underlying SDK', async () => { const service = createService(); const options = { subject_token: '__test_token__', subject_token_type: 'urn:test:token-type', }; - - service - .loginWithCustomTokenExchange(options) - .subscribe((tokenResponse) => { - expect(auth0Client.loginWithCustomTokenExchange).toHaveBeenCalledWith( - options - ); - done(); - }); + await firstValueFrom(service.loginWithCustomTokenExchange(options)); + expect(auth0Client.loginWithCustomTokenExchange).toHaveBeenCalledWith( + options + ); }); - it('should return the token response', (done) => { + it('should return the token response', async () => { const service = createService(); const options = { subject_token: '__test_token__', subject_token_type: 'urn:test:token-type', scope: 'openid profile email', }; - - service - .loginWithCustomTokenExchange(options) - .subscribe((tokenResponse) => { - expect(tokenResponse).toEqual({ - access_token: '__exchanged_access_token__', - id_token: '__exchanged_id_token__', - token_type: 'Bearer', - expires_in: 86400, - }); - done(); - }); + const tokenResponse = await firstValueFrom( + service.loginWithCustomTokenExchange(options) + ); + expect(tokenResponse).toEqual({ + access_token: '__exchanged_access_token__', + id_token: '__exchanged_id_token__', + token_type: 'Bearer', + expires_in: 86400, + }); }); - it('should update auth state after successful token exchange', (done) => { + it('should update auth state after successful token exchange', async () => { const service = createService(); const options = { subject_token: '__test_token__', subject_token_type: 'urn:test:token-type', }; - - jest.spyOn(authState, 'setAccessToken'); - - service.loginWithCustomTokenExchange(options).subscribe(() => { - expect(authState.setAccessToken).toHaveBeenCalledWith( - '__exchanged_access_token__' - ); - done(); - }); + vi.spyOn(authState, 'setAccessToken'); + await firstValueFrom(service.loginWithCustomTokenExchange(options)); + expect(authState.setAccessToken).toHaveBeenCalledWith( + '__exchanged_access_token__' + ); }); - it('should record errors in the error$ observable', (done) => { + it('should record errors in the error$ observable', async () => { const errorObj = new Error('Token exchange failed'); - ( - auth0Client.loginWithCustomTokenExchange as unknown as jest.SpyInstance + auth0Client.loginWithCustomTokenExchange as unknown as MockInstance ).mockRejectedValue(errorObj); - const service = createService(); service .loginWithCustomTokenExchange({ subject_token: '__test_token__', subject_token_type: 'urn:test:token-type', }) - .subscribe({ - error: () => {}, - }); - - service.error$.subscribe((err: Error) => { - expect(err).toBe(errorObj); - done(); - }); + .subscribe({ error: () => {} }); + const err = await firstValueFrom(service.error$); + expect(err).toBe(errorObj); }); - it('should bubble errors', (done) => { + it('should bubble errors', async () => { const errorObj = new Error('Token exchange failed'); - ( - auth0Client.loginWithCustomTokenExchange as unknown as jest.SpyInstance + auth0Client.loginWithCustomTokenExchange as unknown as MockInstance ).mockRejectedValue(errorObj); - const service = createService(); - service - .loginWithCustomTokenExchange({ - subject_token: '__test_token__', - subject_token_type: 'urn:test:token-type', - }) - .subscribe({ - error: (err: Error) => { - expect(err).toBe(errorObj); - done(); - }, - }); + await expect( + firstValueFrom( + service.loginWithCustomTokenExchange({ + subject_token: '__test_token__', + subject_token_type: 'urn:test:token-type', + }) + ) + ).rejects.toBe(errorObj); }); }); @@ -1162,7 +1011,7 @@ describe('AuthService', () => { TestBed.resetTestingModule(); navigator = { - navigateByUrl: jest.fn().mockResolvedValue(true), + navigateByUrl: vi.fn().mockResolvedValue(true), } as any; TestBed.configureTestingModule({ @@ -1189,95 +1038,66 @@ describe('AuthService', () => { window.history.replaceState(null, '', ''); }); - it('should call the underlying SDK', (done) => { + it('should call the underlying SDK', async () => { const localService = createService(); - - localService.handleRedirectCallback().subscribe(() => { - expect(auth0Client.handleRedirectCallback).toHaveBeenCalled(); - done(); - }); + await firstValueFrom(localService.handleRedirectCallback()); + expect(auth0Client.handleRedirectCallback).toHaveBeenCalled(); }); - it('should call the underlying SDK and pass options', (done) => { + it('should call the underlying SDK and pass options', async () => { const url = 'http://localhost'; const localService = createService(); - - localService.handleRedirectCallback(url).subscribe(() => { - expect(auth0Client.handleRedirectCallback).toHaveBeenCalledWith(url); - done(); - }); + await firstValueFrom(localService.handleRedirectCallback(url)); + expect(auth0Client.handleRedirectCallback).toHaveBeenCalledWith(url); }); - it('should refresh the internal state', (done) => { + it('should refresh the internal state', async () => { const localService = createService(); - - localService.isAuthenticated$ - .pipe(bufferCount(2)) - .subscribe((authenticatedStates) => { - expect(authenticatedStates).toEqual([false, true]); - expect(auth0Client.isAuthenticated).toHaveBeenCalled(); - done(); - }); + const pending = firstValueFrom( + localService.isAuthenticated$.pipe(bufferCount(2)) + ); localService.isLoading$ .pipe( filter((isLoading) => !isLoading), tap(() => ( - auth0Client.isAuthenticated as unknown as jest.SpyInstance + auth0Client.isAuthenticated as unknown as MockInstance ).mockResolvedValue(true) ), mergeMap(() => localService.handleRedirectCallback()) ) .subscribe(); - }); - it('should record the appState in the appState$ observable if it is present', (done) => { - const appState = { - myValue: 'State to Preserve', - }; + const authenticatedStates = await pending; + expect(authenticatedStates).toEqual([false, true]); + expect(auth0Client.isAuthenticated).toHaveBeenCalled(); + }); + it('should record the appState in the appState$ observable if it is present', async () => { + const appState = { myValue: 'State to Preserve' }; ( - auth0Client.handleRedirectCallback as unknown as jest.SpyInstance - ).mockResolvedValue({ - appState, - }); - + auth0Client.handleRedirectCallback as unknown as MockInstance + ).mockResolvedValue({ appState }); const localService = createService(); - localService.handleRedirectCallback().subscribe(() => { - localService.appState$.subscribe((recievedState) => { - expect(recievedState).toEqual(appState); - done(); - }); - }); + await firstValueFrom(localService.handleRedirectCallback()); + const recievedState = await firstValueFrom(localService.appState$); + expect(recievedState).toEqual(appState); }); - it('should preserve appState as-is for regular login', (done) => { - const appState = { - myValue: 'State to Preserve', - }; - + it('should preserve appState as-is for regular login', async () => { + const appState = { myValue: 'State to Preserve' }; ( - auth0Client.handleRedirectCallback as unknown as jest.SpyInstance - ).mockResolvedValue({ - appState, - response_type: ResponseType.Code, - }); - + auth0Client.handleRedirectCallback as unknown as MockInstance + ).mockResolvedValue({ appState, response_type: ResponseType.Code }); const localService = createService(); - localService.handleRedirectCallback().subscribe(() => { - localService.appState$.subscribe((receivedState) => { - expect(receivedState).toEqual(appState); - done(); - }); - }); + await firstValueFrom(localService.handleRedirectCallback()); + const receivedState = await firstValueFrom(localService.appState$); + expect(receivedState).toEqual(appState); }); - it('should extract connected account data when response_type is ConnectCode', (done) => { - const appState = { - myValue: 'State to Preserve', - }; - + it('should extract connected account data when response_type is ConnectCode', async () => { + const appState = { myValue: 'State to Preserve' }; const connectedAccount = { id: 'abc123', connection: 'google-oauth2', @@ -1285,29 +1105,24 @@ describe('AuthService', () => { created_at: '2024-01-01T00:00:00.000Z', expires_at: '2024-01-02T00:00:00.000Z', }; - ( - auth0Client.handleRedirectCallback as unknown as jest.SpyInstance + auth0Client.handleRedirectCallback as unknown as MockInstance ).mockResolvedValue({ appState, response_type: ResponseType.ConnectCode, ...connectedAccount, }); - const localService = createService(); - localService.handleRedirectCallback().subscribe(() => { - localService.appState$.subscribe((receivedState) => { - expect(receivedState).toEqual({ - ...appState, - response_type: ResponseType.ConnectCode, - connectedAccount, - }); - done(); - }); + await firstValueFrom(localService.handleRedirectCallback()); + const receivedState = await firstValueFrom(localService.appState$); + expect(receivedState).toEqual({ + ...appState, + response_type: ResponseType.ConnectCode, + connectedAccount, }); }); - it('should handle connected account redirect without initial appState', (done) => { + it('should handle connected account redirect without initial appState', async () => { const connectedAccount = { id: 'xyz789', connection: 'github', @@ -1315,97 +1130,80 @@ describe('AuthService', () => { created_at: '2024-02-01T00:00:00.000Z', expires_at: '2024-02-02T00:00:00.000Z', }; - ( - auth0Client.handleRedirectCallback as unknown as jest.SpyInstance + auth0Client.handleRedirectCallback as unknown as MockInstance ).mockResolvedValue({ response_type: ResponseType.ConnectCode, ...connectedAccount, }); - const localService = createService(); - localService.handleRedirectCallback().subscribe(() => { - localService.appState$.subscribe((receivedState) => { - expect(receivedState.response_type).toBe(ResponseType.ConnectCode); - expect(receivedState.connectedAccount).toEqual(connectedAccount); - done(); - }); - }); + await firstValueFrom(localService.handleRedirectCallback()); + const receivedState = await firstValueFrom(localService.appState$); + expect(receivedState.response_type).toBe(ResponseType.ConnectCode); + expect(receivedState.connectedAccount).toEqual(connectedAccount); }); }); describe('getDpopNonce', () => { - it('should retrieve DPoP nonce from the client', (done) => { + it('should retrieve DPoP nonce from the client', async () => { const service = createService(); - service.getDpopNonce().subscribe((nonce) => { - expect(nonce).toBe('test-nonce-value'); - expect(auth0Client.getDpopNonce).toHaveBeenCalled(); - done(); - }); + const nonce = await firstValueFrom(service.getDpopNonce()); + expect(nonce).toBe('test-nonce-value'); + expect(auth0Client.getDpopNonce).toHaveBeenCalled(); }); - it('should pass domain identifier to the underlying SDK', (done) => { + it('should pass domain identifier to the underlying SDK', async () => { const domainId = 'custom-domain'; const service = createService(); - service.getDpopNonce(domainId).subscribe(() => { - expect(auth0Client.getDpopNonce).toHaveBeenCalledWith(domainId); - done(); - }); + await firstValueFrom(service.getDpopNonce(domainId)); + expect(auth0Client.getDpopNonce).toHaveBeenCalledWith(domainId); }); - it('should handle undefined nonce', (done) => { - (auth0Client.getDpopNonce as jest.Mock).mockResolvedValue(undefined); + it('should handle undefined nonce', async () => { + (auth0Client.getDpopNonce as Mock).mockResolvedValue(undefined); const service = createService(); - service.getDpopNonce().subscribe((nonce) => { - expect(nonce).toBeUndefined(); - done(); - }); + const nonce = await firstValueFrom(service.getDpopNonce()); + expect(nonce).toBeUndefined(); }); }); describe('setDpopNonce', () => { - it('should set DPoP nonce through the client', (done) => { + it('should set DPoP nonce through the client', async () => { const service = createService(); const nonceValue = 'new-nonce-123'; - service.setDpopNonce(nonceValue).subscribe(() => { - expect(auth0Client.setDpopNonce).toHaveBeenCalledWith( - nonceValue, - undefined - ); - done(); - }); + await firstValueFrom(service.setDpopNonce(nonceValue)); + expect(auth0Client.setDpopNonce).toHaveBeenCalledWith( + nonceValue, + undefined + ); }); - it('should pass nonce and domain identifier to the underlying SDK', (done) => { + it('should pass nonce and domain identifier to the underlying SDK', async () => { const service = createService(); const nonceValue = 'nonce-456'; const domainId = 'domain-1'; - service.setDpopNonce(nonceValue, domainId).subscribe(() => { - expect(auth0Client.setDpopNonce).toHaveBeenCalledWith( - nonceValue, - domainId - ); - done(); - }); + await firstValueFrom(service.setDpopNonce(nonceValue, domainId)); + expect(auth0Client.setDpopNonce).toHaveBeenCalledWith( + nonceValue, + domainId + ); }); }); describe('generateDpopProof', () => { - it('should generate DPoP proof JWT', (done) => { + it('should generate DPoP proof JWT', async () => { const service = createService(); const params = { url: 'https://api.example.com/resource', method: 'POST', accessToken: 'access-token-123', }; - service.generateDpopProof(params).subscribe((proof) => { - expect(proof).toBe('test-proof-jwt'); - expect(auth0Client.generateDpopProof).toHaveBeenCalledWith(params); - done(); - }); + const proof = await firstValueFrom(service.generateDpopProof(params)); + expect(proof).toBe('test-proof-jwt'); + expect(auth0Client.generateDpopProof).toHaveBeenCalledWith(params); }); - it('should pass all parameters including nonce', (done) => { + it('should pass all parameters including nonce', async () => { const service = createService(); const params = { url: 'https://api.example.com/data', @@ -1413,10 +1211,8 @@ describe('AuthService', () => { nonce: 'server-nonce', accessToken: 'token-xyz', }; - service.generateDpopProof(params).subscribe(() => { - expect(auth0Client.generateDpopProof).toHaveBeenCalledWith(params); - done(); - }); + await firstValueFrom(service.generateDpopProof(params)); + expect(auth0Client.generateDpopProof).toHaveBeenCalledWith(params); }); }); @@ -1440,220 +1236,178 @@ describe('AuthService', () => { describe('mfa', () => { describe('getAuthenticators', () => { - it('should call the underlying SDK', (done) => { + it('should call the underlying SDK', async () => { const service = createService(); const mfaToken = '__mfa_token__'; - - service.mfa.getAuthenticators(mfaToken).subscribe(() => { - expect(auth0Client.mfa.getAuthenticators).toHaveBeenCalledWith( - mfaToken - ); - done(); - }); + await firstValueFrom(service.mfa.getAuthenticators(mfaToken)); + expect(auth0Client.mfa.getAuthenticators).toHaveBeenCalledWith( + mfaToken + ); }); - it('should return the list of authenticators', (done) => { + it('should return the list of authenticators', async () => { const service = createService(); - - service.mfa.getAuthenticators('__mfa_token__').subscribe((result) => { - expect(result).toEqual([ - { id: 'auth-1', authenticatorType: 'otp', active: true }, - ]); - done(); - }); + const result = await firstValueFrom( + service.mfa.getAuthenticators('__mfa_token__') + ); + expect(result).toEqual([ + { id: 'auth-1', authenticatorType: 'otp', active: true }, + ]); }); - it('should bubble errors', (done) => { + it('should bubble errors', async () => { const errorObj = new Error('getAuthenticators failed'); ( - auth0Client.mfa.getAuthenticators as unknown as jest.SpyInstance + auth0Client.mfa.getAuthenticators as unknown as MockInstance ).mockRejectedValue(errorObj); const service = createService(); - - service.mfa.getAuthenticators('__mfa_token__').subscribe({ - error: (err: Error) => { - expect(err).toBe(errorObj); - done(); - }, - }); + await expect( + firstValueFrom(service.mfa.getAuthenticators('__mfa_token__')) + ).rejects.toBe(errorObj); }); }); describe('enroll', () => { - it('should call the underlying SDK', (done) => { + it('should call the underlying SDK', async () => { const service = createService(); const params = { mfaToken: '__mfa_token__', factorType: 'otp' as const, }; - - service.mfa.enroll(params).subscribe(() => { - expect(auth0Client.mfa.enroll).toHaveBeenCalledWith(params); - done(); - }); + await firstValueFrom(service.mfa.enroll(params)); + expect(auth0Client.mfa.enroll).toHaveBeenCalledWith(params); }); - it('should return the enrollment response', (done) => { + it('should return the enrollment response', async () => { const service = createService(); - - service.mfa - .enroll({ mfaToken: '__mfa_token__', factorType: 'otp' }) - .subscribe((result) => { - expect(result).toEqual({ - authenticatorType: 'otp', - secret: '__totp_secret__', - barcodeUri: '__barcode_uri__', - }); - done(); - }); + const result = await firstValueFrom( + service.mfa.enroll({ mfaToken: '__mfa_token__', factorType: 'otp' }) + ); + expect(result).toEqual({ + authenticatorType: 'otp', + secret: '__totp_secret__', + barcodeUri: '__barcode_uri__', + }); }); - it('should bubble errors', (done) => { + it('should bubble errors', async () => { const errorObj = new Error('enroll failed'); - ( - auth0Client.mfa.enroll as unknown as jest.SpyInstance - ).mockRejectedValue(errorObj); + (auth0Client.mfa.enroll as unknown as MockInstance).mockRejectedValue( + errorObj + ); const service = createService(); - - service.mfa - .enroll({ mfaToken: '__mfa_token__', factorType: 'otp' }) - .subscribe({ - error: (err: Error) => { - expect(err).toBe(errorObj); - done(); - }, - }); + await expect( + firstValueFrom( + service.mfa.enroll({ mfaToken: '__mfa_token__', factorType: 'otp' }) + ) + ).rejects.toBe(errorObj); }); }); describe('challenge', () => { - it('should call the underlying SDK', (done) => { + it('should call the underlying SDK', async () => { const service = createService(); const params = { mfaToken: '__mfa_token__', challengeType: 'otp' as const, }; - - service.mfa.challenge(params).subscribe(() => { - expect(auth0Client.mfa.challenge).toHaveBeenCalledWith(params); - done(); - }); + await firstValueFrom(service.mfa.challenge(params)); + expect(auth0Client.mfa.challenge).toHaveBeenCalledWith(params); }); - it('should return the challenge response', (done) => { + it('should return the challenge response', async () => { const service = createService(); - - service.mfa - .challenge({ mfaToken: '__mfa_token__', challengeType: 'otp' }) - .subscribe((result) => { - expect(result).toEqual({ challengeType: 'otp' }); - done(); - }); + const result = await firstValueFrom( + service.mfa.challenge({ + mfaToken: '__mfa_token__', + challengeType: 'otp', + }) + ); + expect(result).toEqual({ challengeType: 'otp' }); }); - it('should bubble errors', (done) => { + it('should bubble errors', async () => { const errorObj = new Error('challenge failed'); ( - auth0Client.mfa.challenge as unknown as jest.SpyInstance + auth0Client.mfa.challenge as unknown as MockInstance ).mockRejectedValue(errorObj); const service = createService(); - - service.mfa - .challenge({ mfaToken: '__mfa_token__', challengeType: 'otp' }) - .subscribe({ - error: (err: Error) => { - expect(err).toBe(errorObj); - done(); - }, - }); + await expect( + firstValueFrom( + service.mfa.challenge({ + mfaToken: '__mfa_token__', + challengeType: 'otp', + }) + ) + ).rejects.toBe(errorObj); }); }); describe('getEnrollmentFactors', () => { - it('should call the underlying SDK', (done) => { + it('should call the underlying SDK', async () => { const service = createService(); const mfaToken = '__mfa_token__'; - - service.mfa.getEnrollmentFactors(mfaToken).subscribe(() => { - expect(auth0Client.mfa.getEnrollmentFactors).toHaveBeenCalledWith( - mfaToken - ); - done(); - }); + await firstValueFrom(service.mfa.getEnrollmentFactors(mfaToken)); + expect(auth0Client.mfa.getEnrollmentFactors).toHaveBeenCalledWith( + mfaToken + ); }); - it('should return the enrollment factors', (done) => { + it('should return the enrollment factors', async () => { const service = createService(); - - service.mfa - .getEnrollmentFactors('__mfa_token__') - .subscribe((result) => { - expect(result).toEqual([{ type: 'otp' }]); - done(); - }); + const result = await firstValueFrom( + service.mfa.getEnrollmentFactors('__mfa_token__') + ); + expect(result).toEqual([{ type: 'otp' }]); }); - it('should bubble errors', (done) => { + it('should bubble errors', async () => { const errorObj = new Error('getEnrollmentFactors failed'); ( - auth0Client.mfa.getEnrollmentFactors as unknown as jest.SpyInstance + auth0Client.mfa.getEnrollmentFactors as unknown as MockInstance ).mockRejectedValue(errorObj); const service = createService(); - - service.mfa.getEnrollmentFactors('__mfa_token__').subscribe({ - error: (err: Error) => { - expect(err).toBe(errorObj); - done(); - }, - }); + await expect( + firstValueFrom(service.mfa.getEnrollmentFactors('__mfa_token__')) + ).rejects.toBe(errorObj); }); }); describe('verify', () => { - it('should call the underlying SDK', (done) => { + it('should call the underlying SDK', async () => { const service = createService(); const params = { mfaToken: '__mfa_token__', otp: '123456' }; - - service.mfa.verify(params).subscribe(() => { - expect(auth0Client.mfa.verify).toHaveBeenCalledWith(params); - done(); - }); + await firstValueFrom(service.mfa.verify(params)); + expect(auth0Client.mfa.verify).toHaveBeenCalledWith(params); }); - it('should return the token response', (done) => { + it('should return the token response', async () => { const service = createService(); - - service.mfa - .verify({ mfaToken: '__mfa_token__', otp: '123456' }) - .subscribe((result) => { - expect(result).toEqual({ - access_token: '__mfa_access_token__', - id_token: '__mfa_id_token__', - token_type: 'Bearer', - expires_in: 86400, - }); - done(); - }); + const result = await firstValueFrom( + service.mfa.verify({ mfaToken: '__mfa_token__', otp: '123456' }) + ); + expect(result).toEqual({ + access_token: '__mfa_access_token__', + id_token: '__mfa_id_token__', + token_type: 'Bearer', + expires_in: 86400, + }); }); - it('should bubble errors', (done) => { + it('should bubble errors', async () => { const errorObj = new Error('verify failed'); - ( - auth0Client.mfa.verify as unknown as jest.SpyInstance - ).mockRejectedValue(errorObj); + (auth0Client.mfa.verify as unknown as MockInstance).mockRejectedValue( + errorObj + ); const service = createService(); - - service.mfa - .verify({ mfaToken: '__mfa_token__', otp: '123456' }) - .subscribe({ - error: (err: Error) => { - expect(err).toBe(errorObj); - done(); - }, - }); + await expect( + firstValueFrom( + service.mfa.verify({ mfaToken: '__mfa_token__', otp: '123456' }) + ) + ).rejects.toBe(errorObj); }); - it('should not update isAuthenticated$ or user$ after a successful verify', (done) => { + it('should not update isAuthenticated$ or user$ after a successful verify', async () => { // verify() intentionally does not update Angular auth state — callers must // follow up with getAccessTokenSilently() to reflect the new MFA session. const service = createService(); @@ -1663,440 +1417,347 @@ describe('AuthService', () => { service.isAuthenticated$.subscribe(() => isAuthEmissions++); service.user$.subscribe(() => userEmissions++); - loaded(service) - .pipe( + await firstValueFrom( + loaded(service).pipe( mergeMap(() => service.mfa.verify({ mfaToken: '__mfa_token__', otp: '123456' }) ), delay(0) ) - .subscribe(() => { - expect(isAuthEmissions).toBe(1); - expect(userEmissions).toBe(1); - done(); - }); + ); + expect(isAuthEmissions).toBe(1); + expect(userEmissions).toBe(1); }); }); }); describe('passkey', () => { describe('signup', () => { - it('should call the underlying SDK', (done) => { + it('should call the underlying SDK', async () => { const service = createService(); const options = { email: 'user@example.com' }; - - service.passkey.signup(options).subscribe(() => { - expect(auth0Client.passkey.signup).toHaveBeenCalledWith(options); - done(); - }); + await firstValueFrom(service.passkey.signup(options)); + expect(auth0Client.passkey.signup).toHaveBeenCalledWith(options); }); - it('should return the token response', (done) => { + it('should return the token response', async () => { const service = createService(); - - service.passkey - .signup({ email: 'user@example.com' }) - .subscribe((result) => { - expect(result).toEqual({ - access_token: '__passkey_access_token__', - id_token: '__passkey_id_token__', - token_type: 'Bearer', - expires_in: 86400, - }); - done(); - }); + const result = await firstValueFrom( + service.passkey.signup({ email: 'user@example.com' }) + ); + expect(result).toEqual({ + access_token: '__passkey_access_token__', + id_token: '__passkey_id_token__', + token_type: 'Bearer', + expires_in: 86400, + }); }); - it('should set the access token after successful signup', (done) => { + it('should set the access token after successful signup', async () => { const service = createService(); - - jest.spyOn(authState, 'setAccessToken'); - - service.passkey.signup({ email: 'user@example.com' }).subscribe(() => { - expect(authState.setAccessToken).toHaveBeenCalledWith( - '__passkey_access_token__' - ); - done(); - }); + vi.spyOn(authState, 'setAccessToken'); + await firstValueFrom( + service.passkey.signup({ email: 'user@example.com' }) + ); + expect(authState.setAccessToken).toHaveBeenCalledWith( + '__passkey_access_token__' + ); }); - it('should record errors in the error$ observable', (done) => { + it('should record errors in the error$ observable', async () => { const errorObj = new Error('WebAuthn not supported'); ( - auth0Client.passkey.signup as unknown as jest.SpyInstance + auth0Client.passkey.signup as unknown as MockInstance ).mockRejectedValue(errorObj); const service = createService(); - - service.passkey.signup({ email: 'user@example.com' }).subscribe({ - error: () => {}, - }); - - service.error$.subscribe((err: Error) => { - expect(err).toBe(errorObj); - done(); - }); + service.passkey + .signup({ email: 'user@example.com' }) + .subscribe({ error: () => {} }); + const err = await firstValueFrom(service.error$); + expect(err).toBe(errorObj); }); - it('should bubble errors', (done) => { + it('should bubble errors', async () => { const errorObj = new Error('WebAuthn not supported'); ( - auth0Client.passkey.signup as unknown as jest.SpyInstance + auth0Client.passkey.signup as unknown as MockInstance ).mockRejectedValue(errorObj); const service = createService(); - - service.passkey.signup({ email: 'user@example.com' }).subscribe({ - error: (err: Error) => { - expect(err).toBe(errorObj); - done(); - }, - }); + await expect( + firstValueFrom(service.passkey.signup({ email: 'user@example.com' })) + ).rejects.toBe(errorObj); }); }); describe('login', () => { - it('should call the underlying SDK', (done) => { + it('should call the underlying SDK', async () => { const service = createService(); - - service.passkey.login().subscribe(() => { - expect(auth0Client.passkey.login).toHaveBeenCalledWith(undefined); - done(); - }); + await firstValueFrom(service.passkey.login()); + expect(auth0Client.passkey.login).toHaveBeenCalledWith(undefined); }); - it('should forward options to the underlying SDK', (done) => { + it('should forward options to the underlying SDK', async () => { const service = createService(); const options = { realm: 'Username-Password-Authentication', scope: 'openid profile email', }; - - service.passkey.login(options).subscribe(() => { - expect(auth0Client.passkey.login).toHaveBeenCalledWith(options); - done(); - }); + await firstValueFrom(service.passkey.login(options)); + expect(auth0Client.passkey.login).toHaveBeenCalledWith(options); }); - it('should return the token response', (done) => { + it('should return the token response', async () => { const service = createService(); - - service.passkey.login().subscribe((result) => { - expect(result).toEqual({ - access_token: '__passkey_access_token__', - id_token: '__passkey_id_token__', - token_type: 'Bearer', - expires_in: 86400, - }); - done(); + const result = await firstValueFrom(service.passkey.login()); + expect(result).toEqual({ + access_token: '__passkey_access_token__', + id_token: '__passkey_id_token__', + token_type: 'Bearer', + expires_in: 86400, }); }); - it('should set the access token after successful login', (done) => { + it('should set the access token after successful login', async () => { const service = createService(); - - jest.spyOn(authState, 'setAccessToken'); - - service.passkey.login().subscribe(() => { - expect(authState.setAccessToken).toHaveBeenCalledWith( - '__passkey_access_token__' - ); - done(); - }); + vi.spyOn(authState, 'setAccessToken'); + await firstValueFrom(service.passkey.login()); + expect(authState.setAccessToken).toHaveBeenCalledWith( + '__passkey_access_token__' + ); }); - it('should record errors in the error$ observable', (done) => { + it('should record errors in the error$ observable', async () => { const errorObj = new Error('User cancelled'); ( - auth0Client.passkey.login as unknown as jest.SpyInstance + auth0Client.passkey.login as unknown as MockInstance ).mockRejectedValue(errorObj); const service = createService(); - - service.passkey.login().subscribe({ - error: () => {}, - }); - - service.error$.subscribe((err: Error) => { - expect(err).toBe(errorObj); - done(); - }); + service.passkey.login().subscribe({ error: () => {} }); + const err = await firstValueFrom(service.error$); + expect(err).toBe(errorObj); }); - it('should bubble errors', (done) => { + it('should bubble errors', async () => { const errorObj = new Error('User cancelled'); ( - auth0Client.passkey.login as unknown as jest.SpyInstance + auth0Client.passkey.login as unknown as MockInstance ).mockRejectedValue(errorObj); const service = createService(); - - service.passkey.login().subscribe({ - error: (err: Error) => { - expect(err).toBe(errorObj); - done(); - }, - }); + await expect(firstValueFrom(service.passkey.login())).rejects.toBe( + errorObj + ); }); }); }); describe('myAccount', () => { describe('getFactors', () => { - it('should call the underlying SDK', (done) => { + it('should call the underlying SDK', async () => { const service = createService(); - - service.myAccount.getFactors().subscribe(() => { - expect(auth0Client.myAccount.getFactors).toHaveBeenCalled(); - done(); - }); + await firstValueFrom(service.myAccount.getFactors()); + expect(auth0Client.myAccount.getFactors).toHaveBeenCalled(); }); - it('should return the factors list', (done) => { + it('should return the factors list', async () => { const service = createService(); - - service.myAccount.getFactors().subscribe((result) => { - expect(Array.isArray(result)).toBe(true); - done(); - }); + const result = await firstValueFrom(service.myAccount.getFactors()); + expect(Array.isArray(result)).toBe(true); }); - it('should bubble errors', (done) => { + it('should bubble errors', async () => { const errorObj = new Error('getFactors failed'); ( - auth0Client.myAccount.getFactors as unknown as jest.SpyInstance + auth0Client.myAccount.getFactors as unknown as MockInstance ).mockRejectedValue(errorObj); const service = createService(); - - service.myAccount.getFactors().subscribe({ - error: (err: Error) => { - expect(err).toBe(errorObj); - done(); - }, - }); + await expect( + firstValueFrom(service.myAccount.getFactors()) + ).rejects.toBe(errorObj); }); }); describe('getAuthenticationMethods', () => { - it('should call the underlying SDK without a filter', (done) => { + it('should call the underlying SDK without a filter', async () => { const service = createService(); - - service.myAccount.getAuthenticationMethods().subscribe(() => { - expect( - auth0Client.myAccount.getAuthenticationMethods - ).toHaveBeenCalledWith(undefined); - done(); - }); + await firstValueFrom(service.myAccount.getAuthenticationMethods()); + expect( + auth0Client.myAccount.getAuthenticationMethods + ).toHaveBeenCalledWith(undefined); }); - it('should forward the type filter to the underlying SDK', (done) => { + it('should forward the type filter to the underlying SDK', async () => { const service = createService(); - - service.myAccount - .getAuthenticationMethods('passkey') - .subscribe(() => { - expect( - auth0Client.myAccount.getAuthenticationMethods - ).toHaveBeenCalledWith('passkey'); - done(); - }); + await firstValueFrom( + service.myAccount.getAuthenticationMethods('passkey') + ); + expect( + auth0Client.myAccount.getAuthenticationMethods + ).toHaveBeenCalledWith('passkey'); }); - it('should return the list of authentication methods', (done) => { + it('should return the list of authentication methods', async () => { const service = createService(); - - service.myAccount.getAuthenticationMethods().subscribe((result) => { - expect(Array.isArray(result)).toBe(true); - done(); - }); + const result = await firstValueFrom( + service.myAccount.getAuthenticationMethods() + ); + expect(Array.isArray(result)).toBe(true); }); - it('should bubble errors', (done) => { + it('should bubble errors', async () => { const errorObj = new Error('getAuthenticationMethods failed'); ( auth0Client.myAccount - .getAuthenticationMethods as unknown as jest.SpyInstance + .getAuthenticationMethods as unknown as MockInstance ).mockRejectedValue(errorObj); const service = createService(); - - service.myAccount.getAuthenticationMethods().subscribe({ - error: (err: Error) => { - expect(err).toBe(errorObj); - done(); - }, - }); + await expect( + firstValueFrom(service.myAccount.getAuthenticationMethods()) + ).rejects.toBe(errorObj); }); }); describe('getAuthenticationMethod', () => { - it('should call the underlying SDK with the id', (done) => { + it('should call the underlying SDK with the id', async () => { const service = createService(); - - service.myAccount.getAuthenticationMethod('__method_id__').subscribe(() => { - expect( - auth0Client.myAccount.getAuthenticationMethod - ).toHaveBeenCalledWith('__method_id__'); - done(); - }); + await firstValueFrom( + service.myAccount.getAuthenticationMethod('__method_id__') + ); + expect( + auth0Client.myAccount.getAuthenticationMethod + ).toHaveBeenCalledWith('__method_id__'); }); - it('should return the authentication method', (done) => { + it('should return the authentication method', async () => { const service = createService(); - - service.myAccount - .getAuthenticationMethod('__method_id__') - .subscribe((result) => { - expect(result.id).toBe('__method_id__'); - done(); - }); + const result = await firstValueFrom( + service.myAccount.getAuthenticationMethod('__method_id__') + ); + expect(result.id).toBe('__method_id__'); }); - it('should bubble errors', (done) => { + it('should bubble errors', async () => { const errorObj = new Error('getAuthenticationMethod failed'); ( auth0Client.myAccount - .getAuthenticationMethod as unknown as jest.SpyInstance + .getAuthenticationMethod as unknown as MockInstance ).mockRejectedValue(errorObj); const service = createService(); - - service.myAccount.getAuthenticationMethod('__method_id__').subscribe({ - error: (err: Error) => { - expect(err).toBe(errorObj); - done(); - }, - }); + await expect( + firstValueFrom( + service.myAccount.getAuthenticationMethod('__method_id__') + ) + ).rejects.toBe(errorObj); }); }); describe('deleteAuthenticationMethod', () => { - it('should call the underlying SDK with the id', (done) => { + it('should call the underlying SDK with the id', async () => { const service = createService(); - - service.myAccount - .deleteAuthenticationMethod('__method_id__') - .subscribe(() => { - expect( - auth0Client.myAccount.deleteAuthenticationMethod - ).toHaveBeenCalledWith('__method_id__'); - done(); - }); + await firstValueFrom( + service.myAccount.deleteAuthenticationMethod('__method_id__') + ); + expect( + auth0Client.myAccount.deleteAuthenticationMethod + ).toHaveBeenCalledWith('__method_id__'); }); - it('should complete without a value', (done) => { + it('should complete without a value', async () => { const service = createService(); - - service.myAccount - .deleteAuthenticationMethod('__method_id__') - .subscribe((result) => { - expect(result).toBeUndefined(); - done(); - }); + const result = await firstValueFrom( + service.myAccount.deleteAuthenticationMethod('__method_id__') + ); + expect(result).toBeUndefined(); }); - it('should bubble errors', (done) => { + it('should bubble errors', async () => { const errorObj = new Error('deleteAuthenticationMethod failed'); ( auth0Client.myAccount - .deleteAuthenticationMethod as unknown as jest.SpyInstance + .deleteAuthenticationMethod as unknown as MockInstance ).mockRejectedValue(errorObj); const service = createService(); - - service.myAccount - .deleteAuthenticationMethod('__method_id__') - .subscribe({ - error: (err: Error) => { - expect(err).toBe(errorObj); - done(); - }, - }); + await expect( + firstValueFrom( + service.myAccount.deleteAuthenticationMethod('__method_id__') + ) + ).rejects.toBe(errorObj); }); }); describe('updateAuthenticationMethod', () => { - it('should call the underlying SDK with id and data', (done) => { + it('should call the underlying SDK with id and data', async () => { const service = createService(); const data = { name: 'My Passkey' }; - - service.myAccount - .updateAuthenticationMethod('__method_id__', data) - .subscribe(() => { - expect( - auth0Client.myAccount.updateAuthenticationMethod - ).toHaveBeenCalledWith('__method_id__', data); - done(); - }); + await firstValueFrom( + service.myAccount.updateAuthenticationMethod('__method_id__', data) + ); + expect( + auth0Client.myAccount.updateAuthenticationMethod + ).toHaveBeenCalledWith('__method_id__', data); }); - it('should return the updated authentication method', (done) => { + it('should return the updated authentication method', async () => { const service = createService(); - - service.myAccount - .updateAuthenticationMethod('__method_id__', { name: 'My Passkey' }) - .subscribe((result) => { - expect(result.id).toBe('__method_id__'); - done(); - }); + const result = await firstValueFrom( + service.myAccount.updateAuthenticationMethod('__method_id__', { + name: 'My Passkey', + }) + ); + expect(result.id).toBe('__method_id__'); }); - it('should bubble errors', (done) => { + it('should bubble errors', async () => { const errorObj = new Error('updateAuthenticationMethod failed'); ( auth0Client.myAccount - .updateAuthenticationMethod as unknown as jest.SpyInstance + .updateAuthenticationMethod as unknown as MockInstance ).mockRejectedValue(errorObj); const service = createService(); - - service.myAccount - .updateAuthenticationMethod('__method_id__', { name: 'My Passkey' }) - .subscribe({ - error: (err: Error) => { - expect(err).toBe(errorObj); - done(); - }, - }); + await expect( + firstValueFrom( + service.myAccount.updateAuthenticationMethod('__method_id__', { + name: 'My Passkey', + }) + ) + ).rejects.toBe(errorObj); }); }); describe('enrollmentChallenge', () => { - it('should call the underlying SDK with options', (done) => { + it('should call the underlying SDK with options', async () => { const service = createService(); const options = { type: 'totp' as const }; - - service.myAccount.enrollmentChallenge(options).subscribe(() => { - expect( - auth0Client.myAccount.enrollmentChallenge - ).toHaveBeenCalledWith(options); - done(); - }); + await firstValueFrom(service.myAccount.enrollmentChallenge(options)); + expect(auth0Client.myAccount.enrollmentChallenge).toHaveBeenCalledWith( + options + ); }); - it('should return the challenge response', (done) => { + it('should return the challenge response', async () => { const service = createService(); - - service.myAccount - .enrollmentChallenge({ type: 'totp' }) - .subscribe((result) => { - expect(result.id).toBe('__challenge_id__'); - expect(result.auth_session).toBe('__auth_session__'); - done(); - }); + const result = await firstValueFrom( + service.myAccount.enrollmentChallenge({ type: 'totp' }) + ); + expect(result.id).toBe('__challenge_id__'); + expect(result.auth_session).toBe('__auth_session__'); }); - it('should bubble errors', (done) => { + it('should bubble errors', async () => { const errorObj = new Error('enrollmentChallenge failed'); ( - auth0Client.myAccount - .enrollmentChallenge as unknown as jest.SpyInstance + auth0Client.myAccount.enrollmentChallenge as unknown as MockInstance ).mockRejectedValue(errorObj); const service = createService(); - - service.myAccount.enrollmentChallenge({ type: 'totp' }).subscribe({ - error: (err: Error) => { - expect(err).toBe(errorObj); - done(); - }, - }); + await expect( + firstValueFrom( + service.myAccount.enrollmentChallenge({ type: 'totp' }) + ) + ).rejects.toBe(errorObj); }); }); describe('enrollmentVerify', () => { - it('should call the underlying SDK with options', (done) => { + it('should call the underlying SDK with options', async () => { const service = createService(); const options = { type: 'totp' as const, @@ -2104,51 +1765,41 @@ describe('AuthService', () => { auth_session: '__auth_session__', otp_code: '123456', }; - - service.myAccount.enrollmentVerify(options).subscribe(() => { - expect( - auth0Client.myAccount.enrollmentVerify - ).toHaveBeenCalledWith(options); - done(); - }); + await firstValueFrom(service.myAccount.enrollmentVerify(options)); + expect(auth0Client.myAccount.enrollmentVerify).toHaveBeenCalledWith( + options + ); }); - it('should return the created authentication method', (done) => { + it('should return the created authentication method', async () => { const service = createService(); - - service.myAccount - .enrollmentVerify({ + const result = await firstValueFrom( + service.myAccount.enrollmentVerify({ type: 'totp', location: 'https://example.auth0.com/enroll', auth_session: '__auth_session__', otp_code: '123456', }) - .subscribe((result) => { - expect(result.id).toBe('__method_id__'); - done(); - }); + ); + expect(result.id).toBe('__method_id__'); }); - it('should bubble errors', (done) => { + it('should bubble errors', async () => { const errorObj = new Error('enrollmentVerify failed'); ( - auth0Client.myAccount.enrollmentVerify as unknown as jest.SpyInstance + auth0Client.myAccount.enrollmentVerify as unknown as MockInstance ).mockRejectedValue(errorObj); const service = createService(); - - service.myAccount - .enrollmentVerify({ - type: 'totp', - location: 'https://example.auth0.com/enroll', - auth_session: '__auth_session__', - otp_code: '123456', - }) - .subscribe({ - error: (err: Error) => { - expect(err).toBe(errorObj); - done(); - }, - }); + await expect( + firstValueFrom( + service.myAccount.enrollmentVerify({ + type: 'totp', + location: 'https://example.auth0.com/enroll', + auth_session: '__auth_session__', + otp_code: '123456', + }) + ) + ).rejects.toBe(errorObj); }); }); }); diff --git a/projects/auth0-angular/src/test-setup.ts b/projects/auth0-angular/src/test-setup.ts index eb67306b..c44a03a1 100644 --- a/projects/auth0-angular/src/test-setup.ts +++ b/projects/auth0-angular/src/test-setup.ts @@ -1,6 +1,17 @@ -import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; +import 'reflect-metadata'; +import 'zone.js'; +import 'zone.js/testing'; +import '@angular/compiler'; +import { + BrowserTestingModule, + platformBrowserTesting, +} from '@angular/platform-browser/testing'; +import { getTestBed } from '@angular/core/testing'; -setupZoneTestEnv(); +getTestBed().initTestEnvironment( + BrowserTestingModule, + platformBrowserTesting() +); // Provides fetch, Headers, Response, Request — works in Node + JSDOM import 'cross-fetch/polyfill'; diff --git a/projects/auth0-angular/tsconfig.spec.json b/projects/auth0-angular/tsconfig.spec.json index 7b65db87..d3fba01b 100644 --- a/projects/auth0-angular/tsconfig.spec.json +++ b/projects/auth0-angular/tsconfig.spec.json @@ -3,8 +3,11 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/spec", - "types": ["jest", "node"], - "esModuleInterop": true + "types": ["vitest/globals", "node"], + "esModuleInterop": true, + "skipLibCheck": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true }, - "include": ["jest.config.ts", "**/*.spec.ts", "**/*.d.ts"] + "include": ["**/*.spec.ts", "**/*.d.ts"] } diff --git a/projects/playground/jest.config.ts b/projects/playground/jest.config.ts deleted file mode 100644 index c9f39a07..00000000 --- a/projects/playground/jest.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createCjsPreset } from 'jest-preset-angular/presets/index.js'; -/* eslint-disable */ -export default { - displayName: 'playground', - ...createCjsPreset(), - setupFilesAfterEnv: ['/src/test-setup.ts'], - snapshotSerializers: [ - 'jest-preset-angular/build/serializers/no-ng-attributes', - 'jest-preset-angular/build/serializers/ng-snapshot', - 'jest-preset-angular/build/serializers/html-comment', - ], - testPathIgnorePatterns: ['/e2e'], - collectCoverage: false, - coveragePathIgnorePatterns: ['/src'], -}; diff --git a/projects/playground/src/app/app.component.spec.ts b/projects/playground/src/app/app.component.spec.ts index 02d279fa..63ecca0a 100644 --- a/projects/playground/src/app/app.component.spec.ts +++ b/projects/playground/src/app/app.component.spec.ts @@ -1,3 +1,8 @@ +// Each spec file runs in its own jsdom window; zone.js from test-setup.ts +// patched a different window, so it must be imported here too. +import 'zone.js'; +import 'zone.js/testing'; +import '@angular/compiler'; import { TestBed, ComponentFixture } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { AppComponent } from './app.component'; @@ -8,7 +13,18 @@ import { provideHttpClient, withInterceptorsFromDi, } from '@angular/common/http'; -import { expect } from '@jest/globals'; +import type { MockInstance } from 'vitest'; +import { + BrowserTestingModule, + platformBrowserTesting, +} from '@angular/platform-browser/testing'; + +// The forks pool loads setupFiles in a separate module-cache context from spec +// files, so initTestEnvironment called in test-setup.ts targets a different +// TestBed instance. Initialize the environment here in the spec's own context. +beforeAll(() => { + TestBed.initTestEnvironment(BrowserTestingModule, platformBrowserTesting()); +}); describe('AppComponent', () => { let authMock: AuthService; @@ -18,11 +34,11 @@ describe('AppComponent', () => { beforeEach(() => { authMock = { - loginWithRedirect: jest.fn().mockReturnValue(null), - loginWithPopup: jest.fn().mockReturnValue(null), - logout: jest.fn().mockReturnValue(null), - getAccessTokenSilently: jest.fn().mockReturnValue(null), - getAccessTokenWithPopup: jest.fn().mockReturnValue(null), + loginWithRedirect: vi.fn().mockReturnValue(null), + loginWithPopup: vi.fn().mockReturnValue(null), + logout: vi.fn().mockReturnValue(null), + getAccessTokenSilently: vi.fn().mockReturnValue(null), + getAccessTokenWithPopup: vi.fn().mockReturnValue(null), user$: new BehaviorSubject(null), isLoading$: new BehaviorSubject(true), @@ -193,7 +209,7 @@ describe('AppComponent', () => { const form = component.accessTokenOptionsForm.controls; form['usePopup'].setValue(false); ( - authMock.getAccessTokenSilently as unknown as jest.SpyInstance + authMock.getAccessTokenSilently as unknown as MockInstance ).mockReturnValue(of('access token silently')); const btnRefresh = divToken.querySelector('button'); @@ -216,7 +232,7 @@ describe('AppComponent', () => { form['usePopup'].setValue(false); form['ignoreCache'].setValue(false); ( - authMock.getAccessTokenSilently as unknown as jest.SpyInstance + authMock.getAccessTokenSilently as unknown as MockInstance ).mockReturnValue(of('access token silently')); const btnRefresh = divToken.querySelector('button'); @@ -239,7 +255,7 @@ describe('AppComponent', () => { form['usePopup'].setValue(false); form['ignoreCache'].setValue(true); ( - authMock.getAccessTokenSilently as unknown as jest.SpyInstance + authMock.getAccessTokenSilently as unknown as MockInstance ).mockReturnValue(of('access token silently')); const btnRefresh = divToken.querySelector('button'); @@ -261,7 +277,7 @@ describe('AppComponent', () => { const form = component.accessTokenOptionsForm.controls; form['usePopup'].setValue(true); ( - authMock.getAccessTokenWithPopup as unknown as jest.SpyInstance + authMock.getAccessTokenWithPopup as unknown as MockInstance ).mockReturnValue(of('access token popup')); const btnRefresh = divToken.querySelector('button'); diff --git a/projects/playground/src/test-setup.ts b/projects/playground/src/test-setup.ts deleted file mode 100644 index 58c511e0..00000000 --- a/projects/playground/src/test-setup.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; - -setupZoneTestEnv(); diff --git a/projects/playground/tsconfig.spec.json b/projects/playground/tsconfig.spec.json index af883d47..a4d16530 100644 --- a/projects/playground/tsconfig.spec.json +++ b/projects/playground/tsconfig.spec.json @@ -3,8 +3,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/spec", - "types": ["jest", "node"], + "types": ["vitest/globals", "node"], "esModuleInterop": true }, - "include": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] + "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] } diff --git a/references/testing.md b/references/testing.md index bf05ba6d..2e31b15d 100644 --- a/references/testing.md +++ b/references/testing.md @@ -2,27 +2,29 @@ ## Framework -Jest 29 with `jest-preset-angular`. Config: `projects/auth0-angular/jest.config.ts` (delegates to the root `jest.config.ts`). +Vitest 3 with `@analogjs/vite-plugin-angular` (JIT mode). Config: `vitest.config.ts` at the repo root. ## Test Location Unit tests are co-located with source files: + ```text projects/auth0-angular/src/lib/auth.service.spec.ts projects/auth0-angular/src/lib/auth.guard.spec.ts projects/auth0-angular/src/lib/auth.interceptor.spec.ts projects/auth0-angular/src/lib/auth.client.spec.ts projects/auth0-angular/src/lib/auth.config.spec.ts +projects/auth0-angular/src/lib/abstract-navigator.spec.ts ``` ## Unit Test Commands ```bash npm run test:ci # With coverage, silent (CI) -npm test # Local, no coverage flag +npm test # Local, watch mode # Single spec file -npx jest projects/auth0-angular/src/lib/auth.service.spec.ts +npx vitest run projects/auth0-angular/src/lib/auth.service.spec.ts ``` The default `npm run test:ci` suite is unit-only — no credentials required. @@ -31,26 +33,36 @@ The default `npm run test:ci` suite is unit-only — no credentials required. Reports land in `coverage/auth0-angular/`. Codecov uploads automatically in CI (`.github/workflows/test.yml`). +## Vitest Config Notes + +`vitest.config.ts` uses `pool: 'forks'` with `environment: 'jsdom'`. Two non-obvious constraints apply: + +1. **Module-cache isolation**: Vitest clears the Vite module runner cache between `setupFiles` and each spec file. This means `TestBed.initTestEnvironment` called in `test-setup.ts` operates on a different `TestBed` instance than the one the spec file imports. Every spec file that uses `TestBed` must call `TestBed.initTestEnvironment` itself in a top-level `beforeAll`. + +2. **Per-file jsdom window**: Each spec file gets its own jsdom `window`. `zone.js` imported in `test-setup.ts` patches only that file's window. Every spec file that uses `TestBed` must import `zone.js`, `zone.js/testing`, and `@angular/compiler` at its own top level so they patch its window before Angular runs. + +The `test-setup.ts` still handles `reflect-metadata` and `cross-fetch/polyfill` globally. + ## Conventions - Tests use Angular `TestBed` to configure the DI context - `Auth0Client` is instantiated as a real object then spied on: ```typescript auth0Client = new Auth0Client({ domain: '', clientId: '' }); - jest.spyOn(auth0Client, 'checkSession').mockResolvedValue(); + vi.spyOn(auth0Client, 'checkSession').mockResolvedValue(); ``` - The `loaded()` helper filters `isLoading$` to wait for the SDK to finish initializing before asserting: ```typescript - const loaded = (service: AuthService) => - service.isLoading$.pipe(filter((loading) => !loading)); + const loaded = (service: AuthService) => service.isLoading$.pipe(filter((loading) => !loading)); ``` -- Use `fakeAsync` / `tick()` for promise-based async assertions -- Use `bufferCount` or `bufferTime` to collect multiple emissions from an Observable before asserting +- Use plain `async`/`await` for async assertions — `fakeAsync`/`tick` are not available under Vitest +- Use the local `firstValueFrom` polyfill (defined at the top of `auth.service.spec.ts`) to await a single emission — it rejects with `EmptyError` on empty completion, matching RxJS 7 semantics (RxJS 6 `toPromise()` would silently resolve `undefined`) +- Use `bufferCount(n)` or `bufferTime(ms)` to collect multiple emissions before asserting ## Mocking - Inject `Auth0Client` via `Auth0ClientService` token; override the provider in `TestBed.configureTestingModule` -- Use `jest.spyOn` on the `Auth0Client` instance — not on the class +- Use `vi.spyOn` on the `Auth0Client` instance — not on the class - `AbstractNavigator` is provided as a mock that records `navigateByUrl` calls ## E2E Tests (Cypress) diff --git a/tsconfig.spec.json b/tsconfig.spec.json new file mode 100644 index 00000000..d9c66955 --- /dev/null +++ b/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": ["vitest/globals", "node"], + "esModuleInterop": true, + "skipLibCheck": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": [ + "projects/auth0-angular/src/**/*.spec.ts", + "projects/auth0-angular/src/**/*.d.ts", + "projects/playground/src/**/*.spec.ts", + "projects/playground/src/**/*.d.ts" + ] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..824f42dc --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,63 @@ +/// +import { defineConfig } from 'vite'; +import { resolve } from 'path'; +import type { Plugin } from 'vite'; +import angular from '@analogjs/vite-plugin-angular'; + +// Vitest loads Angular's ESM builds natively; rxjs/operators is a directory +// which Node ESM refuses to import — redirect it to the concrete index file. +const fixRxjsEsm: Plugin = { + name: 'fix-rxjs-esm', + enforce: 'pre', + resolveId(id) { + if (id === 'rxjs/operators') + return { + id: require.resolve('rxjs/operators/index.js'), + external: false, + }; + if (id === 'rxjs/ajax') + return { id: require.resolve('rxjs/ajax/index.js'), external: false }; + if (id === 'rxjs/fetch') + return { id: require.resolve('rxjs/fetch/index.js'), external: false }; + if (id === 'rxjs/testing') + return { id: require.resolve('rxjs/testing/index.js'), external: false }; + if (id === 'rxjs/webSocket') + return { + id: require.resolve('rxjs/webSocket/index.js'), + external: false, + }; + }, +}; + +export default defineConfig({ + plugins: [ + fixRxjsEsm, + angular({ + tsconfig: resolve(__dirname, 'tsconfig.spec.json'), + jit: true, + }), + ], + test: { + globals: true, + pool: 'forks', + setupFiles: ['projects/auth0-angular/src/test-setup.ts'], + environment: 'jsdom', + environmentOptions: { + jsdom: { url: 'http://localhost/' }, + }, + include: [ + 'projects/auth0-angular/src/**/*.spec.ts', + 'projects/playground/src/**/*.spec.ts', + ], + server: { + deps: { + inline: [/zone\.js/, /@angular\//, /@auth0\//, /rxjs/], + }, + }, + coverage: { + provider: 'v8', + reportsDirectory: 'coverage/auth0-angular', + include: ['projects/auth0-angular/src/lib/**'], + }, + }, +});