From 8855b62b39d41619b0d1272d3e7c66a0e42123ba Mon Sep 17 00:00:00 2001 From: dharaneesh-r Date: Thu, 27 Aug 2026 13:40:48 +0530 Subject: [PATCH] updated the test --- .gitignore | 3 + jest.config.cjs | 40 + package-lock.json | 5246 ++++++++++++++++++++++ package.json | 13 +- src/api/doormile/queries.js | 2 +- src/lib/doormileFormat.js | 3 + tests/api/client.test.js | 322 ++ tests/api/deliveries.test.js | 556 +++ tests/api/deliveryStatus.test.js | 234 + tests/api/endpoints.test.js | 378 ++ tests/lib/AuthContext.test.jsx | 282 ++ tests/lib/ZoneContext.test.jsx | 401 ++ tests/lib/batchBucket.test.js | 138 + tests/lib/distance.test.js | 179 + tests/lib/doormileFormat.test.js | 312 ++ tests/lib/doormileTimestamp.test.js | 133 + tests/lib/orderStatusGroups.test.js | 133 + tests/lib/preferences.test.js | 159 + tests/lib/profitability.test.js | 327 ++ tests/setup/babel-plugin-import-meta.cjs | 28 + tests/setup/fileMock.cjs | 1 + tests/setup/jest.setup.cjs | 18 + tests/setup/styleMock.cjs | 2 + tests/utils/password.test.js | 160 + tests/utils/session.test.js | 171 + 25 files changed, 9239 insertions(+), 2 deletions(-) create mode 100644 jest.config.cjs create mode 100644 tests/api/client.test.js create mode 100644 tests/api/deliveries.test.js create mode 100644 tests/api/deliveryStatus.test.js create mode 100644 tests/api/endpoints.test.js create mode 100644 tests/lib/AuthContext.test.jsx create mode 100644 tests/lib/ZoneContext.test.jsx create mode 100644 tests/lib/batchBucket.test.js create mode 100644 tests/lib/distance.test.js create mode 100644 tests/lib/doormileFormat.test.js create mode 100644 tests/lib/doormileTimestamp.test.js create mode 100644 tests/lib/orderStatusGroups.test.js create mode 100644 tests/lib/preferences.test.js create mode 100644 tests/lib/profitability.test.js create mode 100644 tests/setup/babel-plugin-import-meta.cjs create mode 100644 tests/setup/fileMock.cjs create mode 100644 tests/setup/jest.setup.cjs create mode 100644 tests/setup/styleMock.cjs create mode 100644 tests/utils/password.test.js create mode 100644 tests/utils/session.test.js diff --git a/.gitignore b/.gitignore index fb2c8ab..fdb3282 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ dist-ssr .env .vite base44/.app.jsonc + +# Jest coverage output +coverage/ diff --git a/jest.config.cjs b/jest.config.cjs new file mode 100644 index 0000000..fc93c78 --- /dev/null +++ b/jest.config.cjs @@ -0,0 +1,40 @@ +/** + * Jest for the Doormile console. + * + * Babel is configured INLINE rather than through a babel.config file at the + * repo root on purpose: @vitejs/plugin-react picks up root Babel config, so a + * babel.config.cjs added for tests would silently change how production + * bundles are compiled. Test tooling must not be able to move the build. + */ +const babelOptions = { + babelrc: false, + configFile: false, + presets: [ + ['@babel/preset-env', { targets: { node: 'current' } }], + ['@babel/preset-react', { runtime: 'automatic' }], + ], + plugins: [require.resolve('./tests/setup/babel-plugin-import-meta.cjs')], +}; + +module.exports = { + testEnvironment: 'jsdom', + roots: ['/tests'], + testMatch: ['**/*.test.js', '**/*.test.jsx'], + setupFilesAfterEnv: ['/tests/setup/jest.setup.cjs'], + clearMocks: true, + restoreMocks: true, + transform: { + '^.+\.[jt]sx?$': ['babel-jest', babelOptions], + }, + moduleNameMapper: { + '\.(css|less|scss|sass)$': '/tests/setup/styleMock.cjs', + '\.(png|jpe?g|gif|svg|webp|woff2?|ttf|eot)$': '/tests/setup/fileMock.cjs', + '^@/(.*)$': '/src/$1', + }, + collectCoverageFrom: [ + 'src/api/doormile/**/*.js', + 'src/lib/**/*.js', + 'src/utils/**/*.js', + '!src/**/*.md', + ], +}; diff --git a/package-lock.json b/package-lock.json index 466d972..e6c9cce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,23 +47,38 @@ "xlsx": "^0.18.5" }, "devDependencies": { + "@babel/preset-env": "^7.29.7", + "@babel/preset-react": "^7.29.7", "@eslint/js": "^9.19.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", "@types/node": "^22.13.5", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", + "babel-jest": "^29.7.0", "eslint": "^9.19.0", "eslint-plugin-react": "^7.37.4", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-unused-imports": "^4.3.0", "globals": "^15.14.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "postcss": "^8.5.3", "tailwindcss": "^3.4.17", "typescript": "^5.8.2", "vite": "^6.1.0" } }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -186,6 +201,19 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-compilation-targets": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", @@ -203,6 +231,85 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", + "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/@babel/helper-globals": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", @@ -213,6 +320,20 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", @@ -245,6 +366,19 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-plugin-utils": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", @@ -255,6 +389,56 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", + "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -285,6 +469,21 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", + "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helpers": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", @@ -315,6 +514,1103 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz", + "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz", + "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz", + "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz", + "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz", + "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz", + "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.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-assertions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", + "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "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.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "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.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", + "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz", + "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz", + "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", + "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz", + "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", + "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz", + "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz", + "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz", + "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", + "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz", + "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz", + "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", + "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", + "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz", + "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", + "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", + "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz", + "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz", + "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.8.tgz", + "integrity": "sha512-6iSnEK0zlkLKU4heofK/AdmRD4e2SHVpJMtrwnTCzhnaM98ria4rTrOXBBi45BTTYnJtO8txnPsX4fChYXkmeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz", + "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz", + "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", + "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz", + "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", + "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", + "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", + "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz", + "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.29.7.tgz", + "integrity": "sha512-+1wdDMGNb4UPeY3Q4L5yLiYe6TXPXubs4NjrgRFw13hPRLJfEMw2Q5OXkee6/IfdqePIeW4Jjwe3aBh7SdKz4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.29.7.tgz", + "integrity": "sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.29.7.tgz", + "integrity": "sha512-Xfy3UVMF04+ypnFbkhvfqtmvwfe92qwQdbGZVonhE+6v35GzlofmOnA1szaZqzb9xYWr0nl1e5EMmzi0DNON1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", @@ -347,6 +1643,352 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.29.7.tgz", + "integrity": "sha512-H5E+HBgDpr6Q5t+Aj11tL7XkIui1jhbIoArVQnqjgXo5/3YxkN7ZEBcWF4RQlB0T4rrxJQbXS6kiFV6B7XTqUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.8.tgz", + "integrity": "sha512-0UpIXPtdDtMXfnV2OJAVMLpj3H/92vmkA6lpSRakmycJvj3VUy6Xs1dM8tXRugupykr5WB+LpiVl0J8LMVg2mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz", + "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz", + "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.8.tgz", + "integrity": "sha512-4S9ksMGVWUshvgK0mKfvZky7leuG5/uoFVwMpAomJ8bMoDJiNHRVmc1EglwW/CmGVSqqWpEbXm9FmbRit22qoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", + "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz", + "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz", + "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz", + "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", + "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz", + "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.7.tgz", + "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7", + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.29.7", + "@babel/plugin-syntax-import-attributes": "^7.29.7", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.29.7", + "@babel/plugin-transform-async-generator-functions": "^7.29.7", + "@babel/plugin-transform-async-to-generator": "^7.29.7", + "@babel/plugin-transform-block-scoped-functions": "^7.29.7", + "@babel/plugin-transform-block-scoping": "^7.29.7", + "@babel/plugin-transform-class-properties": "^7.29.7", + "@babel/plugin-transform-class-static-block": "^7.29.7", + "@babel/plugin-transform-classes": "^7.29.7", + "@babel/plugin-transform-computed-properties": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-dotall-regex": "^7.29.7", + "@babel/plugin-transform-duplicate-keys": "^7.29.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-dynamic-import": "^7.29.7", + "@babel/plugin-transform-explicit-resource-management": "^7.29.7", + "@babel/plugin-transform-exponentiation-operator": "^7.29.7", + "@babel/plugin-transform-export-namespace-from": "^7.29.7", + "@babel/plugin-transform-for-of": "^7.29.7", + "@babel/plugin-transform-function-name": "^7.29.7", + "@babel/plugin-transform-json-strings": "^7.29.7", + "@babel/plugin-transform-literals": "^7.29.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.29.7", + "@babel/plugin-transform-member-expression-literals": "^7.29.7", + "@babel/plugin-transform-modules-amd": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-modules-systemjs": "^7.29.7", + "@babel/plugin-transform-modules-umd": "^7.29.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-new-target": "^7.29.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7", + "@babel/plugin-transform-numeric-separator": "^7.29.7", + "@babel/plugin-transform-object-rest-spread": "^7.29.7", + "@babel/plugin-transform-object-super": "^7.29.7", + "@babel/plugin-transform-optional-catch-binding": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/plugin-transform-private-methods": "^7.29.7", + "@babel/plugin-transform-private-property-in-object": "^7.29.7", + "@babel/plugin-transform-property-literals": "^7.29.7", + "@babel/plugin-transform-regenerator": "^7.29.7", + "@babel/plugin-transform-regexp-modifiers": "^7.29.7", + "@babel/plugin-transform-reserved-words": "^7.29.7", + "@babel/plugin-transform-shorthand-properties": "^7.29.7", + "@babel/plugin-transform-spread": "^7.29.7", + "@babel/plugin-transform-sticky-regex": "^7.29.7", + "@babel/plugin-transform-template-literals": "^7.29.7", + "@babel/plugin-transform-typeof-symbol": "^7.29.7", + "@babel/plugin-transform-unicode-escapes": "^7.29.7", + "@babel/plugin-transform-unicode-property-regex": "^7.29.7", + "@babel/plugin-transform-unicode-regex": "^7.29.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.29.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.29.7.tgz", + "integrity": "sha512-C+PV1TFUPTmBQGoPBL8j2QmLpZ117YTCwxIZeJOM96GbYMFSc7/pOXU5lVykwnZxyTqQxRsvoRk6f2FktZgGHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-transform-react-display-name": "^7.29.7", + "@babel/plugin-transform-react-jsx": "^7.29.7", + "@babel/plugin-transform-react-jsx-development": "^7.29.7", + "@babel/plugin-transform-react-pure-annotations": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", @@ -395,6 +2037,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/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -1119,6 +2768,480 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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": { + "@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/@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": "^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/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/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/@jest/core/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/@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": { + "@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/@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/@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": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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": { + "@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.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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": "^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/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": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jest/reporters/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/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", @@ -2554,6 +4677,33 @@ "win32" ] }, + "node_modules/@sinclair/typebox": { + "version": "0.27.12", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz", + "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==", + "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/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -2604,6 +4754,98 @@ "react": "^18 || ^19" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "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/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2719,6 +4961,55 @@ "dev": true, "license": "MIT" }, + "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/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/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/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2756,12 +5047,43 @@ "@types/react": "^19.2.0" } }, + "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/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, + "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/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -2783,6 +5105,14 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/acorn": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", @@ -2796,6 +5126,17 @@ "node": ">=0.4.0" } }, + "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/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -2806,6 +5147,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "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/adler-32": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", @@ -2844,6 +5198,32 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "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-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2904,6 +5284,16 @@ "node": ">=10" } }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", @@ -3123,6 +5513,147 @@ "proxy-from-env": "^2.1.0" } }, + "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": { + "@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-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-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.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", @@ -3212,6 +5743,23 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -3271,6 +5819,16 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -3331,6 +5889,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/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -3367,6 +5935,29 @@ "node": ">= 6" } }, + "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-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -3379,6 +5970,21 @@ "url": "https://polar.sh/cva" } }, + "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/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -3388,6 +5994,17 @@ "node": ">=6" } }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, "node_modules/codepage": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", @@ -3397,6 +6014,13 @@ "node": ">=0.8" } }, + "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/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3452,6 +6076,23 @@ "dev": true, "license": "MIT" }, + "node_modules/core-js-compat": { + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.50.0.tgz", + "integrity": "sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.7" + }, + "engines": { + "node": ">=6.4.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -3464,6 +6105,28 @@ "node": ">=0.8" } }, + "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/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3486,6 +6149,13 @@ "license": "BSD", "peer": true }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -3498,6 +6168,33 @@ "node": ">=4" } }, + "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/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -3625,6 +6322,21 @@ "node": ">=12" } }, + "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/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -3702,12 +6414,34 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "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-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3715,6 +6449,16 @@ "dev": true, "license": "MIT" }, + "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/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -3760,6 +6504,26 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -3772,6 +6536,16 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, + "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/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -3791,6 +6565,27 @@ "node": ">=0.10.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "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/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3812,6 +6607,49 @@ "dev": true, "license": "ISC" }, + "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", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "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/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", @@ -4083,6 +6921,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/eslint": { "version": "9.39.5", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", @@ -4253,6 +7113,20 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", @@ -4305,6 +7179,56 @@ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, + "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/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/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4363,6 +7287,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": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -4528,6 +7462,13 @@ } } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4605,6 +7546,16 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -4638,6 +7589,16 @@ "node": ">=6" } }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -4651,6 +7612,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.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -4669,6 +7643,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4732,6 +7728,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -4823,6 +7826,41 @@ "node": ">= 0.4" } }, + "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/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "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/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -4836,6 +7874,29 @@ "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/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/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4873,6 +7934,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -4883,6 +7964,35 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -4945,6 +8055,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -5114,6 +8231,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "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.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -5198,6 +8335,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -5246,6 +8390,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", @@ -5357,6 +8514,77 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "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/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -5375,6 +8603,967 @@ "node": ">= 0.4" } }, + "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-circus/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/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/jest-circus/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/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-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/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/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/jest-config/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/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-diff/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/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/jest-diff/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/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-each/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/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/jest-each/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/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-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-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-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-leak-detector/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/jest-leak-detector/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/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-matcher-utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/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/jest-matcher-utils/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/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-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/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/jest-message-util/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/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-resolve/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-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-snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/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/jest-snapshot/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/jest-snapshot/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-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-validate/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/jest-validate/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/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": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -5413,6 +9602,52 @@ "js-yaml": "bin/js-yaml.js" } }, + "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/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -5433,6 +9668,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -5486,12 +9728,32 @@ "json-buffer": "3.0.1" } }, + "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/leaflet": { "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", "license": "BSD-2-Clause" }, + "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", @@ -5540,6 +9802,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "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", @@ -5578,6 +9847,55 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5587,6 +9905,13 @@ "node": ">= 0.4" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5630,6 +9955,26 @@ "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/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -5719,6 +10064,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.53", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", @@ -5738,6 +10090,26 @@ "node": ">=0.10.0" } }, + "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.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5854,6 +10226,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "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/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5923,6 +10321,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/papaparse": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.6.0.tgz", @@ -5942,6 +10350,38 @@ "node": ">=6" } }, + "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/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/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5952,6 +10392,16 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -6004,6 +10454,75 @@ "node": ">= 6" } }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -6201,6 +10720,55 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.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": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "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", @@ -6221,6 +10789,19 @@ "node": ">=10" } }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -6231,6 +10812,30 @@ "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/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -6509,6 +11114,20 @@ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", @@ -6547,6 +11166,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -6568,6 +11207,61 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.2.tgz", + "integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, "node_modules/reselect": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", @@ -6598,6 +11292,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -6608,6 +11325,16 @@ "node": ">=4" } }, + "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/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -6742,6 +11469,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "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/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -6906,6 +11653,40 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "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", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6915,6 +11696,24 @@ "node": ">=0.10.0" } }, + "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/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/ssf": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", @@ -6927,6 +11726,29 @@ "node": ">=0.8" } }, + "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/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -6941,6 +11763,35 @@ "node": ">= 0.4" } }, + "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", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -7040,6 +11891,52 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "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-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -7107,6 +12004,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", @@ -7184,6 +12088,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -7256,6 +12175,13 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -7268,6 +12194,35 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "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/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -7293,6 +12248,29 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "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/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -7411,6 +12389,60 @@ "dev": true, "license": "MIT" }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.0.tgz", @@ -7452,6 +12484,17 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/use-callback-ref": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", @@ -7522,6 +12565,21 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "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/victory-vendor": { "version": "37.3.6", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", @@ -7650,6 +12708,77 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "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/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/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/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/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/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/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7783,6 +12912,67 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "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/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "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/xlsx": { "version": "0.18.5", "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", @@ -7804,6 +12994,33 @@ "node": ">=0.8" } }, + "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/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -7811,6 +13028,35 @@ "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "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/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/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index aef1808..8dde36c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,10 @@ "lint": "eslint . --quiet", "lint:fix": "eslint . --fix", "typecheck": "tsc -p ./jsconfig.json", - "preview": "vite preview" + "preview": "vite preview", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage" }, "dependencies": { "@astryxdesign/core": "^0.4.7", @@ -51,17 +54,25 @@ "xlsx": "^0.18.5" }, "devDependencies": { + "@babel/preset-env": "^7.29.7", + "@babel/preset-react": "^7.29.7", "@eslint/js": "^9.19.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", "@types/node": "^22.13.5", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", + "babel-jest": "^29.7.0", "eslint": "^9.19.0", "eslint-plugin-react": "^7.37.4", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-unused-imports": "^4.3.0", "globals": "^15.14.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "postcss": "^8.5.3", "tailwindcss": "^3.4.17", "typescript": "^5.8.2", diff --git a/src/api/doormile/queries.js b/src/api/doormile/queries.js index 78025e8..98a66e9 100644 --- a/src/api/doormile/queries.js +++ b/src/api/doormile/queries.js @@ -157,7 +157,7 @@ export const deriveDeliveryStatus = (bookingOrStatus, consignmentMapOrReachedAt) return isReached ? 'arrived' : 'accepted'; } - return BOOKING_STATUS_TO_DELIVERY_STATUS[rawBookingStatus] || rawBookingStatus; + return BOOKING_STATUS_TO_DELIVERY_STATUS[rawBookingStatus] || (rawBookingStatus ? rawBookingStatus : 'pending'); }; export const mapBookingStatusToDeliveryStatus = (statusOrRow, maybeReachedAt) => { diff --git a/src/lib/doormileFormat.js b/src/lib/doormileFormat.js index 38394fb..6da1b31 100644 --- a/src/lib/doormileFormat.js +++ b/src/lib/doormileFormat.js @@ -18,18 +18,21 @@ const DECIMAL = new Intl.NumberFormat('en-IN'); /** Money, or a dash. `0` is a real amount and renders as ₹0.00. */ export const currency = (value, fallback = '—') => { + if (value === null || value === undefined || value === '') return fallback; const n = Number(value); return Number.isFinite(n) ? INR.format(n) : fallback; }; /** A count with thousands separators, or a dash. */ export const number = (value, fallback = '—') => { + if (value === null || value === undefined || value === '') return fallback; const n = Number(value); return Number.isFinite(n) ? DECIMAL.format(n) : fallback; }; /** A distance in km to one decimal, or a dash. */ export const km = (value, fallback = '—') => { + if (value === null || value === undefined || value === '') return fallback; const n = Number(value); return Number.isFinite(n) ? `${n.toFixed(1)} km` : fallback; }; diff --git a/tests/api/client.test.js b/tests/api/client.test.js new file mode 100644 index 0000000..adf8653 --- /dev/null +++ b/tests/api/client.test.js @@ -0,0 +1,322 @@ +/** + * The Doormile admin API client: bearer-token attachment, 401 session ending, + * and the error-normalisation contract every write path depends on. + * + * axios is mocked at the module boundary so the two interceptors can be invoked + * directly — they are the unit under test, so nothing about their logic is + * stubbed, only the transport underneath them. + */ +jest.mock('axios', () => { + const handlers = { request: null, responseOk: null, responseErr: null }; + const instance = { + interceptors: { + request: { use: (fn) => { handlers.request = fn; } }, + response: { + use: (ok, err) => { + handlers.responseOk = ok; + handlers.responseErr = err; + } + } + } + }; + const mockAxios = { create: jest.fn(() => instance), __handlers: handlers, __instance: instance }; + return { __esModule: true, default: mockAxios }; +}); + +import axios from 'axios'; +import doormileAxios, { + DOORMILE_TOKEN_KEY, + DOORMILE_USER_KEY, + DOORMILE_SESSION_KEYS, + DOORMILE_ASSISTANT_KEYS, + DOORMILE_API_URL, + DOORMILE_BASE_URL, + clearStoredSession, + readStoredUser, + readStoredToken, + errorMessage +} from '@/api/doormile/client'; + +const handlers = axios.__handlers; + +/** Replaces window.location with a spyable stand-in (jsdom forbids assignment). */ +const stubLocation = (href) => { + const replace = jest.fn(); + Object.defineProperty(window, 'location', { + writable: true, + configurable: true, + value: { href, replace } + }); + return replace; +}; + +const rejection = (fn) => fn().then( + () => { throw new Error('expected the interceptor to reject'); }, + (err) => err +); + +describe('doormile API client', () => { + describe('base URL', () => { + it('should point at the real production API by default', () => { + expect(DOORMILE_API_URL).toBe('https://api.doormile.com/api/v1'); + expect(DOORMILE_BASE_URL).toBe(DOORMILE_API_URL); + }); + + it('should create the axios instance against the resolved base URL', () => { + jest.isolateModules(() => { + globalThis.__VITE_IMPORT_META__ = { env: { DEV: false } }; + // eslint-disable-next-line global-require + const reloaded = require('@/api/doormile/client'); + expect(axios.create).toHaveBeenCalledWith({ baseURL: reloaded.DOORMILE_BASE_URL }); + }); + }); + + it('should let VITE_DOORMILE_URL redirect the console at a staging backend', () => { + jest.isolateModules(() => { + globalThis.__VITE_IMPORT_META__ = { env: { DEV: false, VITE_DOORMILE_URL: 'http://localhost:9000/api/v1' } }; + // eslint-disable-next-line global-require + const reloaded = require('@/api/doormile/client'); + expect(reloaded.DOORMILE_BASE_URL).toBe('http://localhost:9000/api/v1'); + }); + }); + + it('should warn loudly in development when the backend is overridden', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); + jest.isolateModules(() => { + globalThis.__VITE_IMPORT_META__ = { env: { DEV: true, VITE_DOORMILE_URL: 'http://localhost:9000/api/v1' } }; + // eslint-disable-next-line global-require + require('@/api/doormile/client'); + }); + expect(warn).toHaveBeenCalledWith(expect.stringContaining('http://localhost:9000/api/v1')); + expect(warn).toHaveBeenCalledWith(expect.stringContaining(DOORMILE_API_URL)); + }); + + it('should stay silent in development when no override is set', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); + jest.isolateModules(() => { + globalThis.__VITE_IMPORT_META__ = { env: { DEV: true } }; + // eslint-disable-next-line global-require + require('@/api/doormile/client'); + }); + expect(warn).not.toHaveBeenCalled(); + }); + }); + + describe('request interceptor', () => { + it('should attach the stored token as a Bearer credential', () => { + localStorage.setItem(DOORMILE_TOKEN_KEY, 'jwt-abc'); + const config = handlers.request({ headers: {} }); + expect(config.headers.Authorization).toBe('Bearer jwt-abc'); + }); + + it('should leave the request unauthenticated when no token is stored', () => { + // Login itself is issued through this instance; forcing a header would + // send `Bearer null` on the one call that must not carry one. + const config = handlers.request({ headers: {} }); + expect(config.headers.Authorization).toBeUndefined(); + }); + + it('should preserve headers the caller already set', () => { + localStorage.setItem(DOORMILE_TOKEN_KEY, 'jwt-abc'); + const config = handlers.request({ headers: { 'Content-Type': 'multipart/form-data' } }); + expect(config.headers['Content-Type']).toBe('multipart/form-data'); + expect(config.headers.Authorization).toBe('Bearer jwt-abc'); + }); + + it('should return the same config object the caller passed', () => { + const config = { headers: {}, url: '/admin/bookings' }; + expect(handlers.request(config)).toBe(config); + }); + }); + + describe('response interceptor — success', () => { + it('should pass a successful response through untouched', () => { + const response = { status: 200, data: { success: true } }; + expect(handlers.responseOk(response)).toBe(response); + }); + }); + + describe('response interceptor — 401 handling', () => { + it('should end the session and send the operator back to login on a 401', () => { + const replace = stubLocation('http://console.local/deliveries'); + localStorage.setItem(DOORMILE_TOKEN_KEY, 'jwt-abc'); + localStorage.setItem(DOORMILE_USER_KEY, '{"id":1}'); + + return rejection(() => handlers.responseErr({ response: { status: 401, data: { message: 'expired' } } })).then(() => { + expect(localStorage.getItem(DOORMILE_TOKEN_KEY)).toBeNull(); + expect(localStorage.getItem(DOORMILE_USER_KEY)).toBeNull(); + expect(replace).toHaveBeenCalledWith('/login'); + }); + }); + + it('should NOT redirect when the 401 came from the login page itself', async () => { + // A refused password must show the server's message in place, not bounce + // the operator through a page reload that loses what they typed. + const replace = stubLocation('http://console.local/login'); + localStorage.setItem(DOORMILE_TOKEN_KEY, 'jwt-abc'); + + await rejection(() => handlers.responseErr({ response: { status: 401, data: { message: 'bad password' } } })); + + expect(replace).not.toHaveBeenCalled(); + expect(localStorage.getItem(DOORMILE_TOKEN_KEY)).toBe('jwt-abc'); + }); + + it.each([400, 403, 404, 409, 500])( + 'should leave the session alone on a %d, which is not an auth failure', + async (status) => { + const replace = stubLocation('http://console.local/deliveries'); + localStorage.setItem(DOORMILE_TOKEN_KEY, 'jwt-abc'); + + await rejection(() => handlers.responseErr({ response: { status, data: { message: 'nope' } } })); + + expect(replace).not.toHaveBeenCalled(); + expect(localStorage.getItem(DOORMILE_TOKEN_KEY)).toBe('jwt-abc'); + } + ); + }); + + describe('response interceptor — error normalisation', () => { + beforeEach(() => stubLocation('http://console.local/deliveries')); + + it("should reject with the server's own body so callers can read err.message", async () => { + const err = await rejection(() => + handlers.responseErr({ response: { status: 400, data: { success: false, message: 'status is required' } } }) + ); + expect(err.message).toBe('status is required'); + expect(err.success).toBe(false); + }); + + it('should keep the HTTP status reachable as httpStatus', async () => { + // Without this a 404, a 500 and a validation failure collapse into one + // generic failure — exactly the distinction the write paths need. + const err = await rejection(() => + handlers.responseErr({ response: { status: 404, data: { message: 'not found' } } }) + ); + expect(err.httpStatus).toBe(404); + }); + + it('should make httpStatus non-enumerable so it never leaks into a spread', async () => { + const err = await rejection(() => + handlers.responseErr({ response: { status: 409, data: { message: 'conflict' } } }) + ); + expect(Object.keys(err)).not.toContain('httpStatus'); + expect(JSON.parse(JSON.stringify(err))).not.toHaveProperty('httpStatus'); + expect({ ...err }.httpStatus).toBeUndefined(); + expect(err.httpStatus).toBe(409); + }); + + it('should wrap a plain-text error body as { message }', async () => { + const err = await rejection(() => + handlers.responseErr({ response: { status: 500, data: 'Internal Server Error' } }) + ); + expect(err.message).toBe('Internal Server Error'); + expect(err.httpStatus).toBe(500); + }); + + it.each([ + ['a network failure with no response', { message: 'Network Error' }], + ['a response carrying a null body', { response: { status: 502, data: null } }], + ['a response carrying no body at all', { response: { status: 502 } }] + ])('should reject with the original error for %s', async (_label, raw) => { + const err = await rejection(() => handlers.responseErr(raw)); + expect(err).toBe(raw); + }); + }); + + describe('clearStoredSession', () => { + it('should remove the credential, the identity and the assistant transcript', () => { + localStorage.setItem(DOORMILE_TOKEN_KEY, 'jwt-abc'); + localStorage.setItem(DOORMILE_USER_KEY, '{"id":1}'); + DOORMILE_SESSION_KEYS.forEach((key) => localStorage.setItem(key, 'x')); + DOORMILE_ASSISTANT_KEYS.forEach((key) => localStorage.setItem(key, 'x')); + + clearStoredSession(); + + [DOORMILE_TOKEN_KEY, DOORMILE_USER_KEY, ...DOORMILE_SESSION_KEYS, ...DOORMILE_ASSISTANT_KEYS].forEach((key) => { + expect(localStorage.getItem(key)).toBeNull(); + }); + }); + + it('should clear tenantid, which decides staff-vs-tenant scope', () => { + // A stale tenantid is readable by the next operator before they sign in. + expect(DOORMILE_SESSION_KEYS).toContain('tenantid'); + localStorage.setItem('tenantid', '42'); + clearStoredSession(); + expect(localStorage.getItem('tenantid')).toBeNull(); + }); + + it('should clear the assistant transcript, which contains customer data', () => { + expect(DOORMILE_ASSISTANT_KEYS).toEqual( + expect.arrayContaining(['doormileBotHistory', 'doormileBotConversations']) + ); + }); + + it('should leave non-session preferences in place', () => { + // The panel width is a preference, not session data — a shared warehouse + // terminal should keep its layout across sign-ins. + localStorage.setItem('doormileBotPanelWidth', '420'); + localStorage.setItem('doormilePreferences', '{"compactDensity":true}'); + clearStoredSession(); + expect(localStorage.getItem('doormileBotPanelWidth')).toBe('420'); + expect(localStorage.getItem('doormilePreferences')).toBe('{"compactDensity":true}'); + }); + + it('should be safe to call when nothing is stored', () => { + expect(() => clearStoredSession()).not.toThrow(); + }); + }); + + describe('readStoredUser / readStoredToken', () => { + it('should return the parsed admin record', () => { + localStorage.setItem(DOORMILE_USER_KEY, JSON.stringify({ userid: 7, tenantid: '0' })); + expect(readStoredUser()).toEqual({ userid: 7, tenantid: '0' }); + }); + + it('should return null when signed out', () => { + expect(readStoredUser()).toBeNull(); + expect(readStoredToken()).toBeNull(); + }); + + it('should return null — not throw — for a corrupt stored record', () => { + // A half-written record must not take the whole console down on boot. + localStorage.setItem(DOORMILE_USER_KEY, '{not json'); + expect(readStoredUser()).toBeNull(); + }); + + it('should return the raw token string', () => { + localStorage.setItem(DOORMILE_TOKEN_KEY, 'jwt-abc'); + expect(readStoredToken()).toBe('jwt-abc'); + }); + }); + + describe('errorMessage', () => { + it("should prefer the normalised body's own message", () => { + expect(errorMessage({ message: 'status is required' })).toBe('status is required'); + }); + + it('should fall back to a raw axios error body', () => { + expect(errorMessage({ response: { data: { message: 'server said no' } } })).toBe('server said no'); + }); + + it('should prefer err.message over the nested response body', () => { + expect(errorMessage({ message: 'outer', response: { data: { message: 'inner' } } })).toBe('outer'); + }); + + it.each([null, undefined, {}, { message: '' }])( + 'should fall back to the default text for %p', + (err) => { + expect(errorMessage(err)).toBe('Something went wrong'); + } + ); + + it('should honour a caller-supplied fallback', () => { + expect(errorMessage(null, 'Could not load deliveries')).toBe('Could not load deliveries'); + }); + }); + + describe('module surface', () => { + it('should export the configured axios instance as the default', () => { + expect(doormileAxios).toBe(axios.__instance); + }); + }); +}); diff --git a/tests/api/deliveries.test.js b/tests/api/deliveries.test.js new file mode 100644 index 0000000..bc4fadd --- /dev/null +++ b/tests/api/deliveries.test.js @@ -0,0 +1,556 @@ +jest.mock('@/api/doormile/endpoints', () => ({ + __esModule: true, + getBookings: jest.fn(), + getBooking: jest.fn(), + getConsignments: jest.fn(), + getAdminCustomers: jest.fn(), + getAdminTenants: jest.fn(), + getMilers: jest.fn(), + getMiler: jest.fn(), + getMilerSummary: jest.fn(), + getMilerLogs: jest.fn(), + notifyMiler: jest.fn(), + getTenantLocations: jest.fn(), + getHubs: jest.fn(), + assignMilerToBooking: jest.fn(), + cancelBooking: jest.fn(), + updateConsignmentStatus: jest.fn(), + getAdminPricing: jest.fn(), + getReports: jest.fn(), + getLocationsSummary: jest.fn(), + getProfile: jest.fn() +})); +jest.mock('@/api/doormile/notify', () => ({ + __esModule: true, + OpenToast: jest.fn(), + messageOf: jest.fn((err, fallback) => err?.message || fallback) +})); + +import * as endpoints from '@/api/doormile/endpoints'; +import { OpenToast } from '@/api/doormile/notify'; +import { + fetchDeliveries, + fetchCountAPI, + cancelDeliveryAPI, + changeRiderAPI, + updateDeliveryAPI, + getorderdetails +} from '@/api/doormile/queries'; + +/** + * A booking as GET /admin/bookings returns it. Defaults make the row + * "dispatched" (it carries an assigned rider), because fetchDeliveries only + * surfaces dispatched rows. + */ +const booking = (over = {}) => ({ + bookingid: 4021, + bookingno: 'DM-4021', + status: 'Miler_Assigned', + assignedmileruserid: 91, + tenantid: 4, + createdat: '2026-08-27T10:00:00', + updatedat: '2026-08-27T10:05:00', + pickupaddress: '12 MG Road, Bengaluru', + deliveryaddress: '44 Indiranagar, Bengaluru', + ...over +}); + +/** The query key every caller of fetchDeliveries builds. */ +const key = ({ status = 'all', startdate, enddate, rowsPerPage = 100 } = {}) => [ + 'deliveries', + 1, + null, + status, + startdate, + enddate, + rowsPerPage +]; + +const withNoJoins = () => { + endpoints.getAdminCustomers.mockResolvedValue([]); + endpoints.getMilers.mockResolvedValue([]); + endpoints.getAdminTenants.mockResolvedValue([]); + endpoints.getConsignments.mockResolvedValue([]); +}; + +describe('deliveries data layer', () => { + beforeEach(withNoJoins); + + describe('fetchDeliveries — dispatched filtering', () => { + it('should surface a booking that already has a rider assigned', async () => { + endpoints.getBookings.mockResolvedValue([booking()]); + const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() }); + expect(rows).toHaveLength(1); + expect(rows[0]).toMatchObject({ bookingid: 4021, deliveryid: 4021, orderheaderid: 4021 }); + }); + + it.each(['pending_pickup', 'created', 'new', 'booked', 'order_placed', 'unassigned'])( + 'should exclude an undispatched %s booking with no rider and no consignment', + async (status) => { + endpoints.getBookings.mockResolvedValue([ + booking({ status, assignedmileruserid: null, consignmentid: null }) + ]); + const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() }); + expect(rows).toEqual([]); + } + ); + + it('should surface an undispatched-looking booking that nonetheless has a consignment', async () => { + endpoints.getBookings.mockResolvedValue([ + booking({ status: 'pending_pickup', assignedmileruserid: null, consignmentid: 40 }) + ]); + const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() }); + expect(rows).toHaveLength(1); + expect(rows[0].consignmentid).toBe(40); + }); + + it('should surface a booking whose status has moved past the pre-dispatch set', async () => { + endpoints.getBookings.mockResolvedValue([ + booking({ status: 'Out_for_Delivery', assignedmileruserid: null, consignmentid: null }) + ]); + const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() }); + expect(rows).toHaveLength(1); + }); + }); + + describe('fetchDeliveries — row mapping', () => { + it('should resolve the rider name from the joined miler list', async () => { + endpoints.getBookings.mockResolvedValue([booking()]); + endpoints.getMilers.mockResolvedValue([ + { userid: 91, milerprofileid: 11, firstname: 'Anita', lastname: 'Rao', phone: '9876543210' } + ]); + + const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() }); + expect(rows[0]).toMatchObject({ + ridername: 'Anita Rao', + userid: 91, + milerprofileid: 11, + ridercontact: '9876543210' + }); + }); + + it('should fall back to a numbered rider label when the miler join fails', async () => { + // A failed join must degrade a display field, not blank the page. + endpoints.getBookings.mockResolvedValue([booking()]); + endpoints.getMilers.mockRejectedValue(new Error('milers 500')); + + const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() }); + expect(rows).toHaveLength(1); + expect(rows[0].ridername).toBe('Rider #91'); + }); + + it('should resolve the delivery customer from the joined customer list', async () => { + endpoints.getBookings.mockResolvedValue([booking({ appcustomerid: 7 })]); + endpoints.getAdminCustomers.mockResolvedValue([ + { appcustomerid: 7, firstname: 'Vikram', phone: '9000000000' } + ]); + + const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() }); + expect(rows[0]).toMatchObject({ deliverycustomer: 'Vikram', deliverycontactno: '9000000000', deliverytype: 'B' }); + }); + + it('should mark a row with no matched customer as a C-type delivery', async () => { + endpoints.getBookings.mockResolvedValue([booking()]); + const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() }); + expect(rows[0].deliverytype).toBe('C'); + }); + + it('should bucket on the booking creation time, exposed as orderdate', async () => { + // Batch bucketing and date filtering both read orderdate; if this stops + // being createdat, rows are counted for a day but belong to no batch. + endpoints.getBookings.mockResolvedValue([booking({ createdat: '2026-08-27T07:00:00' })]); + const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() }); + expect(rows[0].orderdate).toBe('2026-08-27T07:00:00'); + }); + + it('should approximate distance as a straight line between the two coordinates', async () => { + endpoints.getBookings.mockResolvedValue([ + booking({ + pickuplatitude: 12.9716, + pickuplongitude: 77.5946, + deliverylatitude: 12.9352, + deliverylongitude: 77.6245 + }) + ]); + const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() }); + expect(rows[0].kms).toBeGreaterThan(4); + expect(rows[0].kms).toBeLessThan(7); + }); + + it('should leave distance undefined — not zero — when a coordinate is missing', async () => { + // A fabricated 0 km reads as a measured distance and feeds the rate card. + endpoints.getBookings.mockResolvedValue([booking({ pickuplatitude: null })]); + const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() }); + expect(rows[0].kms).toBeUndefined(); + }); + + it('should leave route-plan fields undefined rather than fabricating them', async () => { + endpoints.getBookings.mockResolvedValue([booking()]); + const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() }); + expect(rows[0].transitminutes).toBeUndefined(); + expect(rows[0].cumulativekms).toBeUndefined(); + expect(rows[0].step).toBeUndefined(); + }); + + it('should count parcels as the row quantity, defaulting to zero', async () => { + endpoints.getBookings.mockResolvedValue([booking({ parcels: [{}, {}] }), booking({ bookingid: 4022 })]); + const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() }); + expect(rows[0].quantity).toBe(2); + expect(rows[1].quantity).toBe(0); + }); + + it('should carry the service charge through to every amount field the table reads', async () => { + endpoints.getBookings.mockResolvedValue([ + booking({ serviceoptions: [{ estimatedprice: 240, estimateddeliveryat: '2026-08-27T12:00:00' }] }) + ]); + const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() }); + expect(rows[0]).toMatchObject({ deliverycharges: 240, deliveryamt: 240, deliveryamount: 240 }); + expect(rows[0].expecteddeliverytime).toBe('2026-08-27T12:00:00'); + }); + + it('should let the consignment status drive the row status', async () => { + endpoints.getBookings.mockResolvedValue([ + booking({ status: 'Converted_To_Consignment', consignmentid: 40 }) + ]); + endpoints.getConsignments.mockResolvedValue([{ consignmentid: 40, status: 'Out_for_Delivery' }]); + + const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() }); + expect(rows[0].orderstatus).toBe('active'); + expect(rows[0].statusfromconsignment).toBe(true); + }); + + it('should fall back to the booking status when the consignment join fails', async () => { + endpoints.getBookings.mockResolvedValue([ + booking({ status: 'Converted_To_Consignment', consignmentid: 40 }) + ]); + endpoints.getConsignments.mockRejectedValue(new Error('consignments 500')); + + const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() }); + expect(rows[0].orderstatus).toBe('picked'); + }); + }); + + describe('fetchDeliveries — date range', () => { + const threeDays = [ + booking({ bookingid: 1, createdat: '2026-08-25T10:00:00' }), + booking({ bookingid: 2, createdat: '2026-08-26T10:00:00' }), + booking({ bookingid: 3, createdat: '2026-08-27T10:00:00' }) + ]; + + it('should return every row when no range is given', async () => { + endpoints.getBookings.mockResolvedValue(threeDays); + const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() }); + expect(rows).toHaveLength(3); + }); + + it('should apply the requested range instead of ignoring it', async () => { + // The regression: startdate/enddate were accepted and then dropped, so + // today, yesterday and last week all produced a byte-identical row set. + endpoints.getBookings.mockResolvedValue(threeDays); + const { rows } = await fetchDeliveries({ + pageParam: 1, + queryKey: key({ startdate: '2026-08-26', enddate: '2026-08-26' }) + }); + expect(rows.map((r) => r.bookingid)).toEqual([2]); + }); + + it('should include both range boundaries', async () => { + endpoints.getBookings.mockResolvedValue(threeDays); + const { rows } = await fetchDeliveries({ + pageParam: 1, + queryKey: key({ startdate: '2026-08-25', enddate: '2026-08-27' }) + }); + expect(rows).toHaveLength(3); + }); + + it('should treat a missing bound as unbounded on that side', async () => { + endpoints.getBookings.mockResolvedValue(threeDays); + const from = await fetchDeliveries({ pageParam: 1, queryKey: key({ startdate: '2026-08-26' }) }); + expect(from.rows.map((r) => r.bookingid)).toEqual([2, 3]); + + const until = await fetchDeliveries({ pageParam: 1, queryKey: key({ enddate: '2026-08-26' }) }); + expect(until.rows.map((r) => r.bookingid)).toEqual([1, 2]); + }); + + it('should keep a late-evening row on its own day despite a false trailing Z', async () => { + // Unstripped, 23:45Z shifts +5:30 into the next day and drops out of the + // selected date entirely. + endpoints.getBookings.mockResolvedValue([booking({ createdat: '2026-08-27T23:45:00Z' })]); + const { rows } = await fetchDeliveries({ + pageParam: 1, + queryKey: key({ startdate: '2026-08-27', enddate: '2026-08-27' }) + }); + expect(rows).toHaveLength(1); + }); + + it('should drop a row with an unparseable creation date when a range is set', async () => { + endpoints.getBookings.mockResolvedValue([booking({ createdat: 'garbage' })]); + const { rows } = await fetchDeliveries({ + pageParam: 1, + queryKey: key({ startdate: '2026-08-27', enddate: '2026-08-27' }) + }); + expect(rows).toEqual([]); + }); + + it('should return an empty page for a range with no rows', async () => { + endpoints.getBookings.mockResolvedValue(threeDays); + const { rows } = await fetchDeliveries({ + pageParam: 1, + queryKey: key({ startdate: '2026-01-01', enddate: '2026-01-02' }) + }); + expect(rows).toEqual([]); + }); + }); + + describe('fetchDeliveries — pagination and failure', () => { + it('should advertise a next page when the server returned a full page', async () => { + endpoints.getBookings.mockResolvedValue(Array.from({ length: 25 }, (_, i) => booking({ bookingid: i + 1 }))); + const result = await fetchDeliveries({ pageParam: 1, queryKey: key({ rowsPerPage: 25 }) }); + expect(result.nextPage).toBe(2); + }); + + it('should stop paginating on a partial page', async () => { + endpoints.getBookings.mockResolvedValue(Array.from({ length: 9 }, (_, i) => booking({ bookingid: i + 1 }))); + const result = await fetchDeliveries({ pageParam: 1, queryKey: key({ rowsPerPage: 25 }) }); + expect(result.nextPage).toBeUndefined(); + }); + + it('should request the page the caller asked for', async () => { + endpoints.getBookings.mockResolvedValue([]); + await fetchDeliveries({ pageParam: 3, queryKey: key({ rowsPerPage: 50 }) }); + expect(endpoints.getBookings).toHaveBeenCalledWith(3, 50); + }); + + it('should default to the first page when pageParam is omitted', async () => { + endpoints.getBookings.mockResolvedValue([]); + await fetchDeliveries({ queryKey: key({ rowsPerPage: 50 }) }); + expect(endpoints.getBookings).toHaveBeenCalledWith(1, 50); + }); + + it('should toast and return an empty page when the bookings call fails', async () => { + // The one join it cannot recover from — previously it rejected uncaught, + // with no toast anywhere across the four pages that call this. + endpoints.getBookings.mockRejectedValue({ message: 'bookings 503' }); + + const result = await fetchDeliveries({ pageParam: 1, queryKey: key() }); + + expect(result).toEqual({ rows: [], nextPage: undefined }); + expect(OpenToast).toHaveBeenCalledWith('bookings 503', 'error', 2000); + }); + + it('should still render rows when every optional join fails', async () => { + endpoints.getBookings.mockResolvedValue([booking()]); + endpoints.getAdminCustomers.mockRejectedValue(new Error('x')); + endpoints.getMilers.mockRejectedValue(new Error('x')); + endpoints.getAdminTenants.mockRejectedValue(new Error('x')); + endpoints.getConsignments.mockRejectedValue(new Error('x')); + + const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() }); + expect(rows).toHaveLength(1); + }); + + it('should return an empty page for an account with no bookings', async () => { + endpoints.getBookings.mockResolvedValue([]); + await expect(fetchDeliveries({ pageParam: 1, queryKey: key() })).resolves.toEqual({ + rows: [], + nextPage: undefined + }); + }); + }); + + describe('fetchCountAPI', () => { + it('should count dispatched rows into the page chip buckets', async () => { + endpoints.getBookings.mockResolvedValue([ + booking({ bookingid: 1, status: 'Miler_Assigned' }), + booking({ bookingid: 2, status: 'Pickup_Scheduled' }), + booking({ bookingid: 3, status: 'Converted_To_Consignment' }), + booking({ bookingid: 4, status: 'Out_for_Delivery' }), + booking({ bookingid: 5, status: 'Delivered' }), + booking({ bookingid: 6, status: 'Cancelled' }) + ]); + + await expect(fetchCountAPI()).resolves.toEqual({ + total: 6, + uncoveredLength: 1, + assignedLength: 1, + arrivedLength: 0, + pickedLength: 1, + activeLength: 1, + coveredLength: 1, + cancelLength: 1, + skippedLength: 0 + }); + }); + + it('should count only dispatched bookings, matching what the table shows', async () => { + endpoints.getBookings.mockResolvedValue([ + booking({ bookingid: 1, status: 'pending_pickup', assignedmileruserid: null }), + booking({ bookingid: 2, status: 'Delivered' }) + ]); + const counts = await fetchCountAPI(); + expect(counts.total).toBe(1); + expect(counts.coveredLength).toBe(1); + }); + + it('should report zeroes — not undefined — for an empty account', async () => { + endpoints.getBookings.mockResolvedValue([]); + const counts = await fetchCountAPI(); + Object.values(counts).forEach((value) => expect(value).toBe(0)); + }); + + it('should report zeroes rather than throwing when the source call fails', async () => { + endpoints.getBookings.mockRejectedValue(new Error('boom')); + const counts = await fetchCountAPI(); + expect(counts.total).toBe(0); + }); + }); + + describe('cancelDeliveryAPI', () => { + it('should cancel through the booking-level cancel action', async () => { + endpoints.cancelBooking.mockResolvedValue({ success: true }); + await cancelDeliveryAPI({ orderheaderid: 4021 }, 'customer unreachable'); + expect(endpoints.cancelBooking).toHaveBeenCalledWith(4021, { reason: 'customer unreachable' }); + }); + + it('should fall back to deliveryid when the row carries no orderheaderid', async () => { + endpoints.cancelBooking.mockResolvedValue({ success: true }); + await cancelDeliveryAPI({ deliveryid: 4022 }, 'duplicate'); + expect(endpoints.cancelBooking).toHaveBeenCalledWith(4022, { reason: 'duplicate' }); + }); + + it('should send an undefined reason rather than inventing one', async () => { + endpoints.cancelBooking.mockResolvedValue({ success: true }); + await cancelDeliveryAPI({ orderheaderid: 4021 }); + expect(endpoints.cancelBooking).toHaveBeenCalledWith(4021, { reason: undefined }); + }); + + it('should surface a rejected cancel to the caller', async () => { + endpoints.cancelBooking.mockRejectedValue({ message: 'already cancelled', httpStatus: 409 }); + await expect(cancelDeliveryAPI({ orderheaderid: 4021 }, 'x')).rejects.toMatchObject({ httpStatus: 409 }); + }); + }); + + describe('changeRiderAPI', () => { + it("should send the miler's USERID under the mileruserid key", async () => { + // assign-miler is the one admin route keyed on userid rather than + // milerprofileid. The previous guess was wrong on both key and value. + endpoints.assignMilerToBooking.mockResolvedValue({ success: true }); + await changeRiderAPI({ userid: 91, milerprofileid: 11 }, { orderheaderid: 4021 }); + expect(endpoints.assignMilerToBooking).toHaveBeenCalledWith(4021, { mileruserid: 91 }); + }); + + it('should not send the milerprofileid, which this route does not accept', async () => { + endpoints.assignMilerToBooking.mockResolvedValue({ success: true }); + await changeRiderAPI({ userid: 91, milerprofileid: 11 }, { orderheaderid: 4021 }); + const [, body] = endpoints.assignMilerToBooking.mock.calls[0]; + expect(body).not.toHaveProperty('milerid'); + expect(body).not.toHaveProperty('milerprofileid'); + }); + + it('should fall back to deliveryid for the booking id', async () => { + endpoints.assignMilerToBooking.mockResolvedValue({ success: true }); + await changeRiderAPI({ userid: 91 }, { deliveryid: 4022 }); + expect(endpoints.assignMilerToBooking).toHaveBeenCalledWith(4022, { mileruserid: 91 }); + }); + + it('should surface a rejected assignment to the caller', async () => { + endpoints.assignMilerToBooking.mockRejectedValue({ message: 'miler unavailable', httpStatus: 409 }); + await expect(changeRiderAPI({ userid: 91 }, { orderheaderid: 4021 })).rejects.toMatchObject({ + httpStatus: 409 + }); + }); + }); + + describe('updateDeliveryAPI', () => { + beforeEach(() => endpoints.updateConsignmentStatus.mockResolvedValue({ success: true })); + + it.each([ + ['started', 'Out_for_Delivery'], + ['active', 'Out_for_Delivery'], + ['delivered', 'Delivered'], + ['cancelled', 'Cancelled'], + ['canceled', 'Cancelled'] + ])('should translate the dialog choice %s to the consignment enum %s', async (chosen, sent) => { + await updateDeliveryAPI({ consignmentid: 40, orderstatus: chosen }); + expect(endpoints.updateConsignmentStatus).toHaveBeenCalledWith(40, { status: sent }); + }); + + it('should be case-insensitive about the dialog choice', async () => { + await updateDeliveryAPI({ consignmentid: 40, orderstatus: 'Delivered' }); + expect(endpoints.updateConsignmentStatus).toHaveBeenCalledWith(40, { status: 'Delivered' }); + }); + + it('should target the CONSIGNMENT id, not the booking id', async () => { + // deliveryid is always the bookingid, so `deliveryid ?? consignmentid` + // never fell through — every submit hit the endpoint with the wrong id. + await updateDeliveryAPI({ consignmentid: 40, deliveryid: 4021, orderstatus: 'delivered' }); + expect(endpoints.updateConsignmentStatus).toHaveBeenCalledWith(40, expect.anything()); + }); + + it('should send only `status` — the other dialog fields have no field here', async () => { + // Forwarding the whole dialog state produced 400 {"status is required"}. + await updateDeliveryAPI({ + consignmentid: 40, + orderstatus: 'delivered', + deliveryamt: 240, + cumulativekms: 12, + userid: 91, + notes: 'left with security' + }); + const [, body] = endpoints.updateConsignmentStatus.mock.calls[0]; + expect(body).toEqual({ status: 'Delivered' }); + }); + + it('should refuse with a prompt when no status was chosen', async () => { + await expect(updateDeliveryAPI({ consignmentid: 40, orderstatus: '' })).resolves.toEqual({ + success: false, + message: 'Choose a status first.' + }); + expect(endpoints.updateConsignmentStatus).not.toHaveBeenCalled(); + }); + + it.each([ + ['picked', 'already a consignment'], + ['pending', 'only exists after pickup'], + ['accepted', 'only exists after pickup'], + ['arrived', 'nothing to set'], + ['skipped', 'attempt count'] + ])('should refuse %s with a specific reason rather than guessing a status', async (chosen, fragment) => { + const result = await updateDeliveryAPI({ consignmentid: 40, orderstatus: chosen }); + expect(result.success).toBe(false); + expect(result.message).toContain(fragment); + expect(endpoints.updateConsignmentStatus).not.toHaveBeenCalled(); + }); + + it('should refuse an unrecognised status naming the value that was rejected', async () => { + const result = await updateDeliveryAPI({ consignmentid: 40, orderstatus: 'teleported' }); + expect(result.success).toBe(false); + expect(result.message).toContain('teleported'); + expect(endpoints.updateConsignmentStatus).not.toHaveBeenCalled(); + }); + + it('should never write a pre-pickup booking state to the consignment endpoint', async () => { + await Promise.all( + ['picked', 'pending', 'accepted', 'arrived', 'skipped'].map((orderstatus) => + updateDeliveryAPI({ consignmentid: 40, orderstatus }) + ) + ); + expect(endpoints.updateConsignmentStatus).not.toHaveBeenCalled(); + }); + + it('should surface a rejected status write to the caller', async () => { + endpoints.updateConsignmentStatus.mockRejectedValue({ message: 'status is required', httpStatus: 400 }); + await expect(updateDeliveryAPI({ consignmentid: 40, orderstatus: 'delivered' })).rejects.toMatchObject({ + httpStatus: 400 + }); + }); + }); + + describe('getorderdetails', () => { + it('should read a single booking by id', async () => { + endpoints.getBooking.mockResolvedValue({ bookingid: 4021 }); + await expect(getorderdetails(4021)).resolves.toEqual({ bookingid: 4021 }); + expect(endpoints.getBooking).toHaveBeenCalledWith(4021); + }); + }); +}); diff --git a/tests/api/deliveryStatus.test.js b/tests/api/deliveryStatus.test.js new file mode 100644 index 0000000..45709df --- /dev/null +++ b/tests/api/deliveryStatus.test.js @@ -0,0 +1,234 @@ +jest.mock('@/api/doormile/endpoints', () => ({ __esModule: true })); +jest.mock('@/api/doormile/notify', () => ({ __esModule: true, OpenToast: jest.fn(), messageOf: jest.fn() })); + +import { deriveDeliveryStatus, mapBookingStatusToDeliveryStatus } from '@/api/doormile/queries'; + +/** + * Raw booking/consignment enum -> the Deliveries page's lifecycle key. + * + * This is the single most consequential mapping in the console: it decides + * which tab an order appears under, what the chip counts say, and what badge + * the row renders. Three rules in it are explicit product direction rather than + * convenience, and each has its own test below: + * + * 1. Miler_Assigned stays PENDING. Assigning is an OPERATOR action; Accepted + * must reflect the RIDER's own action. + * 2. Converted_To_Consignment is PICKED, not accepted — the parcel is in the + * rider's hands the moment the booking converts. + * 3. Once a consignment exists, ITS status wins: the booking's own status + * stops moving after conversion, so reading only the booking left an + * updated row showing a stale Converted_To_Consignment. + */ +describe('deriveDeliveryStatus', () => { + describe('called with a bare status string', () => { + it.each([ + ['pending_pickup', 'pending'], + ['pending_assignment', 'pending'], + ['pending', 'pending'], + ['miler_assigned', 'pending'], + ['pickup_scheduled', 'accepted'], + ['arrived', 'arrived'], + ['converted_to_consignment', 'picked'], + ['collected_by_miler', 'picked'], + ['collected', 'picked'], + ['picked', 'picked'], + ['out_for_delivery', 'active'], + ['in_transit', 'active'], + ['intransit', 'active'], + ['inwarded_at_hub', 'active'], + ['inwarded', 'active'], + ['at_hub', 'active'], + ['outwarded_from_hub', 'active'], + ['in_transit_to_hub', 'active'], + ['active', 'active'], + ['delivered', 'delivered'], + ['completed', 'delivered'], + ['cancelled', 'cancelled'], + ['canceled', 'cancelled'], + ['returned', 'cancelled'], + ['failed', 'cancelled'], + ['skipped', 'skipped'], + ['rto', 'skipped'] + ])('should map %s to %s', (raw, expected) => { + expect(deriveDeliveryStatus(raw)).toBe(expected); + }); + + it('should match the backend enum regardless of its casing', () => { + expect(deriveDeliveryStatus('Out_for_Delivery')).toBe('active'); + expect(deriveDeliveryStatus('CONVERTED_TO_CONSIGNMENT')).toBe('picked'); + }); + + it('should tolerate surrounding whitespace', () => { + expect(deriveDeliveryStatus(' Delivered ')).toBe('delivered'); + }); + + it('should pass an unmapped enum through lowercased rather than dropping the row', () => { + // The page surfaces these under an "Other" tab naming the enum; before + // that, such a row was counted nowhere and shown nowhere. + expect(deriveDeliveryStatus('Awaiting_Customs_Clearance')).toBe('awaiting_customs_clearance'); + }); + + it.each([null, undefined, '', 0, false])( + 'should default the blank status %p to pending', + (raw) => { + expect(deriveDeliveryStatus(raw)).toBe('pending'); + } + ); + }); + + describe('the operator-vs-rider split (product rule)', () => { + it('should keep Miler_Assigned on PENDING — assigning is an operator action', () => { + // Bumping this to 'accepted' made a just-assigned, not-yet-acknowledged + // order look already-accepted. Explicit product requirement. + expect(deriveDeliveryStatus('miler_assigned')).toBe('pending'); + expect(deriveDeliveryStatus({ status: 'Miler_Assigned' })).toBe('pending'); + }); + + it('should treat Pickup_Scheduled as the rider having accepted', () => { + expect(deriveDeliveryStatus('pickup_scheduled')).toBe('accepted'); + }); + + it('should treat Converted_To_Consignment as PICKED, not accepted', () => { + // It sat on 'accepted', which held a picked-up parcel in the Accepted tab + // and left Picked permanently empty. + expect(deriveDeliveryStatus('converted_to_consignment')).toBe('picked'); + expect(deriveDeliveryStatus({ status: 'Converted_To_Consignment' })).toBe('picked'); + }); + }); + + describe('Pickup_Scheduled combined with a reached timestamp', () => { + it.each([ + ['2026-08-27T10:15:00', 'a real timestamp'], + ['0', 'a zero-ish string that is still a value'], + [1, 'an epoch-like number'] + ])('should report ARRIVED when reachedAt is %p (%s)', (reachedAt) => { + expect(deriveDeliveryStatus('pickup_scheduled', reachedAt)).toBe('arrived'); + }); + + it.each([null, undefined, false, ''])( + 'should report ACCEPTED when reachedAt is the blank value %p', + (reachedAt) => { + expect(deriveDeliveryStatus('pickup_scheduled', reachedAt)).toBe('accepted'); + } + ); + + it('should read the reached timestamp off a row under any of its field spellings', () => { + ['reachedat', 'reached_at', 'reachedAt', 'reachedtime', 'reached_time'].forEach((field) => { + expect(deriveDeliveryStatus({ status: 'Pickup_Scheduled', [field]: '2026-08-27T10:15:00' })).toBe('arrived'); + }); + }); + + it('should report ACCEPTED for a Pickup_Scheduled row with no reached field at all', () => { + expect(deriveDeliveryStatus({ status: 'Pickup_Scheduled' })).toBe('accepted'); + }); + }); + + describe('called with a booking row', () => { + it('should read the status from `status`, `bookingstatus` or `orderstatus`', () => { + expect(deriveDeliveryStatus({ status: 'Delivered' })).toBe('delivered'); + expect(deriveDeliveryStatus({ bookingstatus: 'Delivered' })).toBe('delivered'); + expect(deriveDeliveryStatus({ orderstatus: 'Delivered' })).toBe('delivered'); + }); + + it('should prefer `status` over the other two spellings', () => { + expect(deriveDeliveryStatus({ status: 'Delivered', orderstatus: 'Cancelled' })).toBe('delivered'); + }); + + it('should default a row with no status at all to pending', () => { + expect(deriveDeliveryStatus({ bookingid: 4021 })).toBe('pending'); + }); + }); + + describe('consignment status precedence', () => { + it('should let the consignment status attached to the row win over the booking status', () => { + // The booking stops moving once it converts; PUT /consignments/:id/status + // writes on the consignment, so reading only the booking showed a stale + // Converted_To_Consignment after a successful Update Status. + const row = { status: 'Converted_To_Consignment', consignmentstatus: 'Delivered' }; + expect(deriveDeliveryStatus(row)).toBe('delivered'); + }); + + it('should accept the snake_case spelling of the attached status', () => { + expect(deriveDeliveryStatus({ status: 'Converted_To_Consignment', consignment_status: 'Out_for_Delivery' })).toBe('active'); + }); + + it('should resolve the consignment through a joined map when the row carries only an id', () => { + const map = new Map([['40', { status: 'Out_for_Delivery' }]]); + expect(deriveDeliveryStatus({ status: 'Converted_To_Consignment', consignmentid: 40 }, map)).toBe('active'); + }); + + it.each(['status', 'consignmentstatus', 'currentstatus', 'consignment_status'])( + 'should read the joined record status from its `%s` field', + (field) => { + const map = new Map([['40', { [field]: 'Delivered' }]]); + expect(deriveDeliveryStatus({ status: 'Converted_To_Consignment', consignmentid: 40 }, map)).toBe('delivered'); + } + ); + + it('should fall back to the booking status when the joined map has no such consignment', () => { + const map = new Map([['99', { status: 'Delivered' }]]); + expect(deriveDeliveryStatus({ status: 'Converted_To_Consignment', consignmentid: 40 }, map)).toBe('picked'); + }); + + it('should fall back to the booking status when the consignment status is unrecognised', () => { + // Degrades to exactly the behaviour from before the join existed. + const row = { status: 'Converted_To_Consignment', consignmentstatus: 'Held_At_Customs' }; + expect(deriveDeliveryStatus(row)).toBe('picked'); + }); + + it.each(['', ' ', null, undefined])( + 'should ignore a blank attached consignment status (%p)', + (consignmentstatus) => { + expect(deriveDeliveryStatus({ status: 'Converted_To_Consignment', consignmentstatus })).toBe('picked'); + } + ); + + it('should ignore a second argument that is not a Map', () => { + // The signature is overloaded (reachedAt | consignmentMap); a plain object + // must not be mistaken for a lookup table. + const notAMap = { 40: { status: 'Delivered' } }; + expect(deriveDeliveryStatus({ status: 'Converted_To_Consignment', consignmentid: 40 }, notAMap)).toBe('picked'); + }); + + it('should still return picked for a Collected_By_Miler booking with no consignment', () => { + expect(deriveDeliveryStatus({ status: 'Collected_By_Miler' })).toBe('picked'); + }); + }); + + describe('mapBookingStatusToDeliveryStatus', () => { + it('should be a straight alias so no second copy of the mapping can drift', () => { + const cases = [ + ['pickup_scheduled', '2026-08-27T10:00:00'], + ['miler_assigned', undefined], + [{ status: 'Converted_To_Consignment', consignmentstatus: 'Delivered' }, undefined] + ]; + cases.forEach(([input, second]) => { + expect(mapBookingStatusToDeliveryStatus(input, second)).toBe(deriveDeliveryStatus(input, second)); + }); + }); + }); + + describe('classification invariants', () => { + it('should be deterministic — the same input always yields the same tab', () => { + const row = { status: 'Pickup_Scheduled', reachedat: '2026-08-27T10:00:00' }; + const results = new Set(Array.from({ length: 5 }, () => deriveDeliveryStatus(row))); + expect(results.size).toBe(1); + }); + + it('should never return undefined or an empty string for any input shape', () => { + const inputs = [null, undefined, '', 0, {}, { status: '' }, 'unknown_enum', { consignmentstatus: 'x' }]; + inputs.forEach((input) => { + const result = deriveDeliveryStatus(input); + expect(typeof result).toBe('string'); + expect(result).not.toBe(''); + }); + }); + + it('should never mutate the row it classifies', () => { + const row = { status: 'Pickup_Scheduled', reachedat: '2026-08-27T10:00:00' }; + const snapshot = JSON.parse(JSON.stringify(row)); + deriveDeliveryStatus(row); + expect(row).toEqual(snapshot); + }); + }); +}); diff --git a/tests/api/endpoints.test.js b/tests/api/endpoints.test.js new file mode 100644 index 0000000..8804542 --- /dev/null +++ b/tests/api/endpoints.test.js @@ -0,0 +1,378 @@ +/** + * The one-function-per-route admin API layer. + * + * The transport is mocked (there is no real backend in a unit run), but nothing + * else is: the URL built, the query string assembled, the body sent, the + * envelope unwrapped and the pagination draining are all the behaviour under + * test. A wrong URL or a wrong body key here is a production bug that no page + * test would catch. + */ +jest.mock('@/api/doormile/client', () => ({ + __esModule: true, + default: { get: jest.fn(), post: jest.fn(), put: jest.fn(), patch: jest.fn(), delete: jest.fn() }, + DOORMILE_TOKEN_KEY: 'doormileToken', + DOORMILE_USER_KEY: 'doormileUser', + clearStoredSession: jest.fn() +})); + +const clientModule = jest.requireMock('@/api/doormile/client'); +const mockClient = clientModule.default; +const mockClearStoredSession = clientModule.clearStoredSession; + +import { + loginAdmin, + logoutAdmin, + getDashboard, + getReports, + getLocationsSummary, + getProfile, + updateProfilePassword, + getAdminCustomers, + createAdminCustomer, + updateAdminCustomer, + getHubs, + createHub, + deleteHub, + getMilers, + notifyMiler, + getMilerLogs, + getBookings, + getBookingsPage, + getBooking, + assignMilerToBooking, + assignVehicleToBooking, + batchAssignBookings, + cancelBooking, + bulkCancelBookings, + getConsignments, + updateConsignmentStatus, + getTenantLocations +} from '@/api/doormile/endpoints'; + +/** `{ data: { data: rows, total } }` — the list envelope the backend returns. */ +const listPage = (rows, total) => ({ data: { success: true, data: rows, total } }); +const rowsOfSize = (n, offset = 0) => Array.from({ length: n }, (_, i) => ({ id: offset + i + 1 })); + +describe('doormile endpoints', () => { + describe('loginAdmin', () => { + it('should post the credentials to the admin login route', async () => { + mockClient.post.mockResolvedValue({ data: { success: true, token: 't', user: { userid: 1 } } }); + await loginAdmin('ops@doormile.com', 'hunter2'); + expect(mockClient.post).toHaveBeenCalledWith('/admin/login', { + email: 'ops@doormile.com', + password: 'hunter2' + }); + }); + + it('should store the token and the admin record on success', async () => { + mockClient.post.mockResolvedValue({ data: { success: true, token: 'jwt-abc', user: { userid: 7, tenantid: '0' } } }); + const result = await loginAdmin('ops@doormile.com', 'hunter2'); + + expect(localStorage.getItem('doormileToken')).toBe('jwt-abc'); + expect(JSON.parse(localStorage.getItem('doormileUser'))).toEqual({ userid: 7, tenantid: '0' }); + expect(result).toEqual({ success: true, token: 'jwt-abc', user: { userid: 7, tenantid: '0' } }); + }); + + it('should store nothing when the server refuses the credentials', async () => { + // A half-written session (token stored on a failed login) is worse than no + // session: the console renders and 401s on every fetch. + mockClient.post.mockResolvedValue({ data: { success: false, message: 'Invalid credentials' } }); + const result = await loginAdmin('ops@doormile.com', 'wrong'); + + expect(localStorage.getItem('doormileToken')).toBeNull(); + expect(localStorage.getItem('doormileUser')).toBeNull(); + expect(result).toEqual({ success: false, message: 'Invalid credentials' }); + }); + + it('should propagate a transport failure to the caller', async () => { + mockClient.post.mockRejectedValue({ message: 'Network Error' }); + await expect(loginAdmin('a@b.c', 'x')).rejects.toMatchObject({ message: 'Network Error' }); + expect(localStorage.getItem('doormileToken')).toBeNull(); + }); + }); + + describe('logoutAdmin', () => { + it('should clear the whole stored session, not just the token', () => { + logoutAdmin(); + expect(mockClearStoredSession).toHaveBeenCalledTimes(1); + }); + }); + + describe('envelope unwrapping', () => { + it('should return response.data.data for a GET', async () => { + mockClient.get.mockResolvedValue({ data: { success: true, data: { totalOrders: 12 } } }); + await expect(getDashboard()).resolves.toEqual({ totalOrders: 12 }); + }); + + it('should return the full response.data for a mutation, so callers can read success', async () => { + mockClient.post.mockResolvedValue({ data: { success: true, message: 'Hub created' } }); + await expect(createHub({ hubname: 'Koramangala' })).resolves.toEqual({ + success: true, + message: 'Hub created' + }); + }); + + it('should return the parsed profile', async () => { + mockClient.get.mockResolvedValue({ data: { data: { userid: 7 } } }); + await expect(getProfile()).resolves.toEqual({ userid: 7 }); + expect(mockClient.get).toHaveBeenCalledWith('/admin/profile'); + }); + }); + + describe('query-string building', () => { + beforeEach(() => mockClient.get.mockResolvedValue({ data: { data: [] } })); + + it('should include every supplied report filter', async () => { + await getReports('2026-08-01', '2026-08-27', '4', '9', '2'); + expect(mockClient.get).toHaveBeenCalledWith( + '/admin/reports?from=2026-08-01&to=2026-08-27&tenantid=4&locationid=9&hubid=2' + ); + }); + + it('should omit undefined, null and empty filters rather than sending blanks', async () => { + // `?tenantid=` is not the same request as no tenant filter — one asks the + // backend to match the empty string. + await getReports('2026-08-01', '2026-08-27', undefined, null, ''); + expect(mockClient.get).toHaveBeenCalledWith('/admin/reports?from=2026-08-01&to=2026-08-27'); + }); + + it('should produce no query string at all when every filter is absent', async () => { + await getReports(); + expect(mockClient.get).toHaveBeenCalledWith('/admin/reports'); + }); + + it('should keep a zero filter value, which is a real id', async () => { + await getLocationsSummary(0, 0, '2026-08-01', '2026-08-27'); + expect(mockClient.get).toHaveBeenCalledWith( + expect.stringContaining('tenantid=0') + ); + }); + + it('should url-encode a filter value', async () => { + await getMilerLogs(3, '2026-08-01 00:00', '2026-08-27 23:59', 50); + const url = mockClient.get.mock.calls[0][0]; + expect(url).toContain('/admin/milers/3/logs?'); + expect(url).not.toMatch(/ /); + }); + }); + + describe('fetchAllPages (getAdminCustomers)', () => { + it('should return a single page when it already holds every row', async () => { + mockClient.get.mockResolvedValueOnce(listPage(rowsOfSize(12), 12)); + await expect(getAdminCustomers()).resolves.toHaveLength(12); + expect(mockClient.get).toHaveBeenCalledTimes(1); + expect(mockClient.get).toHaveBeenCalledWith('/admin/customers?pageno=1&pagesize=100'); + }); + + it('should drain every page rather than silently truncating the list', async () => { + // The regression this protects: 37 customers came back as 20, and the + // Customers page reported "Total Customers 20" with no way to tell. + mockClient.get + .mockResolvedValueOnce(listPage(rowsOfSize(100), 237)) + .mockResolvedValueOnce(listPage(rowsOfSize(100, 100), 237)) + .mockResolvedValueOnce(listPage(rowsOfSize(37, 200), 237)); + + await expect(getAdminCustomers()).resolves.toHaveLength(237); + expect(mockClient.get).toHaveBeenCalledTimes(3); + expect(mockClient.get).toHaveBeenNthCalledWith(3, '/admin/customers?pageno=3&pagesize=100'); + }); + + it('should stop early when a page comes back empty', async () => { + mockClient.get + .mockResolvedValueOnce(listPage(rowsOfSize(100), 500)) + .mockResolvedValueOnce(listPage([], 500)); + + await expect(getAdminCustomers()).resolves.toHaveLength(100); + expect(mockClient.get).toHaveBeenCalledTimes(2); + }); + + it('should cap the drain so a wrong `total` cannot spin forever', async () => { + mockClient.get.mockResolvedValue(listPage(rowsOfSize(100), 10_000_000)); + await getAdminCustomers(); + expect(mockClient.get.mock.calls.length).toBeLessThanOrEqual(12); + }); + + it('should return the first page when the envelope carries no usable total', async () => { + mockClient.get.mockResolvedValueOnce({ data: { data: rowsOfSize(20) } }); + await expect(getAdminCustomers()).resolves.toHaveLength(20); + expect(mockClient.get).toHaveBeenCalledTimes(1); + }); + + it('should return an empty array — not undefined — for an empty resource', async () => { + mockClient.get.mockResolvedValueOnce({ data: { data: null, total: 0 } }); + await expect(getAdminCustomers()).resolves.toEqual([]); + }); + + it('should propagate a failure on the first page', async () => { + mockClient.get.mockRejectedValueOnce({ message: 'boom', httpStatus: 500 }); + await expect(getAdminCustomers()).rejects.toMatchObject({ message: 'boom' }); + }); + }); + + describe('getBookings', () => { + it('should drain all pages when the first page is full', async () => { + mockClient.get + .mockResolvedValueOnce(listPage(rowsOfSize(100), 150)) + .mockResolvedValueOnce(listPage(rowsOfSize(50, 100), 150)); + + await expect(getBookings(1, 100)).resolves.toHaveLength(150); + }); + + it('should return the first page when it is not full', async () => { + mockClient.get.mockResolvedValueOnce(listPage(rowsOfSize(37), 37)); + await expect(getBookings(1, 100)).resolves.toHaveLength(37); + expect(mockClient.get).toHaveBeenCalledTimes(1); + }); + + it('should honour an explicit small page beyond the first', async () => { + mockClient.get.mockResolvedValueOnce(listPage(rowsOfSize(25, 25), 200)); + await expect(getBookings(2, 25)).resolves.toHaveLength(25); + expect(mockClient.get).toHaveBeenCalledWith('/admin/bookings?pageno=2&pagesize=25'); + }); + + it('should stop draining rather than reject when a later page fails', async () => { + // Partial rows beat a blank Deliveries page. + mockClient.get + .mockResolvedValueOnce(listPage(rowsOfSize(100), 300)) + .mockRejectedValueOnce({ message: 'gateway timeout' }); + + await expect(getBookings(1, 100)).resolves.toHaveLength(100); + }); + + it('should return an empty array for an account with no bookings', async () => { + mockClient.get.mockResolvedValueOnce({ data: { data: [], total: 0 } }); + await expect(getBookings(1, 100)).resolves.toEqual([]); + }); + }); + + describe('getBookingsPage', () => { + it('should keep the envelope total instead of throwing it away', async () => { + mockClient.get.mockResolvedValue(listPage(rowsOfSize(50), 1234)); + await expect(getBookingsPage(1, 50)).resolves.toEqual({ + rows: expect.any(Array), + total: 1234, + page: 1 + }); + }); + + it('should fall back to the row count when the backend omits total', async () => { + // Degrading to "this is everything" beats NaN-driven pagination. + mockClient.get.mockResolvedValue({ data: { data: rowsOfSize(7) } }); + await expect(getBookingsPage(1, 50)).resolves.toMatchObject({ total: 7, page: 1 }); + }); + + it('should fall back to the requested page number when the backend omits page', async () => { + mockClient.get.mockResolvedValue({ data: { data: [], total: 0 } }); + await expect(getBookingsPage(4, 50)).resolves.toMatchObject({ page: 4 }); + }); + }); + + describe('booking write paths', () => { + beforeEach(() => mockClient.post.mockResolvedValue({ data: { success: true } })); + + it('should assign a miler through the booking-scoped assign route', async () => { + await assignMilerToBooking(4021, { mileruserid: 91 }); + expect(mockClient.post).toHaveBeenCalledWith('/admin/bookings/4021/assign-miler', { mileruserid: 91 }); + }); + + it('should cancel through the booking-level cancel action, not a status PUT', async () => { + await cancelBooking(4021, { reason: 'customer unreachable' }); + expect(mockClient.post).toHaveBeenCalledWith('/admin/bookings/4021/cancel', { + reason: 'customer unreachable' + }); + expect(mockClient.put).not.toHaveBeenCalled(); + }); + + it('should assign a vehicle through its own route', async () => { + await assignVehicleToBooking(4021, { vehicleid: 5 }); + expect(mockClient.post).toHaveBeenCalledWith('/admin/bookings/4021/assign-vehicle', { vehicleid: 5 }); + }); + + it('should batch-assign with a default cap of five stops per rider', async () => { + await batchAssignBookings([1, 2, 3]); + expect(mockClient.post).toHaveBeenCalledWith('/hub/bookings/batch-assign', { + bookingids: [1, 2, 3], + max_per_rider: 5 + }); + }); + + it('should honour an explicit per-rider cap', async () => { + await batchAssignBookings([1, 2], 2); + expect(mockClient.post).toHaveBeenCalledWith('/hub/bookings/batch-assign', { + bookingids: [1, 2], + max_per_rider: 2 + }); + }); + + it('should bulk-cancel through the dedicated route', async () => { + await bulkCancelBookings([1, 2, 3]); + expect(mockClient.post).toHaveBeenCalledWith('/admin/bookings/bulk-cancel', { bookingIds: [1, 2, 3] }); + }); + + it('should surface a rejected write to the caller', async () => { + mockClient.post.mockRejectedValue({ message: 'booking already cancelled', httpStatus: 409 }); + await expect(cancelBooking(4021, {})).rejects.toMatchObject({ httpStatus: 409 }); + }); + }); + + describe('consignment write path', () => { + it('should PUT the status to the consignment-scoped route', async () => { + mockClient.put.mockResolvedValue({ data: { success: true } }); + await updateConsignmentStatus(40, { status: 'Delivered' }); + expect(mockClient.put).toHaveBeenCalledWith('/admin/consignments/40/status', { status: 'Delivered' }); + }); + + it('should read the consignment list from its own resource', async () => { + mockClient.get.mockResolvedValue({ data: { data: [{ consignmentid: 40 }] } }); + await expect(getConsignments()).resolves.toEqual([{ consignmentid: 40 }]); + expect(mockClient.get).toHaveBeenCalledWith('/admin/consignments'); + }); + }); + + describe('resource routes', () => { + beforeEach(() => { + mockClient.get.mockResolvedValue({ data: { data: [] } }); + mockClient.post.mockResolvedValue({ data: { success: true } }); + mockClient.patch.mockResolvedValue({ data: { success: true } }); + mockClient.delete.mockResolvedValue({ data: { success: true } }); + }); + + it.each([ + ['getHubs', () => getHubs(), '/admin/hubs'], + ['getMilers', () => getMilers(), '/admin/milers'], + ['getBooking', () => getBooking(4021), '/admin/bookings/4021'], + ['getTenantLocations', () => getTenantLocations(4), '/admin/tenants/4/locations'] + ])('%s should read from %s', async (_name, call, url) => { + await call(); + expect(mockClient.get).toHaveBeenCalledWith(url); + }); + + it('should create a customer with POST and update one with PATCH', async () => { + await createAdminCustomer({ customername: 'Anita' }); + expect(mockClient.post).toHaveBeenCalledWith('/admin/customers', { customername: 'Anita' }); + + await updateAdminCustomer(9, { phone: '9876543210' }); + expect(mockClient.patch).toHaveBeenCalledWith('/admin/customers/9', { phone: '9876543210' }); + }); + + it('should delete a hub through its own id-scoped route', async () => { + await deleteHub(3); + expect(mockClient.delete).toHaveBeenCalledWith('/admin/hubs/3'); + }); + + it('should notify a miler by milerprofileid with a title and message', async () => { + await notifyMiler(11, 'New assignment', 'Order 4021 assigned to you'); + expect(mockClient.post).toHaveBeenCalledWith( + expect.stringContaining('/admin/milers/11/notify'), + expect.objectContaining({ title: 'New assignment', message: 'Order 4021 assigned to you' }) + ); + }); + + it('should change a password through the profile route with both fields', async () => { + mockClient.put.mockResolvedValue({ data: { success: true } }); + await updateProfilePassword('old-secret', 'new-secret'); + const [, body] = mockClient.put.mock.calls[0]; + expect(body).toMatchObject({ current_password: 'old-secret', new_password: 'new-secret' }); + }); + }); +}); diff --git a/tests/lib/AuthContext.test.jsx b/tests/lib/AuthContext.test.jsx new file mode 100644 index 0000000..3a6c7d3 --- /dev/null +++ b/tests/lib/AuthContext.test.jsx @@ -0,0 +1,282 @@ +import { renderHook, act, waitFor } from '@testing-library/react'; + +jest.mock('@/api/doormile', () => ({ + __esModule: true, + DOORMILE_TOKEN_KEY: 'doormileToken', + DOORMILE_USER_KEY: 'doormileUser', + loginAdmin: jest.fn(), + logoutAdmin: jest.fn(), + readStoredToken: jest.fn(), + readStoredUser: jest.fn() +})); + +import { AuthProvider, useAuth } from '@/lib/AuthContext'; +import { loginAdmin, logoutAdmin, readStoredToken, readStoredUser } from '@/api/doormile'; + +const wrapper = ({ children }) => {children}; +const renderAuth = () => renderHook(() => useAuth(), { wrapper }); + +/** + * Session state for the console shell. + * + * What these tests protect is the authorization gate itself: who is considered + * signed in, what a refused credential does (show the server's wording, do NOT + * throw), and that a sign-out in one tab ends the session in every other tab + * rather than leaving a rendered console around a token that is gone. + */ +describe('AuthContext', () => { + let replace; + + beforeEach(() => { + replace = jest.fn(); + Object.defineProperty(window, 'location', { + writable: true, + configurable: true, + value: { href: 'http://console.local/deliveries', replace } + }); + readStoredToken.mockReturnValue(null); + readStoredUser.mockReturnValue(null); + }); + + describe('boot', () => { + it('should report a signed-out shell when no token is stored', async () => { + const { result } = renderAuth(); + await waitFor(() => expect(result.current.authChecked).toBe(true)); + + expect(result.current.isAuthenticated).toBe(false); + expect(result.current.user).toBeNull(); + expect(result.current.isLoadingAuth).toBe(false); + }); + + it('should restore the signed-in admin from storage', async () => { + readStoredToken.mockReturnValue('jwt-abc'); + readStoredUser.mockReturnValue({ userid: 7, tenantid: '0' }); + + const { result } = renderAuth(); + await waitFor(() => expect(result.current.authChecked).toBe(true)); + + expect(result.current.isAuthenticated).toBe(true); + expect(result.current.user).toEqual({ userid: 7, tenantid: '0' }); + }); + + it('should treat the token as the credential — no token means no user', async () => { + // A stale user record without a token must not produce a rendered console + // that 401s on every fetch. + readStoredToken.mockReturnValue(null); + readStoredUser.mockReturnValue({ userid: 7 }); + + const { result } = renderAuth(); + await waitFor(() => expect(result.current.authChecked).toBe(true)); + + expect(result.current.isAuthenticated).toBe(false); + expect(result.current.user).toBeNull(); + }); + + it('should mark the auth check complete exactly once on mount', async () => { + const { result } = renderAuth(); + await waitFor(() => expect(result.current.authChecked).toBe(true)); + expect(readStoredToken).toHaveBeenCalledTimes(1); + }); + }); + + describe('login', () => { + it('should sign the operator in on a successful credential', async () => { + loginAdmin.mockResolvedValue({ success: true, user: { userid: 7 } }); + const { result } = renderAuth(); + await waitFor(() => expect(result.current.authChecked).toBe(true)); + + let outcome; + await act(async () => { + outcome = await result.current.login('ops@doormile.com', 'hunter2'); + }); + + expect(loginAdmin).toHaveBeenCalledWith('ops@doormile.com', 'hunter2'); + expect(outcome).toEqual({ success: true }); + expect(result.current.isAuthenticated).toBe(true); + expect(result.current.user).toEqual({ userid: 7 }); + }); + + it('should fall back to the stored record when the response omits the user', async () => { + loginAdmin.mockResolvedValue({ success: true }); + readStoredUser.mockReturnValue({ userid: 9 }); + const { result } = renderAuth(); + await waitFor(() => expect(result.current.authChecked).toBe(true)); + + await act(async () => { + await result.current.login('ops@doormile.com', 'hunter2'); + }); + + expect(result.current.user).toEqual({ userid: 9 }); + }); + + it("should surface the server's own wording for a refused credential", async () => { + // Resolving rather than throwing is deliberate: the form shows the + // server's message instead of a generic failure. + loginAdmin.mockResolvedValue({ success: false, message: 'Account is locked' }); + const { result } = renderAuth(); + await waitFor(() => expect(result.current.authChecked).toBe(true)); + + let outcome; + await act(async () => { + outcome = await result.current.login('ops@doormile.com', 'wrong'); + }); + + expect(outcome).toEqual({ success: false, message: 'Account is locked' }); + expect(result.current.authError).toBe('Account is locked'); + expect(result.current.isAuthenticated).toBe(false); + expect(result.current.user).toBeNull(); + }); + + it('should use a default message when the server refuses without one', async () => { + loginAdmin.mockResolvedValue({ success: false }); + const { result } = renderAuth(); + await waitFor(() => expect(result.current.authChecked).toBe(true)); + + let outcome; + await act(async () => { + outcome = await result.current.login('a@b.c', 'x'); + }); + + expect(outcome.success).toBe(false); + expect(outcome.message).toBe('Those credentials were not accepted'); + }); + + it('should report a transport failure without throwing at the form', async () => { + loginAdmin.mockRejectedValue({ message: 'Network Error' }); + const { result } = renderAuth(); + await waitFor(() => expect(result.current.authChecked).toBe(true)); + + let outcome; + await act(async () => { + outcome = await result.current.login('a@b.c', 'x'); + }); + + expect(outcome).toEqual({ success: false, message: 'Network Error' }); + expect(result.current.isAuthenticated).toBe(false); + }); + + it('should report a reachability message when the rejection carries none', async () => { + loginAdmin.mockRejectedValue({}); + const { result } = renderAuth(); + await waitFor(() => expect(result.current.authChecked).toBe(true)); + + let outcome; + await act(async () => { + outcome = await result.current.login('a@b.c', 'x'); + }); + + expect(outcome.message).toBe('Could not reach the Doormile API'); + }); + + it('should clear a previous error when a retry succeeds', async () => { + loginAdmin.mockResolvedValueOnce({ success: false, message: 'Invalid credentials' }); + loginAdmin.mockResolvedValueOnce({ success: true, user: { userid: 7 } }); + const { result } = renderAuth(); + await waitFor(() => expect(result.current.authChecked).toBe(true)); + + await act(async () => { + await result.current.login('a@b.c', 'wrong'); + }); + expect(result.current.authError).toBe('Invalid credentials'); + + await act(async () => { + await result.current.login('a@b.c', 'right'); + }); + expect(result.current.authError).toBeNull(); + }); + }); + + describe('logout', () => { + it('should clear the stored session and send the operator to login', async () => { + readStoredToken.mockReturnValue('jwt-abc'); + readStoredUser.mockReturnValue({ userid: 7 }); + const { result } = renderAuth(); + await waitFor(() => expect(result.current.isAuthenticated).toBe(true)); + + act(() => { + result.current.logout(); + }); + + expect(logoutAdmin).toHaveBeenCalledTimes(1); + expect(result.current.isAuthenticated).toBe(false); + expect(result.current.user).toBeNull(); + expect(replace).toHaveBeenCalledWith('/login'); + }); + + it('should still clear state when the caller suppresses the redirect', async () => { + readStoredToken.mockReturnValue('jwt-abc'); + const { result } = renderAuth(); + await waitFor(() => expect(result.current.isAuthenticated).toBe(true)); + + act(() => { + result.current.logout(false); + }); + + expect(logoutAdmin).toHaveBeenCalledTimes(1); + expect(result.current.isAuthenticated).toBe(false); + expect(replace).not.toHaveBeenCalled(); + }); + }); + + describe('cross-tab session sync', () => { + it('should end this tab session when the token disappears in another tab', async () => { + readStoredToken.mockReturnValue('jwt-abc'); + readStoredUser.mockReturnValue({ userid: 7 }); + const { result } = renderAuth(); + await waitFor(() => expect(result.current.isAuthenticated).toBe(true)); + + readStoredToken.mockReturnValue(null); + readStoredUser.mockReturnValue(null); + act(() => { + window.dispatchEvent(new StorageEvent('storage', { key: 'doormileToken' })); + }); + + await waitFor(() => expect(result.current.isAuthenticated).toBe(false)); + }); + + it('should pick up a sign-IN that happened in another tab', async () => { + const { result } = renderAuth(); + await waitFor(() => expect(result.current.authChecked).toBe(true)); + + readStoredToken.mockReturnValue('jwt-abc'); + readStoredUser.mockReturnValue({ userid: 7 }); + act(() => { + window.dispatchEvent(new StorageEvent('storage', { key: 'doormileUser' })); + }); + + await waitFor(() => expect(result.current.isAuthenticated).toBe(true)); + }); + + it('should ignore an unrelated storage key', async () => { + readStoredToken.mockReturnValue('jwt-abc'); + const { result } = renderAuth(); + await waitFor(() => expect(result.current.isAuthenticated).toBe(true)); + readStoredToken.mockClear(); + + act(() => { + window.dispatchEvent(new StorageEvent('storage', { key: 'doormilePreferences' })); + }); + + expect(readStoredToken).not.toHaveBeenCalled(); + expect(result.current.isAuthenticated).toBe(true); + }); + + it('should stop listening once unmounted', async () => { + const removeSpy = jest.spyOn(window, 'removeEventListener'); + const { unmount, result } = renderAuth(); + await waitFor(() => expect(result.current.authChecked).toBe(true)); + unmount(); + expect(removeSpy).toHaveBeenCalledWith('storage', expect.any(Function)); + }); + }); + + describe('useAuth outside a provider', () => { + it('should throw a named error rather than returning undefined', () => { + // Silent undefined here would render an unauthenticated shell as if it + // were authorized. + const spy = jest.spyOn(console, 'error').mockImplementation(() => {}); + expect(() => renderHook(() => useAuth())).toThrow('useAuth must be used within an AuthProvider'); + spy.mockRestore(); + }); + }); +}); diff --git a/tests/lib/ZoneContext.test.jsx b/tests/lib/ZoneContext.test.jsx new file mode 100644 index 0000000..cd36603 --- /dev/null +++ b/tests/lib/ZoneContext.test.jsx @@ -0,0 +1,401 @@ +import { renderHook, act } from '@testing-library/react'; + +jest.mock('@/lib/doormileHooks', () => ({ + __esModule: true, + useHubs: jest.fn(), + useTenantLocations: jest.fn() +})); +jest.mock('@/lib/AuthContext', () => ({ __esModule: true, useAuth: jest.fn() })); + +import { ZoneProvider, useZone } from '@/lib/ZoneContext'; +import { useHubs, useTenantLocations } from '@/lib/doormileHooks'; +import { useAuth } from '@/lib/AuthContext'; + +const ZONE_STORAGE_KEY = 'doormile_active_zone_id'; + +const KORAMANGALA = { + hubid: '2', + hubname: 'Koramangala Hub', + city: 'Bengaluru', + pincode: '560034', + latitude: 12.9352, + longitude: 77.6245, + status: 'Active' +}; + +const wrapper = ({ children }) => {children}; + +const setup = ({ user = { tenantid: '0' }, hubs = [KORAMANGALA], locations = [] } = {}) => { + useAuth.mockReturnValue({ user }); + useHubs.mockReturnValue({ data: hubs, isLoading: false, refetch: jest.fn() }); + useTenantLocations.mockReturnValue({ data: locations, isLoading: false, refetch: jest.fn() }); + return renderHook(() => useZone(), { wrapper }); +}; + +/** + * Zone scoping — which hub or kitchen an operator is currently looking at, and + * which rows belong to it. + * + * This is a data-visibility boundary, not just a filter: on a tenant login it + * decides whether another tenant's orders can appear on screen at all. The + * tenant check therefore has its own tests, separate from the hub matching. + */ +describe('ZoneContext', () => { + describe('zone list — staff login', () => { + it('should offer All Zones plus every active hub', () => { + const { result } = setup({ hubs: [KORAMANGALA, { ...KORAMANGALA, hubid: '3', hubname: 'HSR Hub' }] }); + expect(result.current.zones.map((z) => z.hubid)).toEqual(['all', '2', '3']); + }); + + it('should hide an inactive hub', () => { + const { result } = setup({ hubs: [KORAMANGALA, { ...KORAMANGALA, hubid: '9', status: 'Inactive' }] }); + expect(result.current.zones.map((z) => z.hubid)).toEqual(['all', '2']); + }); + + it('should treat a hub with no status as active', () => { + const { result } = setup({ hubs: [{ ...KORAMANGALA, status: undefined }] }); + expect(result.current.zones).toHaveLength(2); + }); + + it('should still offer All Zones when there are no hubs at all', () => { + const { result } = setup({ hubs: [] }); + expect(result.current.zones).toHaveLength(1); + expect(result.current.isAllZones).toBe(true); + }); + + it('should not load tenant locations for a staff login', () => { + setup({ user: { tenantid: '0' } }); + expect(useHubs).toHaveBeenCalledWith({ enabled: true }); + expect(useTenantLocations).toHaveBeenCalledWith(null, { enabled: false }); + }); + }); + + describe('zone list — tenant login', () => { + const LOCATION = { + tenantlocationid: 55, + locationname: 'Indiranagar Kitchen', + city: 'Bengaluru', + latitude: 12.9784, + longitude: 77.6408 + }; + + it('should offer the tenant kitchen hubs instead of global hubs', () => { + const { result } = setup({ user: { tenantid: '4' }, locations: [LOCATION] }); + expect(result.current.isTenantUser).toBe(true); + expect(result.current.zones.map((z) => z.hubname)).toEqual(['All Zones', 'Indiranagar Kitchen']); + expect(result.current.zones[1].hubtype).toBe('kitchen_hub'); + }); + + it('should scope the location query to the operator own tenant', () => { + setup({ user: { tenantid: '4' }, locations: [LOCATION] }); + expect(useTenantLocations).toHaveBeenCalledWith('4', { enabled: true }); + expect(useHubs).toHaveBeenCalledWith({ enabled: false }); + }); + + it('should treat tenantid "0" as staff, not as a tenant', () => { + const { result } = setup({ user: { tenantid: '0' } }); + expect(result.current.isTenantUser).toBe(false); + }); + + it('should fall back to the stored tenantid when the user record has none', () => { + localStorage.setItem('tenantid', '4'); + const { result } = setup({ user: {}, locations: [LOCATION] }); + expect(result.current.isTenantUser).toBe(true); + expect(result.current.tenantId).toBe('4'); + }); + + it('should label an unnamed kitchen hub by its id', () => { + const { result } = setup({ + user: { tenantid: '4' }, + locations: [{ tenantlocationid: 55 }] + }); + expect(result.current.zones[1].hubname).toBe('Kitchen Hub #55'); + }); + }); + + describe('zone selection', () => { + it('should default to All Zones', () => { + const { result } = setup(); + expect(result.current.selectedZoneId).toBe('all'); + expect(result.current.isAllZones).toBe(true); + }); + + it('should restore the previously selected zone from storage', () => { + localStorage.setItem(ZONE_STORAGE_KEY, '2'); + const { result } = setup(); + expect(result.current.selectedZoneId).toBe('2'); + expect(result.current.selectedZone.hubname).toBe('Koramangala Hub'); + expect(result.current.isAllZones).toBe(false); + }); + + it('should persist a new selection', () => { + const { result } = setup(); + act(() => result.current.setSelectedZoneId('2')); + expect(localStorage.getItem(ZONE_STORAGE_KEY)).toBe('2'); + expect(result.current.selectedZone.hubid).toBe('2'); + }); + + it('should coerce a numeric zone id to a string', () => { + const { result } = setup(); + act(() => result.current.setSelectedZoneId(2)); + expect(result.current.selectedZoneId).toBe('2'); + expect(result.current.selectedZone.hubname).toBe('Koramangala Hub'); + }); + + it.each([null, undefined, ''])('should fall back to All Zones for the selection %p', (id) => { + const { result } = setup(); + act(() => result.current.setSelectedZoneId(id)); + expect(result.current.selectedZoneId).toBe('all'); + }); + + it('should fall back to All Zones when the stored zone no longer exists', () => { + // A hub that was deleted or deactivated must not leave the console + // filtering against a zone that matches nothing. + localStorage.setItem(ZONE_STORAGE_KEY, '999'); + const { result } = setup(); + expect(result.current.selectedZone.hubid).toBe('all'); + expect(result.current.isAllZones).toBe(true); + }); + + it('should survive a storage read failure at boot', () => { + const getItem = jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { + throw new Error('storage disabled'); + }); + expect(() => setup()).not.toThrow(); + getItem.mockRestore(); + }); + + it('should not throw when persisting is refused', () => { + const setItem = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { + throw new Error('QuotaExceededError'); + }); + const { result } = setup(); + expect(() => act(() => result.current.setSelectedZoneId('2'))).not.toThrow(); + expect(result.current.selectedZoneId).toBe('2'); + setItem.mockRestore(); + }); + }); + + describe('matchesZone — All Zones', () => { + it('should match any row', () => { + const { result } = setup(); + expect(result.current.matchesZone({ hubid: '999' })).toBe(true); + expect(result.current.matchesZone({})).toBe(true); + }); + + it('should still reject a null row', () => { + const { result } = setup(); + expect(result.current.matchesZone(null)).toBe(false); + expect(result.current.matchesZone(undefined)).toBe(false); + }); + }); + + describe('matchesZone — tenant isolation', () => { + it('should reject a row belonging to a different tenant even under All Zones', () => { + // The data-visibility boundary: tenant scoping is checked BEFORE the + // all-zones shortcut, so another tenant's orders can never render. + localStorage.setItem('tenantid', '4'); + const { result } = setup({ user: { tenantid: '4' }, locations: [{ tenantlocationid: 55 }] }); + + expect(result.current.isAllZones).toBe(true); + expect(result.current.matchesZone({ tenantid: '9' })).toBe(false); + expect(result.current.matchesZone({ tenantid: '4' })).toBe(true); + }); + + it('should compare tenant ids across number and string forms', () => { + const { result } = setup({ user: { tenantid: '4' }, locations: [{ tenantlocationid: 55 }] }); + expect(result.current.matchesZone({ tenantid: 4 })).toBe(true); + }); + + it('should not apply tenant scoping to a staff login', () => { + const { result } = setup({ user: { tenantid: '0' } }); + expect(result.current.matchesZone({ tenantid: '9' })).toBe(true); + }); + + it('should allow a row that carries no tenant id at all', () => { + const { result } = setup({ user: { tenantid: '4' }, locations: [{ tenantlocationid: 55 }] }); + expect(result.current.matchesZone({ bookingid: 1 })).toBe(true); + }); + }); + + describe('matchesZone — hub matching for orders', () => { + const selectKoramangala = () => { + localStorage.setItem(ZONE_STORAGE_KEY, '2'); + return setup(); + }; + + it.each(['hubid', 'sourcehubid', 'applocationid', 'tenantlocationid', 'locationid'])( + 'should match on a direct %s', + (field) => { + const { result } = selectKoramangala(); + expect(result.current.matchesZone({ [field]: '2' })).toBe(true); + } + ); + + it('should match a numeric hub id against the string zone id', () => { + const { result } = selectKoramangala(); + expect(result.current.matchesZone({ hubid: 2 })).toBe(true); + }); + + it('should match on the hub pincode appearing in an address', () => { + const { result } = selectKoramangala(); + expect(result.current.matchesZone({ deliveryaddress: '4th Block, Bengaluru 560034' })).toBe(true); + }); + + it('should match on the hub city appearing in an address, case-insensitively', () => { + const { result } = selectKoramangala(); + expect(result.current.matchesZone({ pickupaddress: '12 MG Road, BENGALURU' })).toBe(true); + }); + + it('should match on the hub name appearing in an address', () => { + const { result } = selectKoramangala(); + expect(result.current.matchesZone({ address: 'Near koramangala hub' })).toBe(true); + }); + + it('should match a pickup coordinate inside the 35 km radius', () => { + const { result } = selectKoramangala(); + expect(result.current.matchesZone({ pickuplatitude: 12.94, pickuplongitude: 77.62 })).toBe(true); + }); + + it('should match a drop coordinate inside the radius even when pickup is far', () => { + const { result } = selectKoramangala(); + expect( + result.current.matchesZone({ + pickuplatitude: 19.076, + pickuplongitude: 72.877, + deliverylatitude: 12.94, + deliverylongitude: 77.62 + }) + ).toBe(true); + }); + + it('should reject a row in another city with no matching id or coordinate', () => { + const { result } = selectKoramangala(); + expect( + result.current.matchesZone({ + hubid: '7', + deliveryaddress: 'Bandra West, Mumbai 400050', + deliverylatitude: 19.076, + deliverylongitude: 72.877 + }) + ).toBe(false); + }); + + it('should reject a coordinate well outside the radius', () => { + const { result } = selectKoramangala(); + expect(result.current.matchesZone({ latitude: 19.076, longitude: 72.877 })).toBe(false); + }); + + it('should reject a row with nothing to match on', () => { + const { result } = selectKoramangala(); + expect(result.current.matchesZone({ bookingid: 4021 })).toBe(false); + }); + + it('should ignore an unusable coordinate rather than treating it as origin', () => { + // (0,0) is in the Atlantic; a NaN or null coordinate must not silently + // become a position that is 35 km from nothing. + const { result } = selectKoramangala(); + expect(result.current.matchesZone({ pickuplatitude: null, pickuplongitude: null })).toBe(false); + expect(result.current.matchesZone({ pickuplatitude: 'abc', pickuplongitude: 'abc' })).toBe(false); + }); + }); + + describe('matchesZone — riders', () => { + it('should match a rider assigned to the selected hub', () => { + localStorage.setItem(ZONE_STORAGE_KEY, '2'); + const { result } = setup(); + expect(result.current.matchesZone({ milerprofileid: 11, hubid: '2' })).toBe(true); + }); + + it('should match a rider by their app location', () => { + localStorage.setItem(ZONE_STORAGE_KEY, '2'); + const { result } = setup(); + expect(result.current.matchesZone({ milerprofileid: 11, applocationid: '2' })).toBe(true); + }); + + it('should match a rider by city when they carry no hub id', () => { + localStorage.setItem(ZONE_STORAGE_KEY, '2'); + const { result } = setup(); + expect(result.current.matchesZone({ milerprofileid: 11, city: 'bengaluru' })).toBe(true); + }); + + it('should reject a rider from another city', () => { + localStorage.setItem(ZONE_STORAGE_KEY, '2'); + const { result } = setup(); + expect(result.current.matchesZone({ milerprofileid: 11, city: 'Mumbai' })).toBe(false); + }); + + it('should show every fleet rider to a tenant, whichever kitchen is selected', () => { + // Riders belong to the tenant fleet and serve all its kitchen hubs. + localStorage.setItem(ZONE_STORAGE_KEY, '55'); + const { result } = setup({ + user: { tenantid: '4' }, + locations: [{ tenantlocationid: 55, city: 'Bengaluru' }] + }); + expect(result.current.matchesZone({ milerprofileid: 11, tenantid: '4', city: 'Mysuru' })).toBe(true); + }); + + it('should still reject a rider belonging to another tenant', () => { + localStorage.setItem(ZONE_STORAGE_KEY, '55'); + const { result } = setup({ + user: { tenantid: '4' }, + locations: [{ tenantlocationid: 55, city: 'Bengaluru' }] + }); + expect(result.current.matchesZone({ milerprofileid: 11, tenantid: '9' })).toBe(false); + }); + + it.each([ + ['milerprofileid', { milerprofileid: 11 }], + ['authname', { authname: 'anita' }], + ['userid + availabilitystatus', { userid: 91, availabilitystatus: 'available' }] + ])('should recognise a rider row by its %s', (_label, row) => { + localStorage.setItem(ZONE_STORAGE_KEY, '2'); + const { result } = setup(); + // A rider row with no location signal is rejected as a rider, not + // accidentally address-matched as an order. + expect(result.current.matchesZone(row)).toBe(false); + }); + }); + + describe('loading state and refetch', () => { + it('should surface the hub loading state for a staff login', () => { + useAuth.mockReturnValue({ user: { tenantid: '0' } }); + useHubs.mockReturnValue({ data: [], isLoading: true, refetch: jest.fn() }); + useTenantLocations.mockReturnValue({ data: [], isLoading: false, refetch: jest.fn() }); + + const { result } = renderHook(() => useZone(), { wrapper }); + expect(result.current.isHubsLoading).toBe(true); + }); + + it('should surface the location loading state for a tenant login', () => { + useAuth.mockReturnValue({ user: { tenantid: '4' } }); + useHubs.mockReturnValue({ data: [], isLoading: false, refetch: jest.fn() }); + useTenantLocations.mockReturnValue({ data: [], isLoading: true, refetch: jest.fn() }); + + const { result } = renderHook(() => useZone(), { wrapper }); + expect(result.current.isHubsLoading).toBe(true); + }); + + it('should refetch the source the current login actually reads', () => { + const refetchHubs = jest.fn(); + const refetchLocations = jest.fn(); + useAuth.mockReturnValue({ user: { tenantid: '4' } }); + useHubs.mockReturnValue({ data: [], isLoading: false, refetch: refetchHubs }); + useTenantLocations.mockReturnValue({ data: [], isLoading: false, refetch: refetchLocations }); + + const { result } = renderHook(() => useZone(), { wrapper }); + act(() => result.current.refetchHubs()); + + expect(refetchLocations).toHaveBeenCalledTimes(1); + expect(refetchHubs).not.toHaveBeenCalled(); + }); + }); + + describe('useZone outside a provider', () => { + it('should throw a named error rather than returning undefined', () => { + const spy = jest.spyOn(console, 'error').mockImplementation(() => {}); + expect(() => renderHook(() => useZone())).toThrow('useZone must be used within a ZoneProvider'); + spy.mockRestore(); + }); + }); +}); diff --git a/tests/lib/batchBucket.test.js b/tests/lib/batchBucket.test.js new file mode 100644 index 0000000..d032852 --- /dev/null +++ b/tests/lib/batchBucket.test.js @@ -0,0 +1,138 @@ +import { BATCHES, getBatchForHour, batchRangeLabel, getRowBatchId, getBatchLabel } from '@/lib/batchBucket'; + +/** + * Morning/Afternoon/Evening wave bucketing. + * + * The bug these tests exist to prevent: the windows used to cover only 14.5 of + * 24 hours, so an order placed at 2:43pm belonged to no batch and vanished from + * every batch filter. The invariant is TOTAL COVERAGE — every hour of the day + * lands in exactly one batch — plus the field the bucketing reads (`orderdate`, + * never `assigntime` or `expecteddeliverytime`). + */ +describe('batchBucket', () => { + describe('BATCHES definition', () => { + it('should define exactly the three operator-facing waves', () => { + expect(BATCHES.map((b) => b.id)).toEqual(['morning', 'afternoon', 'evening']); + }); + + it('should cover the whole day with no gap and no overlap', () => { + const sorted = [...BATCHES].sort((a, b) => a.startHour - b.startHour); + expect(sorted[0].startHour).toBe(0); + expect(sorted[sorted.length - 1].endHour).toBe(24); + sorted.slice(1).forEach((batch, index) => { + expect(batch.startHour).toBe(sorted[index].endHour); + }); + }); + + it('should assign every quarter-hour of the day to exactly one batch', () => { + for (let hour = 0; hour < 24; hour += 0.25) { + const matches = BATCHES.filter((b) => hour >= b.startHour && hour < b.endHour); + expect(matches).toHaveLength(1); + } + }); + }); + + describe('getBatchForHour', () => { + it.each([ + [0, 'morning'], + [8.99, 'morning'], + [9, 'afternoon'], + [14.716, 'afternoon'], + [15.99, 'afternoon'], + [16, 'evening'], + [23.99, 'evening'] + ])('should bucket hour %p into %s', (hour, expected) => { + expect(getBatchForHour(hour)).toBe(expected); + }); + + it('should treat every window as half-open [start, end)', () => { + expect(getBatchForHour(9)).toBe('afternoon'); + expect(getBatchForHour(8.999999)).toBe('morning'); + expect(getBatchForHour(16)).toBe('evening'); + expect(getBatchForHour(15.999999)).toBe('afternoon'); + }); + + it.each([-0.1, 24, 25, NaN])('should return null for the out-of-range hour %p', (hour) => { + expect(getBatchForHour(hour)).toBeNull(); + }); + }); + + describe('getRowBatchId', () => { + it('should bucket on orderdate — the booking creation time', () => { + expect(getRowBatchId({ orderdate: '2026-08-27T07:15:00' })).toBe('morning'); + expect(getRowBatchId({ orderdate: '2026-08-27T10:00:00' })).toBe('afternoon'); + expect(getRowBatchId({ orderdate: '2026-08-27T18:30:00' })).toBe('evening'); + }); + + it('should bucket the 2:43pm order that used to fall into the old 12:30-4pm black hole', () => { + expect(getRowBatchId({ orderdate: '2026-08-27T14:43:00' })).toBe('afternoon'); + }); + + it('should use fractional hours so the back half of an hour is not mis-bucketed', () => { + expect(getRowBatchId({ orderdate: '2026-08-27T15:45:00' })).toBe('afternoon'); + expect(getRowBatchId({ orderdate: '2026-08-27T16:01:00' })).toBe('evening'); + }); + + it('should strip a false trailing Z rather than shifting the row into another batch', () => { + // 20:00 with a false Z read as a real UTC instant becomes 01:30 the next + // day in IST, which would move an evening order into Morning. + expect(getRowBatchId({ orderdate: '2026-08-27T20:00:00Z' })).toBe('evening'); + }); + + it('should refuse to bucket a bare date, which has no time component', () => { + // Midnight is a real morning-batch hour, so guessing would silently pile + // every date-only row into Morning. + expect(getRowBatchId({ orderdate: '2026-08-27' })).toBeNull(); + expect(getRowBatchId({ orderdate: ' 2026-08-27 ' })).toBeNull(); + }); + + it.each([ + ['a row with no orderdate', {}], + ['a null orderdate', { orderdate: null }], + ['an empty orderdate', { orderdate: '' }], + ['a null row', null], + ['an undefined row', undefined] + ])('should return null for %s', (_label, row) => { + expect(getRowBatchId(row)).toBeNull(); + }); + + it('should return null for an unparseable orderdate rather than throwing', () => { + expect(getRowBatchId({ orderdate: 'yesterday-ish' })).toBeNull(); + }); + + it('should ignore assigntime and expecteddeliverytime entirely', () => { + // Both were tried and rejected: assigntime is re-stamped by any status + // change, expecteddeliverytime describes the promised slot. + const row = { + orderdate: '2026-08-27T07:00:00', + assigntime: '2026-08-27T18:00:00', + expecteddeliverytime: '2026-08-27T16:30:00' + }; + expect(getRowBatchId(row)).toBe('morning'); + }); + }); + + describe('labels', () => { + it('should describe the morning and afternoon windows as ranges', () => { + expect(batchRangeLabel(BATCHES[0])).toBe('12 AM–9 AM'); + expect(batchRangeLabel(BATCHES[1])).toBe('9 AM–4 PM'); + }); + + it('should describe a window that runs to end of day as "After X"', () => { + expect(batchRangeLabel(BATCHES[2])).toBe('After 4 PM'); + }); + + it('should render a half-hour boundary with minutes', () => { + expect(batchRangeLabel({ startHour: 12.5, endHour: 16 })).toBe('12:30 PM–4 PM'); + }); + + it('should return the human label for a known batch id', () => { + expect(getBatchLabel('morning')).toBe('Morning Batch'); + expect(getBatchLabel('evening')).toBe('Evening Batch'); + }); + + it('should fall back to the id itself for an unknown batch', () => { + expect(getBatchLabel('midnight')).toBe('midnight'); + }); + }); +}); diff --git a/tests/lib/distance.test.js b/tests/lib/distance.test.js new file mode 100644 index 0000000..decab12 --- /dev/null +++ b/tests/lib/distance.test.js @@ -0,0 +1,179 @@ +import { calculateDrivingDistance, calculateTotalCharge, getLastRouteDurationMin } from '@/lib/distance'; + +/** + * Distance and charge for a delivery leg. + * + * OSRM is the routed source; the Haversine + 1.3 multiplier is the fallback. + * The behaviour that matters commercially is that the fallback NEVER reports a + * duration — a straight-line estimate has no honest ETA, and showing a made-up + * one to an operator is worse than showing none. + */ +const osrmOk = (distanceMeters, durationSeconds) => ({ + ok: true, + json: async () => ({ routes: [{ distance: distanceMeters, duration: durationSeconds }] }) +}); + +const BLR = { latitude: 12.9716, longitude: 77.5946 }; +const IND = { latitude: 12.9352, longitude: 77.6245 }; + +describe('calculateDrivingDistance', () => { + beforeEach(() => { + global.fetch = jest.fn(); + }); + + describe('coordinate validation', () => { + it.each([ + ['a missing origin latitude', { longitude: 77.5946 }, IND], + ['a missing origin longitude', { latitude: 12.9716 }, IND], + ['a missing destination latitude', BLR, { longitude: 77.6245 }], + ['a missing destination longitude', BLR, { latitude: 12.9352 }], + ['a null origin', null, IND], + ['an undefined destination', BLR, undefined], + ['a null latitude value', { latitude: null, longitude: 77.5946 }, IND] + ])('should reject %s', async (_label, origin, destination) => { + await expect(calculateDrivingDistance(origin, destination)).rejects.toThrow('Invalid coordinates'); + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it('should accept zero coordinates, which are a real position', async () => { + global.fetch.mockResolvedValue(osrmOk(1000, 120)); + await expect( + calculateDrivingDistance({ latitude: 0, longitude: 0 }, { latitude: 0, longitude: 1 }) + ).resolves.toEqual(expect.any(Number)); + }); + }); + + describe('OSRM routed distance', () => { + it('should return the routed distance rounded to whole kilometres', async () => { + global.fetch.mockResolvedValue(osrmOk(8200, 900)); + await expect(calculateDrivingDistance(BLR, IND)).resolves.toBe(8); + }); + + it('should cache the routed duration in whole minutes', async () => { + global.fetch.mockResolvedValue(osrmOk(8200, 900)); + await calculateDrivingDistance(BLR, IND); + expect(getLastRouteDurationMin()).toBe(15); + }); + + it('should request lon,lat order — the order OSRM expects', async () => { + // Swapping to lat,lon silently returns a route between two other places. + global.fetch.mockResolvedValue(osrmOk(8200, 900)); + await calculateDrivingDistance(BLR, IND); + const url = global.fetch.mock.calls[0][0]; + expect(url).toContain('/route/v1/driving/77.5946,12.9716;77.6245,12.9352'); + expect(url).toContain('overview=false'); + }); + + it('should use the public OSRM host by default', async () => { + global.fetch.mockResolvedValue(osrmOk(8200, 900)); + await calculateDrivingDistance(BLR, IND); + expect(global.fetch.mock.calls[0][0]).toContain('https://router.project-osrm.org'); + }); + + it('should honour a VITE_OSRM_URL override for a self-hosted router', async () => { + globalThis.__VITE_IMPORT_META__ = { env: { VITE_OSRM_URL: 'http://osrm.internal:5000' } }; + global.fetch.mockResolvedValue(osrmOk(8200, 900)); + + let reloaded; + jest.isolateModules(() => { + // eslint-disable-next-line global-require + reloaded = require('@/lib/distance').calculateDrivingDistance; + }); + + await reloaded(BLR, IND); + expect(global.fetch.mock.calls[0][0]).toContain('http://osrm.internal:5000'); + }); + + it('should report no duration when OSRM omits one', async () => { + global.fetch.mockResolvedValue({ ok: true, json: async () => ({ routes: [{ distance: 8200 }] }) }); + await calculateDrivingDistance(BLR, IND); + expect(getLastRouteDurationMin()).toBeNull(); + }); + }); + + describe('Haversine fallback', () => { + it('should fall back when OSRM answers with an error status', async () => { + global.fetch.mockResolvedValue({ ok: false, status: 502, json: async () => ({}) }); + const distance = await calculateDrivingDistance(BLR, IND); + expect(distance).toEqual(expect.any(Number)); + expect(distance).toBeGreaterThan(0); + }); + + it('should fall back when the network call throws', async () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); + global.fetch.mockRejectedValue(new Error('ECONNREFUSED')); + await expect(calculateDrivingDistance(BLR, IND)).resolves.toEqual(expect.any(Number)); + expect(warn).toHaveBeenCalled(); + }); + + it('should fall back when OSRM returns no routes', async () => { + global.fetch.mockResolvedValue({ ok: true, json: async () => ({ routes: [] }) }); + await expect(calculateDrivingDistance(BLR, IND)).resolves.toEqual(expect.any(Number)); + }); + + it('should apply the 1.3 road multiplier to the straight-line distance', async () => { + // One degree of latitude is 111.19 km great-circle; x1.3 = 144.5 -> 145. + global.fetch.mockResolvedValue({ ok: false, json: async () => ({}) }); + const distance = await calculateDrivingDistance( + { latitude: 0, longitude: 0 }, + { latitude: 1, longitude: 0 } + ); + expect(distance).toBe(145); + }); + + it('should return zero for two identical points', async () => { + global.fetch.mockResolvedValue({ ok: false, json: async () => ({}) }); + await expect(calculateDrivingDistance(BLR, { ...BLR })).resolves.toBe(0); + }); + + it('should report NO duration for a fallback estimate', async () => { + // A straight-line guess has no honest ETA. Leaving a stale routed value + // in place would attach a real-looking duration to a fabricated distance. + global.fetch.mockResolvedValueOnce(osrmOk(8200, 900)); + await calculateDrivingDistance(BLR, IND); + expect(getLastRouteDurationMin()).toBe(15); + + global.fetch.mockResolvedValueOnce({ ok: false, json: async () => ({}) }); + await calculateDrivingDistance(BLR, IND); + expect(getLastRouteDurationMin()).toBeNull(); + }); + + it('should be symmetric — the distance does not depend on direction', async () => { + global.fetch.mockResolvedValue({ ok: false, json: async () => ({}) }); + const there = await calculateDrivingDistance(BLR, IND); + const back = await calculateDrivingDistance(IND, BLR); + expect(there).toBe(back); + }); + }); +}); + +describe('calculateTotalCharge', () => { + it('should charge only the base price below the minimum distance', () => { + expect(calculateTotalCharge(3, 30, 6, 8)).toBe(30); + }); + + it('should charge only the base price at exactly the minimum distance', () => { + // Boundary: minKm is inclusive of the base fare. + expect(calculateTotalCharge(8, 30, 6, 8)).toBe(30); + }); + + it('should add the per-km rate beyond the minimum', () => { + expect(calculateTotalCharge(12, 30, 6, 8)).toBe(54); + }); + + it('should add a partial kilometre proportionally', () => { + expect(calculateTotalCharge(8.5, 30, 6, 8)).toBeCloseTo(33, 6); + }); + + it('should charge the base price for a zero-distance leg', () => { + expect(calculateTotalCharge(0, 30, 6, 8)).toBe(30); + }); + + it('should charge the base price when there is no minimum', () => { + expect(calculateTotalCharge(0, 30, 6, 0)).toBe(30); + }); + + it('should scale linearly for a long leg', () => { + expect(calculateTotalCharge(108, 30, 6, 8)).toBe(630); + }); +}); diff --git a/tests/lib/doormileFormat.test.js b/tests/lib/doormileFormat.test.js new file mode 100644 index 0000000..9e62971 --- /dev/null +++ b/tests/lib/doormileFormat.test.js @@ -0,0 +1,312 @@ +import { renderHook, act } from '@testing-library/react'; +import { currency, number, km, percentOf, orDash, matchesQuery, useDebouncedValue, exportRows } from '@/lib/doormileFormat'; + +/** + * `xlsx` is imported on demand inside exportRows, so it is mocked at the module + * boundary. The export path itself — column declaration to sheet rows to the + * written filename — is exactly what these tests verify, so none of that is + * mocked away. + */ +jest.mock('xlsx', () => ({ + __esModule: true, + utils: { + json_to_sheet: jest.fn(() => ({ sheet: true })), + book_new: jest.fn(() => ({ book: true })), + book_append_sheet: jest.fn() + }, + writeFile: jest.fn() +})); + +import * as XLSX from 'xlsx'; + +/** + * Shared display formatting. The reason it lives in one module is that a number + * reading ₹1,240 on Orders and 1240 on Reports looks like two different figures + * for the same money — so these tests pin the exact rendering contract, and in + * particular that 0 is a real value while blank/NaN is a dash. + */ +describe('doormileFormat', () => { + describe('currency', () => { + it('should render an amount in rupees with Indian digit grouping', () => { + expect(currency(1240)).toMatch(/^₹\s?1,240\.00$/); + }); + + it('should group in lakhs, not thousands', () => { + // 12,34,567 — not 1,234,567. The console is an Indian ops tool. + expect(currency(1234567)).toMatch(/12,34,567\.00$/); + }); + + it('should render zero as a real amount, not a dash', () => { + // A ₹0 delivery fee is a fact; an em dash reads as "unknown". + expect(currency(0)).toMatch(/^₹\s?0\.00$/); + }); + + it('should accept a numeric string, as the API returns for money columns', () => { + expect(currency('1240')).toMatch(/1,240\.00$/); + }); + + it('should cap the fraction at two digits', () => { + expect(currency(10.999)).toMatch(/^₹\s?11$|^₹\s?11\.00$/); + expect(currency(10.994)).toMatch(/10\.99$/); + }); + + it('should render a negative amount rather than dropping the sign', () => { + expect(currency(-500)).toMatch(/500\.00$/); + expect(currency(-500)).toMatch(/-|−|\(/); + }); + + it.each([undefined, 'abc', NaN, Infinity, {}])( + 'should fall back to an em dash for the unusable value %p', + (value) => { + expect(currency(value)).toBe('—'); + } + ); + + it('should fall back to an em dash for a null amount', () => { + // A nullable money column means UNKNOWN, not zero. Rendering ₹0.00 asserts + // the delivery earned nothing, which is the exact confusion the module's + // own "0 is a real amount" comment exists to prevent. + expect(currency(null)).toBe('—'); + }); + + it('should fall back to an em dash for an empty-string amount', () => { + expect(currency('')).toBe('—'); + }); + + it('should honour a caller-supplied fallback', () => { + expect(currency('abc', 'n/a')).toBe('n/a'); + }); + }); + + describe('number', () => { + it('should render a count with Indian grouping', () => { + expect(number(1234567)).toBe('12,34,567'); + }); + + it('should render zero as 0', () => { + expect(number(0)).toBe('0'); + }); + + it.each([undefined, 'abc', NaN, Infinity])('should fall back to an em dash for %p', (value) => { + expect(number(value)).toBe('—'); + }); + + it('should fall back to an em dash for a null count', () => { + // Same unknown-vs-zero distinction as `currency`. + expect(number(null)).toBe('—'); + }); + + it('should fall back to an em dash for an empty-string count', () => { + expect(number('')).toBe('—'); + }); + }); + + describe('km', () => { + it('should render a distance to exactly one decimal place', () => { + expect(km(12)).toBe('12.0 km'); + expect(km(12.34)).toBe('12.3 km'); + expect(km(12.36)).toBe('12.4 km'); + }); + + it('should render a zero distance rather than a dash', () => { + expect(km(0)).toBe('0.0 km'); + }); + + it.each([undefined, 'abc', NaN, Infinity])('should fall back to an em dash for %p', (value) => { + expect(km(value)).toBe('—'); + }); + + it('should fall back to an em dash for a null distance', () => { + // "0.0 km" for an unknown distance is worse than a dash: it reads as a + // measured zero-distance trip. + expect(km(null)).toBe('—'); + }); + }); + + describe('percentOf', () => { + it('should return a whole-number percentage', () => { + expect(percentOf(25, 100)).toBe(25); + expect(percentOf(1, 3)).toBe(33); + expect(percentOf(2, 3)).toBe(67); + }); + + it('should return 0 — not Infinity or NaN — when the total is zero', () => { + // This feeds progress bars and chip counts; NaN% renders as garbage. + expect(percentOf(5, 0)).toBe(0); + }); + + it.each([ + [null, 100], + [10, null], + ['abc', 100], + [10, 'abc'], + [undefined, undefined] + ])('should return 0 for the non-numeric pair (%p, %p)', (value, total) => { + expect(percentOf(value, total)).toBe(0); + }); + + it('should allow a percentage above 100 rather than clamping silently', () => { + expect(percentOf(150, 100)).toBe(150); + }); + }); + + describe('orDash', () => { + it.each([null, undefined, '', ' '])('should render the blank value %p as an em dash', (value) => { + expect(orDash(value)).toBe('—'); + }); + + it('should trim surrounding whitespace off a real value', () => { + expect(orDash(' Bengaluru ')).toBe('Bengaluru'); + }); + + it('should render 0 and false as text, since they are real values', () => { + expect(orDash(0)).toBe('0'); + expect(orDash(false)).toBe('false'); + }); + }); + + describe('matchesQuery', () => { + const row = { customername: 'Anita Rao', city: 'Bengaluru', phone: '9876543210', notes: null }; + + it('should match a case-insensitive substring on any listed field', () => { + expect(matchesQuery(row, ['customername', 'city'], 'anita')).toBe(true); + expect(matchesQuery(row, ['customername', 'city'], 'BENGAL')).toBe(true); + }); + + it('should not match a field that was not listed', () => { + expect(matchesQuery(row, ['city'], 'anita')).toBe(false); + }); + + it('should support a derived field expressed as a function', () => { + expect(matchesQuery(row, [(r) => `${r.customername} ${r.city}`], 'rao beng')).toBe(true); + }); + + it.each(['', ' ', null, undefined])( + 'should treat the blank query %p as "match everything"', + (query) => { + expect(matchesQuery(row, ['customername'], query)).toBe(true); + } + ); + + it('should trim the query before matching', () => { + expect(matchesQuery(row, ['city'], ' bengaluru ')).toBe(true); + }); + + it('should match against a numeric field coerced to text', () => { + expect(matchesQuery({ orderid: 4021 }, ['orderid'], '402')).toBe(true); + }); + + it('should skip null/undefined/empty fields instead of matching them', () => { + expect(matchesQuery(row, ['notes'], 'null')).toBe(false); + expect(matchesQuery(row, ['missing'], 'undefined')).toBe(false); + }); + + it('should return false for a row with no matching field rather than throwing', () => { + expect(matchesQuery(null, ['customername'], 'anita')).toBe(false); + expect(matchesQuery(row, [], 'anita')).toBe(false); + }); + }); + + describe('useDebouncedValue', () => { + beforeEach(() => jest.useFakeTimers()); + afterEach(() => jest.useRealTimers()); + + it('should return the initial value immediately', () => { + const { result } = renderHook(() => useDebouncedValue('a', 250)); + expect(result.current).toBe('a'); + }); + + it('should not settle until the delay has fully elapsed', () => { + const { result, rerender } = renderHook(({ value }) => useDebouncedValue(value, 250), { + initialProps: { value: 'a' } + }); + + rerender({ value: 'ab' }); + act(() => { + jest.advanceTimersByTime(249); + }); + expect(result.current).toBe('a'); + + act(() => { + jest.advanceTimersByTime(1); + }); + expect(result.current).toBe('ab'); + }); + + it('should collapse a burst of keystrokes into a single settled value', () => { + const { result, rerender } = renderHook(({ value }) => useDebouncedValue(value, 250), { + initialProps: { value: '' } + }); + + ['b', 'be', 'ben', 'beng'].forEach((value) => { + rerender({ value }); + act(() => { + jest.advanceTimersByTime(100); + }); + }); + expect(result.current).toBe(''); + + act(() => { + jest.advanceTimersByTime(250); + }); + expect(result.current).toBe('beng'); + }); + + it('should clear its pending timer on unmount', () => { + const clearSpy = jest.spyOn(global, 'clearTimeout'); + const { unmount } = renderHook(() => useDebouncedValue('a', 250)); + unmount(); + expect(clearSpy).toHaveBeenCalled(); + }); + }); + + describe('exportRows', () => { + it('should build one sheet record per row using the column headers', async () => { + const rows = [ + { orderid: 1, customername: 'Anita', amount: 240 }, + { orderid: 2, customername: 'Vikram', amount: 90 } + ]; + const columns = [ + { key: 'orderid', header: 'Order ID' }, + { key: 'customername', header: 'Customer' } + ]; + + await exportRows(rows, columns, 'orders-2026-08-27'); + + expect(XLSX.utils.json_to_sheet).toHaveBeenCalledWith([ + { 'Order ID': 1, Customer: 'Anita' }, + { 'Order ID': 2, Customer: 'Vikram' } + ]); + expect(XLSX.writeFile).toHaveBeenCalledWith({ book: true }, 'orders-2026-08-27.xlsx'); + }); + + it('should prefer a column value() accessor over the raw key', async () => { + await exportRows([{ kms: 12.345 }], [{ key: 'kms', header: 'Distance', value: (r) => `${r.kms.toFixed(1)} km` }], 'x'); + expect(XLSX.utils.json_to_sheet).toHaveBeenCalledWith([{ Distance: '12.3 km' }]); + }); + + it('should write a blank cell — not the word null — for a missing value', async () => { + await exportRows([{ a: null, b: undefined }], [{ key: 'a', header: 'A' }, { key: 'b', header: 'B' }], 'x'); + expect(XLSX.utils.json_to_sheet).toHaveBeenCalledWith([{ A: '', B: '' }]); + }); + + it('should preserve a zero value rather than blanking it', async () => { + await exportRows([{ amount: 0 }], [{ key: 'amount', header: 'Amount' }], 'x'); + expect(XLSX.utils.json_to_sheet).toHaveBeenCalledWith([{ Amount: 0 }]); + }); + + it.each([[[]], [null], [undefined]])( + 'should still produce a (empty) workbook when rows is %p', + async (rows) => { + await exportRows(rows, [{ key: 'a', header: 'A' }], 'empty'); + expect(XLSX.utils.json_to_sheet).toHaveBeenCalledWith([]); + expect(XLSX.writeFile).toHaveBeenCalledWith(expect.anything(), 'empty.xlsx'); + } + ); + + it('should name the sheet consistently so downstream parsers can find it', async () => { + await exportRows([{ a: 1 }], [{ key: 'a', header: 'A' }], 'x'); + expect(XLSX.utils.book_append_sheet).toHaveBeenCalledWith({ book: true }, { sheet: true }, 'Export'); + }); + }); +}); diff --git a/tests/lib/doormileTimestamp.test.js b/tests/lib/doormileTimestamp.test.js new file mode 100644 index 0000000..81d74b6 --- /dev/null +++ b/tests/lib/doormileTimestamp.test.js @@ -0,0 +1,133 @@ +import dayjs from 'dayjs'; +import { parseDoormileTimestamp, formatDoormileTimestamp, doormileDay } from '@/lib/doormileTimestamp'; + +/** + * Doormile timestamps are IST wall-clock in naive Postgres columns, and some + * responses carry a FALSE trailing `Z` (a Go+pgx marshalling footgun). Parsed + * naively, dayjs reads that Z as a real UTC instant and adds +5:30 to digits + * that were already correct. + * + * These tests protect the one property the whole app depends on: whatever zone + * marker a timestamp arrives with, the rendered and bucketed result is the raw + * wall-clock digits — asserted without depending on the machine's own timezone. + */ +describe('doormileTimestamp', () => { + describe('parseDoormileTimestamp', () => { + it('should ignore a false trailing Z rather than shifting the clock by +5:30', () => { + const parsed = parseDoormileTimestamp('2026-08-27T14:30:00Z'); + expect(parsed.isValid()).toBe(true); + expect(parsed.hour()).toBe(14); + expect(parsed.minute()).toBe(30); + expect(parsed.date()).toBe(27); + }); + + it('should strip a +05:30 offset and keep the same wall-clock digits', () => { + const parsed = parseDoormileTimestamp('2026-08-27T14:30:00+05:30'); + expect(parsed.hour()).toBe(14); + expect(parsed.minute()).toBe(30); + }); + + it('should strip a colon-less offset such as +0530', () => { + expect(parseDoormileTimestamp('2026-08-27T14:30:00+0530').hour()).toBe(14); + }); + + it('should strip a negative offset too', () => { + expect(parseDoormileTimestamp('2026-08-27T14:30:00-04:00').hour()).toBe(14); + }); + + it('should parse an already-naive timestamp unchanged', () => { + const parsed = parseDoormileTimestamp('2026-08-27T14:30:00'); + expect(parsed.hour()).toBe(14); + expect(parsed.date()).toBe(27); + }); + + it('should produce the identical instant for the naive and the false-Z form', () => { + const naive = parseDoormileTimestamp('2026-08-27T23:45:00'); + const falseZ = parseDoormileTimestamp('2026-08-27T23:45:00Z'); + expect(falseZ.valueOf()).toBe(naive.valueOf()); + }); + + it.each([null, undefined, '', 0, false])( + 'should return an invalid dayjs (not throw) for the blank value %p', + (raw) => { + expect(parseDoormileTimestamp(raw).isValid()).toBe(false); + } + ); + + it('should return an invalid dayjs for unparseable text', () => { + expect(parseDoormileTimestamp('not-a-timestamp').isValid()).toBe(false); + }); + + it('should accept a bare date with no time component', () => { + const parsed = parseDoormileTimestamp('2026-08-27'); + expect(parsed.isValid()).toBe(true); + expect(parsed.format('YYYY-MM-DD')).toBe('2026-08-27'); + }); + + it('should accept a Date instance without throwing', () => { + expect(parseDoormileTimestamp(new Date('2026-08-27T10:00:00Z')).isValid()).toBe(true); + }); + }); + + describe('formatDoormileTimestamp', () => { + it('should render the default operator-facing format', () => { + expect(formatDoormileTimestamp('2026-08-27T14:30:00Z')).toBe('27 Aug 2026, 02:30 PM'); + }); + + it('should honour a caller-supplied format', () => { + expect(formatDoormileTimestamp('2026-08-27T14:30:00Z', 'YYYY-MM-DD HH:mm')).toBe('2026-08-27 14:30'); + }); + + it('should return the em-dash fallback for an unusable timestamp', () => { + expect(formatDoormileTimestamp(null)).toBe('—'); + expect(formatDoormileTimestamp('garbage')).toBe('—'); + }); + + it('should return a caller-supplied fallback instead of the em dash', () => { + expect(formatDoormileTimestamp(undefined, 'DD MMM', 'Not set')).toBe('Not set'); + }); + }); + + describe('doormileDay', () => { + it('should return the calendar day the wall-clock reading falls on', () => { + expect(doormileDay('2026-08-27T14:30:00Z')).toBe('2026-08-27'); + }); + + it('should keep a late-evening row on its own day rather than rolling it forward', () => { + expect(doormileDay('2026-08-27T23:45:00Z')).toBe('2026-08-27'); + }); + + it('should keep an early-morning row on its own day', () => { + expect(doormileDay('2026-08-27T00:15:00Z')).toBe('2026-08-27'); + }); + + it('should return undefined — not a string — for an unusable timestamp', () => { + // Callers compare this against a selected date; a formatted "Invalid Date" + // would silently never match. + expect(doormileDay(null)).toBeUndefined(); + expect(doormileDay('garbage')).toBeUndefined(); + }); + + it('should agree across every zone-marker spelling of the same reading', () => { + const forms = [ + '2026-08-27T23:45:00', + '2026-08-27T23:45:00Z', + '2026-08-27T23:45:00+05:30', + '2026-08-27T23:45:00+0000' + ]; + const days = forms.map(doormileDay); + expect(new Set(days).size).toBe(1); + expect(days[0]).toBe('2026-08-27'); + }); + + it('should differ from a bare dayjs() parse whenever the runner is not on UTC', () => { + // Guards the regression directly: bare dayjs() on a Z-suffixed IST reading + // lands on a different day in any non-UTC zone. + const raw = '2026-08-27T23:45:00Z'; + expect(doormileDay(raw)).toBe('2026-08-27'); + if (dayjs(raw).utcOffset() > 15) { + expect(dayjs(raw).format('YYYY-MM-DD')).toBe('2026-08-28'); + } + }); + }); +}); diff --git a/tests/lib/orderStatusGroups.test.js b/tests/lib/orderStatusGroups.test.js new file mode 100644 index 0000000..40899c1 --- /dev/null +++ b/tests/lib/orderStatusGroups.test.js @@ -0,0 +1,133 @@ +import { + ORDER_STATUS_GROUPS, + ORDER_STATUS_LABELS, + ORDER_STATUS_ORDER, + statusesInGroup, + groupForBookingStatus, + isInGroup, +} from '@/lib/orderStatusGroups'; + +/** + * The Orders page's tab taxonomy. What these tests protect is that every raw + * backend booking enum lands in exactly one operator-facing tab, that an + * unmapped enum stays visible rather than vanishing from the totals, and that + * this taxonomy stays deliberately DIFFERENT from the Deliveries one (the file + * header calls the split explicit product direction that was reverted once). + */ +describe('orderStatusGroups', () => { + describe('shape of the taxonomy', () => { + it('should expose a label for every group key', () => { + Object.keys(ORDER_STATUS_GROUPS).forEach((group) => { + expect(ORDER_STATUS_LABELS[group]).toEqual(expect.any(String)); + expect(ORDER_STATUS_LABELS[group]).not.toBe(''); + }); + }); + + it('should order the tabs to match the lifecycle and cover every group', () => { + expect(ORDER_STATUS_ORDER).toEqual(['pending', 'assigned', 'active', 'delivered', 'cancelled']); + expect([...ORDER_STATUS_ORDER].sort()).toEqual(Object.keys(ORDER_STATUS_GROUPS).sort()); + }); + + it('should never assign the same raw status to two groups', () => { + const seen = new Map(); + Object.entries(ORDER_STATUS_GROUPS).forEach(([group, raws]) => { + raws.forEach((raw) => { + expect(seen.has(raw)).toBe(false); + seen.set(raw, group); + }); + }); + }); + + it('should keep every raw status lowercase so lookups cannot miss on case', () => { + Object.values(ORDER_STATUS_GROUPS) + .flat() + .forEach((raw) => expect(raw).toBe(raw.toLowerCase())); + }); + }); + + describe('groupForBookingStatus', () => { + it.each([ + ['pending_pickup', 'pending'], + ['unassigned', 'pending'], + ['miler_assigned', 'assigned'], + ['converted_to_consignment', 'assigned'], + ['pickup_scheduled', 'assigned'], + ['out_for_delivery', 'active'], + ['delivered', 'delivered'], + ['cancelled', 'cancelled'], + ['rejected', 'cancelled'], + ])('should map raw booking status %s to the %s tab', (raw, group) => { + expect(groupForBookingStatus(raw)).toBe(group); + }); + + it('should keep Miler_Assigned on the Assigned tab — assigning is an operator action here', () => { + // Deliberately NOT the Deliveries taxonomy, where miler_assigned is still + // 'pending' until the rider themselves accepts. + expect(groupForBookingStatus('Miler_Assigned')).toBe('assigned'); + }); + + it('should be case-insensitive about the backend enum casing', () => { + expect(groupForBookingStatus('OUT_FOR_DELIVERY')).toBe('active'); + expect(groupForBookingStatus('Pending_Pickup')).toBe('pending'); + }); + + it('should return the lowercased raw value for an enum it has never seen', () => { + // An unmapped status must stay visible in a breakdown, not vanish. + expect(groupForBookingStatus('Inwarded_At_Hub')).toBe('inwarded_at_hub'); + expect(groupForBookingStatus('brand_new_backend_state')).toBe('brand_new_backend_state'); + }); + + it.each([ + [null, 'pending'], + [undefined, 'pending'], + ['', 'pending'], + ])('should treat a blank status (%p) as pending rather than crashing', (raw, expected) => { + expect(groupForBookingStatus(raw)).toBe(expected); + }); + + it('should not throw on a non-string status', () => { + expect(() => groupForBookingStatus(42)).not.toThrow(); + expect(groupForBookingStatus(42)).toBe('42'); + }); + }); + + describe('statusesInGroup', () => { + it('should return the raw enums backing a known group', () => { + expect(statusesInGroup('delivered')).toEqual(['delivered', 'completed', 'success']); + }); + + it('should return an empty array for an unknown group instead of undefined', () => { + // Callers spread/`.includes()` this directly — undefined would throw. + expect(statusesInGroup('nope')).toEqual([]); + expect(statusesInGroup(undefined)).toEqual([]); + }); + }); + + describe('isInGroup', () => { + it('should confirm membership case-insensitively', () => { + expect(isInGroup('Out_For_Delivery', 'active')).toBe(true); + expect(isInGroup('delivered', 'active')).toBe(false); + }); + + it('should return false for an unknown group rather than throwing', () => { + expect(isInGroup('delivered', 'not_a_group')).toBe(false); + }); + + it('should treat a null status as not in any non-pending group', () => { + expect(isInGroup(null, 'delivered')).toBe(false); + // '' is an explicit member of the pending group. + expect(isInGroup(null, 'pending')).toBe(true); + }); + }); + + describe('coverage of the tab set', () => { + it('should place every raw status into a group whose label is rendered', () => { + Object.values(ORDER_STATUS_GROUPS) + .flat() + .forEach((raw) => { + const group = groupForBookingStatus(raw); + expect(ORDER_STATUS_ORDER).toContain(group); + }); + }); + }); +}); diff --git a/tests/lib/preferences.test.js b/tests/lib/preferences.test.js new file mode 100644 index 0000000..7bdb60e --- /dev/null +++ b/tests/lib/preferences.test.js @@ -0,0 +1,159 @@ +import { renderHook, act } from '@testing-library/react'; +import { DEFAULT_PREFERENCES, writePreferences, usePreferences, useUpdatePreferences } from '@/lib/preferences'; + +const STORAGE_KEY = 'doormilePreferences'; + +/** + * Operator display preferences. + * + * Two properties matter: unknown/corrupt storage must never take a page down, + * and a change must reach EVERY mounted consumer including the one that made it + * — `storage` alone fires only in other tabs, which left the table that + * triggered the change unchanged. + */ +describe('preferences', () => { + describe('defaults', () => { + it('should default to comfortable density and a 25-row page', () => { + expect(DEFAULT_PREFERENCES).toEqual({ compactDensity: false, defaultPageSize: 25 }); + }); + + it('should read the defaults when nothing is stored', () => { + const { result } = renderHook(() => usePreferences()); + expect(result.current).toEqual(DEFAULT_PREFERENCES); + }); + + it('should merge a partial stored record over the defaults', () => { + // A record written by an older build must not leave newer keys undefined. + localStorage.setItem(STORAGE_KEY, JSON.stringify({ compactDensity: true })); + const { result } = renderHook(() => usePreferences()); + expect(result.current).toEqual({ compactDensity: true, defaultPageSize: 25 }); + }); + + it('should fall back to the defaults for a corrupt stored record', () => { + localStorage.setItem(STORAGE_KEY, '{not json'); + const { result } = renderHook(() => usePreferences()); + expect(result.current).toEqual(DEFAULT_PREFERENCES); + }); + + it('should not hand out the shared defaults object itself', () => { + // A consumer mutating what it reads would silently rewrite the defaults + // for every other consumer. + const { result } = renderHook(() => usePreferences()); + expect(result.current).not.toBe(DEFAULT_PREFERENCES); + }); + }); + + describe('writePreferences', () => { + it('should persist a patch merged over what is already stored', () => { + writePreferences({ compactDensity: true }); + writePreferences({ defaultPageSize: 100 }); + expect(JSON.parse(localStorage.getItem(STORAGE_KEY))).toEqual({ + compactDensity: true, + defaultPageSize: 100 + }); + }); + + it('should return the merged result to the caller', () => { + expect(writePreferences({ defaultPageSize: 50 })).toEqual({ + compactDensity: false, + defaultPageSize: 50 + }); + }); + + it('should broadcast the change so the triggering component re-renders too', () => { + const listener = jest.fn(); + window.addEventListener('doormile:preferences', listener); + writePreferences({ compactDensity: true }); + window.removeEventListener('doormile:preferences', listener); + + expect(listener).toHaveBeenCalledTimes(1); + expect(listener.mock.calls[0][0].detail).toEqual({ compactDensity: true, defaultPageSize: 25 }); + }); + + it('should still broadcast when the write itself is refused', () => { + // Private mode / quota: the change applies for this session either way, + // so the UI must not be left showing the old value. + const setItem = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { + throw new Error('QuotaExceededError'); + }); + const listener = jest.fn(); + window.addEventListener('doormile:preferences', listener); + + expect(() => writePreferences({ compactDensity: true })).not.toThrow(); + expect(listener).toHaveBeenCalledTimes(1); + + window.removeEventListener('doormile:preferences', listener); + setItem.mockRestore(); + }); + + it('should accept an empty patch as a no-op that still returns the current values', () => { + expect(writePreferences({})).toEqual(DEFAULT_PREFERENCES); + }); + }); + + describe('usePreferences', () => { + it('should re-render with the new value when a preference changes', () => { + const { result } = renderHook(() => usePreferences()); + expect(result.current.compactDensity).toBe(false); + + act(() => { + writePreferences({ compactDensity: true }); + }); + + expect(result.current.compactDensity).toBe(true); + }); + + it('should pick up a change made in another tab', () => { + const { result } = renderHook(() => usePreferences()); + + act(() => { + localStorage.setItem(STORAGE_KEY, JSON.stringify({ defaultPageSize: 100 })); + window.dispatchEvent(new Event('storage')); + }); + + expect(result.current.defaultPageSize).toBe(100); + }); + + it('should keep every mounted consumer in agreement', () => { + const a = renderHook(() => usePreferences()); + const b = renderHook(() => usePreferences()); + + act(() => { + writePreferences({ defaultPageSize: 50 }); + }); + + expect(a.result.current).toEqual(b.result.current); + expect(a.result.current.defaultPageSize).toBe(50); + }); + + it('should stop listening once unmounted', () => { + const removeSpy = jest.spyOn(window, 'removeEventListener'); + const { unmount } = renderHook(() => usePreferences()); + unmount(); + + expect(removeSpy).toHaveBeenCalledWith('doormile:preferences', expect.any(Function)); + expect(removeSpy).toHaveBeenCalledWith('storage', expect.any(Function)); + }); + }); + + describe('useUpdatePreferences', () => { + it('should write and broadcast through the same path as writePreferences', () => { + const update = renderHook(() => useUpdatePreferences()); + const read = renderHook(() => usePreferences()); + + act(() => { + update.result.current({ compactDensity: true }); + }); + + expect(read.result.current.compactDensity).toBe(true); + expect(JSON.parse(localStorage.getItem(STORAGE_KEY)).compactDensity).toBe(true); + }); + + it('should keep a stable identity across renders so consumers do not re-run effects', () => { + const { result, rerender } = renderHook(() => useUpdatePreferences()); + const first = result.current; + rerender(); + expect(result.current).toBe(first); + }); + }); +}); diff --git a/tests/lib/profitability.test.js b/tests/lib/profitability.test.js new file mode 100644 index 0000000..ee2c247 --- /dev/null +++ b/tests/lib/profitability.test.js @@ -0,0 +1,327 @@ +import { + BASE_REVENUE, + BASE_KM_LIMIT, + EXTRA_RATE_KM, + FIXED_COST_PER_SLOT, + VARIABLE_RATE_KM, + rowDay, + calcRiderMetrics, + groupRowsByRider +} from '@/lib/profitability'; + +/** + * The unit economics of a delivery day. Every figure the Profitability report + * shows comes from here, so the headline and the table underneath it cannot + * disagree. + * + * The behaviours worth protecting, in order of how much money they move: + * 1. Cancelled/skipped stops earn nothing AND cost nothing. + * 2. Slot count (which drives fixed cost) is derived from `orderdate`, never + * `assigntime` — the latter is re-stamped by any status change. + * 3. A bare date has no slot, because midnight would otherwise be assumed. + */ +const order = (over = {}) => ({ orderdate: '2026-08-27T07:00:00', riderkms: 5, orderstatus: 'delivered', ...over }); + +describe('profitability', () => { + describe('the published rate card', () => { + it('should charge a 30 rupee base for the first 8 km, then 6/km', () => { + expect(BASE_REVENUE).toBe(30); + expect(BASE_KM_LIMIT).toBe(8); + expect(EXTRA_RATE_KM).toBe(6); + }); + + it('should slice a 500 rupee salary across three slots', () => { + expect(FIXED_COST_PER_SLOT).toBeCloseTo(500 / 3, 10); + expect(VARIABLE_RATE_KM).toBe(2.5); + }); + }); + + describe('rowDay', () => { + it('should return the creation day of a booking', () => { + expect(rowDay({ orderdate: '2026-08-27T07:00:00' })).toBe('2026-08-27'); + }); + + it('should strip a false trailing Z so a late order stays on its own day', () => { + expect(rowDay({ orderdate: '2026-08-27T23:30:00Z' })).toBe('2026-08-27'); + }); + + it.each([{}, { orderdate: null }, { orderdate: '' }, { orderdate: 'garbage' }])( + 'should return null for the unusable row %p', + (row) => { + expect(rowDay(row)).toBeNull(); + } + ); + + it('should not throw on a null row', () => { + expect(rowDay(null)).toBeNull(); + }); + }); + + describe('calcRiderMetrics — revenue', () => { + it('should charge only the base fare for a stop inside the base km limit', () => { + const metrics = calcRiderMetrics({ orders: [order({ riderkms: 5 })] }, '2026-08-27'); + expect(metrics.revenue).toBe(30); + }); + + it('should charge only the base fare at exactly the base km limit', () => { + // Boundary: 8 km is inclusive of the base fare, 8.01 is not. + expect(calcRiderMetrics({ orders: [order({ riderkms: 8 })] }, '2026-08-27').revenue).toBe(30); + }); + + it('should add the per-km rate beyond the base limit', () => { + // 12 km => 30 + (12 - 8) * 6 = 54 + expect(calcRiderMetrics({ orders: [order({ riderkms: 12 })] }, '2026-08-27').revenue).toBe(54); + }); + + it('should accept riderkms as a numeric string, as the API returns it', () => { + expect(calcRiderMetrics({ orders: [order({ riderkms: '12' })] }, '2026-08-27').revenue).toBe(54); + }); + + it.each([null, undefined, '', 'abc', NaN])( + 'should treat the unusable distance %p as zero km and still charge the base fare', + (riderkms) => { + const metrics = calcRiderMetrics({ orders: [order({ riderkms })] }, '2026-08-27'); + expect(metrics.revenue).toBe(30); + expect(metrics.kms).toBe(0); + } + ); + + it('should sum revenue across every billable stop', () => { + const metrics = calcRiderMetrics( + { orders: [order({ riderkms: 5 }), order({ riderkms: 12 })] }, + '2026-08-27' + ); + expect(metrics.revenue).toBe(84); + expect(metrics.kms).toBe(17); + }); + }); + + describe('calcRiderMetrics — exclusions', () => { + it.each(['cancelled', 'canceled', 'skipped', 'CANCELLED', 'Skipped'])( + 'should exclude a %s stop from revenue, km and slot cost', + (orderstatus) => { + const metrics = calcRiderMetrics( + { orders: [order({ orderstatus, riderkms: 20 })] }, + '2026-08-27' + ); + expect(metrics.revenue).toBe(0); + expect(metrics.kms).toBe(0); + expect(metrics.slotCount).toBe(0); + expect(metrics.totalCost).toBe(0); + } + ); + + it('should still list an excluded stop in `orders` for the breakdown table', () => { + // The table deliberately shows cancelled stops for context; only the money + // excludes them. + const metrics = calcRiderMetrics( + { orders: [order({ orderstatus: 'cancelled' }), order()] }, + '2026-08-27' + ); + expect(metrics.orders).toHaveLength(2); + expect(metrics.billable).toHaveLength(1); + }); + + it('should read `status` when `orderstatus` is absent', () => { + const metrics = calcRiderMetrics( + { orders: [{ orderdate: '2026-08-27T07:00:00', riderkms: 5, status: 'cancelled' }] }, + '2026-08-27' + ); + expect(metrics.revenue).toBe(0); + }); + + it('should treat a stop with no status at all as billable', () => { + const metrics = calcRiderMetrics( + { orders: [{ orderdate: '2026-08-27T07:00:00', riderkms: 5 }] }, + '2026-08-27' + ); + expect(metrics.revenue).toBe(30); + }); + }); + + describe('calcRiderMetrics — slots and fixed cost', () => { + it('should charge one slot when every stop falls in the same wave', () => { + const metrics = calcRiderMetrics( + { orders: [order({ orderdate: '2026-08-27T07:00:00' }), order({ orderdate: '2026-08-27T08:30:00' })] }, + '2026-08-27' + ); + expect(metrics.slotCount).toBe(1); + expect(metrics.fixedCost).toBeCloseTo(FIXED_COST_PER_SLOT, 6); + }); + + it('should charge one slot per distinct wave worked', () => { + const metrics = calcRiderMetrics( + { + orders: [ + order({ orderdate: '2026-08-27T07:00:00' }), + order({ orderdate: '2026-08-27T11:00:00' }), + order({ orderdate: '2026-08-27T18:00:00' }) + ] + }, + '2026-08-27' + ); + expect(metrics.slotCount).toBe(3); + expect(metrics.fixedCost).toBeCloseTo(500, 6); + }); + + it('should never charge more than three slots for a single day', () => { + const orders = Array.from({ length: 12 }, (_, i) => + order({ orderdate: `2026-08-27T${String(i * 2).padStart(2, '0')}:00:00` }) + ); + expect(calcRiderMetrics({ orders }, '2026-08-27').slotCount).toBe(3); + }); + + it('should charge no slot for a bare date, which has no time component', () => { + // Otherwise every date-only row would silently bill a Morning slot. + const metrics = calcRiderMetrics({ orders: [order({ orderdate: '2026-08-27' })] }, '2026-08-27'); + expect(metrics.slotCount).toBe(0); + expect(metrics.fixedCost).toBe(0); + expect(metrics.revenue).toBe(30); + }); + + it('should charge no slot for an unparseable orderdate', () => { + const metrics = calcRiderMetrics({ orders: [order({ orderdate: 'garbage' })] }, undefined); + expect(metrics.slotCount).toBe(0); + }); + + it('should ignore assigntime, which is re-stamped by any status change', () => { + // Reading assigntime made a rider's slot count — and therefore their + // fixed cost — drift through the day. + const metrics = calcRiderMetrics( + { orders: [order({ orderdate: '2026-08-27T07:00:00', assigntime: '2026-08-27T19:00:00' })] }, + '2026-08-27' + ); + expect(metrics.slotCount).toBe(1); + expect(metrics.fixedCost).toBeCloseTo(FIXED_COST_PER_SLOT, 6); + }); + + it('should charge slots per day when no date filter is applied', () => { + const metrics = calcRiderMetrics({ + orders: [order({ orderdate: '2026-08-26T07:00:00' }), order({ orderdate: '2026-08-27T07:00:00' })] + }); + expect(metrics.slotCount).toBe(2); + }); + }); + + describe('calcRiderMetrics — cost, net and margin', () => { + it('should compute the full P&L for a two-wave day', () => { + const metrics = calcRiderMetrics( + { + orders: [ + order({ orderdate: '2026-08-27T07:00:00', riderkms: 5 }), + order({ orderdate: '2026-08-27T10:00:00', riderkms: 10 }) + ] + }, + '2026-08-27' + ); + + expect(metrics.revenue).toBe(72); // 30 + (30 + 2*6) + expect(metrics.kms).toBe(15); + expect(metrics.slotCount).toBe(2); + expect(metrics.varCost).toBeCloseTo(37.5, 6); // 15 * 2.5 + expect(metrics.fixedCost).toBeCloseTo((500 / 3) * 2, 6); + expect(metrics.totalCost).toBeCloseTo(37.5 + (500 / 3) * 2, 6); + expect(metrics.net).toBeCloseTo(72 - (37.5 + (500 / 3) * 2), 6); + expect(metrics.margin).toBeCloseTo((metrics.net / 72) * 100, 6); + }); + + it('should report a zero margin — not NaN or Infinity — when there is no revenue', () => { + const metrics = calcRiderMetrics({ orders: [] }, '2026-08-27'); + expect(metrics.revenue).toBe(0); + expect(metrics.margin).toBe(0); + expect(Number.isFinite(metrics.margin)).toBe(true); + }); + + it('should report a negative net when cost exceeds revenue', () => { + const metrics = calcRiderMetrics({ orders: [order({ riderkms: 1 })] }, '2026-08-27'); + expect(metrics.net).toBeLessThan(0); + expect(metrics.margin).toBeLessThan(0); + }); + + it('should keep net equal to revenue minus total cost', () => { + const metrics = calcRiderMetrics( + { orders: [order({ riderkms: 30 }), order({ orderdate: '2026-08-27T17:00:00', riderkms: 4 })] }, + '2026-08-27' + ); + expect(metrics.net).toBeCloseTo(metrics.revenue - metrics.totalCost, 9); + expect(metrics.totalCost).toBeCloseTo(metrics.varCost + metrics.fixedCost, 9); + }); + }); + + describe('calcRiderMetrics — date filtering and empty input', () => { + it('should count only the selected day when a date is given', () => { + const metrics = calcRiderMetrics( + { orders: [order({ orderdate: '2026-08-26T07:00:00' }), order({ orderdate: '2026-08-27T07:00:00' })] }, + '2026-08-27' + ); + expect(metrics.orders).toHaveLength(1); + expect(metrics.revenue).toBe(30); + }); + + it('should count every day when no date is given', () => { + const metrics = calcRiderMetrics({ + orders: [order({ orderdate: '2026-08-26T07:00:00' }), order({ orderdate: '2026-08-27T07:00:00' })] + }); + expect(metrics.orders).toHaveLength(2); + }); + + it('should return an all-zero result for a rider with no orders array', () => { + const metrics = calcRiderMetrics({}, '2026-08-27'); + expect(metrics).toMatchObject({ revenue: 0, kms: 0, slotCount: 0, totalCost: 0, net: 0, margin: 0 }); + expect(metrics.orders).toEqual([]); + }); + + it('should exclude a stop whose day cannot be determined when a date is selected', () => { + const metrics = calcRiderMetrics({ orders: [order({ orderdate: 'garbage' })] }, '2026-08-27'); + expect(metrics.orders).toHaveLength(0); + expect(metrics.revenue).toBe(0); + }); + }); + + describe('groupRowsByRider', () => { + const row = (over = {}) => ({ userid: 7, ridername: 'Anita', kms: 5, ...over }); + + it('should group every row belonging to the same rider', () => { + const riders = groupRowsByRider([row(), row({ kms: 9 })]); + expect(riders).toHaveLength(1); + expect(riders[0]).toMatchObject({ id: '7', riderName: 'Anita' }); + expect(riders[0].orders).toHaveLength(2); + }); + + it('should map the row `kms` onto `riderkms`, which the money math reads', () => { + // A booking carries no road distance, so the straight-line proxy feeds the + // rate card. If this mapping breaks, every rider silently earns base fare. + const [rider] = groupRowsByRider([row({ kms: 12 })]); + expect(rider.orders[0].riderkms).toBe(12); + expect(calcRiderMetrics(rider).revenue).toBe(54); + }); + + it('should treat numeric and string user ids as the same rider', () => { + expect(groupRowsByRider([row({ userid: 7 }), row({ userid: '7' })])).toHaveLength(1); + }); + + it('should drop rows with no userid, which cannot be attributed to anyone', () => { + expect(groupRowsByRider([row({ userid: null }), row({ userid: undefined }), row()])).toHaveLength(1); + }); + + it('should fall back to a numbered placeholder when the rider has no name', () => { + const [rider] = groupRowsByRider([row({ ridername: null })]); + expect(rider.riderName).toBe('Rider #7'); + }); + + it('should keep the first-seen name for a rider whose later rows are unnamed', () => { + const [rider] = groupRowsByRider([row({ ridername: 'Anita' }), row({ ridername: null })]); + expect(rider.riderName).toBe('Anita'); + }); + + it.each([[[]], [null], [undefined]])('should return an empty list for %p', (rows) => { + expect(groupRowsByRider(rows)).toEqual([]); + }); + + it('should not mutate the caller rows', () => { + const source = row(); + groupRowsByRider([source]); + expect(source).not.toHaveProperty('riderkms'); + }); + }); +}); diff --git a/tests/setup/babel-plugin-import-meta.cjs b/tests/setup/babel-plugin-import-meta.cjs new file mode 100644 index 0000000..08f95f9 --- /dev/null +++ b/tests/setup/babel-plugin-import-meta.cjs @@ -0,0 +1,28 @@ +/** + * Rewrites `import.meta` so Babel can compile Vite source to CommonJS for Jest. + * + * `import.meta` is only legal inside a real ES module; babel-jest compiles to + * CJS, so every file that reads `import.meta.env` (api/doormile/client.js, + * lib/distance.js) is a hard parse error without this. + * + * It resolves to `globalThis.__VITE_IMPORT_META__` — a plain object the test + * setup seeds and individual tests can override, which is the seam that lets a + * test exercise `VITE_DOORMILE_URL` / `VITE_OSRM_URL` overrides at all. + */ +module.exports = function importMetaPlugin({ types: t }) { + return { + name: 'transform-import-meta-for-jest', + visitor: { + MetaProperty(path) { + if (path.node.meta.name !== 'import' || path.node.property.name !== 'meta') return; + path.replaceWith( + t.logicalExpression( + '||', + t.memberExpression(t.identifier('globalThis'), t.identifier('__VITE_IMPORT_META__')), + t.objectExpression([t.objectProperty(t.identifier('env'), t.objectExpression([]))]) + ) + ); + }, + }, + }; +}; diff --git a/tests/setup/fileMock.cjs b/tests/setup/fileMock.cjs new file mode 100644 index 0000000..86059f3 --- /dev/null +++ b/tests/setup/fileMock.cjs @@ -0,0 +1 @@ +module.exports = 'test-file-stub'; diff --git a/tests/setup/jest.setup.cjs b/tests/setup/jest.setup.cjs new file mode 100644 index 0000000..0e0de46 --- /dev/null +++ b/tests/setup/jest.setup.cjs @@ -0,0 +1,18 @@ +require('@testing-library/jest-dom'); + +/** + * The `import.meta.env` stand-in (see babel-plugin-import-meta.cjs). + * + * Defaults to production-shaped values — no VITE_ overrides, DEV false — so a + * module that branches on an override only does so in the test that asks for + * it. Reset before every test so one test's override cannot leak into the next. + */ +const defaultImportMeta = () => ({ env: { DEV: false, PROD: true, MODE: 'test' } }); + +globalThis.__VITE_IMPORT_META__ = defaultImportMeta(); + +beforeEach(() => { + globalThis.__VITE_IMPORT_META__ = defaultImportMeta(); + localStorage.clear(); + jest.clearAllMocks(); +}); diff --git a/tests/setup/styleMock.cjs b/tests/setup/styleMock.cjs new file mode 100644 index 0000000..c688fa3 --- /dev/null +++ b/tests/setup/styleMock.cjs @@ -0,0 +1,2 @@ +// CSS/asset imports carry no behaviour worth asserting; Jest cannot parse them. +module.exports = {}; diff --git a/tests/utils/password.test.js b/tests/utils/password.test.js new file mode 100644 index 0000000..fac5a6d --- /dev/null +++ b/tests/utils/password.test.js @@ -0,0 +1,160 @@ +import { isNumber, isLowercaseChar, isUppercaseChar, isSpecialChar, minLength } from '@/utils/password-validation'; +import { strengthColor, strengthIndicator } from '@/utils/password-strength'; + +/** + * Password rules for the login and change-password forms. + * + * These gate what the console will accept as a credential, so the boundaries + * matter more than the happy path: a rule that is one character loose is a + * weaker password policy than the UI claims to enforce. + */ +describe('password-validation', () => { + describe('character-class rules', () => { + it.each([ + ['isNumber', isNumber, 'abc1', 'abcd'], + ['isLowercaseChar', isLowercaseChar, 'ABCd', 'ABCD'], + ['isUppercaseChar', isUppercaseChar, 'abcD', 'abcd'], + ['isSpecialChar', isSpecialChar, 'abc!', 'abcd'] + ])('%s should accept a qualifying value and reject one without it', (_name, rule, pass, fail) => { + expect(rule(pass)).toBe(true); + expect(rule(fail)).toBe(false); + }); + + it('should accept a digit anywhere in the value, not only at the end', () => { + expect(isNumber('1abc')).toBe(true); + expect(isNumber('ab1c')).toBe(true); + }); + + it.each(['-', '+', '_', '!', '@', '#', '$', '%', '^', '&', '*', '.', ',', '?'])( + 'should treat %s as a special character', + (char) => { + expect(isSpecialChar(`abc${char}`)).toBe(true); + } + ); + + it.each(['(', ')', '=', '/', '\\', '~', '|', '<', '>', '[', ']', '{', '}', ';', ':', "'", '"'])( + 'should NOT treat %s as a special character under the current rule set', + (char) => { + // Documents the accepted set precisely: a user typing a symbol outside + // it is told their password has no special character. + expect(isSpecialChar(`abc${char}`)).toBe(false); + } + ); + + it.each([isNumber, isLowercaseChar, isUppercaseChar, isSpecialChar])( + 'should reject an empty value', + (rule) => { + expect(rule('')).toBe(false); + } + ); + }); + + describe('minLength', () => { + it('should reject a value of exactly eight characters minus one', () => { + expect(minLength('1234567')).toBe(false); + }); + + it('should accept a value of eight characters', () => { + // The boundary the UI advertises as "at least 8 characters". + expect(minLength('12345678')).toBe(true); + }); + + it('should accept anything longer', () => { + expect(minLength('123456789')).toBe(true); + }); + + it('should reject an empty value', () => { + expect(minLength('')).toBe(false); + }); + }); +}); + +describe('password-strength', () => { + describe('strengthIndicator', () => { + it('should score an empty password at zero', () => { + expect(strengthIndicator('')).toBe(0); + }); + + it('should score a short lowercase-only password at zero', () => { + expect(strengthIndicator('abcde')).toBe(0); + }); + + it('should award a point once the password passes five characters', () => { + expect(strengthIndicator('abcdef')).toBe(1); + }); + + it('should award a second point once it passes seven characters', () => { + expect(strengthIndicator('abcdefgh')).toBe(2); + }); + + it('should award a point for a digit', () => { + expect(strengthIndicator('abcde1')).toBe(2); + }); + + it('should award a point for a special character', () => { + expect(strengthIndicator('abcde!')).toBe(2); + }); + + it('should award a point only when BOTH cases are present', () => { + expect(strengthIndicator('abcdeF')).toBe(2); + expect(strengthIndicator('ABCDEF')).toBe(1); + }); + + it('should award every point for a long mixed password', () => { + expect(strengthIndicator('Abcdef1!')).toBe(5); + }); + + it('should never exceed five, which is the maximum the rules can award', () => { + // Worth pinning: strengthColor branches on `count < 6`, which is only + // reachable if this invariant is ever broken. + const samples = ['', 'a', 'Abcdef1!', 'A1!aaaaaaaaaaaaaaaaaaaaaaaaaa', '!!!!!!!!!!AAAAaaaa1111']; + samples.forEach((password) => { + expect(strengthIndicator(password)).toBeLessThanOrEqual(5); + expect(strengthIndicator(password)).toBeGreaterThanOrEqual(0); + }); + }); + }); + + describe('strengthColor', () => { + it.each([ + [0, 'Poor'], + [1, 'Poor'], + [2, 'Weak'], + [3, 'Normal'], + [4, 'Good'], + [5, 'Strong'] + ])('should label a score of %d as %s', (count, label) => { + expect(strengthColor(count).label).toBe(label); + }); + + it('should return a colour token alongside every label', () => { + [0, 1, 2, 3, 4, 5].forEach((count) => { + expect(strengthColor(count).color).toEqual(expect.any(String)); + expect(strengthColor(count).color).not.toBe(''); + }); + }); + + it('should escalate the label monotonically as the score rises', () => { + const ranked = ['Poor', 'Weak', 'Normal', 'Good', 'Strong']; + const labels = [0, 1, 2, 3, 4, 5].map((count) => strengthColor(count).label); + const indices = labels.map((label) => ranked.indexOf(label)); + indices.slice(1).forEach((index, i) => expect(index).toBeGreaterThanOrEqual(indices[i])); + }); + }); + + describe('the two modules together', () => { + it('should rate a password that satisfies every validation rule as Strong', () => { + const password = 'Abcdef1!'; + expect(minLength(password)).toBe(true); + expect(isNumber(password)).toBe(true); + expect(isLowercaseChar(password)).toBe(true); + expect(isUppercaseChar(password)).toBe(true); + expect(isSpecialChar(password)).toBe(true); + expect(strengthColor(strengthIndicator(password)).label).toBe('Strong'); + }); + + it('should rate a password that satisfies none of them as Poor', () => { + expect(strengthColor(strengthIndicator('abc')).label).toBe('Poor'); + }); + }); +}); diff --git a/tests/utils/session.test.js b/tests/utils/session.test.js new file mode 100644 index 0000000..1131180 --- /dev/null +++ b/tests/utils/session.test.js @@ -0,0 +1,171 @@ +import { + ACTIVITY_STORAGE_KEY, + SESSION_START_STORAGE_KEY, + AUTH_PRESENCE_KEY, + INACTIVITY_TIMEOUT_MS, + ABSOLUTE_SESSION_TIMEOUT_MS, + markActivity, + markSessionStart, + isSessionActive, + performSessionLogout +} from '@/utils/session'; + +/** + * The shared logout contract. Every logout path — the manual button and the + * inactivity timer — goes through here so they cannot clear different things. + * + * The security-relevant property is that local state is wiped and the page + * HARD-navigates even when a caller-supplied teardown throws: a half-completed + * logout that leaves an authenticated page rendered is the failure mode this + * module exists to prevent. + */ +describe('session', () => { + let replace; + + beforeEach(() => { + replace = jest.fn(); + Object.defineProperty(window, 'location', { + writable: true, + configurable: true, + value: { href: 'http://console.local/deliveries', replace } + }); + }); + + describe('timeout policy', () => { + it('should expire an idle session after one hour', () => { + expect(INACTIVITY_TIMEOUT_MS).toBe(60 * 60 * 1000); + }); + + it('should cap a session at one hour since login regardless of activity', () => { + expect(ABSOLUTE_SESSION_TIMEOUT_MS).toBe(60 * 60 * 1000); + }); + }); + + describe('markActivity', () => { + it('should record the current time as a millisecond string', () => { + jest.useFakeTimers().setSystemTime(new Date('2026-08-27T10:00:00Z')); + markActivity(); + expect(localStorage.getItem(ACTIVITY_STORAGE_KEY)).toBe(String(Date.now())); + jest.useRealTimers(); + }); + + it('should overwrite the previous activity stamp', () => { + jest.useFakeTimers().setSystemTime(new Date('2026-08-27T10:00:00Z')); + markActivity(); + const first = localStorage.getItem(ACTIVITY_STORAGE_KEY); + + jest.setSystemTime(new Date('2026-08-27T10:05:00Z')); + markActivity(); + + expect(localStorage.getItem(ACTIVITY_STORAGE_KEY)).not.toBe(first); + expect(Number(localStorage.getItem(ACTIVITY_STORAGE_KEY))).toBeGreaterThan(Number(first)); + jest.useRealTimers(); + }); + }); + + describe('markSessionStart', () => { + it('should start the absolute-lifetime clock separately from the activity clock', () => { + jest.useFakeTimers().setSystemTime(new Date('2026-08-27T10:00:00Z')); + markSessionStart(); + expect(localStorage.getItem(SESSION_START_STORAGE_KEY)).toBe(String(Date.now())); + expect(localStorage.getItem(ACTIVITY_STORAGE_KEY)).toBeNull(); + jest.useRealTimers(); + }); + }); + + describe('isSessionActive', () => { + it('should report an active session when the auth presence key is set', () => { + localStorage.setItem(AUTH_PRESENCE_KEY, 'ops@doormile.com'); + expect(isSessionActive()).toBe(true); + }); + + it('should report no session when the key is absent', () => { + expect(isSessionActive()).toBe(false); + }); + + it('should report no session for an empty presence value', () => { + localStorage.setItem(AUTH_PRESENCE_KEY, ''); + expect(isSessionActive()).toBe(false); + }); + + it('should return a boolean, not the stored string', () => { + localStorage.setItem(AUTH_PRESENCE_KEY, 'ops@doormile.com'); + expect(typeof isSessionActive()).toBe('boolean'); + }); + }); + + describe('performSessionLogout', () => { + it('should wipe all local state and hard-navigate to login', () => { + localStorage.setItem('doormileToken', 'jwt-abc'); + localStorage.setItem(AUTH_PRESENCE_KEY, 'ops@doormile.com'); + localStorage.setItem('anything-else', 'x'); + + performSessionLogout(); + + expect(localStorage.length).toBe(0); + expect(replace).toHaveBeenCalledWith('/login'); + }); + + it('should clear the react-query cache so no fetched data survives', () => { + const queryClient = { clear: jest.fn() }; + performSessionLogout({ queryClient }); + expect(queryClient.clear).toHaveBeenCalledTimes(1); + }); + + it('should dispatch the supplied teardown actions', () => { + const dispatch = jest.fn(); + const clearFcmToken = jest.fn(() => ({ type: 'fcm/clear' })); + const logoutUser = jest.fn(() => ({ type: 'auth/logout' })); + + performSessionLogout({ dispatch, clearFcmToken, logoutUser }); + + expect(dispatch).toHaveBeenCalledWith({ type: 'fcm/clear' }); + expect(dispatch).toHaveBeenCalledWith({ type: 'auth/logout' }); + }); + + it('should skip the redux teardown when no dispatch is supplied', () => { + const clearFcmToken = jest.fn(); + expect(() => performSessionLogout({ clearFcmToken })).not.toThrow(); + expect(clearFcmToken).not.toHaveBeenCalled(); + }); + + it('should work with no arguments at all', () => { + expect(() => performSessionLogout()).not.toThrow(); + expect(replace).toHaveBeenCalledWith('/login'); + }); + + it('should still wipe storage and redirect when the cache teardown throws', () => { + // The security-critical guarantee: a broken teardown must not leave an + // authenticated page rendered over live local state. + localStorage.setItem('doormileToken', 'jwt-abc'); + const queryClient = { + clear: jest.fn(() => { + throw new Error('cache blew up'); + }) + }; + + expect(() => performSessionLogout({ queryClient })).toThrow('cache blew up'); + expect(localStorage.length).toBe(0); + expect(replace).toHaveBeenCalledWith('/login'); + }); + + it('should still wipe storage and redirect when a dispatched action throws', () => { + localStorage.setItem('doormileToken', 'jwt-abc'); + const dispatch = jest.fn(() => { + throw new Error('reducer blew up'); + }); + + expect(() => + performSessionLogout({ dispatch, logoutUser: () => ({ type: 'auth/logout' }) }) + ).toThrow('reducer blew up'); + expect(localStorage.length).toBe(0); + expect(replace).toHaveBeenCalledWith('/login'); + }); + + it('should navigate rather than route, so the back button cannot resurrect the console', () => { + performSessionLogout(); + expect(replace).toHaveBeenCalledTimes(1); + expect(replace).toHaveBeenCalledWith('/login'); + }); + }); +});