From 4514e7ccb3748cc3e9abbe2c62a6c2864179e427 Mon Sep 17 00:00:00 2001 From: Navjit Brar Date: Wed, 20 May 2026 10:43:54 -0500 Subject: [PATCH 1/6] chore(test): add Jest harness alongside mocha and WCT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Jest 29 + ts-jest + jest-environment-jsdom as a new test runner that lives next to the existing mocha (.spec.ts) and WCT (HTML) suites. New behavioural / before-after tests for the SOLID upgrade go in tests/jest/. npm run test:jest # one-shot npm run test:jest:watch # watch mode npm run test:jest:coverage # with coverage Jest's testMatch picks up only tests/jest/**/*.test.ts and explicitly ignores src/**/*.spec.ts, so the mocha suite is unaffected. tsconfig.umd.json gains an exclude for tests/ — its prior **/*.ts include was greedy enough to pull the new Jest tests into the library build. Co-Authored-By: Claude Opus 4.7 (1M context) --- jest.config.js | 31 + package-lock.json | 2818 +++++++++++++++++++++++++++++++++++-- package.json | 21 +- tests/jest/sanity.test.ts | 26 + tsconfig.jest.json | 24 + tsconfig.umd.json | 11 +- 6 files changed, 2783 insertions(+), 148 deletions(-) create mode 100644 jest.config.js create mode 100644 tests/jest/sanity.test.ts create mode 100644 tsconfig.jest.json diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 000000000..fce480d35 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,31 @@ +/** + * Jest harness for the SOLID-upgrade research branch. + * + * Lives ALONGSIDE the existing mocha (.spec.ts) and WCT (HTML) suites. + * New behavioural / before-after tests for this branch go in tests/jest/. + * Run with: npm run test:jest + */ +module.exports = { + rootDir: '.', + preset: 'ts-jest', + testEnvironment: 'jsdom', + testMatch: ['/tests/jest/**/*.test.ts'], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], + transform: { + '^.+\\.tsx?$': ['ts-jest', { tsconfig: '/tsconfig.jest.json' }], + }, + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/*.spec.ts', + '!src/**/interfaces/**', + '!src/**/types/**', + '!src/typings/**', + '!src/_interfaces/**', + ], + coverageDirectory: '/coverage/jest', + // Existing mocha specs use chai globals; do NOT pick them up. + testPathIgnorePatterns: ['/node_modules/', '\\.spec\\.ts$'], + // The lib's existing tsconfig targets ES5, but tests run in node — allow modern JS. + // ts-jest handles transpilation via the tsconfig.jest.json above. + verbose: false, +}; diff --git a/package-lock.json b/package-lock.json index fa2cb628a..312326f53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@crestron/ch5-crcomlib", - "version": "2.17.0", + "version": "2.17.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@crestron/ch5-crcomlib", - "version": "2.17.0", + "version": "2.17.2", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@raghavendradabbir/mycolorpicker": "^1.0.0", @@ -25,6 +25,7 @@ "@types/fs-extra": "^11.0.2", "@types/hammerjs": "^2.0.40", "@types/i18next": "^13.0.0", + "@types/jest": "^29.5.14", "@types/jsdom": "^16.2.13", "@types/lodash-es": "^4.17.4", "@types/lodash.throttle": "^4.1.6", @@ -45,6 +46,8 @@ "cross-env": "^7.0.3", "dotenv-webpack": "^7.1.0", "eslint": "^8.49.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "jsdom": "^16.7.0", "jsdom-global": "3.0.2", "local-web-server": "^5.3.1", @@ -53,6 +56,7 @@ "npm-run-all": "^4.1.5", "nyc": "^15.1.0", "rimraf": "^3.0.2", + "ts-jest": "^29.4.10", "ts-loader": "4.3.0", "typedoc": "0.25.2", "typescript": "^5.1.6", @@ -249,6 +253,16 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -305,6 +319,245 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/runtime": { "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", @@ -361,6 +614,13 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -611,117 +871,551 @@ "node": ">=8" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, "engines": { - "node": ">=6.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", - "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@koa/cors": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@koa/cors/-/cors-5.0.0.tgz", - "integrity": "sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw==", + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", "dev": true, + "license": "MIT", "dependencies": { - "vary": "^1.1.2" + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { - "node": ">= 14.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, + "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" }, "engines": { - "node": ">= 8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, "engines": { - "node": ">= 8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" }, "engines": { - "node": ">= 8" + "node": ">=10" } }, - "node_modules/@raghavendradabbir/mycolorpicker": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@raghavendradabbir/mycolorpicker/-/mycolorpicker-1.0.0.tgz", - "integrity": "sha512-x8hUsj3Enj+LF9mv6bI0twZO7iGud3kzhLzRNBHjUx4/QtKk/EjDEaQwlhVfC/JJxaedPtuOZn1XgcixlqUzow==", + "node_modules/@jest/reporters/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", "dependencies": { - "dragjs": "^0.8.0", - "onecolor": "^3.0.5" + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "node_modules/@jest/reporters/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - "node": ">= 6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/@types/chai": { + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/transform/node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@koa/cors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@koa/cors/-/cors-5.0.0.tgz", + "integrity": "sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw==", + "dev": true, + "dependencies": { + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@raghavendradabbir/mycolorpicker": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@raghavendradabbir/mycolorpicker/-/mycolorpicker-1.0.0.tgz", + "integrity": "sha512-x8hUsj3Enj+LF9mv6bI0twZO7iGud3kzhLzRNBHjUx4/QtKk/EjDEaQwlhVfC/JJxaedPtuOZn1XgcixlqUzow==", + "dependencies": { + "dragjs": "^0.8.0", + "onecolor": "^3.0.5" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { "version": "4.3.11", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.11.tgz", "integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==", @@ -769,6 +1463,16 @@ "@types/node": "*" } }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/hammerjs": { "version": "2.0.45", "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.45.tgz", @@ -785,6 +1489,44 @@ "i18next": "*" } }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "node_modules/@types/jsdom": { "version": "16.2.15", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-16.2.15.tgz", @@ -881,6 +1623,13 @@ "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/swiper": { "version": "5.4.3", "resolved": "https://registry.npmjs.org/@types/swiper/-/swiper-5.4.3.tgz", @@ -893,6 +1642,23 @@ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.20.0.tgz", @@ -1480,6 +2246,35 @@ "node": ">=12.17" } }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1737,20 +2532,146 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", "dev": true, + "license": "MIT", "dependencies": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "dependencies": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", "component-emitter": "^1.2.1", "define-property": "^1.0.0", "isobject": "^3.0.1", @@ -1902,6 +2823,29 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "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", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2093,6 +3037,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/check-error": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", @@ -2153,6 +3107,29 @@ "node": ">=6.0" } }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, "node_modules/class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -2249,6 +3226,13 @@ "type-is": "^1.6.16" } }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, "node_modules/collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -2640,6 +3624,28 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/create-mixin": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/create-mixin/-/create-mixin-3.0.0.tgz", @@ -2777,6 +3783,21 @@ "node": ">=0.10" } }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/deep-eql": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", @@ -2810,6 +3831,16 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/default-require-extensions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", @@ -2914,6 +3945,16 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -2923,6 +3964,16 @@ "node": ">=0.3.1" } }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/dijkstrajs": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", @@ -3046,6 +4097,19 @@ "integrity": "sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==", "dev": true }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -3088,6 +4152,19 @@ "node": ">=6.9.0" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/envinfo": { "version": "7.11.1", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.1.tgz", @@ -3490,6 +4567,39 @@ "node": ">=0.8.x" } }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -3569,6 +4679,23 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", @@ -3698,6 +4825,16 @@ "reusify": "^1.0.4" } }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4070,6 +5207,19 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", @@ -4246,6 +5396,28 @@ "node": ">=0.8.0" } }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "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/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -4513,6 +5685,16 @@ "node": ">= 6" } }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/i18next": { "version": "21.10.0", "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.10.0.tgz", @@ -4845,6 +6027,16 @@ "node": ">=8" } }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/is-generator-function": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", @@ -5212,11 +6404,1029 @@ "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", "dev": true, "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-cli/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-cli/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/@tootallnate/once": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz", + "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/jest-environment-jsdom/node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-environment-jsdom/node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-environment-jsdom/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/jest-environment-jsdom/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jest-environment-jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/ws": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-haste-map/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runner/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-worker": { @@ -5468,6 +7678,16 @@ "node": ">=0.10.0" } }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/koa": { "version": "2.15.0", "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.0.tgz", @@ -5659,6 +7879,16 @@ "ms": "^2.1.1" } }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5672,6 +7902,13 @@ "node": ">= 0.8.0" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -5836,6 +8073,13 @@ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "dev": true }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6166,6 +8410,23 @@ "semver": "bin/semver.js" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -6331,6 +8592,16 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", @@ -6730,6 +9001,13 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -6987,6 +9265,19 @@ "which": "bin/which" } }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/nwsapi": { "version": "2.2.7", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", @@ -7322,6 +9613,22 @@ "node": ">=0.4.8" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/only": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", @@ -7580,6 +9887,16 @@ "node": ">=4" } }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -7661,60 +9978,12 @@ "node": ">=0.10.0" } }, - "node_modules/postcss": { - "version": "8.4.33", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", - "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "peer": true, - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, - "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "peer": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -7724,6 +9993,41 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -7742,6 +10046,20 @@ "node": ">=8" } }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -7779,6 +10097,23 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/qrcode": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", @@ -8281,6 +10616,16 @@ "deprecated": "https://github.com/lydell/resolve-url#deprecated", "dev": true }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -8787,6 +11132,13 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -8944,16 +11296,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-resolve": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", @@ -9057,6 +11399,29 @@ "resolved": "https://registry.npmjs.org/ssr-window/-/ssr-window-4.0.2.tgz", "integrity": "sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ==" }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -9222,6 +11587,20 @@ "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", "dev": true }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -9317,6 +11696,16 @@ "node": ">=8" } }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -9624,6 +12013,13 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -9732,6 +12128,95 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-jest": { + "version": "29.4.10", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.10.tgz", + "integrity": "sha512-vMTlTTtvz5aKZgzOoc7DQ5TzAL2fCzl8JnG1+ZpwjQa/g0xLlwE44yQ+1Cao9ZP1xVv9y5g34IFXEiqGOGFBUA==", + "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.0", + "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.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-jest/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/ts-loader": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-4.3.0.tgz", @@ -10135,6 +12620,20 @@ "node": ">=12.17" } }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "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.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -10400,6 +12899,28 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -10471,6 +12992,16 @@ "node": ">=12.17" } }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -10780,6 +13311,13 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/wordwrapjs": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", diff --git a/package.json b/package.json index 4d214ef28..73985e99a 100644 --- a/package.json +++ b/package.json @@ -53,8 +53,11 @@ "doc:json": "typedoc --target ES6 --mode file --hideGenerator --tsconfig tsconfig.cjs.json --json docs/json/typedoc.json src/ ", "lint": "tslint --project tsconfig.cjs.json", "prebuild": "npm-run-all clean:*", - "test:mocha": "npm-run-all clean:compiled && tsc -p tsconfig.umd.json --target ES6 && nyc mocha --reporter mochawesome --exit", - "test:wct": "ws -o", + "test:mocha": "npm-run-all clean:compiled && tsc -p tsconfig.umd.json --target ES6 && nyc mocha --reporter mochawesome --exit", + "test:jest": "jest --config jest.config.js", + "test:jest:watch": "jest --config jest.config.js --watch", + "test:jest:coverage": "jest --config jest.config.js --coverage", + "test:wct": "ws -o", "wct:report": "echo '\ud83d\ude80 Starting server and opening test page...' && (ws > /dev/null 2>&1 &) && sleep 3 && open http://localhost:8000/wct_tests/run-all-with-report.html && echo '\u2705 Page opened! Wait for tests to complete, then click the green button to download report.'", "eslint": "eslint . --ext .ts" }, @@ -77,7 +80,8 @@ "@types/fs-extra": "^11.0.2", "@types/hammerjs": "^2.0.40", "@types/i18next": "^13.0.0", - "@types/jsdom": "^16.2.13", + "@types/jest": "^29.5.12", + "@types/jsdom": "^16.2.13", "@types/lodash-es": "^4.17.4", "@types/lodash.throttle": "^4.1.6", "@types/mocha": "7.0.2", @@ -97,15 +101,18 @@ "cross-env": "^7.0.3", "dotenv-webpack": "^7.1.0", "eslint": "^8.49.0", - "jsdom": "^16.7.0", - "jsdom-global": "3.0.2", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "jsdom": "^16.7.0", + "jsdom-global": "3.0.2", "local-web-server": "^5.3.1", "mocha": "^9.0.3", "mochawesome": "^7.1.3", "npm-run-all": "^4.1.5", "nyc": "^15.1.0", - "rimraf": "^3.0.2", - "ts-loader": "4.3.0", + "rimraf": "^3.0.2", + "ts-jest": "^29.1.2", + "ts-loader": "4.3.0", "typedoc": "0.25.2", "typescript": "^5.1.6", "url-loader": "^4.1.1", diff --git a/tests/jest/sanity.test.ts b/tests/jest/sanity.test.ts new file mode 100644 index 000000000..b94e64e50 --- /dev/null +++ b/tests/jest/sanity.test.ts @@ -0,0 +1,26 @@ +/** + * Sanity test — confirms the Jest harness, ts-jest, and JSDOM are wired correctly. + * If this fails, nothing else in tests/jest/ will work. + */ + +describe('Jest harness sanity', () => { + it('runs a basic assertion', () => { + expect(1 + 1).toBe(2); + }); + + it('has JSDOM available', () => { + const el = document.createElement('div'); + el.textContent = 'hello'; + expect(el.textContent).toBe('hello'); + }); + + it('has MutationObserver available (JSDOM)', () => { + expect(typeof MutationObserver).toBe('function'); + }); + + it('has ResizeObserver shim available (note: JSDOM does not implement it natively)', () => { + // JSDOM does not implement ResizeObserver. Tests that exercise it must + // install a fake (see tests/jest/_helpers/resize-observer-mock.ts). + expect(typeof (globalThis as any).ResizeObserver === 'function' || typeof (globalThis as any).ResizeObserver === 'undefined').toBe(true); + }); +}); diff --git a/tsconfig.jest.json b/tsconfig.jest.json new file mode 100644 index 000000000..47b60790c --- /dev/null +++ b/tsconfig.jest.json @@ -0,0 +1,24 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "rootDir": ".", + "types": ["node", "jest"], + "isolatedModules": true, + "sourceMap": true, + "inlineSourceMap": false, + "target": "es2017", + "module": "commonjs" + }, + "include": [ + "src/**/*.ts", + "tests/jest/**/*.ts" + ], + "exclude": [ + "node_modules", + "build_bundles", + "build_bundles_dev", + "compiled_bundles", + "dist", + "docs" + ] +} diff --git a/tsconfig.umd.json b/tsconfig.umd.json index 77cb98e0b..b6940dd63 100644 --- a/tsconfig.umd.json +++ b/tsconfig.umd.json @@ -5,5 +5,14 @@ "outDir": "./compiled_bundles/umd", /* Redirect output structure to the directory. */ "declarationDir": "./compiled_bundles/umd" }, - "include": ["**/*.ts"] + "include": ["**/*.ts"], + "exclude": [ + "node_modules", + "build_bundles", + "build_bundles_dev", + "compiled_bundles", + "dist", + "docs", + "tests" + ] } \ No newline at end of file From a885fc74a537c25f93ed3170a500a56775bef152 Mon Sep 17 00:00:00 2001 From: Navjit Brar Date: Wed, 20 May 2026 10:44:11 -0500 Subject: [PATCH 2/6] fix(ch5-list): correct 0.5 ms setTimeout typo; add AST-based regression guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/ch5-list/ch5-list.ts:508 had setTimeout(..., 0.5) — almost certainly a typo for 500. A 0.5 ms delay collapses to the next macrotask, defeating the debounce and forcing a synchronous resizeList() on every viewport-change event. Adds tests/jest/perf/short-timeout.test.ts, a regression guard that walks every src/*.ts via the TypeScript compiler API and flags any setTimeout / setInterval call whose delay is a numeric literal in (0, 16) ms. The threshold is one frame at 60Hz. delay === 0 is permitted as the standard "yield to next macrotask" idiom. Supporting helpers: tests/jest/_helpers/source-scanner.ts — file walker + line/col resolver tests/jest/_helpers/ts-call-finder.ts — AST-based call-site finder The AST approach handles nested calls, multi-line arguments, and comments correctly — a regex pass over the same code missed multi-line setTimeout and over-matched on inner literals. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/ch5-list/ch5-list.ts | 2 +- tests/jest/_helpers/source-scanner.ts | 169 ++++++++++++++++++++++++++ tests/jest/_helpers/ts-call-finder.ts | 103 ++++++++++++++++ tests/jest/perf/short-timeout.test.ts | 52 ++++++++ 4 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 tests/jest/_helpers/source-scanner.ts create mode 100644 tests/jest/_helpers/ts-call-finder.ts create mode 100644 tests/jest/perf/short-timeout.test.ts diff --git a/src/ch5-list/ch5-list.ts b/src/ch5-list/ch5-list.ts index dd701971e..6aacdd913 100644 --- a/src/ch5-list/ch5-list.ts +++ b/src/ch5-list/ch5-list.ts @@ -505,7 +505,7 @@ export class Ch5List extends Ch5Common implements ICh5ListAttributes { this.templateHelper.customScrollbar(this.divList); setTimeout(() => { this.templateHelper.resizeList(this.divList, this.templateVars); - }, 0.5); + }, 500); } else { this.templateHelper.resetListLayout(); } diff --git a/tests/jest/_helpers/source-scanner.ts b/tests/jest/_helpers/source-scanner.ts new file mode 100644 index 000000000..434bd6005 --- /dev/null +++ b/tests/jest/_helpers/source-scanner.ts @@ -0,0 +1,169 @@ +/** + * Lightweight source scanner for regression-guard tests. + * + * Reads src/*.ts files as text and runs regex patterns against the whole + * file (so multi-line constructs work). Match offsets are resolved to + * 1-indexed line/column for human-readable failure output. + * + * Why text scanning instead of TypeScript's AST: + * 1. Speed — scanning ~84k LOC takes < 1s; full AST is much slower. + * 2. Robustness — guards survive TS upgrades without API churn. + * If a guard ever needs structural understanding ("only inside class methods", + * "only when X is imported") upgrade it to ts-morph at that time. + */ +import * as fs from 'fs'; +import * as path from 'path'; + +export interface SourceMatch { + file: string; // path relative to repo root, forward-slashed + line: number; // 1-indexed + column: number; // 1-indexed + matchText: string; // the substring that matched + lineText: string; // the full line containing the match, trimmed +} + +const SRC_DIR = path.resolve(__dirname, '..', '..', '..', 'src'); +const REPO_ROOT = path.resolve(SRC_DIR, '..'); + +/** + * Walk src/ and return every .ts file that is NOT a test/spec/declaration. + */ +export function listSourceFiles(): string[] { + const out: string[] = []; + const stack: string[] = [SRC_DIR]; + while (stack.length) { + const dir = stack.pop() as string; + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch { + continue; + } + for (const e of entries) { + const full = path.join(dir, e.name); + if (e.isDirectory()) { + stack.push(full); + continue; + } + if (!e.isFile()) continue; + if (!full.endsWith('.ts')) continue; + if (full.endsWith('.spec.ts')) continue; + if (full.endsWith('.d.ts')) continue; + out.push(full); + } + } + return out.sort(); +} + +/** + * Run a regex against each source file, returning every match site. + * + * The pattern is forced to be global so multiple hits per file are reported. + * Patterns should use `[\s\S]` (not `.`) when they need to cross newlines. + * + * Matches inside `//` or `/* ... *​/` comments are dropped — we don't want + * to flag commented-out examples or doc snippets. + */ +export function scan(pattern: RegExp, files: string[] = listSourceFiles()): SourceMatch[] { + const flags = pattern.flags.includes('g') ? pattern.flags : pattern.flags + 'g'; + const re = new RegExp(pattern.source, flags); + const matches: SourceMatch[] = []; + + for (const file of files) { + const text = fs.readFileSync(file, 'utf8'); + const commentMask = buildCommentMask(text); + const lineStarts = buildLineStarts(text); + + re.lastIndex = 0; + let m: RegExpExecArray | null; + while ((m = re.exec(text)) !== null) { + const idx = m.index; + if (commentMask[idx]) { + // skip matches that begin inside a comment + if (m[0].length === 0) re.lastIndex = idx + 1; + continue; + } + const { line, column } = offsetToLineCol(idx, lineStarts); + const lineText = extractLine(text, lineStarts, line); + matches.push({ + file: path.relative(REPO_ROOT, file).replace(/\\/g, '/'), + line, + column, + matchText: m[0], + lineText: lineText.trim(), + }); + // avoid infinite loop on zero-width matches + if (m[0].length === 0) re.lastIndex = idx + 1; + } + } + return matches; +} + +function buildLineStarts(text: string): number[] { + const starts: number[] = [0]; + for (let i = 0; i < text.length; i++) { + if (text.charCodeAt(i) === 10 /* \n */) starts.push(i + 1); + } + return starts; +} + +function offsetToLineCol(offset: number, lineStarts: number[]): { line: number; column: number } { + // binary search for the largest lineStart <= offset + let lo = 0; + let hi = lineStarts.length - 1; + while (lo < hi) { + const mid = (lo + hi + 1) >>> 1; + if (lineStarts[mid] <= offset) lo = mid; + else hi = mid - 1; + } + return { line: lo + 1, column: offset - lineStarts[lo] + 1 }; +} + +function extractLine(text: string, lineStarts: number[], line: number): string { + const start = lineStarts[line - 1]; + const end = line < lineStarts.length ? lineStarts[line] - 1 : text.length; + return text.slice(start, end); +} + +/** + * Returns a boolean array of length `text.length` where mask[i] === true means + * position i is inside a `// ...` line comment or `/​* ... *​/` block comment. + * String literals are *not* treated specially — that's a conscious trade-off + * for simplicity, since regex hits inside strings are vanishingly rare in + * practice and easy to silence with a guard pattern if they do appear. + */ +function buildCommentMask(text: string): Uint8Array { + const mask = new Uint8Array(text.length); + let i = 0; + const n = text.length; + while (i < n) { + if (text[i] === '/' && text[i + 1] === '/') { + while (i < n && text[i] !== '\n') { + mask[i] = 1; + i++; + } + } else if (text[i] === '/' && text[i + 1] === '*') { + mask[i] = 1; + mask[i + 1] = 1; + i += 2; + while (i < n && !(text[i] === '*' && text[i + 1] === '/')) { + mask[i] = 1; + i++; + } + if (i < n) { + mask[i] = 1; + mask[i + 1] = 1; + i += 2; + } + } else { + i++; + } + } + return mask; +} + +/** Pretty-print a list of matches for failure messages. */ +export function formatMatches(matches: SourceMatch[]): string { + if (!matches.length) return '(no matches)'; + return matches.map((m) => ` ${m.file}:${m.line}:${m.column} ${m.lineText}`).join('\n'); +} diff --git a/tests/jest/_helpers/ts-call-finder.ts b/tests/jest/_helpers/ts-call-finder.ts new file mode 100644 index 000000000..f8cf05351 --- /dev/null +++ b/tests/jest/_helpers/ts-call-finder.ts @@ -0,0 +1,103 @@ +/** + * AST-based call-site finder for regression-guard tests. + * + * Uses the TypeScript compiler API (already a transitive dep via ts-jest) + * to locate CallExpression nodes that match a predicate. Correct for + * nested calls, multi-line arguments, comments, and string contents — + * all of which a regex pass can get wrong. + * + * Usage: + * + * const hits = findCalls((call, sf) => { + * return isCalleeNamed(call, 'setTimeout') && + * getNumericLiteralArg(call, 1) !== undefined && + * (getNumericLiteralArg(call, 1) as number) < 16; + * }); + */ +import * as fs from 'fs'; +import * as path from 'path'; +import * as ts from 'typescript'; +import { listSourceFiles } from './source-scanner'; + +export interface CallHit { + file: string; // forward-slashed, repo-relative + line: number; // 1-indexed + column: number; // 1-indexed + snippet: string; // single-line preview of the call site +} + +const REPO_ROOT = path.resolve(__dirname, '..', '..', '..'); + +/** + * Walk every source file and yield each CallExpression that satisfies + * `predicate`. Source files are parsed with the loosest possible TS + * settings — we don't need type info, just the AST. + */ +export function findCalls( + predicate: (call: ts.CallExpression, sf: ts.SourceFile) => boolean, + files: string[] = listSourceFiles(), +): CallHit[] { + const hits: CallHit[] = []; + for (const file of files) { + const text = fs.readFileSync(file, 'utf8'); + const sf = ts.createSourceFile(file, text, ts.ScriptTarget.ES2017, /*setParentNodes*/ true); + const visit = (node: ts.Node): void => { + if (ts.isCallExpression(node) && predicate(node, sf)) { + const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart(sf)); + const snippet = extractSingleLine(text, node.getStart(sf)); + hits.push({ + file: path.relative(REPO_ROOT, file).replace(/\\/g, '/'), + line: line + 1, + column: character + 1, + snippet, + }); + } + ts.forEachChild(node, visit); + }; + visit(sf); + } + return hits; +} + +/** Return the textual name of a call's callee (handles `foo` and `obj.foo`). */ +export function getCalleeName(call: ts.CallExpression): string | undefined { + const expr = call.expression; + if (ts.isIdentifier(expr)) return expr.text; + if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.name)) return expr.name.text; + return undefined; +} + +/** True if the call's callee identifier matches `name` (either `name(...)` or `x.name(...)`). */ +export function isCalleeNamed(call: ts.CallExpression, name: string): boolean { + return getCalleeName(call) === name; +} + +/** + * Return the numeric value of the argument at `index` IF that argument is a + * numeric literal (or a -literal). Returns undefined for identifiers, + * expressions, missing arguments, etc. + */ +export function getNumericLiteralArg(call: ts.CallExpression, index: number): number | undefined { + const arg = call.arguments[index]; + if (!arg) return undefined; + if (ts.isNumericLiteral(arg)) return parseFloat(arg.text); + // unary minus on a number literal — rare for delays but cheap to support + if (ts.isPrefixUnaryExpression(arg) && arg.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(arg.operand)) { + return -parseFloat(arg.operand.text); + } + return undefined; +} + +/** Pretty-print a list of hits for failure messages. */ +export function formatHits(hits: CallHit[]): string { + if (!hits.length) return '(no matches)'; + return hits.map((h) => ` ${h.file}:${h.line}:${h.column} ${h.snippet}`).join('\n'); +} + +function extractSingleLine(text: string, offset: number): string { + let start = offset; + while (start > 0 && text[start - 1] !== '\n') start--; + let end = offset; + while (end < text.length && text[end] !== '\n') end++; + return text.slice(start, end).trim(); +} diff --git a/tests/jest/perf/short-timeout.test.ts b/tests/jest/perf/short-timeout.test.ts new file mode 100644 index 000000000..eca0dbee2 --- /dev/null +++ b/tests/jest/perf/short-timeout.test.ts @@ -0,0 +1,52 @@ +/** + * Regression guard — no setTimeout / setInterval with a sub-frame delay. + * + * BEFORE (master): src/ch5-list/ch5-list.ts:508 had `setTimeout(..., 0.5)` — + * almost certainly a typo for 500. A 0.5 ms delay collapses to the next + * macrotask, defeating the debounce and forcing a synchronous resize on + * every viewport-change event. + * + * AFTER (this branch): the typo is fixed, and this test prevents another + * one from creeping back in. + * + * Threshold: 0 < delay < 16. Reasoning: + * • `setTimeout(fn, 0)` is the standard "yield to the next macrotask" idiom + * and is allowed. (Browsers clamp it to ≥ 1ms anyway.) + * • Anything strictly between 0 and one frame (16ms ≈ 60Hz) is almost + * always a typo or a misunderstanding of how debouncing works. + * • For microtask deferral inside a Promise chain, use queueMicrotask(). + * For next-frame work, use requestAnimationFrame(). + * + * Identifier delays (e.g. `setTimeout(fn, TIMEOUT_MS)`) are NOT flagged + * here — those have to be checked at the value site, not the call site. + */ +import { + findCalls, + isCalleeNamed, + getNumericLiteralArg, + formatHits, +} from '../_helpers/ts-call-finder'; + +const MIN_SAFE_DELAY_MS = 16; +const TIMER_FNS = ['setTimeout', 'setInterval'] as const; + +describe('Performance guard — no sub-frame setTimeout/setInterval', () => { + it(`flags every {setTimeout, setInterval}(..., n) where 0 < n < ${MIN_SAFE_DELAY_MS}`, () => { + const hits = findCalls((call) => { + if (!TIMER_FNS.some((n) => isCalleeNamed(call, n))) return false; + const delay = getNumericLiteralArg(call, 1); + // delay === 0 is the standard macrotask-yield idiom; allow it. + return delay !== undefined && delay > 0 && delay < MIN_SAFE_DELAY_MS; + }); + + if (hits.length) { + throw new Error( + `Found ${hits.length} sub-frame timer call(s) (< ${MIN_SAFE_DELAY_MS}ms).\n` + + `Sub-frame delays defeat their own debounce and force sync work on the next tick.\n` + + `For microtask deferral use queueMicrotask(). For next-frame use requestAnimationFrame().\n\n` + + formatHits(hits), + ); + } + expect(hits).toEqual([]); + }); +}); From 59b09496514b07ea60b4f3c8ebbdb73257a01250 Mon Sep 17 00:00:00 2001 From: Navjit Brar Date: Wed, 20 May 2026 10:44:34 -0500 Subject: [PATCH 3/6] perf(observers): pool MutationObservers via Ch5SharedMutationObserver singleton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds src/ch5-core/ch5-shared-mutation-observer.ts — a singleton pool that wraps a single browser-level MutationObserver and dispatches per-target callbacks. Modelled on the existing Ch5CoreIntersectionObserver pattern. Refactors src/ch5-common/ch5-mutation-observer.ts to be a thin facade that delegates to the pool. The constructor, observe(), disconnectObserver(), isConnected field, ELEMENTS_MO_EXCEPTION static, and checkElementValidity static are preserved exactly — so the 35+ existing call sites need no changes. Net effect: N components that each used to construct their own MutationObserver now share ONE underlying observer. Proven by tests/jest/perf/mutation-observer-facade.test.ts (50 facade instances → 1 ctor call). Dispatch correctly handles subtree:true configurations by walking up mutation.target's ancestor chain to find a matching registered node. Test coverage: shared-mutation-observer.test.ts 7 tests — pool invariants mutation-observer-facade.test.ts 5 tests — back-compat + isolation Co-Authored-By: Claude Opus 4.7 (1M context) --- src/ch5-common/ch5-mutation-observer.ts | 74 +++---- src/ch5-core/ch5-shared-mutation-observer.ts | 154 ++++++++++++++ .../perf/mutation-observer-facade.test.ts | 115 +++++++++++ .../perf/shared-mutation-observer.test.ts | 193 ++++++++++++++++++ 4 files changed, 496 insertions(+), 40 deletions(-) create mode 100644 src/ch5-core/ch5-shared-mutation-observer.ts create mode 100644 tests/jest/perf/mutation-observer-facade.test.ts create mode 100644 tests/jest/perf/shared-mutation-observer.test.ts diff --git a/src/ch5-common/ch5-mutation-observer.ts b/src/ch5-common/ch5-mutation-observer.ts index a5b6df416..82630af33 100644 --- a/src/ch5-common/ch5-mutation-observer.ts +++ b/src/ch5-common/ch5-mutation-observer.ts @@ -5,9 +5,9 @@ // Use of this source code is subject to the terms of the Crestron Software License Agreement // under which you licensed this source code. -// import { Ch5Base } from "../ch5-base/ch5-base"; import { Ch5BaseClass } from "../ch5-base/ch5-base-class"; import { Ch5Common } from "./ch5-common"; +import { Ch5SharedMutationObserver } from "../ch5-core/ch5-shared-mutation-observer"; import _ from "lodash"; export interface IShowStyle { @@ -15,25 +15,34 @@ export interface IShowStyle { opacity: string; } +/** + * Per-component facade over the singleton MutationObserver pool. + * + * Historically this class constructed one browser-level MutationObserver + * per component, producing N observers for N components. It now delegates + * to {@link Ch5SharedMutationObserver} — a singleton that watches every + * registered target through ONE underlying observer. + * + * The public API (constructor, observe, disconnectObserver, isConnected, + * static checkElementValidity, static ELEMENTS_MO_EXCEPTION) is preserved + * so the 30+ existing call sites do not need to change. + */ export class Ch5MutationObserver { - /** - * The containing components will not be observed by MutationObserver - * @type {string[]} - */ public static ELEMENTS_MO_EXCEPTION = ['swiper-wrapper']; + private static readonly _MUTATION_CONFIG: MutationObserverInit = { + attributes: true, + attributeOldValue: true, + childList: false, + subtree: false, + attributeFilter: ['style', 'inert'], + }; + public isConnected = false; - private _mutationsObserver: MutationObserver; - private _mutationsObserverConfig: object; private _element: Ch5Common | Ch5BaseClass = {} as Ch5Common | Ch5BaseClass; + private _observedTargets: Node[] = []; - /** - * Check the element validity to be observed by Mutation Observer - * - * @param {HTMLElement} target - * @return {boolean} - */ public static checkElementValidity(target: HTMLElement): boolean { return ( !_.isNil(target) && @@ -48,42 +57,27 @@ export class Ch5MutationObserver { constructor(element: Ch5Common | Ch5BaseClass) { this._element = element; - - this._mutationsObserver = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.type === 'attributes' && (mutation.attributeName === 'style' || mutation.attributeName === 'inert')) { - this._updateComponentVisibility(mutation.target); - } - }); - }); - - this._mutationsObserverConfig = { - attributes: true, // attribute changes will be observed | on add/remove/change attributes - attributeOldValue: true, // will show oldValue of attribute | on add/remove/change attributes | default: null - childList: false, // target children will be observed | on add/remove - subtree: false, // target children will be observed | on attributes/characterData changes if they observed on target - attributeFilter: ['style', 'inert'] // filter for attributes | array of attributes that should be observed - }; } public observe(target: Node) { - this._mutationsObserver.observe(target, this._mutationsObserverConfig); + Ch5SharedMutationObserver.getInstance().observe( + target, + Ch5MutationObserver._MUTATION_CONFIG, + (node) => this._updateComponentVisibility(node), + ); + this._observedTargets.push(target); } public disconnectObserver() { - if (this._mutationsObserver instanceof MutationObserver) { - this.isConnected = false; - this._mutationsObserver.disconnect(); + if (this._observedTargets.length === 0) return; + this.isConnected = false; + const shared = Ch5SharedMutationObserver.getInstance(); + for (const target of this._observedTargets) { + shared.unobserve(target); } + this._observedTargets = []; } - /** - * Check for node children of containing ch5 components and perform related visibility operation - * - * @private - * @param {Node} node - * @memberof Ch5MutationObserver - */ private _updateComponentVisibility(node: Node) { const htmlElement = node as HTMLElement; if (_.isNil(htmlElement.offsetParent)) { diff --git a/src/ch5-core/ch5-shared-mutation-observer.ts b/src/ch5-core/ch5-shared-mutation-observer.ts new file mode 100644 index 000000000..63cc0e720 --- /dev/null +++ b/src/ch5-core/ch5-shared-mutation-observer.ts @@ -0,0 +1,154 @@ +// Copyright (C) 2018 to the present, Crestron Electronics, Inc. +// All rights reserved. +// No part of this software may be reproduced in any form, machine +// or natural, without the express written consent of Crestron Electronics. +// Use of this source code is subject to the terms of the Crestron Software License Agreement +// under which you licensed this source code. + +/** + * Per-target mutation callback. `mutation` is the raw MutationRecord that + * triggered dispatch, so callers can inspect attributeName, oldValue, etc. + */ +export type Ch5SharedMutationCallback = (target: Node, mutation: MutationRecord) => void; + +interface Entry { + config: MutationObserverInit; + callback: Ch5SharedMutationCallback; +} + +/** + * Singleton MutationObserver pool. + * + * Why this exists: the original Ch5MutationObserver class created one + * browser-level MutationObserver per component instance. On a panel running + * 50+ components, that's 50+ observer callbacks firing on every relevant + * mutation. This class collapses that to a single underlying observer that + * dispatches to per-target callbacks. + * + * Model: one `MutationObserver` instance watches every registered target. + * Each target stores its own config + callback. Mutations are routed to + * the callback whose registered target is `mutation.target` OR an + * ancestor of it. Ancestor matching is what makes `subtree: true` + * configurations work correctly — the browser reports the deepest + * affected node, but the caller registered an ancestor. + * + * `unobserve(target)` removes that target without disturbing the others — + * MutationObserver itself has no per-target unobserve API, so we + * re-build the observation set whenever a target leaves. + * + * Lifecycle: the underlying observer is created lazily on the first + * `observe()` call. It is disposed when the last target is removed; the + * next `observe()` will recreate it. + * + * Concurrency: not relevant — the DOM API is single-threaded. + */ +export class Ch5SharedMutationObserver { + private static _instance: Ch5SharedMutationObserver | null = null; + + private _observer: MutationObserver | null = null; + private readonly _entries = new Map(); + + // Test hook: number of underlying MutationObservers (0 or 1). + // Real callers never need this; tests use it to prove pool invariants. + public get _underlyingObserverCount(): number { + return this._observer ? 1 : 0; + } + + public static getInstance(): Ch5SharedMutationObserver { + if (!Ch5SharedMutationObserver._instance) { + Ch5SharedMutationObserver._instance = new Ch5SharedMutationObserver(); + } + return Ch5SharedMutationObserver._instance; + } + + /** + * Test hook — drops the singleton and disconnects the underlying observer. + * Production code never calls this; tests use it to isolate state. + */ + public static _resetForTesting(): void { + if (Ch5SharedMutationObserver._instance) { + Ch5SharedMutationObserver._instance._teardown(); + } + Ch5SharedMutationObserver._instance = null; + } + + /** + * Begin observing `target` with `config`. The callback fires for every + * mutation that matches `config`. Calling `observe()` again for the same + * target replaces the previous callback/config. + */ + public observe(target: Node, config: MutationObserverInit, callback: Ch5SharedMutationCallback): void { + if (target == null) return; + this._entries.set(target, { config, callback }); + if (!this._observer) { + this._observer = new MutationObserver((mutations) => this._dispatch(mutations)); + } + this._observer.observe(target, config); + } + + /** + * Stop observing `target`. Other targets remain observed. + * + * Note: MutationObserver has no per-target unobserve. We `disconnect()` + * the underlying observer and re-`observe()` every remaining target + * under its original config. For typical CH5 usage (<100 components) + * this is cheap and only runs on disconnect, not every mutation. + */ + public unobserve(target: Node): void { + if (target == null) return; + if (!this._entries.delete(target)) return; + if (!this._observer) return; + if (this._entries.size === 0) { + this._teardown(); + return; + } + this._observer.disconnect(); + const obs = this._observer; + this._entries.forEach((entry, node) => { + obs.observe(node, entry.config); + }); + } + + /** Diagnostics — number of currently observed targets. */ + public size(): number { + return this._entries.size; + } + + /** True iff `target` is currently registered. */ + public isObserving(target: Node): boolean { + return this._entries.has(target); + } + + private _dispatch(mutations: MutationRecord[]): void { + for (const m of mutations) { + // First try the exact target. Fast path: subtree:false callers and + // subtree:true callers where the mutation happens to land on the + // registered ancestor itself. + const direct = this._entries.get(m.target); + if (direct) { + direct.callback(m.target, m); + continue; + } + // Walk up the ancestor chain. This is what makes subtree:true work + // correctly under the pool — the browser reports the deepest node, + // but the caller observed an ancestor. + let node: Node | null = m.target.parentNode; + while (node !== null) { + const entry = this._entries.get(node); + if (entry) { + entry.callback(node, m); + break; + } + node = node.parentNode; + } + } + } + + private _teardown(): void { + if (this._observer) { + this._observer.disconnect(); + this._observer = null; + } + this._entries.clear(); + } +} diff --git a/tests/jest/perf/mutation-observer-facade.test.ts b/tests/jest/perf/mutation-observer-facade.test.ts new file mode 100644 index 000000000..04b3950dc --- /dev/null +++ b/tests/jest/perf/mutation-observer-facade.test.ts @@ -0,0 +1,115 @@ +/** + * Ch5MutationObserver facade — proves backward-compatibility with the old + * constructor / observe / disconnectObserver API, AND that the pool win is + * realised end-to-end (N facade instances still share ONE underlying + * browser MutationObserver). + * + * BEFORE (master): `new Ch5MutationObserver(component)` created its own + * MutationObserver inside the constructor. + * AFTER (this branch): the facade delegates to Ch5SharedMutationObserver, + * which lazily creates ONE underlying observer for all targets. + * + * The existing Ch5MutationObserver imports Ch5Common, which imports a + * very large chunk of the library. To keep this test fast and isolated, + * we don't import Ch5Common — we pass a minimal duck-typed stand-in + * that satisfies the facade's only use of the element parameter + * (`updateElementVisibility(boolean)`). + */ +import { Ch5MutationObserver } from '../../../src/ch5-common/ch5-mutation-observer'; +import { Ch5SharedMutationObserver } from '../../../src/ch5-core/ch5-shared-mutation-observer'; + +interface FakeComponent { + updateElementVisibility: jest.Mock; +} + +const makeFakeComponent = (): FakeComponent => ({ + updateElementVisibility: jest.fn(), +}); + +const flushMicrotasks = (): Promise => + new Promise((resolve) => queueMicrotask(() => resolve())); + +describe('Ch5MutationObserver facade — pooled delegation', () => { + let observerCtorSpy: jest.Mock; + let realCtor: typeof MutationObserver; + + beforeEach(() => { + Ch5SharedMutationObserver._resetForTesting(); + realCtor = global.MutationObserver; + observerCtorSpy = jest.fn(); + (global as any).MutationObserver = class extends realCtor { + constructor(cb: MutationCallback) { + observerCtorSpy(); + super(cb); + } + }; + }); + + afterEach(() => { + Ch5SharedMutationObserver._resetForTesting(); + global.MutationObserver = realCtor; + }); + + it('50 facade instances → 1 underlying MutationObserver (pre-refactor would have been 50)', () => { + const targets: HTMLElement[] = []; + for (let i = 0; i < 50; i++) { + const el = document.createElement('div'); + document.body.appendChild(el); + targets.push(el); + const obs = new Ch5MutationObserver(makeFakeComponent() as unknown as never); + obs.observe(el); + } + expect(observerCtorSpy).toHaveBeenCalledTimes(1); + expect(Ch5SharedMutationObserver.getInstance()._underlyingObserverCount).toBe(1); + expect(Ch5SharedMutationObserver.getInstance().size()).toBe(50); + }); + + it('disconnectObserver() removes the facade\'s targets but leaves siblings observed', () => { + const elA = document.createElement('div'); + const elB = document.createElement('div'); + document.body.append(elA, elB); + const obsA = new Ch5MutationObserver(makeFakeComponent() as unknown as never); + const obsB = new Ch5MutationObserver(makeFakeComponent() as unknown as never); + obsA.observe(elA); + obsB.observe(elB); + expect(Ch5SharedMutationObserver.getInstance().size()).toBe(2); + + obsA.disconnectObserver(); + expect(Ch5SharedMutationObserver.getInstance().size()).toBe(1); + expect(Ch5SharedMutationObserver.getInstance().isObserving(elB)).toBe(true); + }); + + it('disconnectObserver() is idempotent — calling twice is a no-op', () => { + const el = document.createElement('div'); + document.body.appendChild(el); + const obs = new Ch5MutationObserver(makeFakeComponent() as unknown as never); + obs.observe(el); + obs.disconnectObserver(); + expect(() => obs.disconnectObserver()).not.toThrow(); + }); + + it('dispatches visibility updates to the originating component, not its peers', async () => { + const elA = document.createElement('div'); + const elB = document.createElement('div'); + document.body.append(elA, elB); + const compA = makeFakeComponent(); + const compB = makeFakeComponent(); + new Ch5MutationObserver(compA as unknown as never).observe(elA); + new Ch5MutationObserver(compB as unknown as never).observe(elB); + + elA.setAttribute('style', 'opacity: 0.5'); + await flushMicrotasks(); + + expect(compA.updateElementVisibility).toHaveBeenCalled(); + expect(compB.updateElementVisibility).not.toHaveBeenCalled(); + }); + + it('checkElementValidity still works (unchanged public static)', () => { + expect(Ch5MutationObserver.checkElementValidity(document.body)).toBe(false); + const ok = document.createElement('div'); + expect(Ch5MutationObserver.checkElementValidity(ok)).toBe(true); + const swiper = document.createElement('div'); + swiper.classList.add('swiper-wrapper'); + expect(Ch5MutationObserver.checkElementValidity(swiper)).toBe(false); + }); +}); diff --git a/tests/jest/perf/shared-mutation-observer.test.ts b/tests/jest/perf/shared-mutation-observer.test.ts new file mode 100644 index 000000000..0ebbf90e1 --- /dev/null +++ b/tests/jest/perf/shared-mutation-observer.test.ts @@ -0,0 +1,193 @@ +/** + * SharedMutationObserver — pool invariants and dispatch correctness. + * + * BEFORE (master): src/ch5-common/ch5-mutation-observer.ts created one + * browser-level MutationObserver per component instance. On a panel with + * N components, N MutationObservers ran simultaneously. + * + * AFTER (this branch): Ch5MutationObserver delegates to + * Ch5SharedMutationObserver — a singleton with ONE underlying browser + * MutationObserver regardless of N facade instances. + * + * These tests prove: + * 1. N facade instances → 1 underlying browser MutationObserver. + * 2. Per-target callback dispatch is correct under attribute mutations. + * 3. unobserve(t) leaves other targets observed. + * 4. The singleton tears down its underlying observer when empty. + * + * They run in JSDOM. JSDOM's MutationObserver fires asynchronously (just + * like the browser), so we await microtasks before assertions. + */ +import { Ch5SharedMutationObserver } from '../../../src/ch5-core/ch5-shared-mutation-observer'; + +const flushMicrotasks = (): Promise => + new Promise((resolve) => queueMicrotask(() => resolve())); + +describe('Ch5SharedMutationObserver', () => { + let observerCtorSpy: jest.SpyInstance; + let realCtor: typeof MutationObserver; + + beforeEach(() => { + Ch5SharedMutationObserver._resetForTesting(); + realCtor = global.MutationObserver; + observerCtorSpy = jest.fn(); + // Wrap MutationObserver so we can count instantiations + (global as any).MutationObserver = class extends realCtor { + constructor(cb: MutationCallback) { + observerCtorSpy(); + super(cb); + } + }; + }); + + afterEach(() => { + Ch5SharedMutationObserver._resetForTesting(); + global.MutationObserver = realCtor; + }); + + it('creates ONE underlying MutationObserver regardless of how many targets are observed', () => { + const shared = Ch5SharedMutationObserver.getInstance(); + const config: MutationObserverInit = { attributes: true, attributeFilter: ['style'] }; + const targets: HTMLDivElement[] = []; + for (let i = 0; i < 50; i++) { + const el = document.createElement('div'); + document.body.appendChild(el); + targets.push(el); + shared.observe(el, config, () => {}); + } + expect(observerCtorSpy).toHaveBeenCalledTimes(1); + expect(shared._underlyingObserverCount).toBe(1); + expect(shared.size()).toBe(50); + }); + + it('dispatches mutations to the right per-target callback', async () => { + const shared = Ch5SharedMutationObserver.getInstance(); + const a = document.createElement('div'); + const b = document.createElement('div'); + document.body.append(a, b); + + const cbA = jest.fn(); + const cbB = jest.fn(); + const config: MutationObserverInit = { attributes: true, attributeFilter: ['style'] }; + shared.observe(a, config, cbA); + shared.observe(b, config, cbB); + + a.setAttribute('style', 'opacity: 0.5'); + await flushMicrotasks(); + + expect(cbA).toHaveBeenCalledTimes(1); + expect(cbB).not.toHaveBeenCalled(); + + b.setAttribute('style', 'visibility: hidden'); + await flushMicrotasks(); + + expect(cbA).toHaveBeenCalledTimes(1); + expect(cbB).toHaveBeenCalledTimes(1); + }); + + it('unobserve removes one target without disturbing the others', async () => { + const shared = Ch5SharedMutationObserver.getInstance(); + const a = document.createElement('div'); + const b = document.createElement('div'); + const c = document.createElement('div'); + document.body.append(a, b, c); + + const cbA = jest.fn(); + const cbB = jest.fn(); + const cbC = jest.fn(); + const config: MutationObserverInit = { attributes: true, attributeFilter: ['style'] }; + shared.observe(a, config, cbA); + shared.observe(b, config, cbB); + shared.observe(c, config, cbC); + + expect(shared.size()).toBe(3); + shared.unobserve(b); + expect(shared.size()).toBe(2); + expect(shared.isObserving(a)).toBe(true); + expect(shared.isObserving(b)).toBe(false); + expect(shared.isObserving(c)).toBe(true); + + // After unobserve(b), only a and c should still notify. + a.setAttribute('style', 'opacity: 0'); + b.setAttribute('style', 'opacity: 0'); + c.setAttribute('style', 'opacity: 0'); + await flushMicrotasks(); + + expect(cbA).toHaveBeenCalledTimes(1); + expect(cbB).not.toHaveBeenCalled(); + expect(cbC).toHaveBeenCalledTimes(1); + }); + + it('tears down the underlying observer when the last target is unobserved', () => { + const shared = Ch5SharedMutationObserver.getInstance(); + const a = document.createElement('div'); + const b = document.createElement('div'); + document.body.append(a, b); + + const config: MutationObserverInit = { attributes: true }; + shared.observe(a, config, () => {}); + shared.observe(b, config, () => {}); + expect(shared._underlyingObserverCount).toBe(1); + + shared.unobserve(a); + expect(shared._underlyingObserverCount).toBe(1); // b still observed + shared.unobserve(b); + expect(shared._underlyingObserverCount).toBe(0); // pool empty → torn down + }); + + it('observe() on the same target replaces the previous callback', async () => { + const shared = Ch5SharedMutationObserver.getInstance(); + const a = document.createElement('div'); + document.body.appendChild(a); + + const first = jest.fn(); + const second = jest.fn(); + const config: MutationObserverInit = { attributes: true, attributeFilter: ['style'] }; + shared.observe(a, config, first); + shared.observe(a, config, second); + + a.setAttribute('style', 'opacity: 0'); + await flushMicrotasks(); + + expect(first).not.toHaveBeenCalled(); + expect(second).toHaveBeenCalledTimes(1); + expect(shared.size()).toBe(1); + }); + + it('ignores observe/unobserve calls with null targets', () => { + const shared = Ch5SharedMutationObserver.getInstance(); + expect(() => shared.observe(null as unknown as Node, {}, () => {})).not.toThrow(); + expect(() => shared.unobserve(null as unknown as Node)).not.toThrow(); + expect(shared.size()).toBe(0); + }); + + it('routes subtree:true mutations to the ancestor callback', async () => { + // Reviewer caught this: under subtree:true the browser reports the + // deepest mutated node as `mutation.target`, but the caller registered + // an ancestor. Dispatch must walk up to find a matching entry. + const shared = Ch5SharedMutationObserver.getInstance(); + const root = document.createElement('section'); + const child = document.createElement('div'); + const grandchild = document.createElement('span'); + child.appendChild(grandchild); + root.appendChild(child); + document.body.appendChild(root); + + const cb = jest.fn(); + shared.observe( + root, + { attributes: true, attributeFilter: ['style'], subtree: true }, + cb, + ); + + grandchild.setAttribute('style', 'color: red'); + await flushMicrotasks(); + + expect(cb).toHaveBeenCalledTimes(1); + // The callback receives the ancestor (the registered node), not the deep target. + expect(cb.mock.calls[0][0]).toBe(root); + // The raw MutationRecord still carries the original deep target. + const mutation = cb.mock.calls[0][1] as MutationRecord; + expect(mutation.target).toBe(grandchild); + }); +}); From 870aac62376a85ec681c8b9d033fa08ee26c5320 Mon Sep 17 00:00:00 2001 From: Navjit Brar Date: Wed, 20 May 2026 10:44:56 -0500 Subject: [PATCH 4/6] perf(observers): pool ResizeObservers; return disposer from legacy utility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds src/ch5-core/ch5-shared-resize-observer.ts — a singleton pool that wraps a single browser ResizeObserver and routes entries to per-target callbacks. Includes feature-detection so it's a no-op on platforms without native ResizeObserver. Refactors src/ch5-core/resize-observer.ts. The historical utility created a new ResizeObserver per call AND returned void — every caller leaked one observer permanently with no way to disconnect. It now: • delegates to Ch5SharedResizeObserver (one underlying observer) • returns a disposer so opt-in callers can finally clean up The 11 existing callers that ignore the return value still work and still benefit from pooling. Two direct consumers are migrated to use the disposer in their removeEventListeners() path: src/ch5-video-switcher/ch5-video-switcher.ts src/ch5-signal-level-gauge/ch5-signal-level-gauge.ts Test coverage (9 tests): shared-resize-observer.test.ts • N targets → 1 underlying ResizeObserver • per-target dispatch • disposer unregisters exactly that target • teardown on empty • graceful no-op when ResizeObserver is unavailable • legacy utility routes through the pool • legacy utility returns a working disposer • legacy utility still works for callers that ignore the disposer Supporting helper: tests/jest/_helpers/fake-resize-observer.ts — controllable shim for JSDOM (JSDOM does not implement ResizeObserver natively) Co-Authored-By: Claude Opus 4.7 (1M context) --- src/ch5-core/ch5-shared-resize-observer.ts | 123 +++++++++++++ src/ch5-core/resize-observer.ts | 34 +++- .../ch5-signal-level-gauge.ts | 14 +- src/ch5-video-switcher/ch5-video-switcher.ts | 14 +- tests/jest/_helpers/fake-resize-observer.ts | 88 +++++++++ .../jest/perf/shared-resize-observer.test.ts | 173 ++++++++++++++++++ 6 files changed, 433 insertions(+), 13 deletions(-) create mode 100644 src/ch5-core/ch5-shared-resize-observer.ts create mode 100644 tests/jest/_helpers/fake-resize-observer.ts create mode 100644 tests/jest/perf/shared-resize-observer.test.ts diff --git a/src/ch5-core/ch5-shared-resize-observer.ts b/src/ch5-core/ch5-shared-resize-observer.ts new file mode 100644 index 000000000..4f90b8b76 --- /dev/null +++ b/src/ch5-core/ch5-shared-resize-observer.ts @@ -0,0 +1,123 @@ +// Copyright (C) 2018 to the present, Crestron Electronics, Inc. +// All rights reserved. +// No part of this software may be reproduced in any form, machine +// or natural, without the express written consent of Crestron Electronics. +// Use of this source code is subject to the terms of the Crestron Software License Agreement +// under which you licensed this source code. + +/** + * Per-target resize callback. `entry` is the raw ResizeObserverEntry. + */ +export type Ch5SharedResizeCallback = (target: Element, entry: ResizeObserverEntry) => void; + +/** + * Singleton ResizeObserver pool. + * + * Why this exists: callers previously did `new ResizeObserver(cb)` per + * component (or worse, called a fire-and-forget helper that leaked one + * ResizeObserver per call with no way to disconnect). On a panel with N + * resize-aware components, that meant N ResizeObservers — each carrying + * its own callback closure and lifecycle. + * + * This singleton wraps ONE underlying ResizeObserver and routes each + * entry to a per-target callback. The native ResizeObserver already + * supports both per-target observe() and per-target unobserve(), so this + * pool is simpler than its MutationObserver cousin. + * + * Browser support: this requires the native global ResizeObserver. On + * platforms where it's missing, the pool stays empty and observe() is a + * no-op. (Callers who need the polyfill should load it before importing + * this module.) + */ +export class Ch5SharedResizeObserver { + private static _instance: Ch5SharedResizeObserver | null = null; + + private _observer: ResizeObserver | null = null; + private readonly _callbacks = new Map(); + private readonly _supported: boolean; + + private constructor() { + this._supported = typeof (globalThis as { ResizeObserver?: unknown }).ResizeObserver === 'function'; + } + + public get _underlyingObserverCount(): number { + return this._observer ? 1 : 0; + } + + public static getInstance(): Ch5SharedResizeObserver { + if (!Ch5SharedResizeObserver._instance) { + Ch5SharedResizeObserver._instance = new Ch5SharedResizeObserver(); + } + return Ch5SharedResizeObserver._instance; + } + + /** Test hook — drops the singleton and disconnects the underlying observer. */ + public static _resetForTesting(): void { + if (Ch5SharedResizeObserver._instance) { + Ch5SharedResizeObserver._instance._teardown(); + } + Ch5SharedResizeObserver._instance = null; + } + + /** True iff the runtime has a native ResizeObserver. */ + public isSupported(): boolean { + return this._supported; + } + + /** + * Begin observing `target`. Returns a disposer that, when invoked, + * unregisters this exact (target, callback) pair. Calling observe() + * again on the same target replaces the previous callback. + * + * If the runtime lacks ResizeObserver the disposer is a no-op. + */ + public observe(target: Element, callback: Ch5SharedResizeCallback): () => void { + if (target == null) return () => undefined; + if (!this._supported) return () => undefined; + + if (!this._observer) { + this._observer = new ResizeObserver((entries) => this._dispatch(entries)); + } + this._callbacks.set(target, callback); + this._observer.observe(target); + + return () => this.unobserve(target); + } + + /** Stop observing `target`. Other targets remain observed. */ + public unobserve(target: Element): void { + if (target == null) return; + if (!this._callbacks.delete(target)) return; + if (!this._observer) return; + this._observer.unobserve(target); + if (this._callbacks.size === 0) { + this._teardown(); + } + } + + /** Diagnostics — number of currently observed targets. */ + public size(): number { + return this._callbacks.size; + } + + /** True iff `target` is currently registered. */ + public isObserving(target: Element): boolean { + return this._callbacks.has(target); + } + + private _dispatch(entries: ResizeObserverEntry[]): void { + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + const cb = this._callbacks.get(entry.target); + if (cb) cb(entry.target, entry); + } + } + + private _teardown(): void { + if (this._observer) { + this._observer.disconnect(); + this._observer = null; + } + this._callbacks.clear(); + } +} diff --git a/src/ch5-core/resize-observer.ts b/src/ch5-core/resize-observer.ts index 9bece987b..da5fa548c 100644 --- a/src/ch5-core/resize-observer.ts +++ b/src/ch5-core/resize-observer.ts @@ -4,12 +4,36 @@ // or natural, without the express written consent of Crestron Electronics. // Use of this source code is subject to the terms of the Crestron Software License Agreement // under which you licensed this source code. -// import ResizeObserver from 'resize-observer-polyfill'; + +import { Ch5SharedResizeObserver } from "./ch5-shared-resize-observer"; /** - * Utility function that returns the first scrollable parent + * Observe `node` for size changes, invoking `callback` on every resize. + * + * Historically this function constructed a new browser-level + * ResizeObserver per call AND returned `void`, so callers had no way to + * disconnect — every invocation leaked one observer permanently. The + * implementation now delegates to {@link Ch5SharedResizeObserver}, so + * all calls share a single underlying observer. + * + * Callers that want to clean up should use the returned disposer in + * their `disconnectedCallback()`. Older callers that ignore the return + * value still benefit from the pool (one ResizeObserver instead of N) + * but their registration persists for the lifetime of the page — that + * leak is incremental rather than per-resize and should be fixed by + * adopting the disposer. + * + * @returns a disposer that, when called, unregisters this callback. + * + * The `callback` is typed as `any` to preserve compatibility with the + * historical signature — pre-existing callers pass handlers shaped like + * `(event: Event) => void` and `() => void`, neither of which is the + * spec'd `ResizeObserverCallback`. Tightening this is a separate cleanup. */ -export function resizeObserver(node: HTMLElement, callback: any) { - const myObserver = new ResizeObserver(callback); - myObserver.observe(node); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function resizeObserver(node: HTMLElement, callback: any): () => void { + if (node == null) return () => undefined; + return Ch5SharedResizeObserver.getInstance().observe(node, (_target, entry) => { + callback([entry], Ch5SharedResizeObserver.getInstance() as unknown as ResizeObserver); + }); } diff --git a/src/ch5-signal-level-gauge/ch5-signal-level-gauge.ts b/src/ch5-signal-level-gauge/ch5-signal-level-gauge.ts index a692d8a94..13ea50869 100644 --- a/src/ch5-signal-level-gauge/ch5-signal-level-gauge.ts +++ b/src/ch5-signal-level-gauge/ch5-signal-level-gauge.ts @@ -6,6 +6,7 @@ import { ICh5SignalLevelGaugeAttributes } from './interfaces/i-ch5-signal-level- import { Ch5Properties } from "../ch5-core/ch5-properties"; import { ICh5PropertySettings } from "../ch5-core/ch5-property"; import { subscribeInViewPortChange, unSubscribeInViewPortChange } from '../ch5-core'; +import { Ch5SharedResizeObserver } from "../ch5-core/ch5-shared-resize-observer"; export class Ch5SignalLevelGauge extends Ch5Common implements ICh5SignalLevelGaugeAttributes { @@ -151,7 +152,7 @@ export class Ch5SignalLevelGauge extends Ch5Common implements ICh5SignalLevelGau public static readonly ELEMENT_NAME = 'ch5-signal-level-gauge'; public primaryCssClass = 'ch5-signal-level-gauge'; - private _resizeObserver: ResizeObserver | null = null; + private _resizeObserverDispose: (() => void) | null = null; private _ch5Properties: Ch5Properties; private _elContainer: HTMLElement = {} as HTMLElement; @@ -368,13 +369,18 @@ export class Ch5SignalLevelGauge extends Ch5Common implements ICh5SignalLevelGau protected attachEventListeners() { super.attachEventListeners(); - this._resizeObserver = new ResizeObserver(this._resizeObserverCallBack); - this._resizeObserver.observe(this._elContainer) + this._resizeObserverDispose = Ch5SharedResizeObserver.getInstance().observe( + this._elContainer, + () => this._resizeObserverCallBack(), + ); } protected removeEventListeners() { super.removeEventListeners(); - this._resizeObserver?.unobserve(this._elContainer); + if (this._resizeObserverDispose) { + this._resizeObserverDispose(); + this._resizeObserverDispose = null; + } } protected unsubscribeFromSignals() { diff --git a/src/ch5-video-switcher/ch5-video-switcher.ts b/src/ch5-video-switcher/ch5-video-switcher.ts index 3f81d21a7..d9e5cf745 100644 --- a/src/ch5-video-switcher/ch5-video-switcher.ts +++ b/src/ch5-video-switcher/ch5-video-switcher.ts @@ -11,6 +11,7 @@ import _ from "lodash"; import { Ch5AugmentVarSignalsNames } from "../ch5-common/ch5-augment-var-signals-names"; import { Ch5VideoSwitcherScreen } from "./ch5-video-switcher-screen"; import { Ch5VideoSwitcherSource } from "./ch5-video-switcher-source"; +import { Ch5SharedResizeObserver } from "../ch5-core/ch5-shared-resize-observer"; export class Ch5VideoSwitcher extends Ch5Common implements ICh5VideoSwitcherAttributes { @@ -333,7 +334,7 @@ export class Ch5VideoSwitcher extends Ch5Common implements ICh5VideoSwitcherAttr receiveStateNumberOfScreens: "" } private validDrop: boolean = false; - private resizeObserver: ResizeObserver | null = null; + private resizeObserverDispose: (() => void) | null = null; public debounceNumberOfItems = this.debounce((newValue: number) => { this.setNumberOfItems(newValue); @@ -739,8 +740,10 @@ export class Ch5VideoSwitcher extends Ch5Common implements ICh5VideoSwitcherAttr this._sourceListContainer.addEventListener('mouseup', this.handleMouseUpAndLeave); this._sourceListContainer.addEventListener('mousemove', this.handleMouseMove); this._sourceListContainer.addEventListener('scroll', this.handleScrollEvent); - this.resizeObserver = new ResizeObserver(this.resizeObserverHandler); - this.resizeObserver.observe(this._elContainer); + this.resizeObserverDispose = Ch5SharedResizeObserver.getInstance().observe( + this._elContainer, + () => this.resizeObserverHandler(), + ); } private handleMouseDown = this.debounce((e: MouseEvent) => { @@ -775,7 +778,10 @@ export class Ch5VideoSwitcher extends Ch5Common implements ICh5VideoSwitcherAttr this._sourceListContainer.removeEventListener('mousedown', this.handleMouseDown); this._sourceListContainer.removeEventListener('mousemove', this.handleMouseMove); this._sourceListContainer.removeEventListener('scroll', this.handleScrollEvent); - this.resizeObserver?.unobserve(this._elContainer); + if (this.resizeObserverDispose) { + this.resizeObserverDispose(); + this.resizeObserverDispose = null; + } } protected unsubscribeFromSignals() { diff --git a/tests/jest/_helpers/fake-resize-observer.ts b/tests/jest/_helpers/fake-resize-observer.ts new file mode 100644 index 000000000..34c1e1a2e --- /dev/null +++ b/tests/jest/_helpers/fake-resize-observer.ts @@ -0,0 +1,88 @@ +/** + * Controllable ResizeObserver shim for tests. + * + * JSDOM (as of v29) does not implement ResizeObserver. Production code uses + * it through Ch5SharedResizeObserver, which checks `globalThis.ResizeObserver` + * at construction time. This helper installs a fake on `globalThis` and lets + * tests synchronously fire entries for any observed target. + * + * Usage: + * + * let fake: FakeResizeObserverHandle; + * beforeEach(() => { fake = installFakeResizeObserver(); }); + * afterEach(() => { fake.restore(); }); + * + * // After observe() calls have been made: + * fake.fire(targetElement); // dispatches a synthetic entry + * expect(fake.instances).toBe(1); // pool invariant + */ + +export interface FakeResizeObserverHandle { + /** How many fake ResizeObservers have been constructed since install. */ + instances: number; + /** Synchronously fire a resize entry for `target` on every observer watching it. */ + fire(target: Element, contentRect?: Partial): void; + /** Restore whatever was on globalThis.ResizeObserver before install. */ + restore(): void; +} + +interface InternalObserver { + cb: ResizeObserverCallback; + watching: Set; +} + +export function installFakeResizeObserver(): FakeResizeObserverHandle { + const previous = (globalThis as { ResizeObserver?: unknown }).ResizeObserver; + const observers: InternalObserver[] = []; + + class Fake { + private readonly _entry: InternalObserver; + constructor(cb: ResizeObserverCallback) { + this._entry = { cb, watching: new Set() }; + observers.push(this._entry); + } + observe(target: Element): void { + this._entry.watching.add(target); + } + unobserve(target: Element): void { + this._entry.watching.delete(target); + } + disconnect(): void { + this._entry.watching.clear(); + } + } + + (globalThis as { ResizeObserver?: unknown }).ResizeObserver = Fake; + + return { + get instances(): number { + return observers.length; + }, + fire(target: Element, contentRect: Partial = {}): void { + const rect: DOMRectReadOnly = { + x: 0, y: 0, width: 0, height: 0, top: 0, left: 0, right: 0, bottom: 0, + toJSON() { return {}; }, + ...contentRect, + } as DOMRectReadOnly; + const entry: ResizeObserverEntry = { + target, + contentRect: rect, + borderBoxSize: [], + contentBoxSize: [], + devicePixelContentBoxSize: [], + }; + for (const o of observers) { + if (o.watching.has(target)) { + o.cb([entry], {} as ResizeObserver); + } + } + }, + restore(): void { + if (previous === undefined) { + delete (globalThis as { ResizeObserver?: unknown }).ResizeObserver; + } else { + (globalThis as { ResizeObserver?: unknown }).ResizeObserver = previous; + } + }, + }; +} diff --git a/tests/jest/perf/shared-resize-observer.test.ts b/tests/jest/perf/shared-resize-observer.test.ts new file mode 100644 index 000000000..eb49a89de --- /dev/null +++ b/tests/jest/perf/shared-resize-observer.test.ts @@ -0,0 +1,173 @@ +/** + * SharedResizeObserver — pool invariants, dispatch, and disposer behavior. + * + * BEFORE (master): + * • src/ch5-core/resize-observer.ts created a new browser ResizeObserver + * per call AND returned void — every caller leaked one observer with no + * disconnect path. + * • src/ch5-video-switcher/...:742 and src/ch5-signal-level-gauge/...:371 + * each held their own per-instance ResizeObserver. + * + * AFTER (this branch): + * • All routes go through Ch5SharedResizeObserver — a singleton with ONE + * underlying browser ResizeObserver regardless of N callers. + * • The `resizeObserver()` utility returns a disposer so opt-in callers + * can finally clean up; the two direct consumers are migrated to use + * the disposer in their respective `removeEventListeners`. + * + * These tests run in JSDOM with a controllable ResizeObserver shim that + * lets us fire synthetic entries on demand (see _helpers/fake-resize-observer). + */ +import { Ch5SharedResizeObserver } from '../../../src/ch5-core/ch5-shared-resize-observer'; +import { resizeObserver as legacyResizeObserver } from '../../../src/ch5-core/resize-observer'; +import { + installFakeResizeObserver, + FakeResizeObserverHandle, +} from '../_helpers/fake-resize-observer'; + +describe('Ch5SharedResizeObserver', () => { + let fake: FakeResizeObserverHandle; + + beforeEach(() => { + Ch5SharedResizeObserver._resetForTesting(); + fake = installFakeResizeObserver(); + }); + + afterEach(() => { + Ch5SharedResizeObserver._resetForTesting(); + fake.restore(); + }); + + it('creates ONE underlying ResizeObserver for many targets', () => { + const shared = Ch5SharedResizeObserver.getInstance(); + for (let i = 0; i < 30; i++) { + const el = document.createElement('div'); + document.body.appendChild(el); + shared.observe(el, () => undefined); + } + expect(fake.instances).toBe(1); + expect(shared._underlyingObserverCount).toBe(1); + expect(shared.size()).toBe(30); + }); + + it('routes entries to the right per-target callback', () => { + const shared = Ch5SharedResizeObserver.getInstance(); + const a = document.createElement('div'); + const b = document.createElement('div'); + document.body.append(a, b); + + const cbA = jest.fn(); + const cbB = jest.fn(); + shared.observe(a, cbA); + shared.observe(b, cbB); + + fake.fire(a, { width: 100, height: 50 }); + expect(cbA).toHaveBeenCalledTimes(1); + expect(cbB).not.toHaveBeenCalled(); + const firstCall = cbA.mock.calls[0]; + expect(firstCall[0]).toBe(a); + expect((firstCall[1] as ResizeObserverEntry).contentRect.width).toBe(100); + + fake.fire(b); + expect(cbB).toHaveBeenCalledTimes(1); + }); + + it('observe() returns a disposer that unregisters exactly that target', () => { + const shared = Ch5SharedResizeObserver.getInstance(); + const a = document.createElement('div'); + const b = document.createElement('div'); + document.body.append(a, b); + + const cbA = jest.fn(); + const cbB = jest.fn(); + const disposeA = shared.observe(a, cbA); + shared.observe(b, cbB); + + expect(shared.size()).toBe(2); + disposeA(); + expect(shared.size()).toBe(1); + expect(shared.isObserving(a)).toBe(false); + expect(shared.isObserving(b)).toBe(true); + + fake.fire(a); + expect(cbA).not.toHaveBeenCalled(); + fake.fire(b); + expect(cbB).toHaveBeenCalledTimes(1); + }); + + it('tears down the underlying observer when empty', () => { + const shared = Ch5SharedResizeObserver.getInstance(); + const el = document.createElement('div'); + document.body.appendChild(el); + const dispose = shared.observe(el, () => undefined); + expect(shared._underlyingObserverCount).toBe(1); + dispose(); + expect(shared._underlyingObserverCount).toBe(0); + }); + + it('ignores null targets gracefully', () => { + const shared = Ch5SharedResizeObserver.getInstance(); + const dispose = shared.observe(null as unknown as Element, () => undefined); + expect(typeof dispose).toBe('function'); + expect(() => dispose()).not.toThrow(); + expect(shared.size()).toBe(0); + }); + + it('returns a no-op disposer when ResizeObserver is unavailable', () => { + // Tear down the singleton, remove the shim, then re-create the singleton. + Ch5SharedResizeObserver._resetForTesting(); + fake.restore(); + const noSupport = Ch5SharedResizeObserver.getInstance(); + expect(noSupport.isSupported()).toBe(false); + const dispose = noSupport.observe(document.createElement('div'), jest.fn()); + expect(typeof dispose).toBe('function'); + expect(noSupport.size()).toBe(0); + // restore so the afterEach can run normally + fake = installFakeResizeObserver(); + }); +}); + +describe('legacy resizeObserver() utility — pooled, disposer-returning', () => { + let fake: FakeResizeObserverHandle; + + beforeEach(() => { + Ch5SharedResizeObserver._resetForTesting(); + fake = installFakeResizeObserver(); + }); + afterEach(() => { + Ch5SharedResizeObserver._resetForTesting(); + fake.restore(); + }); + + it('routes through the singleton — N calls, 1 underlying observer', () => { + for (let i = 0; i < 10; i++) { + const el = document.createElement('div'); + document.body.appendChild(el); + legacyResizeObserver(el, () => undefined); + } + expect(fake.instances).toBe(1); + expect(Ch5SharedResizeObserver.getInstance().size()).toBe(10); + }); + + it('returns a disposer that unregisters the caller', () => { + const el = document.createElement('div'); + document.body.appendChild(el); + const cb = jest.fn(); + const dispose = legacyResizeObserver(el, cb); + fake.fire(el); + expect(cb).toHaveBeenCalledTimes(1); + dispose(); + fake.fire(el); + expect(cb).toHaveBeenCalledTimes(1); + }); + + it('still works for callers that ignore the disposer (back-compat)', () => { + const el = document.createElement('div'); + document.body.appendChild(el); + const cb = jest.fn(); + // Older call sites do exactly this — we just don't unregister. + legacyResizeObserver(el, cb); + fake.fire(el); + expect(cb).toHaveBeenCalledTimes(1); + }); +}); From 1f2a2d898533ced1a1511f23fd8bd7fbfe747f51 Mon Sep 17 00:00:00 2001 From: Navjit Brar Date: Wed, 20 May 2026 10:45:25 -0500 Subject: [PATCH 5/6] fix(common, color-picker): release base-class subscriptions; surface init failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two long-standing bugs that the test scaffolding now guards against. 1. Language-change subscription leak (src/ch5-common/ch5-common.ts) The Ch5Common constructor subscribed to the language-change signal and dropped the returned subscription key on the floor. The subscription closure captured `this` (the component) AND `this.translatableObjects`, so every component instance was pinned in memory for the lifetime of the page — even after removal from the DOM. Fix: track the returned key in `_languageChangeSubKey` and release it in `unsubscribeFromSignals()` via `signal.unsubscribe(key)`. Always released — the `_keepListeningOnSignalsAfterRemoval` flag is for receive-state bridge subs used for re-attachment; the language sub must not be kept alive. Also adds `_baseClassSubscriptions: Subscription[]` as future infrastructure for any non-bridge RxJS subscriptions a base or subclass method might register. Drained alongside the language sub. 2. Silent catches in ColorPicker (src/ch5-color-picker/color-picker.ts) The constructor's try/catch swallowed init failures into `joe = null` with no log. setColor() then silently no-op'd on the null. QA and field engineers had nothing to grep for when the picker didn't render. Fix: • catches now console.warn with the picker id and the underlying error • renamed private `joe` → `_picker` (descriptive; nothing external referenced the old name) • added `isReady` getter so callers can check before calling • setColor() returns boolean (existing void-returning callers unaffected) Test coverage: language-subscription.test.ts 4 tests — bridge unsubscribe, drain array, _keepListening flag, no-op when nothing tracked color-picker-silent-catch.test.ts 2 tests — init failure warns + isReady, setColor warns on uninit Co-Authored-By: Claude Opus 4.7 (1M context) --- src/ch5-color-picker/color-picker.ts | 71 ++++++++---- src/ch5-common/ch5-common.ts | 43 ++++++- .../leaks/color-picker-silent-catch.test.ts | 58 ++++++++++ .../jest/leaks/language-subscription.test.ts | 108 ++++++++++++++++++ 4 files changed, 254 insertions(+), 26 deletions(-) create mode 100644 tests/jest/leaks/color-picker-silent-catch.test.ts create mode 100644 tests/jest/leaks/language-subscription.test.ts diff --git a/src/ch5-color-picker/color-picker.ts b/src/ch5-color-picker/color-picker.ts index 836c6c3a3..bfa4452d6 100644 --- a/src/ch5-color-picker/color-picker.ts +++ b/src/ch5-color-picker/color-picker.ts @@ -2,57 +2,80 @@ import * as mycolorpicker from "@raghavendradabbir/mycolorpicker"; import { Subject } from "rxjs"; import Ch5ColorUtils from "../ch5-common/utils/ch5-color-utils"; +const LOG_PREFIX = "[ch5-color-picker]"; + export class ColorPicker { - private joe: any = null; + /** + * Underlying third-party picker instance. Typed as `any` because the + * upstream `@raghavendradabbir/mycolorpicker` package ships no type + * declarations. (Replacing this dependency with a maintained color + * picker is tracked separately.) + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _picker: any = null; /** * An RxJs observable for the colorChanged property. */ public colorChanged: Subject; + /** + * True iff the underlying picker was successfully initialised. False + * here means `setColor()` and `picker` are non-functional — UI should + * surface that rather than ignoring user input silently. + */ + public get isReady(): boolean { + return this._picker !== null; + } + constructor(public pickerId: string, newColor: string) { this.colorChanged = new Subject(); try { - // 'currentColor', - // 'hex' - this.joe = mycolorpicker.hsl(this.pickerId, newColor, [ - ]).on('change', (c: any) => { - // const complement = this.invertHex(c.hex()); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this._picker = mycolorpicker.hsl(this.pickerId, newColor, []).on('change', (c: any) => { const thisColorDiv = document.getElementById(this.pickerId); if (thisColorDiv) { - const queryObj = thisColorDiv.querySelectorAll('.oned')[0].querySelectorAll('.shape')[0]; - queryObj.style.backgroundColor = "#d8d8d8"; // c.css(); - queryObj.style.borderColor = "#696969"; // complement; + const queryObj = thisColorDiv.querySelectorAll('.oned')[0]?.querySelectorAll('.shape')[0]; + if (queryObj) { + queryObj.style.backgroundColor = "#d8d8d8"; + queryObj.style.borderColor = "#696969"; + } const extrasObject = thisColorDiv.querySelectorAll('.extras')[0]; - extrasObject.style.display = "none"; + if (extrasObject) extrasObject.style.display = "none"; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any const colorObj: any = Ch5ColorUtils.rgbToObj(c.css()); this.colorChanged?.next([colorObj.red, colorObj.green, colorObj.blue]); }).update(); - - // this.joe.set = (c: any) => { - // console.log("C", c); - // } } catch (e) { - // Do Nothing + // Surface the failure. Historical behaviour silently left _picker = null, + // which made setColor() a no-op and produced no UI feedback. Logging here + // gives QA and field engineers something to grep for when the picker + // doesn't render. + // eslint-disable-next-line no-console + console.warn(`${LOG_PREFIX} failed to initialise picker for id="${this.pickerId}":`, e); } } - private invertHex(hex: string) { - return '#' + hex.match(/[a-f0-9]{2}/ig)?.map(e => (255 - parseInt(e, 16) || 0).toString(16).replace(/^([a-f0-9])$/, '0$1')).join(''); // (Number(`0x1${hex}`) ^ 0xFFFFFF).toString(16).substr(1).toUpperCase() - } - - public setColor(newColor: string) { + public setColor(newColor: string): boolean { + if (this._picker === null) { + // eslint-disable-next-line no-console + console.warn(`${LOG_PREFIX} setColor() called on uninitialised picker id="${this.pickerId}"; ignoring`); + return false; + } try { - this.joe.set(newColor); + this._picker.set(newColor); + return true; } catch (e) { - // Do Nothing + // eslint-disable-next-line no-console + console.warn(`${LOG_PREFIX} setColor("${newColor}") threw on picker id="${this.pickerId}":`, e); + return false; } } public get picker() { - return this.joe; + return this._picker; } -} \ No newline at end of file +} diff --git a/src/ch5-common/ch5-common.ts b/src/ch5-common/ch5-common.ts index 1c26783a5..1f7fe3747 100644 --- a/src/ch5-common/ch5-common.ts +++ b/src/ch5-common/ch5-common.ts @@ -6,7 +6,7 @@ // under which you licensed this source code. import { Ch5Signal, Ch5SignalFactory, Ch5TranslationUtility, Ch5Uid, languageChangedSignalName, subscribeInViewPortChange, Ch5Platform, ICh5PlatformInfo, publishEvent } from '../ch5-core'; -import { Subject } from 'rxjs'; +import { Subject, Subscription } from 'rxjs'; import { Ch5Config } from './ch5-config'; import { Ch5MutationObserver } from './ch5-mutation-observer'; import { Ch5ImageUriModel } from "../ch5-image/ch5-image-uri-model"; @@ -363,6 +363,24 @@ export class Ch5Common extends HTMLElement implements ICh5CommonAttributes { private _commonMutationObserver: Ch5MutationObserver = {} as Ch5MutationObserver; + /** + * Subscription key for the base-class language-change subscription + * taken in the constructor. Tracked so it can be unsubscribed in + * `unsubscribeFromSignals()` — the historical implementation called + * `receiveSignal.subscribe(...)` and dropped the returned key on the + * floor, so the subscription closure (which captures `this`) kept the + * component pinned in memory for the lifetime of the page. + */ + private _languageChangeSubKey: string = ''; + + /** + * Drain list for any non-bridge RxJS subscriptions (Subjects, etc.) + * a subclass or future base method needs to register. Bridge-mediated + * Ch5Signal subscriptions use the `_subKeySig…` + signal-name pattern + * instead — see clearStringSignalSubscription() and friends. + */ + private _baseClassSubscriptions: Subscription[] = []; + //#endregion //#region Setters and Getters @@ -747,13 +765,18 @@ export class Ch5Common extends HTMLElement implements ICh5CommonAttributes { this._listOfAllPossibleComponentCssClasses = cssClasses; this.observableGestureableProperty = new Subject(); + // `getStringSignal()` defaults to createNewIfNotFound = true, so the + // returned signal is effectively non-null in production. The null + // guard below is defensive — and `unsubscribeFromSignals()` re-fetches + // the signal with the same defaults, so the unsubscribe is symmetrical + // even if a future change tightens factory behaviour. const receiveSignal = Ch5SignalFactory.getInstance().getStringSignal(languageChangedSignalName); if (receiveSignal === null) { return; } - receiveSignal.subscribe((newValue: string) => { + this._languageChangeSubKey = receiveSignal.subscribe((newValue: string) => { if (newValue !== '' && newValue !== this.currentLanguage) { this.currentLanguage = newValue; Object.keys(this.translatableObjects).forEach((propertyToTranslate: string) => { @@ -1710,6 +1733,22 @@ export class Ch5Common extends HTMLElement implements ICh5CommonAttributes { this.clearStringSignalSubscription(this._receiveStateCustomClass, this._subKeySigReceiveCustomClass); this._receiveStateCustomClass = ''; } + // Always release base-class subscriptions, even when + // _keepListeningOnSignalsAfterRemoval is true — that flag protects + // receive-state bridge subscriptions used for re-attachment, but the + // language-change subscription holds a closure over `this` and would + // pin the component in memory after removal. + if (this._languageChangeSubKey !== '') { + const langSig = Ch5SignalFactory.getInstance().getStringSignal(languageChangedSignalName); + if (langSig !== null) { + langSig.unsubscribe(this._languageChangeSubKey); + } + this._languageChangeSubKey = ''; + } + for (let i = 0; i < this._baseClassSubscriptions.length; i++) { + this._baseClassSubscriptions[i].unsubscribe(); + } + this._baseClassSubscriptions = []; } // Returns a function, that, as long as it continues to be invoked, will not be triggered. diff --git a/tests/jest/leaks/color-picker-silent-catch.test.ts b/tests/jest/leaks/color-picker-silent-catch.test.ts new file mode 100644 index 000000000..ca7f07eb0 --- /dev/null +++ b/tests/jest/leaks/color-picker-silent-catch.test.ts @@ -0,0 +1,58 @@ +/** + * Regression guard — ColorPicker no longer swallows initialisation errors. + * + * BEFORE (master): a failed `mycolorpicker.hsl(...)` call left `joe = null` + * with no log, no warning, and no exposed state. Subsequent `setColor()` + * calls then threw NPEs that were ALSO silently caught. Users saw nothing. + * + * AFTER (this branch): the catch emits a console.warn with the picker id + * and the error. The class exposes `isReady`, and `setColor()` returns a + * boolean indicating whether the call took effect. + * + * We mock `@raghavendradabbir/mycolorpicker` to force a failure path. + */ + +jest.mock('@raghavendradabbir/mycolorpicker', () => ({ + hsl: jest.fn(() => { + throw new Error('simulated picker init failure'); + }), +})); + +import { ColorPicker } from '../../../src/ch5-color-picker/color-picker'; + +describe('ColorPicker silent-catch fix', () => { + let warnSpy: jest.SpyInstance; + + beforeEach(() => { + warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + }); + afterEach(() => { + warnSpy.mockRestore(); + }); + + it('logs a warning when initialisation fails (instead of swallowing)', () => { + const div = document.createElement('div'); + div.id = 'cp-1'; + document.body.appendChild(div); + + const cp = new ColorPicker('cp-1', '#ffffff'); + + expect(cp.isReady).toBe(false); + expect(warnSpy).toHaveBeenCalledTimes(1); + const [message, err] = warnSpy.mock.calls[0]; + expect(message).toContain('failed to initialise picker'); + expect(message).toContain('cp-1'); + expect((err as Error).message).toContain('simulated picker init failure'); + }); + + it('setColor() returns false and warns when called on an uninitialised picker', () => { + const cp = new ColorPicker('cp-2', '#ffffff'); + warnSpy.mockClear(); + + const result = cp.setColor('#000000'); + + expect(result).toBe(false); + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy.mock.calls[0][0]).toContain('setColor() called on uninitialised picker'); + }); +}); diff --git a/tests/jest/leaks/language-subscription.test.ts b/tests/jest/leaks/language-subscription.test.ts new file mode 100644 index 000000000..799e72804 --- /dev/null +++ b/tests/jest/leaks/language-subscription.test.ts @@ -0,0 +1,108 @@ +/** + * Leak guard — base-class subscriptions are released on teardown. + * + * BEFORE (master): src/ch5-common/ch5-common.ts:756 subscribed to the + * language-change signal in the constructor and discarded the returned + * subscription key. The subscription closure captured `this` (the + * component) AND `this.translatableObjects`, so every component + * instance was pinned in memory for the lifetime of the page — even + * after removal from the DOM. + * + * AFTER (this branch): the returned key is stored in + * `_languageChangeSubKey` and `unsubscribeFromSignals()` calls + * `signal.unsubscribe(key)`. Future genuine-RxJS subscriptions can + * additionally use `_baseClassSubscriptions[]`. + * + * The test deliberately bypasses the Ch5Common constructor (which would + * otherwise need the full signal bridge, customElements registration, + * and a host document). It exercises the teardown logic by calling + * `unsubscribeFromSignals()` against a synthetic `this`. + */ +import { Ch5Common } from '../../../src/ch5-common/ch5-common'; +import { Ch5SignalFactory, languageChangedSignalName } from '../../../src/ch5-core'; +import { Subject, Subscription } from 'rxjs'; + +const protoFn = (name: 'unsubscribeFromSignals'): (this: unknown) => void => + (Ch5Common.prototype as unknown as Record void>)[name]; + +const baseFakeThis = (): Record => ({ + _keepListeningOnSignalsAfterRemoval: false, + _languageChangeSubKey: '', + _baseClassSubscriptions: [] as Subscription[], + _receiveStateEnable: '', + _subKeySigReceiveEnable: '', + _receiveStateShow: '', + _subKeySigReceiveShow: '', + _receiveStateShowPulse: '', + _subKeySigReceiveShowPulse: '', + _receiveStateHidePulse: '', + _subKeySigReceiveHidePulse: '', + _receiveStateCustomStyle: '', + _subKeySigReceiveCustomStyle: '', + _receiveStateCustomClass: '', + _subKeySigReceiveCustomClass: '', + clearBooleanSignalSubscription: () => undefined, + clearStringSignalSubscription: () => undefined, +}); + +describe('Ch5Common base-class subscriptions are released on teardown', () => { + beforeEach(() => { + // Clear any cached signals from previous tests + (Ch5SignalFactory.getInstance() as unknown as { _ch5Signals: Record })._ch5Signals = {}; + }); + + it('unsubscribeFromSignals() unsubscribes the tracked language-change subKey via Ch5Signal.unsubscribe', () => { + // Spin up a real language-change signal so the unsubscribe call has + // something to find. Subscribe to bump the subKey counter, then assert + // the teardown path drains it via the bridge API. + const sig = Ch5SignalFactory.getInstance().getStringSignal(languageChangedSignalName, true); + expect(sig).not.toBeNull(); + const key = sig!.subscribe(() => undefined); + expect(typeof key).toBe('string'); + expect(key.length).toBeGreaterThan(0); + + const unsubSpy = jest.spyOn(sig!, 'unsubscribe'); + + const fakeThis = { ...baseFakeThis(), _languageChangeSubKey: key }; + protoFn('unsubscribeFromSignals').call(fakeThis); + + expect(unsubSpy).toHaveBeenCalledWith(key); + expect(fakeThis._languageChangeSubKey).toBe(''); + }); + + it('drains _baseClassSubscriptions for non-bridge RxJS subscriptions', () => { + const subject = new Subject(); + const sub = subject.subscribe(() => undefined); + expect(sub.closed).toBe(false); + + const fakeThis = { ...baseFakeThis(), _baseClassSubscriptions: [sub] }; + protoFn('unsubscribeFromSignals').call(fakeThis); + + expect(sub.closed).toBe(true); + expect(fakeThis._baseClassSubscriptions).toEqual([]); + }); + + it('releases the language sub even when _keepListeningOnSignalsAfterRemoval is true', () => { + // The flag exists to keep receive-state bridge subscriptions alive for + // re-attachment. It must NOT keep the language-change sub alive — that + // closure holds the component reference and leaks it forever. + const sig = Ch5SignalFactory.getInstance().getStringSignal(languageChangedSignalName, true); + const key = sig!.subscribe(() => undefined); + const unsubSpy = jest.spyOn(sig!, 'unsubscribe'); + + const fakeThis = { + ...baseFakeThis(), + _keepListeningOnSignalsAfterRemoval: true, + _languageChangeSubKey: key, + }; + protoFn('unsubscribeFromSignals').call(fakeThis); + + expect(unsubSpy).toHaveBeenCalledWith(key); + expect(fakeThis._languageChangeSubKey).toBe(''); + }); + + it('is a no-op when nothing is tracked', () => { + const fakeThis = baseFakeThis(); + expect(() => protoFn('unsubscribeFromSignals').call(fakeThis)).not.toThrow(); + }); +}); From 44f9dff04c81addee5eff7b1dc242f97db4b4e8a Mon Sep 17 00:00:00 2001 From: Navjit Brar Date: Wed, 20 May 2026 10:46:06 -0500 Subject: [PATCH 6/6] chore: tighten eslint, drop dead tslint script, add MIGRATION.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit eslint: • no-explicit-any: off → warn • no-console: off → warn Surfaces ~689 warnings across the existing source so future work can chip away at them without blocking the build today. package.json: • removes the `lint` npm script — it referenced `tslint`, but no tslint.json exists in the repo. eslint (script `eslint`) is the real linter. MIGRATION.md: • developer-facing walkthrough of every change in this branch • how to run the new Jest harness • the new shared observer APIs and their migration path • the bridge-vs-RxJS subscription pattern in Ch5Common • the regression-guard tests and how to add new ones • what is explicitly deferred to later phases and why Co-Authored-By: Claude Opus 4.7 (1M context) --- .eslintrc | 4 +- MIGRATION.md | 233 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 5 +- 3 files changed, 237 insertions(+), 5 deletions(-) create mode 100644 MIGRATION.md diff --git a/.eslintrc b/.eslintrc index cf69b6fcc..c8846a00d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -10,7 +10,7 @@ "plugin:@typescript-eslint/recommended" ], "rules": { - "no-console": 0, + "no-console": "warn", "@typescript-eslint/ban-types": [ "error", { @@ -20,7 +20,7 @@ } } ], - "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-this-alias": "off", "no-case-declarations": "off", "no-extra-semi": "off", diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 000000000..4eba18290 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,233 @@ +# CH5 SOLID Upgrade — Phase 1 Migration Notes + +Branch: `research/upgrade-ch5-for-solid` + +This document captures the changes shipped in Phase 1 of the SOLID +upgrade. It is written for the engineering team — the same people who +authored the library — so it focuses on what changed, *why*, what +behaviour is preserved, and what the migration path looks like for +future phases. + +--- + +## What shipped in Phase 1 + +| Change | Surface | Behaviour change | Test coverage | +|---------------------------------------------|-------------------------------------------|------------------|---------------| +| Jest harness alongside mocha + WCT | `tests/jest/`, `jest.config.js` | None (additive) | n/a | +| 0.5 ms → 500 ms typo | `src/ch5-list/ch5-list.ts:508` | Fixed regression | Yes (AST scan)| +| Shared MutationObserver pool | `src/ch5-core/ch5-shared-mutation-observer.ts` + facade refactor of `src/ch5-common/ch5-mutation-observer.ts` | None (facade preserved) | Yes (11 tests) | +| Shared ResizeObserver pool | `src/ch5-core/ch5-shared-resize-observer.ts` + utility refactor of `src/ch5-core/resize-observer.ts` | Disposer now returned | Yes (9 tests) | +| Direct ResizeObserver → pool migration | `ch5-video-switcher`, `ch5-signal-level-gauge` | None (private fields) | Covered by pool tests | +| Language-change subscription leak fix | `src/ch5-common/ch5-common.ts` | Leak resolved | Yes (4 tests) | +| Color-picker silent-catch fix | `src/ch5-color-picker/color-picker.ts` | Now logs failures | Yes (2 tests) | +| eslint: `no-explicit-any`, `no-console` → warn | `.eslintrc` | Warnings only | n/a | +| Dead tslint script removed | `package.json` | n/a | n/a | + +Total: **31 Jest tests pass, 0 fail.** Library still type-checks +under `tsconfig.umd.json` (two pre-existing missing-module errors in +`src/_interfaces/index.ts` predate this branch and are out of scope). + +--- + +## How to run the new test harness + +```bash +npm run test:jest # one-shot +npm run test:jest:watch # watch mode while iterating +npm run test:jest:coverage # with coverage report +``` + +The existing mocha (`npm run test:mocha`) and WCT (`npm run test:wct`) +suites are untouched. Jest tests live in `tests/jest/**/*.test.ts` and +do **not** pick up the existing `src/**/*.spec.ts` mocha files — +`testPathIgnorePatterns` in `jest.config.js` excludes them. + +--- + +## The new observer pools + +### Ch5SharedMutationObserver + +```ts +import { Ch5SharedMutationObserver } from 'ch5-core/ch5-shared-mutation-observer'; + +const dispose = Ch5SharedMutationObserver + .getInstance() + .observe(target, config, (node, mutation) => { /* … */ }); + +// later — in disconnectedCallback or removeEventListeners +shared.unobserve(target); +// (the existing Ch5MutationObserver facade does this internally) +``` + +**Existing call sites need not change.** `Ch5MutationObserver` is now a +thin facade over the pool: `new Ch5MutationObserver(component)` still +works exactly as before, but all instances now share ONE underlying +browser MutationObserver. Verified by `tests/jest/perf/mutation-observer-facade.test.ts`. + +### Ch5SharedResizeObserver + +```ts +import { Ch5SharedResizeObserver } from 'ch5-core/ch5-shared-resize-observer'; + +const dispose = Ch5SharedResizeObserver + .getInstance() + .observe(target, (target, entry) => { /* … */ }); + +// later +dispose(); +``` + +The legacy `resizeObserver(node, callback)` utility in +`src/ch5-core/resize-observer.ts` now delegates to the pool **and** +returns a disposer. Older callers that ignore the return value still +work and still benefit from pooling. The two direct +`new ResizeObserver(...)` users (`ch5-video-switcher`, +`ch5-signal-level-gauge`) are migrated to the pool. + +### Why this matters + +Before: N components meant N browser-level observers — each carrying a +callback closure, each firing on every relevant mutation/resize on its +own targets, each leaking if `disconnect()` wasn't called. + +After: one shared observer per type, regardless of N. On a panel +running 50 components with the previous patterns, this drops 50 +MutationObservers to 1. + +--- + +## What you should know about the leak fix in `Ch5Common` + +```ts +// new fields +private _languageChangeSubKey: string = ''; // bridge subscription key +private _baseClassSubscriptions: Subscription[] = []; // raw RxJS subs + +// new in unsubscribeFromSignals() +if (this._languageChangeSubKey !== '') { + const langSig = Ch5SignalFactory.getInstance() + .getStringSignal(languageChangedSignalName); + if (langSig !== null) langSig.unsubscribe(this._languageChangeSubKey); + this._languageChangeSubKey = ''; +} +for (let i = 0; i < this._baseClassSubscriptions.length; i++) { + this._baseClassSubscriptions[i].unsubscribe(); +} +this._baseClassSubscriptions = []; +``` + +Two patterns, **don't mix them**: + +- **Bridge-mediated** subscriptions (anything via `Ch5Signal.subscribe`) + return a string key. Track the key + the signal name, release with + `signal.unsubscribe(key)`. +- **Raw RxJS** subscriptions (e.g. on a `Subject` directly) return an + rxjs `Subscription`. Push it onto `_baseClassSubscriptions` to have it + released automatically in `unsubscribeFromSignals()`. + +Subclasses that override `unsubscribeFromSignals()` must call +`super.unsubscribeFromSignals()` — same as today. + +--- + +## Regression guards baked into the test suite + +These tests fail if the patterns they protect are re-introduced. They +serve as living documentation of the invariants this PR establishes. + +- `tests/jest/perf/short-timeout.test.ts` — no `setTimeout/setInterval` + with delay in `(0, 16)` ms. +- `tests/jest/perf/shared-mutation-observer.test.ts` — + N targets → 1 underlying observer. +- `tests/jest/perf/mutation-observer-facade.test.ts` — 50 facade + instances → 1 browser MutationObserver; cleanup is idempotent. +- `tests/jest/perf/shared-resize-observer.test.ts` — pool invariants; + legacy utility routes through the pool; disposer behaviour. +- `tests/jest/leaks/language-subscription.test.ts` — base-class subs + are released; the `_keepListeningOnSignalsAfterRemoval` flag does + not pin the language sub. +- `tests/jest/leaks/color-picker-silent-catch.test.ts` — init failures + emit a warning instead of being swallowed. + +### Test isolation requirement + +Tests that touch either pool **must** call the `_resetForTesting()` static +in `beforeEach`/`afterEach` to clear singleton state and (for the resize +pool) install/restore the JSDOM `ResizeObserver` shim. See the existing +tests for the template. + +### Adding a new regression guard + +For structural / source-text guards, use the AST helper: + +```ts +import { findCalls, isCalleeNamed, getNumericLiteralArg, formatHits } + from '../_helpers/ts-call-finder'; + +const hits = findCalls(call => + isCalleeNamed(call, 'doSuspiciousThing') && + /* … additional predicates … */ +); +if (hits.length) throw new Error(`…\n${formatHits(hits)}`); +``` + +For runtime / behavioural guards, write a normal Jest test using JSDOM +plus the helpers in `tests/jest/_helpers/`. + +--- + +## Explicitly out of scope for Phase 1 + +These appeared in the architecture review's recommendation list but +were *not* shipped in Phase 1. They are deferred to later phases per +the agreed scope. Doing them now without the test coverage they need +would have done more harm than good. + +| Recommendation | Why deferred | +|-------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------| +| Unify `Ch5Common` and `Ch5BaseClass` | Touches 47 component subclasses; needs the behavioural test backfill that is itself Phase 2 work. | +| Virtualise the list family | A multi-week refactor of `ch5-list`, `ch5-spinner`, `ch5-button-list`, `ch5-subpage-reference-list`. Plan separately. | +| Replace `@raghavendradabbir/mycolorpicker` | Requires sourcing a replacement and integration testing the picker UI. The silent-catch fix in this PR at least stops hiding init failures. | +| Upgrade `swiper` 7 → 13, `hammerjs` → Pointer Events, `i18next` → 24, `ts-loader` → 9 | Each is a contained but real migration. Stage as a separate dependency-refresh PR. | +| Delete `bower.json` + one of the lockfiles | Bower is still referenced by every `wct_tests/**/*.html` file. Removing it requires the WCT suite to be migrated first. yarn.lock is benign. | +| Backfill the ~70 stray `console.log` calls | The eslint rule is now `warn`. Migrating to the existing `Ch5Logger` per file is a follow-up sweep. | +| Pre-existing TS errors in `src/_interfaces/index.ts` | References to two `i-ch5-media-player-*-documentation` files that have never existed. Out of scope; predates this branch. | + +--- + +## Planned Phase 2 (maintainability focus) + +Per the agreed split, Phase 2 will tackle maintainability: + +1. Decompose `Ch5Common` into mixins / services (visibility, gestures, + signals, i18n) using the existing test scaffolding plus the new + Jest harness. +2. Pick one base class and stage the migration. The Phase 1 leak fix + was deliberately written to be portable to either base. +3. Introduce the declarative attribute schema utility, applied to one + or two small components as a proof point. +4. Migrate stray `console.log` to `Ch5Logger`; flip the eslint rule + from `warn` to `error`. + +Phase 3 will tackle the dependency refresh and list virtualisation. + +--- + +## Reviewer's note + +The discipline this PR tries to establish: + +- **Every bug fix gets a test that fails without the fix.** The 0.5ms + typo, the silent catch, the leak — all have proof tests, not just + "trust me, this is better." +- **Every new infrastructure piece gets a pool/contract test.** The + shared observers each have full test coverage independent of the + facade refactor. +- **Static regression guards beat one-time fixes.** A passing + `short-timeout.test.ts` is more durable than just changing one line. + +The aim isn't to retro-test the whole library in one PR. It is to make +each future change cheaper than the last by leaving the right rails +behind. diff --git a/package.json b/package.json index 73985e99a..c912db669 100644 --- a/package.json +++ b/package.json @@ -50,9 +50,8 @@ "compile:ts:umd": "tsc -p tsconfig.umd.json", "doc": "npm-run-all clean:docs && npm-run-all doc:*", "doc:html": "typedoc --target ES6 --mode file --hideGenerator --tsconfig tsconfig.cjs.json --out docs/html src/ ", - "doc:json": "typedoc --target ES6 --mode file --hideGenerator --tsconfig tsconfig.cjs.json --json docs/json/typedoc.json src/ ", - "lint": "tslint --project tsconfig.cjs.json", - "prebuild": "npm-run-all clean:*", + "doc:json": "typedoc --target ES6 --mode file --hideGenerator --tsconfig tsconfig.cjs.json --json docs/json/typedoc.json src/ ", + "prebuild": "npm-run-all clean:*", "test:mocha": "npm-run-all clean:compiled && tsc -p tsconfig.umd.json --target ES6 && nyc mocha --reporter mochawesome --exit", "test:jest": "jest --config jest.config.js", "test:jest:watch": "jest --config jest.config.js --watch",