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/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..c912db669 100644 --- a/package.json +++ b/package.json @@ -50,11 +50,13 @@ "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:*", - "test:mocha": "npm-run-all clean:compiled && tsc -p tsconfig.umd.json --target ES6 && nyc mocha --reporter mochawesome --exit", - "test:wct": "ws -o", + "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", + "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 +79,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 +100,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/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/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/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-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/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/_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/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(); + }); +}); 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); + }); +}); 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); + }); +}); 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([]); + }); +}); 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