diff --git a/.gitignore b/.gitignore index 3ff476c..386e636 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ stats.html .env.development.local .env.test.local .env.production.local + +coverage/ diff --git a/index.html b/index.html index e89427e..26bfe58 100644 --- a/index.html +++ b/index.html @@ -10,43 +10,6 @@ - - diff --git a/package-lock.json b/package-lock.json index 192be62..15262fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "@types/react-image-gallery": "^1.2.4", "@types/turndown": "^5.0.5", "@vitejs/plugin-react": "^4.3.4", + "@vitest/coverage-v8": "^3.1.4", "@vitest/ui": "^3.1.4", "eslint": "^9.23.0", "eslint-plugin-import": "^2.31.0", @@ -57,9 +58,9 @@ "jsdom": "^26.1.0", "replace-in-file": "^8.3.0", "rollup-plugin-visualizer": "^5.14.0", - "typescript": "~5.6.3", + "typescript": "^5.8.3", "typescript-eslint": "^8.27.0", - "vite": "6.3.4", + "vite": "^6.3.5", "vitest": "^3.1.4" } }, @@ -120,32 +121,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", - "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", + "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.7.tgz", - "integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", + "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", "dev": true, - "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.5", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.7", - "@babel/parser": "^7.26.7", - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.26.7", - "@babel/types": "^7.26.7", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helpers": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -161,13 +160,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", - "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", - "license": "MIT", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", + "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", "dependencies": { - "@babel/parser": "^7.26.5", - "@babel/types": "^7.26.5", + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -177,14 +175,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", - "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -194,28 +191,26 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", - "license": "MIT", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", + "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -253,11 +248,10 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -347,16 +341,15 @@ } }, "node_modules/@babel/traverse": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz", - "integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==", - "license": "MIT", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", + "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.5", - "@babel/parser": "^7.26.7", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.7", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -386,6 +379,15 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", @@ -1132,11 +1134,10 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, - "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" }, @@ -1174,11 +1175,10 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", @@ -1189,21 +1189,19 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.0.tgz", - "integrity": "sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", + "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", - "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -1249,13 +1247,15 @@ } }, "node_modules/@eslint/js": { - "version": "9.23.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.23.0.tgz", - "integrity": "sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==", + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", + "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -1263,19 +1263,17 @@ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", - "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", + "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.12.0", + "@eslint/core": "^0.14.0", "levn": "^0.4.1" }, "engines": { @@ -1451,6 +1449,15 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", @@ -1516,10 +1523,9 @@ } }, "node_modules/@mui/icons-material": { - "version": "6.4.8", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.8.tgz", - "integrity": "sha512-LKGWiLWRyoOw3dWxZQ+lV//mK+4DVTTAiLd2ljmJdD6XV0rDB8JFKjRD9nyn9cJAU5XgWnii7ZR3c93ttUnMKg==", - "license": "MIT", + "version": "6.4.11", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.11.tgz", + "integrity": "sha512-+jjJGIrB1awNbMv4ZVPPdN/p7O1UKFZ+xqRvNIQ8B1KnlID5hPMPBLM6UUbRF4bu3UDCbu79rn9Nye5LGNzmeA==", "dependencies": { "@babel/runtime": "^7.26.0" }, @@ -1531,7 +1537,7 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@mui/material": "^6.4.8", + "@mui/material": "^6.4.11", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -1752,7 +1758,6 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, - "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -1766,7 +1771,6 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, - "license": "MIT", "engines": { "node": ">= 8" } @@ -1776,7 +1780,6 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, - "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -2341,8 +2344,7 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@types/json5": { "version": "0.0.29", @@ -2352,21 +2354,19 @@ "license": "MIT" }, "node_modules/@types/leaflet": { - "version": "1.9.16", - "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.16.tgz", - "integrity": "sha512-wzZoyySUxkgMZ0ihJ7IaUIblG8Rdc8AbbZKLneyn+QjYsj5q1QU7TEKYqwTr10BGSzY5LI7tJk9Ifo+mEjdFRw==", + "version": "1.9.18", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.18.tgz", + "integrity": "sha512-ht2vsoPjezor5Pmzi5hdsA7F++v5UGq9OlUduWHmMZiuQGIpJ2WS5+Gg9HaAA79gNh1AIPtCqhzejcIZ3lPzXQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/geojson": "*" } }, "node_modules/@types/node": { - "version": "22.15.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.14.tgz", - "integrity": "sha512-BL1eyu/XWsFGTtDWOYULQEs4KR0qdtYfCxYAUYRoB7JP7h9ETYLgQTww6kH8Sj2C0pFGgrpM0XKv6/kbIzYJ1g==", + "version": "22.15.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", + "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", "dev": true, - "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } @@ -2384,20 +2384,18 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.0.12", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.12.tgz", - "integrity": "sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==", - "license": "MIT", + "version": "19.1.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.5.tgz", + "integrity": "sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==", "dependencies": { "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "19.0.4", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.4.tgz", - "integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==", + "version": "19.1.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.5.tgz", + "integrity": "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==", "dev": true, - "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" } @@ -2429,21 +2427,20 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.27.0.tgz", - "integrity": "sha512-4henw4zkePi5p252c8ncBLzLce52SEUz2Ebj8faDnuUXz2UuHEONYcJ+G0oaCF+bYCWVZtrGzq3FD7YXetmnSA==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", + "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.27.0", - "@typescript-eslint/type-utils": "8.27.0", - "@typescript-eslint/utils": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/type-utils": "8.32.1", + "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2458,17 +2455,25 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/@typescript-eslint/parser": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.27.0.tgz", - "integrity": "sha512-XGwIabPallYipmcOk45DpsBSgLC64A0yvdAkrwEzwZ2viqGqRUJ8eEYoPz0CWnutgAFbNMPdsGGvzjSmcWVlEA==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", + "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.27.0", - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/typescript-estree": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4" }, "engines": { @@ -2484,14 +2489,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.27.0.tgz", - "integrity": "sha512-8oI9GwPMQmBryaaxG1tOZdxXVeMDte6NyJA4i7/TWa4fBwgnAXYlIQP+uYOeqAaLJ2JRxlG9CAyL+C+YE9Xknw==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", + "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0" + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2502,16 +2506,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.27.0.tgz", - "integrity": "sha512-wVArTVcz1oJOIEJxui/nRhV0TXzD/zMSOYi/ggCfNq78EIszddXcJb7r4RCp/oBrjt8n9A0BSxRMKxHftpDxDA==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", + "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.27.0", - "@typescript-eslint/utils": "8.27.0", + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/utils": "8.32.1", "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2526,11 +2529,10 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.27.0.tgz", - "integrity": "sha512-/6cp9yL72yUHAYq9g6DsAU+vVfvQmd1a8KyA81uvfDE21O2DwQ/qxlM4AR8TSdAu+kJLBDrEHKC5/W2/nxsY0A==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", + "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -2540,20 +2542,19 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.27.0.tgz", - "integrity": "sha512-BnKq8cqPVoMw71O38a1tEb6iebEgGA80icSxW7g+kndx0o6ot6696HjG7NdgfuAVmVEtwXUr3L8R9ZuVjoQL6A==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", + "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2571,7 +2572,6 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -2581,7 +2581,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -2593,11 +2592,10 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -2606,16 +2604,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.27.0.tgz", - "integrity": "sha512-njkodcwH1yvmo31YWgRHNb/x1Xhhq4/m81PhtvmRngD8iHPehxffz1SNCO+kwaePhATC+kOa/ggmvPoPza5i0Q==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", + "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", "dev": true, - "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.27.0", - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/typescript-estree": "8.27.0" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2630,13 +2627,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.27.0.tgz", - "integrity": "sha512-WsXQwMkILJvffP6z4U3FYJPlbf/j07HIxmDjZpbNvBJkMfvwXj5ACRkkHwBDvLBbDbtX5TdU64/rcvKJ/vuInQ==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", + "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", + "@typescript-eslint/types": "8.32.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2648,17 +2644,16 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", - "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", + "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/core": "^7.26.0", + "@babel/core": "^7.26.10", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", - "react-refresh": "^0.14.2" + "react-refresh": "^0.17.0" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -2667,6 +2662,38 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.4.tgz", + "integrity": "sha512-G4p6OtioySL+hPV7Y6JHlhpsODbJzt1ndwHAFkyk6vVjpK03PFsKnauZIzcd0PrK4zAbc5lc+jeZ+eNGiMA+iw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "debug": "^4.4.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.1.4", + "vitest": "3.1.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.4.tgz", @@ -3095,10 +3122,9 @@ } }, "node_modules/axios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", - "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", - "license": "MIT", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -3143,7 +3169,6 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, - "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -3152,9 +3177,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", "dev": true, "funding": [ { @@ -3170,12 +3195,11 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -3214,11 +3238,10 @@ } }, "node_modules/call-bind-apply-helpers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", - "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -3228,14 +3251,13 @@ } }, "node_modules/call-bound": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", - "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "get-intrinsic": "^1.2.6" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -3254,9 +3276,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001695", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz", - "integrity": "sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==", + "version": "1.0.30001718", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", + "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", "dev": true, "funding": [ { @@ -3271,8 +3293,7 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/chai": { "version": "5.2.0", @@ -3698,11 +3719,10 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.88", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.88.tgz", - "integrity": "sha512-K3C2qf1o+bGzbilTDCTBhTQcMS9KW60yTAaTeeXsfvQuTDDwlokLam/AdqlqcSy9u4UainDgsHV23ksXAOgamw==", - "dev": true, - "license": "ISC" + "version": "1.5.155", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", + "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", + "dev": true }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -3975,20 +3995,19 @@ } }, "node_modules/eslint": { - "version": "9.23.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.23.0.tgz", - "integrity": "sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==", + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", + "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.2", - "@eslint/config-helpers": "^0.2.0", - "@eslint/core": "^0.12.0", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.23.0", - "@eslint/plugin-kit": "^0.2.7", + "@eslint/js": "9.27.0", + "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -4130,11 +4149,10 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.37.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", - "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==", + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, - "license": "MIT", "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -4146,7 +4164,7 @@ "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.8", + "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", @@ -4176,11 +4194,10 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.19.tgz", - "integrity": "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==", + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", "dev": true, - "license": "MIT", "peerDependencies": { "eslint": ">=8.40" } @@ -4329,7 +4346,6 @@ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, - "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -4346,7 +4362,6 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -4373,7 +4388,6 @@ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, - "license": "ISC", "dependencies": { "reusify": "^1.0.4" } @@ -4409,7 +4423,6 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, - "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -4610,18 +4623,17 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", - "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", + "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "get-proto": "^1.0.0", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", @@ -4773,8 +4785,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/has-bigints": { "version": "1.1.0", @@ -4891,6 +4902,12 @@ "node": ">=18" } }, + "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 + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -5247,7 +5264,6 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -5448,6 +5464,56 @@ "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, + "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, + "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": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "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", @@ -5720,7 +5786,6 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, - "license": "ISC", "dependencies": { "yallist": "^3.0.2" } @@ -5746,6 +5811,44 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5761,7 +5864,6 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 8" } @@ -5771,7 +5873,6 @@ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, - "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -5890,8 +5991,7 @@ "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/nwsapi": { "version": "2.2.20", @@ -5954,15 +6054,15 @@ } }, "node_modules/object.entries": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", - "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" + "es-object-atoms": "^1.1.1" }, "engines": { "node": ">= 0.4" @@ -6250,7 +6350,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.6" }, @@ -6396,28 +6495,25 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "MIT" + ] }, "node_modules/react": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", - "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", - "license": "MIT", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", - "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", - "license": "MIT", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "dependencies": { - "scheduler": "^0.25.0" + "scheduler": "^0.26.0" }, "peerDependencies": { - "react": "^19.0.0" + "react": "^19.1.0" } }, "node_modules/react-ga4": { @@ -6465,11 +6561,10 @@ } }, "node_modules/react-refresh": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -6695,7 +6790,6 @@ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, - "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -6821,7 +6915,6 @@ "url": "https://feross.org/support" } ], - "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -6908,10 +7001,9 @@ } }, "node_modules/scheduler": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", - "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", - "license": "MIT" + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==" }, "node_modules/semver": { "version": "6.3.1", @@ -7390,6 +7482,44 @@ "dev": true, "license": "MIT" }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -7450,10 +7580,9 @@ } }, "node_modules/tinymce": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-7.6.1.tgz", - "integrity": "sha512-5cHhaAoyyTHfAVTInNfoSp0KkUHmeVUbGSu37QKQbOFIPqxYPhqBiaLm1WVLgoNBYOHRProVc3xzxnNTeWHyoQ==", - "license": "GPL-2.0-or-later" + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-7.9.0.tgz", + "integrity": "sha512-tTrUmUGWqy1BY1WwDYh4WiuHm23LiRTcE1Xq3WLO8HKFzde/d0bTF/hXWOa97zqGh0ndJHx/nysQaNC9Gcd16g==" }, "node_modules/tinypool": { "version": "1.0.2", @@ -7510,7 +7639,6 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, - "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -7700,11 +7828,10 @@ } }, "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7714,15 +7841,14 @@ } }, "node_modules/typescript-eslint": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.27.0.tgz", - "integrity": "sha512-ZZ/8+Y0rRUMuW1gJaPtLWe4ryHbsPLzzibk5Sq+IFa2aOH1Vo0gPr1fbA6pOnzBke7zC2Da4w8AyCgxKXo3lqA==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", + "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.27.0", - "@typescript-eslint/parser": "8.27.0", - "@typescript-eslint/utils": "8.27.0" + "@typescript-eslint/eslint-plugin": "8.32.1", + "@typescript-eslint/parser": "8.32.1", + "@typescript-eslint/utils": "8.32.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7763,9 +7889,9 @@ "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", - "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { @@ -7781,7 +7907,6 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" @@ -7823,11 +7948,10 @@ } }, "node_modules/vite": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz", - "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, - "license": "MIT", "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -8300,8 +8424,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/yaml": { "version": "2.7.0", diff --git a/package.json b/package.json index 2f2dd97..45c4e7c 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@types/react-image-gallery": "^1.2.4", "@types/turndown": "^5.0.5", "@vitejs/plugin-react": "^4.3.4", + "@vitest/coverage-v8": "^3.1.4", "@vitest/ui": "^3.1.4", "eslint": "^9.23.0", "eslint-plugin-import": "^2.31.0", @@ -62,9 +63,9 @@ "jsdom": "^26.1.0", "replace-in-file": "^8.3.0", "rollup-plugin-visualizer": "^5.14.0", - "typescript": "~5.6.3", + "typescript": "^5.8.3", "typescript-eslint": "^8.27.0", - "vite": "6.3.4", + "vite": "^6.3.5", "vitest": "^3.1.4" }, "overrides": { diff --git a/public/docs/img/online_trip_explorer_qr.svg b/public/docs/img/online_trip_explorer_qr.svg index d535e6d..155b6af 100644 --- a/public/docs/img/online_trip_explorer_qr.svg +++ b/public/docs/img/online_trip_explorer_qr.svg @@ -1,361 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/App.test.tsx b/src/App.test.tsx new file mode 100644 index 0000000..831887c --- /dev/null +++ b/src/App.test.tsx @@ -0,0 +1,166 @@ +import '@testing-library/jest-dom/vitest' // For toBeInTheDocument +import { describe, it, expect, beforeEach, afterEach, vi, SpyInstance, Mock } from 'vitest' +import { render, screen, waitFor, cleanup } from '@testing-library/react' +import { MemoryRouter } from 'react-router-dom' +import React from 'react' + +import App from './App' + +// Import the modules to be mocked for type casting and clearing +import TopMenu from './components/TopMenu/TopMenu'; +import Destinations from './pages/Destinations/Destinations'; +import { WesternAustralia } from './pages/Australia/WesternAustralia'; // Named import +import WelcomeModal from './components/WelcomeModal/WelcomeModal'; +import ReactGA from 'react-ga4'; // Import ReactGA for clearing its mocks + +// 1. Mock child components +vi.mock('./components/TopMenu/TopMenu', () => ({ + default: vi.fn(() =>
TopMenu
), +})) +vi.mock('./pages/Destinations/Destinations', () => ({ + default: vi.fn(() =>
Destinations Page
), +})) +vi.mock('./pages/Australia/WesternAustralia', () => ({ + WesternAustralia: vi.fn(() =>
Western Australia Page
), +})) +vi.mock('./components/WelcomeModal/WelcomeModal', () => ({ + default: vi.fn(({ open, onClose }) => open ?
WelcomeModal
: null), +})); + +// 2. Mock react-ga4 +vi.mock('react-ga4', () => ({ + default: { + initialize: vi.fn(), + send: vi.fn(), + }, +})); + +// 3. Mock HashRouter from react-router-dom +vi.mock('react-router-dom', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + HashRouter: ({ children }: { children: React.ReactNode }) => <>{children}, + }; +}); + +// 4. Mock src/config.ts +vi.mock('./config', () => ({ + default: { + ga: { + measurementId: 'G-TESTID', // Provide a dummy ID + }, + }, +})); + +describe('App component', () => { + let localStorageMock: { + getItem: SpyInstance<[key: string], string | null>; + setItem: SpyInstance<[key: string, value: string], void>; + }; + let sessionStorageMock: { + getItem: SpyInstance<[key: string], string | null>; + setItem: SpyInstance<[key: string, value: string], void>; + removeItem: SpyInstance<[key: string], void>; + }; + + beforeEach(() => { + // Mock localStorage + const lsStore: Record = {}; + localStorageMock = { + getItem: vi.fn((key: string) => lsStore[key] || null), + setItem: vi.fn((key: string, value: string) => { lsStore[key] = value; }), + } as any; + vi.stubGlobal('localStorage', localStorageMock); + + // Mock sessionStorage + const ssStore: Record = {}; + sessionStorageMock = { + getItem: vi.fn((key: string) => ssStore[key] || null), + setItem: vi.fn((key: string, value: string) => { ssStore[key] = value; }), + removeItem: vi.fn((key: string) => { delete ssStore[key]; }), + } as any; + vi.stubGlobal('sessionStorage', sessionStorageMock); + + // Reset mocks for components + (TopMenu as Mock).mockClear(); + (Destinations as Mock).mockClear(); + (WesternAustralia as Mock).mockClear(); + (WelcomeModal as Mock).mockClear(); + + // Reset react-ga4 mocks + (ReactGA.initialize as Mock).mockClear(); + (ReactGA.send as Mock).mockClear(); + }); + + afterEach(() => { + cleanup(); + vi.unstubAllGlobals(); + vi.clearAllMocks(); + }); + + const renderApp = (initialRoute = '/') => { + return render( + + + + ); + }; + + it('renders successfully and TopMenu is present', () => { + renderApp(); + expect(screen.getByTestId('mock-top-menu')).toBeInTheDocument(); + }); + + it('renders TopMenu component initially and mock is called', () => { + renderApp(); + expect(screen.getByTestId('mock-top-menu')).toBeInTheDocument(); + expect(TopMenu).toHaveBeenCalled(); + }); + + it('renders Destinations component for the default route "/"', async () => { + renderApp('/'); + await waitFor(() => { + expect(screen.getByTestId('mock-destinations-page')).toBeInTheDocument(); + }); + }); + + describe('WelcomeModal behavior', () => { + it('appears when localStorage has no "hasShownModal" item', async () => { + localStorageMock.getItem.mockReturnValue(null); + renderApp('/'); + await waitFor(() => { + expect(screen.getByTestId('mock-welcome-modal')).toBeInTheDocument(); + }); + expect(localStorageMock.setItem).toHaveBeenCalledWith('hasShownModal', 'true'); + }); + + it('does not appear when localStorage has "hasShownModal" set to "true"', async () => { + localStorageMock.setItem('hasShownModal', 'true'); + localStorageMock.getItem.mockImplementation((key) => (key === 'hasShownModal' ? 'true' : null)); + + renderApp('/'); + + expect(screen.queryByTestId('mock-welcome-modal')).not.toBeInTheDocument(); + + // Check that setItem was called for 'hasShownModal' only once (during setup) + const setItemCalls = localStorageMock.setItem.mock.calls; + const hasShownModalCallCount = setItemCalls.filter(call => call[0] === 'hasShownModal').length; + expect(hasShownModalCallCount).toBe(1); + + // Optionally, verify that the provider did its job for 'savedFeatures' + const savedFeaturesCallCount = setItemCalls.filter(call => call[0] === 'savedFeatures').length; + expect(savedFeaturesCallCount).toBeGreaterThanOrEqual(1); + }); + }); + + describe('Routing functionality', () => { + it('navigates to /westernAustralia and renders the WesternAustralia page', async () => { + renderApp('/westernAustralia'); + await waitFor(() => { + expect(screen.getByTestId('mock-western-australia-page')).toBeInTheDocument(); + }); + expect(screen.queryByTestId('mock-destinations-page')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/App.tsx b/src/App.tsx index 417eed6..42a827c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -47,7 +47,7 @@ function App(): React.ReactNode { const hasShownModal = localStorage.getItem("hasShownModal") if (!hasShownModal) { setWelcomeDialogOpen(true) - // localStorage.setItem("hasShownModal", "true") + localStorage.setItem("hasShownModal", "true") } }, []) diff --git a/src/components/SavedFeaturesDrawer/TabList/TabList.tsx b/src/components/SavedFeaturesDrawer/TabList/TabList.tsx index 55761b7..be979e9 100644 --- a/src/components/SavedFeaturesDrawer/TabList/TabList.tsx +++ b/src/components/SavedFeaturesDrawer/TabList/TabList.tsx @@ -19,7 +19,7 @@ export const TabList: React.FC = ({ tabs, selectedTab, handleTabCh variant="scrollable" value={selectedTab} onChange={handleTabChange} - onContextMenu={(event) => handleTabContextMenu(event, NULL_TAB)} + onContextMenu={(event) => handleTabContextMenu(event as React.MouseEvent, NULL_TAB)} aria-label="Saved Features Tabs" sx={{ height: "100%" }} > @@ -28,7 +28,7 @@ export const TabList: React.FC = ({ tabs, selectedTab, handleTabCh key={tab} tab={tab} value={tab} - onContextMenu={(event) => handleTabContextMenu(event, tab)} + onContextMenu={(event) => handleTabContextMenu(event as React.MouseEvent, tab)} /> ))} diff --git a/src/components/TopMenu/TopMenu.test.tsx b/src/components/TopMenu/TopMenu.test.tsx new file mode 100644 index 0000000..dc0f109 --- /dev/null +++ b/src/components/TopMenu/TopMenu.test.tsx @@ -0,0 +1,183 @@ +import '@testing-library/jest-dom/vitest' +import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest' +import { render, screen, cleanup, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import React from 'react' +import { MemoryRouter } from 'react-router-dom' + +import TopMenu from './TopMenu' +// Import the functions that will be mocked +import { saveAsGeoJson } from './saveAsGeoJson' +import { saveAsKml } from './saveAsKml' +import { saveAsBackup } from './saveAsBackup' +import { importBackup } from './importBackup' + +import SavedFeaturesContext, { SavedFeaturesContextType, SavedFeaturesStateType, DEFAULT_CATEGORY } from '../../contexts/SavedFeaturesContext' + +// Mock imported utility functions +vi.mock('./saveAsGeoJson', () => ({ saveAsGeoJson: vi.fn() })) +vi.mock('./saveAsKml', () => ({ saveAsKml: vi.fn() })) +vi.mock('./saveAsBackup', () => ({ saveAsBackup: vi.fn() })) +vi.mock('./importBackup', () => ({ importBackup: vi.fn() })) + +vi.mock('../WelcomeModal/WelcomeModal', () => ({ + default: vi.fn(({ open }) => open ?
Mock Welcome Modal
: null), +})) + +const mockSavedFeatures: SavedFeaturesStateType = { + [DEFAULT_CATEGORY]: [], +}; +const mockSetSavedFeatures = vi.fn(); +const mockAddFeature = vi.fn(); +const mockRemoveFeature = vi.fn(); +const mockUpdateFeature = vi.fn(); +const mockSaveToLocalStorage = vi.fn(); +const mockLoadFromLocalStorage = vi.fn(); + +const mockContextValue: SavedFeaturesContextType = { + savedFeatures: mockSavedFeatures, + setSavedFeatures: mockSetSavedFeatures, + addFeature: mockAddFeature, + removeFeature: mockRemoveFeature, + updateFeature: mockUpdateFeature, + saveToLocalStorage: mockSaveToLocalStorage, + loadFromLocalStorage: mockLoadFromLocalStorage, +}; + +const renderTopMenuWithContext = (onMenuClick: () => void) => { + return render( + + + + + + ); +}; + +describe('TopMenu component', () => { + const onMenuClickMock = vi.fn() + + beforeEach(() => { + vi.clearAllMocks() + }) + + afterEach(() => { + cleanup(); + }) + + it('renders successfully with title and main buttons', () => { + renderTopMenuWithContext(onMenuClickMock) + expect(screen.getByText('Trip Explorer')).toBeInTheDocument() + expect(screen.getByRole('button', { name: /Saved Features/i })).toBeInTheDocument() + expect(screen.getByRole('button', { name: /Export/i })).toBeInTheDocument() + expect(screen.getByRole('button', { name: /Import/i })).toBeInTheDocument() + expect(screen.getByRole('button', { name: /Destinations/i })).toBeInTheDocument() + expect(screen.getByRole('button', { name: /Help/i })).toBeInTheDocument() + }) + + it('calls onMenuClick when the menu button (Saved Features) is clicked', async () => { + renderTopMenuWithContext(onMenuClickMock) + const menuButton = screen.getByRole('button', { name: /Saved Features/i }) + await userEvent.click(menuButton) + expect(onMenuClickMock).toHaveBeenCalledTimes(1) + }) + + describe('Save As (Export) menu interactions', () => { + it('opens the "Export" dropdown menu and shows options', async () => { + renderTopMenuWithContext(onMenuClickMock) + const exportButton = screen.getByRole('button', { name: /Export/i }) + await userEvent.click(exportButton) + expect(await screen.findByText('To GeoJson')).toBeInTheDocument() + expect(screen.getByText('To KML')).toBeInTheDocument() + expect(screen.getByText('Export backup')).toBeInTheDocument() + }) + + it('calls saveAsGeoJson when "To GeoJson" menu item is clicked', async () => { + renderTopMenuWithContext(onMenuClickMock) + await userEvent.click(screen.getByRole('button', { name: /Export/i })) + const geoJsonItem = await screen.findByText('To GeoJson') + await userEvent.click(geoJsonItem) + expect(saveAsGeoJson).toHaveBeenCalledTimes(1) + expect(saveAsGeoJson).toHaveBeenCalledWith(mockSavedFeatures) + }) + + it('calls saveAsKml when "To KML" menu item is clicked', async () => { + renderTopMenuWithContext(onMenuClickMock) + await userEvent.click(screen.getByRole('button', { name: /Export/i })) + const kmlItem = await screen.findByText('To KML') + await userEvent.click(kmlItem) + expect(saveAsKml).toHaveBeenCalledTimes(1) + expect(saveAsKml).toHaveBeenCalledWith(mockSavedFeatures) + }) + + it('calls saveAsBackup when "Export backup" menu item is clicked', async () => { + renderTopMenuWithContext(onMenuClickMock) + await userEvent.click(screen.getByRole('button', { name: /Export/i })) + const backupItem = await screen.findByText('Export backup') + await userEvent.click(backupItem) + expect(saveAsBackup).toHaveBeenCalledTimes(1) + expect(saveAsBackup).toHaveBeenCalledWith(mockSavedFeatures) + }) + }) + + describe('Import menu interactions', () => { + it('opens the "Import" dropdown menu and shows options', async () => { + renderTopMenuWithContext(onMenuClickMock); + const importButton = screen.getByRole('button', { name: /Import/i }); + await userEvent.click(importButton); + expect(await screen.findByText('Override existing POIs')).toBeInTheDocument(); + expect(screen.getByText('Append categories')).toBeInTheDocument(); + expect(screen.getByText('Merge Categories')).toBeInTheDocument(); + }); + + it('calls importBackup with "override" when "Override existing POIs" is clicked', async () => { + renderTopMenuWithContext(onMenuClickMock) + await userEvent.click(screen.getByRole('button', { name: /Import/i })) + const overrideItem = await screen.findByText('Override existing POIs') + await userEvent.click(overrideItem) + expect(importBackup).toHaveBeenCalledTimes(1) + expect(importBackup).toHaveBeenCalledWith("override", mockSetSavedFeatures) + }) + + it('calls importBackup with "append" when "Append categories" is clicked', async () => { + renderTopMenuWithContext(onMenuClickMock) + await userEvent.click(screen.getByRole('button', { name: /Import/i })) + const appendItem = await screen.findByText('Append categories') + await userEvent.click(appendItem) + expect(importBackup).toHaveBeenCalledTimes(1) + expect(importBackup).toHaveBeenCalledWith("append", mockSetSavedFeatures) + }) + + it('calls importBackup with "merge" when "Merge Categories" is clicked', async () => { + renderTopMenuWithContext(onMenuClickMock) + await userEvent.click(screen.getByRole('button', { name: /Import/i })) + const mergeItem = await screen.findByText('Merge Categories') + await userEvent.click(mergeItem) + expect(importBackup).toHaveBeenCalledTimes(1) + expect(importBackup).toHaveBeenCalledWith("merge", mockSetSavedFeatures) + }) + }) + + it('opens the WelcomeModal when Help button is clicked', async () => { + renderTopMenuWithContext(onMenuClickMock); + const helpButton = screen.getByRole('button', { name: /Help/i }); + await userEvent.click(helpButton); + expect(screen.getByTestId('mock-welcome-modal')).toBeInTheDocument(); + }); + + it('opens and closes the Destinations menu', async () => { + renderTopMenuWithContext(onMenuClickMock); + const destinationsButton = screen.getByRole('button', { name: /Destinations/i }); + + await userEvent.click(destinationsButton); + expect(await screen.findByText('Australia')).toBeInTheDocument(); + expect(screen.getByText('New South Wales')).toBeInTheDocument(); + + const nswLink = screen.getByText('New South Wales'); + await userEvent.click(nswLink); + + await waitFor(() => { + expect(screen.queryByText('New South Wales')).not.toBeInTheDocument(); + }); + }); +}) diff --git a/src/contexts/SavedFeaturesProvider.test.tsx b/src/contexts/SavedFeaturesProvider.test.tsx new file mode 100644 index 0000000..1f6ca38 --- /dev/null +++ b/src/contexts/SavedFeaturesProvider.test.tsx @@ -0,0 +1,337 @@ +import { describe, it, expect, beforeEach, afterEach, vi, SpyInstance } from 'vitest' +import { render, act } from '@testing-library/react' +import React, { useContext } from 'react' + +import SavedFeaturesProvider from './SavedFeaturesProvider' +import SavedFeaturesContext, { + DEFAULT_CATEGORY, + SavedFeaturesStateType, + selectionInfo, + SavedFeaturesContextType +} from "./SavedFeaturesContext" +import { GeoJsonFeature } from "../data/types" + + +// Helper component to consume the context +const TestConsumer: React.FC<{ + onContextValue?: (value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any +}> = ({ onContextValue }) => { + const context = useContext(SavedFeaturesContext) + if (!context) { + throw new Error("TestConsumer must be used within a SavedFeaturesProvider") + } + if (onContextValue) { + onContextValue(context) + } + return null +} + +// Mock GeoJSON features +const createMockFeature = (id: string, name: string, category?: string): GeoJsonFeature => ({ + type: "Feature", + geometry: { type: "Point", coordinates: [0, 0] }, + properties: { id, name, category }, +}) + +describe("SavedFeaturesProvider", () => { + let localStorageMock: { + getItem: SpyInstance<[key: string], string | null>; + setItem: SpyInstance<[key: string, value: string], void>; + removeItem: SpyInstance<[key: string], void>; + clear: SpyInstance<[], void>; + }; + + beforeEach(() => { + // Create a mock object that adheres to the Storage interface + const store: Record = {}; + localStorageMock = { + getItem: vi.fn((key: string) => store[key] || null), + setItem: vi.fn((key: string, value: string) => { + store[key] = value; + }), + removeItem: vi.fn((key: string) => { + delete store[key]; + }), + clear: vi.fn(() => { + for (const key in store) { + delete store[key]; + } + }), + }; + vi.stubGlobal('localStorage', localStorageMock); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + vi.clearAllMocks(); + }); + + it("initializes with default state if localStorage is empty", () => { + localStorageMock.getItem.mockReturnValue(null); // Explicitly ensure it's null for this test + let contextValue: SavedFeaturesContextType | null = null; + render( + + contextValue = v as SavedFeaturesContextType} /> + + ) + expect(contextValue?.savedFeatures).toEqual({ [DEFAULT_CATEGORY]: [] }); + }); + + it("loads initial state from localStorage if present", () => { + const initialState: SavedFeaturesStateType = { + [DEFAULT_CATEGORY]: [createMockFeature("1", "Feature 1")], + "customCategory": [createMockFeature("2", "Feature 2")], + }; + localStorageMock.setItem(STORAGE_KEY, JSON.stringify(initialState)); // Use setItem for setup + localStorageMock.getItem.mockImplementation((key) => key === STORAGE_KEY ? JSON.stringify(initialState) : null); + + + let contextValue: SavedFeaturesContextType | null = null; + render( + + contextValue = v as SavedFeaturesContextType} /> + + ) + expect(contextValue?.savedFeatures).toEqual(initialState); + }); + + it("adds a feature to the DEFAULT_CATEGORY", () => { + let contextValue: SavedFeaturesContextType | null = null; + render( + + contextValue = v as SavedFeaturesContextType} /> + + ) + const feature1 = createMockFeature("f1", "Feature 1"); + act(() => { + contextValue?.addFeature(DEFAULT_CATEGORY, feature1); + }); + expect(contextValue?.savedFeatures[DEFAULT_CATEGORY]).toContainEqual(feature1); + expect(localStorageMock.setItem).toHaveBeenCalled(); + }); + + it("adds a feature to a custom category and removes it from DEFAULT_CATEGORY", () => { + let contextValue: SavedFeaturesContextType | null = null; + const feature1 = createMockFeature("f1", "Feature 1"); + + // Initial state: feature1 is in DEFAULT_CATEGORY + const initialState: SavedFeaturesStateType = { + [DEFAULT_CATEGORY]: [feature1], + "custom": [], + }; + // localStorageMock.setItem(STORAGE_KEY, JSON.stringify(initialState)); // REMOVE THIS LINE + localStorageMock.getItem.mockImplementation((key) => key === STORAGE_KEY ? JSON.stringify(initialState) : null); + + + render( + + contextValue = v as SavedFeaturesContextType} /> + + ); + + act(() => { + // contextValue should be initialized by now from localStorage + contextValue?.addFeature("custom", feature1); + }); + + expect(contextValue?.savedFeatures["custom"]).toContainEqual(feature1); + expect(contextValue?.savedFeatures[DEFAULT_CATEGORY]).not.toContainEqual(feature1); + expect(localStorageMock.setItem).toHaveBeenCalledTimes(2); // initial load + addFeature + }) + + + it("removes a feature from a category", () => { + const feature1 = createMockFeature("f1", "Feature 1") + const initialFeatures = { [DEFAULT_CATEGORY]: [feature1, createMockFeature("f2", "Feature 2")] } + localStorageMock.getItem.mockReturnValue(JSON.stringify(initialFeatures)) + + let contextValue: SavedFeaturesContextType | null = null; + render( + + contextValue = v as SavedFeaturesContextType} /> + + ) + + const selection: selectionInfo = { feature: feature1, category: DEFAULT_CATEGORY, index: 0 } + act(() => { + contextValue?.removeFeature(DEFAULT_CATEGORY, selection) + }) + + expect(contextValue?.savedFeatures[DEFAULT_CATEGORY].length).toBe(1) + expect(contextValue?.savedFeatures[DEFAULT_CATEGORY][0].properties?.id).toBe("f2") + expect(localStorageMock.setItem).toHaveBeenCalledTimes(2) // load, then remove + }) + + it("updates a feature across all categories", () => { + const oldFeature = createMockFeature("f1", "Old Name") + const newFeatureData = createMockFeature("f1", "New Name") + newFeatureData.properties!.description = "Updated description" + + const initialState: SavedFeaturesStateType = { + [DEFAULT_CATEGORY]: [oldFeature, createMockFeature("f2", "Other Feature")], + "custom1": [createMockFeature("f3", "Another"), oldFeature], + } + localStorageMock.getItem.mockReturnValue(JSON.stringify(initialState)) + + let contextValue: any; // eslint-disable-line @typescript-eslint/no-explicit-any + render( + + contextValue = v} /> + + ) + + act(() => { + contextValue.updateFeature(oldFeature, newFeatureData) + }) + + expect(contextValue.savedFeatures[DEFAULT_CATEGORY][0].properties?.name).toBe("New Name") + expect(contextValue.savedFeatures[DEFAULT_CATEGORY][0].properties?.description).toBe("Updated description") + expect(contextValue.savedFeatures["custom1"][1].properties?.name).toBe("New Name") + expect(contextValue.savedFeatures["custom1"][1].properties?.description).toBe("Updated description") + expect(localStorageMock.setItem).toHaveBeenCalledTimes(2) // load, then update + }) + + it("creates a new category using setSavedFeatures", () => { + let contextValue: any; // eslint-disable-line @typescript-eslint/no-explicit-any + render( + + contextValue = v} /> + + ) + act(() => { + contextValue.setSavedFeatures((prev: SavedFeaturesStateType) => ({ + ...prev, + "newCategory": [], + })) + }) + expect(contextValue.savedFeatures["newCategory"]).toEqual([]) + expect(localStorageMock.setItem).toHaveBeenCalled() + }) + + it("deletes a category using setSavedFeatures (features in deleted category are lost if not re-categorized)", () => { + const initialState: SavedFeaturesStateType = { + [DEFAULT_CATEGORY]: [], + "toBeDeleted": [createMockFeature("f1", "Feature 1")], + } + localStorageMock.getItem.mockReturnValue(JSON.stringify(initialState)) + let contextValue: any; // eslint-disable-line @typescript-eslint/no-explicit-any + render( + + contextValue = v} /> + + ) + act(() => { + contextValue.setSavedFeatures((prev: SavedFeaturesStateType) => { + const newState = { ...prev } + delete newState["toBeDeleted"] + return newState + }) + }) + expect(contextValue.savedFeatures["toBeDeleted"]).toBeUndefined() + expect(localStorageMock.setItem).toHaveBeenCalledTimes(2) // load, then set + }) + + it("renames a category using setSavedFeatures", () => { + const feature1 = createMockFeature("f1", "Feature 1"); + const initialState: SavedFeaturesStateType = { + [DEFAULT_CATEGORY]: [], + "oldName": [feature1], + }; + localStorageMock.getItem.mockReturnValue(JSON.stringify(initialState)); + let contextValue: any; // eslint-disable-line @typescript-eslint/no-explicit-any + render( + + contextValue = v} /> + + ); + act(() => { + contextValue.setSavedFeatures((prev: SavedFeaturesStateType) => { + const { oldName, ...rest } = prev; + return { + ...rest, + "newName": oldName || [], + }; + }); + }); + expect(contextValue.savedFeatures["oldName"]).toBeUndefined(); + expect(contextValue.savedFeatures["newName"]).toContainEqual(feature1); + expect(localStorageMock.setItem).toHaveBeenCalledTimes(2); + }); + + it("reorders features within a category using setSavedFeatures", () => { + const feature1 = createMockFeature("f1", "Feature 1") + const feature2 = createMockFeature("f2", "Feature 2") + const initialState: SavedFeaturesStateType = { [DEFAULT_CATEGORY]: [feature1, feature2] } + localStorageMock.getItem.mockReturnValue(JSON.stringify(initialState)) + let contextValue: any; // eslint-disable-line @typescript-eslint/no-explicit-any + render( + + contextValue = v} /> + + ) + act(() => { + contextValue.setSavedFeatures((prev: SavedFeaturesStateType) => ({ + ...prev, + [DEFAULT_CATEGORY]: [feature2, feature1], + })) + }) + expect(contextValue.savedFeatures[DEFAULT_CATEGORY]).toEqual([feature2, feature1]) + expect(localStorageMock.setItem).toHaveBeenCalledTimes(2) + }) + + it("reorders categories using setSavedFeatures (order in object keys is not guaranteed, test checks data integrity)", () => { + // Note: Object key order is not guaranteed in JS, but we can test if the data remains correct. + // For UI representation of order, an array of keys would typically be stored separately. + const cat1Data = [createMockFeature("c1f1", "Cat1 Feature1")] + const cat2Data = [createMockFeature("c2f1", "Cat2 Feature1")] + const initialState: SavedFeaturesStateType = { "category1": cat1Data, "category2": cat2Data } + localStorageMock.getItem.mockReturnValue(JSON.stringify(initialState)) + let contextValue: any; // eslint-disable-line @typescript-eslint/no-explicit-any + render( + + contextValue = v} /> + + ) + act(() => { + // Simulate reordering by changing the structure (e.g. if a supporting ordered key list changed) + contextValue.setSavedFeatures({ "category2": cat2Data, "category1": cat1Data }) + }) + // Check data integrity, not key order + expect(contextValue.savedFeatures["category1"]).toEqual(cat1Data) + expect(contextValue.savedFeatures["category2"]).toEqual(cat2Data) + expect(localStorageMock.setItem).toHaveBeenCalledTimes(2) + }) + + it("saveToLocalStorage and loadFromLocalStorage work correctly", () => { + let contextValue: any; // eslint-disable-line @typescript-eslint/no-explicit-any + render( + + contextValue = v} /> + + ) + const testState: SavedFeaturesStateType = { [DEFAULT_CATEGORY]: [createMockFeature("test", "Test")]} + + // Simulate setting state and saving + act(() => { + contextValue.setSavedFeatures(testState) + // saveToLocalStorage is called automatically by useEffect, but we can call it explicitly if needed + // contextValue.saveToLocalStorage(); // Already called by useEffect + }); + expect(localStorageMock.setItem).toHaveBeenCalledWith(STORAGE_KEY, JSON.stringify(testState)) + + // Simulate loading + localStorageMock.getItem.mockReturnValue(JSON.stringify(testState)) // Ensure getItem returns the new state + act(() => { + contextValue.loadFromLocalStorage() + }) + expect(contextValue.savedFeatures).toEqual(testState) + }) +}) + +// Need to ensure DEFAULT_CATEGORY is correctly sourced. +// If SavedFeaturesProvider doesn't export it, but SavedFeaturesContext does: +// import SavedFeaturesContext, { DEFAULT_CATEGORY as ActualDefaultCategory } from "./SavedFeaturesContext"; +// const DEFAULT_CATEGORY = ActualDefaultCategory; +// For now, assuming SavedFeaturesProvider exports it or it's available. +// If not, I will define it as 'all' based on the provider's logic. +const STORAGE_KEY = "savedFeatures"; // Re-define if not exported from provider for test usage diff --git a/src/pages/NotFound/Illustration.tsx b/src/pages/NotFound/Illustration.tsx index f6d568b..550df36 100644 --- a/src/pages/NotFound/Illustration.tsx +++ b/src/pages/NotFound/Illustration.tsx @@ -6,15 +6,15 @@ export function Illustration(props: React.ComponentPropsWithoutRef<"svg">) { ) diff --git a/src/utils/deepmerge.test.ts b/src/utils/deepmerge.test.ts new file mode 100644 index 0000000..bd89f3d --- /dev/null +++ b/src/utils/deepmerge.test.ts @@ -0,0 +1,164 @@ +import { describe, it, expect } from 'vitest' +import deepMerge from "./deepmerge" // Assuming TAny is correctly handled or not strictly needed for tests +import { TAny } from "../data/types"; // Import TAny if your test setup requires it for type checking + +describe("deepMerge", () => { + it("should merge source properties into target", () => { + const target = { a: 1, b: 2 } + const source = { b: 3, c: 4 } + const result = deepMerge(target, source) + expect(result).toEqual({ a: 1, b: 3, c: 4 }) + }) + + it("should return a new object, not modify target in place", () => { + const target = { a: 1, b: 2 } + const source = { b: 3, c: 4 } + const result = deepMerge(target, source) + expect(result).not.toBe(target) + expect(target).toEqual({ a: 1, b: 2 }) // Target should be unchanged + }) + + it("should deeply merge nested objects", () => { + const target = { a: 1, b: { x: 10, y: 20 } } + const source = { b: { y: 30, z: 40 }, c: 3 } + const result = deepMerge(target, source) + expect(result).toEqual({ a: 1, b: { x: 10, y: 30, z: 40 }, c: 3 }) + }) + + it("should add new nested objects from source", () => { + const target = { a: 1 } + const source = { b: { x: 10, y: 20 } } + const result = deepMerge(target, source) + expect(result).toEqual({ a: 1, b: { x: 10, y: 20 } }) + }) + + it("should handle empty target object", () => { + const target = {} + const source = { a: 1, b: { x: 10 } } + const result = deepMerge(target, source) + expect(result).toEqual({ a: 1, b: { x: 10 } }) + }) + + it("should handle empty source object", () => { + const target = { a: 1, b: { x: 10 } } + const source = {} + const result = deepMerge(target, source) + expect(result).toEqual({ a: 1, b: { x: 10 } }) // Should be a copy of target + }) + + it("should handle both target and source being empty objects", () => { + const target = {} + const source = {} + const result = deepMerge(target, source) + expect(result).toEqual({}) + }) + + it("should overwrite non-object properties in target with source properties", () => { + const target = { a: 1, b: "hello" } + const source = { b: "world", c: true } + const result = deepMerge(target, source) + expect(result).toEqual({ a: 1, b: "world", c: true }) + }) + + it("should replace a non-object in target with an object from source", () => { + const target = { a: 1, b: "not an object" } + const source = { b: { x: 10, y: 20 } } + const result = deepMerge(target, source) + expect(result).toEqual({ a: 1, b: { x: 10, y: 20 } }) + }) + + it("should replace an object in target with a non-object from source", () => { + const target = { a: 1, b: { x: 10, y: 20 } } + const source = { b: "now a string" } + const result = deepMerge(target, source) + expect(result).toEqual({ a: 1, b: "now a string" }) + }) + + it("should not merge arrays, but replace them (as per isObject logic)", () => { + const target = { a: [1, 2], b: { x: 1 } } + const source = { a: [3, 4], c: 3 } // isObject(source[key]) will be false for array 'a' + const result = deepMerge(target, source) + expect(result).toEqual({ a: [3, 4], b: { x: 1 }, c: 3 }) + }) + + it("should handle properties with null values", () => { + const target = { a: 1, b: null } + const source = { b: { x: 10 }, c: null } + const result = deepMerge(target, source) + // b in target is null (not an object by isObject), so source.b (an object) replaces it. + // c in source is null (not an object), so it's assigned. + expect(result).toEqual({ a: 1, b: { x: 10 }, c: null }) + }) + + it("should handle source properties that are undefined", () => { + const target = { a: 1, b: 2 } + const source: Record = { b: undefined, c: 3 } + const result = deepMerge(target, source) + expect(result).toEqual({ a: 1, b: undefined, c: 3 }) + }) + + it("should merge complex nested structures", () => { + const target = { + id: "target", + data: { + name: "Target Data", + version: 1.0, + settings: { + theme: "dark", + notifications: { + email: true, + sms: false, + }, + }, + }, + user: { + id: 100, + }, + } + const source = { + data: { + version: 1.1, + settings: { + notifications: { + sms: true, + push: true, + }, + privacy: "high", + }, + tags: ["test", "merge"], + }, + user: { + name: "Test User", + }, + newProperty: { + value: "new value" + } + } + + const result = deepMerge(target, source); + expect(result).toEqual({ + id: "target", // from target + data: { + name: "Target Data", // from target + version: 1.1, // from source + settings: { + theme: "dark", // from target + notifications: { + email: true, // from target + sms: true, // from source + push: true, // from source + }, + privacy: "high", // from source + }, + tags: ["test", "merge"], // from source (array replacement) + }, + user: { // object 'user' from target merged with object 'user' from source + id: 100, // from target + name: "Test User" // from source + }, + newProperty: { // from source + value: "new value" + } + }) + }) +}) diff --git a/src/utils/deepmerge.ts b/src/utils/deepmerge.ts index 5f593c8..2941c4f 100644 --- a/src/utils/deepmerge.ts +++ b/src/utils/deepmerge.ts @@ -5,13 +5,23 @@ export default function deepMerge(target: Record, source: Record { - if (isObject(source[key])) { - if (!(key in target)) Object.assign(output, { [key]: source[key] }) - else output[key] = deepMerge(target[key] as Record, source[key] as Record) + const sourceValue = source[key]; + const targetValue = target[key]; + + if (isObject(sourceValue)) { + if (isObject(targetValue)) { + // Both source and target values are objects, so merge them + output[key] = deepMerge(targetValue as Record, sourceValue as Record); + } else { + // Source value is an object, but target value is not (or doesn't exist at all) + // Overwrite whatever was in target[key] with the source object value + output[key] = sourceValue; + } } else { - Object.assign(output, { [key]: source[key] }) + // Source value is not an object, so assign it directly (overwrite) + output[key] = sourceValue; } - }) + }); } return output } diff --git a/src/utils/formatFeature.test.ts b/src/utils/formatFeature.test.ts new file mode 100644 index 0000000..185e1cc --- /dev/null +++ b/src/utils/formatFeature.test.ts @@ -0,0 +1,158 @@ +import { describe, it, expect } from 'vitest' +import { GeoJsonFeature, GeoJsonProperties } from "../data/types" +import formatFeature from "./formatFeature" + +describe("formatFeature", () => { + const baseFeature: GeoJsonFeature = { + type: "Feature", + geometry: { + type: "Point", + coordinates: [0, 0], + }, + properties: { + name: "Test Feature", + description: "Original description.", + }, + } + + it("should return a new feature object, not modify in place", () => { + const feature = { ...baseFeature } + const formattedFeature = formatFeature(feature) + expect(formattedFeature).not.toBe(feature) + expect(feature.properties?.description).toBe("Original description.") // Original should be unchanged + }) + + it("should initialize properties if it does not exist", () => { + const feature: GeoJsonFeature = { + type: "Feature", + geometry: { + type: "Point", + coordinates: [0, 0], + }, + properties: null, // Properties is null + } + const formattedFeature = formatFeature(feature) + expect(formattedFeature.properties).toBeDefined() + expect(formattedFeature.properties).toEqual({}) // Should be an empty object after deletions + }) + + it("should convert tripNotes from HTML to Markdown and prepend to description", () => { + const feature: GeoJsonFeature = { + ...baseFeature, + properties: { + ...baseFeature.properties, + tripNotes: "

Hello world

", + description: "Existing details.", + }, + } + const formattedFeature = formatFeature(feature) + expect(formattedFeature.properties?.description).toContain("Hello **world**") // Turndown's default for strong + expect(formattedFeature.properties?.description).toContain("\n\n--------------------\n\nExisting details.") + }) + + it("should handle existing description being undefined when tripNotes are present", () => { + const feature: GeoJsonFeature = { + ...baseFeature, + properties: { + name: "Test Feature", + tripNotes: "

Title

", + // description is undefined + }, + } + const formattedFeature = formatFeature(feature) + // Check for setext style heading (Title\n=====) or atx (# Title) as turndown default can vary or be configured + expect(formattedFeature.properties?.description).toMatch(/^Title\n=+\n/m) // Check for "Title" followed by "===" + expect(formattedFeature.properties?.description).toContain("\n\n--------------------\n\nundefined") + }) + + it("should handle existing description being null when tripNotes are present", () => { + const feature: GeoJsonFeature = { + ...baseFeature, + properties: { + name: "Test Feature", + tripNotes: "

Subtitle

", + description: null, + }, + } + const formattedFeature = formatFeature(feature) + expect(formattedFeature.properties?.description).toMatch(/^Subtitle\n-+\n/m) // Check for "Subtitle" followed by "---" + expect(formattedFeature.properties?.description).toContain("\n\n--------------------\n\nnull") + }) + + + it("should not modify description if tripNotes are not present", () => { + const feature = { ...baseFeature } + const formattedFeature = formatFeature(feature) + expect(formattedFeature.properties?.description).toBe("Original description.") + }) + + it("should not modify description if tripNotes are empty", () => { + const feature: GeoJsonFeature = { + ...baseFeature, + properties: { + ...baseFeature.properties, + tripNotes: "", + description: "Existing details.", + }, + } + const formattedFeature = formatFeature(feature) + // Turndown might convert empty string to empty string, or an empty line. + // The key is that the original description is preserved and not prepended by "undefined" or similar. + // If tripNotes is empty, the `if (tripNotes)` condition in formatFeature should be false. + expect(formattedFeature.properties?.description).toBe("Existing details.") + }) + + it("should delete id, style, images, and tripNotes from properties", () => { + const feature: GeoJsonFeature = { + ...baseFeature, + properties: { + ...baseFeature.properties, + id: "123", + style: { color: "red" }, + images: [{ url: "test.jpg" }], + tripNotes: "Some notes.", + }, + } + const formattedFeature = formatFeature(feature) + expect(formattedFeature.properties?.id).toBeUndefined() + expect(formattedFeature.properties?.style).toBeUndefined() + expect(formattedFeature.properties?.images).toBeUndefined() + expect(formattedFeature.properties?.tripNotes).toBeUndefined() + }) + + it("should retain other properties", () => { + const customProperty = { custom: "value" } + const feature: GeoJsonFeature = { + ...baseFeature, + properties: { + ...baseFeature.properties, + ...customProperty, + }, + } + const formattedFeature = formatFeature(feature) + expect(formattedFeature.properties?.name).toBe("Test Feature") + expect(formattedFeature.properties?.custom).toBe("value") + }) + + it("should handle feature with minimal properties", () => { + const feature: GeoJsonFeature = { + type: "Feature", + geometry: { type: "Point", coordinates: [0,0]}, + properties: { name: "Minimal" } + } + const formattedFeature = formatFeature(feature) + expect(formattedFeature.properties?.name).toBe("Minimal") + expect(Object.keys(formattedFeature.properties as GeoJsonProperties).length).toBe(1) // Only name should remain + }) + + it("should handle feature with no properties initially, then add tripNotes", () => { + // This case is more about how properties are initialized if null + const feature: GeoJsonFeature = { + type: "Feature", + geometry: { type: "Point", coordinates: [0,0]}, + properties: { tripNotes: "

Notes

", description: "Desc" } // Properties exist here + } + const formattedFeature = formatFeature(feature) + expect(formattedFeature.properties?.description).toMatch(/^Notes\n=+\n/m) + }) +}) diff --git a/vite.config.ts b/vite.config.ts index ab56a17..680c898 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,3 +1,4 @@ +/// import react from "@vitejs/plugin-react" import { visualizer } from "rollup-plugin-visualizer" import { defineConfig } from "vite" @@ -8,26 +9,7 @@ export default defineConfig({ build: { rollupOptions: { external: [ - "axios", - "file-saver", - "jszip", - "geojson-to-kml", - "leaflet", - "react", - "react-ga4", - "react-image-gallery", - "react-leaflet", - "react-router-dom", - "tinymce", - "turndown", - "uuid", - /^@dnd-kit\/.*/, - /^@emotion\/.*/, - /^@mui\/.*/, - /^@tinymce\/.*/, - /^react-dom\/?.*/, - /^react\/jsx-runtime/, - /^react-toastify\/?.*/, + "tinymce", // Core TinyMCE is loaded globally from /public/tinymce ], }, },