From c0612d4662c97dde78d5c37bfe6bbc1cde81b455 Mon Sep 17 00:00:00 2001 From: maiano Date: Mon, 14 Jul 2025 03:07:18 +0200 Subject: [PATCH 01/20] chore: install vitest and set up test config --- package-lock.json | 1799 +++++++++++++++++++++++++++++++++++++++++++-- package.json | 11 +- vite.config.ts | 38 +- vitest.setup.mjs | 28 + 4 files changed, 1822 insertions(+), 54 deletions(-) create mode 100644 vitest.setup.mjs diff --git a/package-lock.json b/package-lock.json index 56fc51d..e17047e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,12 +18,15 @@ "@commitlint/config-conventional": "^19.8.1", "@eslint/js": "^9.29.0", "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", "@types/node": "^22.15.2", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@typescript-eslint/eslint-plugin": "^8.31.0", "@typescript-eslint/parser": "^8.31.0", "@vitejs/plugin-react": "^4.5.2", + "@vitest/coverage-v8": "^3.2.4", "eslint": "^9.29.0", "eslint-config-prettier": "^10.1.5", "eslint-import-resolver-typescript": "^4.3.4", @@ -34,12 +37,14 @@ "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.2.0", "husky": "^9.1.7", + "jsdom": "^26.1.0", "lint-staged": "^16.1.2", "prettier": "^3.6.2", "typescript": "~5.8.3", "typescript-eslint": "^8.34.1", "vite": "^7.0.0", - "vite-plugin-svgr": "^4.3.0" + "vite-plugin-svgr": "^4.3.0", + "vitest": "^3.2.4" } }, "node_modules/@adobe/css-tools": { @@ -62,6 +67,27 @@ "node": ">=6.0.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -296,6 +322,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -344,6 +380,16 @@ "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, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@commitlint/cli": { "version": "19.8.1", "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-19.8.1.tgz", @@ -776,6 +822,121 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", + "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@emnapi/core": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", @@ -1440,6 +1601,73 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -1452,6 +1680,16 @@ "node": ">=18.0.0" } }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.12", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", @@ -1537,6 +1775,17 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgr/core": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", @@ -2374,6 +2623,46 @@ "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/dom/node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@testing-library/jest-dom": { "version": "6.6.3", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", @@ -2409,6 +2698,48 @@ "node": ">=8" } }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", @@ -2419,6 +2750,14 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2464,6 +2803,16 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, "node_modules/@types/conventional-commits-parser": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.1.tgz", @@ -2474,6 +2823,13 @@ "@types/node": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3084,54 +3440,223 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" } }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "url": "https://opencollective.com/vitest" } }, - "node_modules/ansi-escapes": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", - "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", "dev": true, "license": "MIT", "dependencies": { - "environment": "^1.0.0" + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" }, "engines": { "node": ">=18" @@ -3315,6 +3840,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -3322,6 +3857,35 @@ "dev": true, "license": "MIT" }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.3.tgz", + "integrity": "sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -3432,6 +3996,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -3526,6 +4100,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", + "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3543,6 +4134,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", @@ -3847,6 +4448,20 @@ "dev": true, "license": "MIT" }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -3874,6 +4489,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -3946,6 +4575,23 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3989,6 +4635,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -4057,6 +4714,13 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.179", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.179.tgz", @@ -4219,6 +4883,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -4765,6 +5436,16 @@ "dev": true, "license": "MIT" }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4930,6 +5611,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -5106,6 +5804,27 @@ "node": ">=16" } }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -5119,6 +5838,32 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/global-directory": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", @@ -5285,22 +6030,83 @@ "node": ">= 0.4" } }, - "node_modules/husky": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", - "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, "license": "MIT", - "bin": { - "husky": "bin.js" + "dependencies": { + "whatwg-encoding": "^3.1.1" }, "engines": { "node": ">=18" - }, - "funding": { + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { "url": "https://github.com/sponsors/typicode" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -5683,6 +6489,13 @@ "node": ">=8" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -5855,6 +6668,76 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "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, + "license": "BSD-3-Clause", + "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, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", @@ -5884,6 +6767,46 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -6484,6 +7407,13 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/loupe": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", + "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==", + "dev": true, + "license": "MIT" + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -6504,6 +7434,17 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -6513,6 +7454,47 @@ "@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, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.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" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -6721,6 +7703,13 @@ "dev": true, "license": "MIT" }, + "node_modules/nwsapi": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", + "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "dev": true, + "license": "MIT" + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -6902,6 +7891,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6934,6 +7930,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -6961,6 +7983,30 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -6971,6 +8017,23 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7080,6 +8143,47 @@ "node": ">=6.0.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -7132,6 +8236,14 @@ "react": "^19.1.0" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -7335,6 +8447,13 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -7414,6 +8533,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", @@ -7578,6 +8717,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -7661,6 +8807,20 @@ "node": ">=12.0.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -7703,17 +8863,73 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/string-width/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, - "license": "MIT" - }, - "node_modules/string.prototype.includes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", - "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", "dev": true, "license": "MIT", "dependencies": { @@ -7800,6 +9016,30 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -7836,6 +9076,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -7869,6 +9129,13 @@ "dev": true, "license": "MIT" }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/synckit": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", @@ -7926,6 +9193,47 @@ "node": ">=18" } }, + "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, + "license": "ISC", + "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.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/text-extensions": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", @@ -7946,6 +9254,13 @@ "dev": true, "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", @@ -7995,6 +9310,56 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -8008,6 +9373,32 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -8371,6 +9762,29 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/vite-plugin-svgr": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.3.0.tgz", @@ -8412,6 +9826,159 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -8517,6 +10084,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -8545,6 +10129,80 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", @@ -8558,6 +10216,45 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index ebc7862..14ae8da 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "lint": "eslint ./src", "lint:fix": "eslint --fix --color ./src", "preview": "vite preview", - "prepare": "husky" + "prepare": "husky", + "test": "vitest run", + "coverage": "vitest run --coverage" }, "dependencies": { "@tailwindcss/vite": "^4.1.11", @@ -24,12 +26,15 @@ "@commitlint/config-conventional": "^19.8.1", "@eslint/js": "^9.29.0", "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", "@types/node": "^22.15.2", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@typescript-eslint/eslint-plugin": "^8.31.0", "@typescript-eslint/parser": "^8.31.0", "@vitejs/plugin-react": "^4.5.2", + "@vitest/coverage-v8": "^3.2.4", "eslint": "^9.29.0", "eslint-config-prettier": "^10.1.5", "eslint-import-resolver-typescript": "^4.3.4", @@ -40,12 +45,14 @@ "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.2.0", "husky": "^9.1.7", + "jsdom": "^26.1.0", "lint-staged": "^16.1.2", "prettier": "^3.6.2", "typescript": "~5.8.3", "typescript-eslint": "^8.34.1", "vite": "^7.0.0", - "vite-plugin-svgr": "^4.3.0" + "vite-plugin-svgr": "^4.3.0", + "vitest": "^3.2.4" }, "lint-staged": { "*.{json, md, css, scss}": "prettier --list-different --ignore-unknown", diff --git a/vite.config.ts b/vite.config.ts index 1ed6123..8be5f52 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,4 +1,4 @@ -import { defineConfig } from 'vite'; +import { defineConfig } from 'vitest/config'; import react from '@vitejs/plugin-react'; import tailwindcss from '@tailwindcss/vite'; import path from 'path'; @@ -12,4 +12,40 @@ export default defineConfig({ '@': path.resolve(__dirname, './src'), }, }, + test: { + coverage: { + all: true, + exclude: [ + 'src/test/**', + '**/types/**', + '**/*.d.ts', + 'src/**/index.ts', + 'src/main.tsx', + ], + extension: ['.ts', '.tsx'], + include: ['src/**/*'], + provider: 'v8', + reporter: ['text', 'json', 'html'], + thresholds: { + statements: 80, + lines: 50, + functions: 50, + branches: 50, + }, + }, + typecheck: { + enabled: true, + include: ['**/*.test-d.ts'], + }, + css: { + modules: { classNameStrategy: 'non-scoped' }, + }, + exclude: ['**/node_modules/**', '**/e2e/**'], + include: ['**/*.{test,spec}.{ts,tsx}'], + environment: 'jsdom', + globals: true, + maxConcurrency: 4, + setupFiles: ['./vitest.setup.mjs'], + cache: { dir: './node_modules/.vite/.vitest-cache' }, + }, }); diff --git a/vitest.setup.mjs b/vitest.setup.mjs new file mode 100644 index 0000000..ac096df --- /dev/null +++ b/vitest.setup.mjs @@ -0,0 +1,28 @@ +import '@testing-library/jest-dom/vitest'; +import { vi } from 'vitest'; + +const { getComputedStyle } = window; +window.getComputedStyle = (elt) => getComputedStyle(elt); +window.HTMLElement.prototype.scrollIntoView = () => {}; + +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +}); + +class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +} + +window.ResizeObserver = ResizeObserver; From f437587da7254f0e8976b64f0f4b10f7402dafb5 Mon Sep 17 00:00:00 2001 From: maiano Date: Mon, 14 Jul 2025 15:40:45 +0200 Subject: [PATCH 02/20] test: add tests for search bar component --- src/components/SearchBar/SearchBar.test.tsx | 52 ++++++++++++++++++++ src/components/{ => SearchBar}/SearchBar.tsx | 4 +- src/components/SearchBar/index.ts | 1 + src/pages/HomePage.tsx | 2 +- vitest.setup.mjs | 3 +- 5 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 src/components/SearchBar/SearchBar.test.tsx rename src/components/{ => SearchBar}/SearchBar.tsx (95%) create mode 100644 src/components/SearchBar/index.ts diff --git a/src/components/SearchBar/SearchBar.test.tsx b/src/components/SearchBar/SearchBar.test.tsx new file mode 100644 index 0000000..cc1c753 --- /dev/null +++ b/src/components/SearchBar/SearchBar.test.tsx @@ -0,0 +1,52 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { SearchBar } from './SearchBar'; + +describe('SearchBar component', () => { + const setup = (props = {}) => { + const onSearch = vi.fn(); + const utils = render( + , + ); + + const input = screen.getByPlaceholderText(/pickle/i); + const button = screen.getByRole('button', { name: /scan/i }); + + return { + input, + button, + onSearch, + ...utils, + }; + }; + + it('renders input and button', () => { + const { input, button } = setup(); + expect(input).toBeInTheDocument(); + expect(button).toBeInTheDocument(); + }); + + it('renders initial term in input', () => { + const { input } = setup({ term: 'Morty' }); + expect(input).toHaveValue('Morty'); + }); + + it('updates input value on user input', () => { + const { input } = setup(); + fireEvent.change(input, { target: { value: 'Summer' } }); + expect(input).toHaveValue('Summer'); + }); + + it('calls onSearch with trimmed input on submit', () => { + const { input, button, onSearch } = setup(); + fireEvent.change(input, { target: { value: ' Morty ' } }); + fireEvent.click(button); + expect(onSearch).toHaveBeenCalledWith('Morty'); + }); + + it('disables input and button when loading is true', () => { + const { input, button } = setup({ loading: true }); + expect(input).toBeDisabled(); + expect(button).toBeDisabled(); + }); +}); diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar/SearchBar.tsx similarity index 95% rename from src/components/SearchBar.tsx rename to src/components/SearchBar/SearchBar.tsx index e78c521..c7c6e0a 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar/SearchBar.tsx @@ -14,7 +14,7 @@ type State = { searchText: string; }; -class SearchBar extends Component { +export class SearchBar extends Component { state: State = { searchText: this.props.term, }; @@ -67,5 +67,3 @@ class SearchBar extends Component { ); } } - -export default SearchBar; diff --git a/src/components/SearchBar/index.ts b/src/components/SearchBar/index.ts new file mode 100644 index 0000000..8c19331 --- /dev/null +++ b/src/components/SearchBar/index.ts @@ -0,0 +1 @@ +export { SearchBar } from './SearchBar'; diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 49ac1ce..0216cd5 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -3,7 +3,7 @@ import spinner from '@/assets/spinner-gap-thin.svg'; import { CardList } from '@/components/CardList'; import { LoadingOverlay } from '@/components/LoadingOverlay'; import { Pagination } from '@/components/Pagination'; -import SearchBar from '@/components/SearchBar'; +import { SearchBar } from '@/components/SearchBar'; import { fetchCharacters } from '@/shared/utils/fetch-сharacters'; import { searchStorage } from '@/shared/utils/local-storage'; import type { ApiInfo, Character } from '@/types/character'; diff --git a/vitest.setup.mjs b/vitest.setup.mjs index ac096df..946c034 100644 --- a/vitest.setup.mjs +++ b/vitest.setup.mjs @@ -3,7 +3,8 @@ import { vi } from 'vitest'; const { getComputedStyle } = window; window.getComputedStyle = (elt) => getComputedStyle(elt); -window.HTMLElement.prototype.scrollIntoView = () => {}; + +window.HTMLElement.prototype.scrollIntoView = vi.fn(); Object.defineProperty(window, 'matchMedia', { writable: true, From ab7de3442f0bdb862ec90bb429b44179bbb1f686 Mon Sep 17 00:00:00 2001 From: maiano Date: Mon, 14 Jul 2025 22:27:36 +0200 Subject: [PATCH 03/20] refactor: move UI strings to constants file --- src/components/ErrorBoundary.tsx | 10 +++++----- src/components/Footer.tsx | 3 ++- src/components/SearchBar/SearchBar.tsx | 5 +++-- src/shared/constants/errors.ts | 8 ++++++++ src/shared/constants/ui-strings.ts | 6 ++++++ "src/shared/utils/fetch-\321\201haracters.ts" | 5 +++-- 6 files changed, 27 insertions(+), 10 deletions(-) create mode 100644 src/shared/constants/errors.ts create mode 100644 src/shared/constants/ui-strings.ts diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 661ff85..c5714d1 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -1,6 +1,7 @@ import { Component, type ReactNode, type ErrorInfo } from 'react'; import summer from '@/assets/Rick-And-Morty-PNG-Pic-Background.png'; import { Button } from '@/components/Button'; +import { ERROR_UI_STRINGS } from '@/shared/constants/errors'; type Props = { children: ReactNode; @@ -29,21 +30,20 @@ class ErrorBoundary extends Component {
Summer in shock

- Seriously? You crashed the multiverse’s mainframe! + {ERROR_UI_STRINGS.heading}

- Like, this is lamer than Rick’s parenting or Morty’s entire - existence. Smash the button, or be a loser who looks stuff up! + {ERROR_UI_STRINGS.description}

); diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 408cae8..0d1de13 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,5 +1,6 @@ import { Component, type ReactNode } from 'react'; import { Button } from '@/components/Button'; +import { UI_STRINGS } from '@/shared/constants/ui-strings'; type FooterProps = { onThrowError: () => void; @@ -13,7 +14,7 @@ class Footer extends Component { className="text-red-500 cursor-pointer" onClick={this.props.onThrowError} > - Break the Universe + {UI_STRINGS.errorButton} ); diff --git a/src/components/SearchBar/SearchBar.tsx b/src/components/SearchBar/SearchBar.tsx index c7c6e0a..6635deb 100644 --- a/src/components/SearchBar/SearchBar.tsx +++ b/src/components/SearchBar/SearchBar.tsx @@ -1,6 +1,7 @@ import { Component, type ReactNode } from 'react'; import { Button } from '@/components/Button'; import { Input } from '@/components/Input'; +import { UI_STRINGS } from '@/shared/constants/ui-strings'; import { debug } from '@/shared/utils/debug-log'; import { searchStorage } from '@/shared/utils/local-storage'; @@ -52,7 +53,7 @@ export class SearchBar extends Component { value={this.state.searchText} onChange={this.handleInputChange} type="text" - placeholder={'Pickle? Prove it!'} + placeholder={UI_STRINGS.searchPlaceholder} disabled={loading} /> diff --git a/src/shared/constants/errors.ts b/src/shared/constants/errors.ts new file mode 100644 index 0000000..10dc868 --- /dev/null +++ b/src/shared/constants/errors.ts @@ -0,0 +1,8 @@ +export const ERROR_UI_STRINGS = { + imageAlt: 'Summer in shock', + heading: 'Seriously? You crashed the multiverse’s mainframe!', + description: + 'Like, this is lamer than Rick’s parenting or Morty’s entire existence. Smash the button, or be a loser who looks stuff up!', + buttonText: 'Reset the Multiverse', + notFound: 'Seriously? 404, multiverse not found!', +} as const; diff --git a/src/shared/constants/ui-strings.ts b/src/shared/constants/ui-strings.ts new file mode 100644 index 0000000..55a7863 --- /dev/null +++ b/src/shared/constants/ui-strings.ts @@ -0,0 +1,6 @@ +export const UI_STRINGS = { + searchPlaceholder: 'Pickle? Prove it!', + searchButton: 'Scan the Multiverse', + title: 'Rick and Morty', + errorButton: 'Break the Universe', +} as const; diff --git "a/src/shared/utils/fetch-\321\201haracters.ts" "b/src/shared/utils/fetch-\321\201haracters.ts" index 4d9384f..ab668a5 100644 --- "a/src/shared/utils/fetch-\321\201haracters.ts" +++ "b/src/shared/utils/fetch-\321\201haracters.ts" @@ -1,4 +1,5 @@ -import { API_ERROR_404, BASE_API_URL } from '@/shared/constants/api'; +import { BASE_API_URL } from '@/shared/constants/api'; +import { ERROR_UI_STRINGS } from '@/shared/constants/errors'; import type { CharacterApiResponse } from '@/types/character'; export async function fetchCharacters( @@ -10,7 +11,7 @@ export async function fetchCharacters( const response = await fetch(url); if (!response.ok) { - throw new Error(API_ERROR_404); + throw new Error(ERROR_UI_STRINGS.notFound); } return (await response.json()) as CharacterApiResponse; From c15439177ada463f26f56df293ec5a0bb7941f13 Mon Sep 17 00:00:00 2001 From: maiano Date: Mon, 14 Jul 2025 23:06:43 +0200 Subject: [PATCH 04/20] refactor: move Card UI strings to constants --- src/components/Card.tsx | 27 ++++++++++++++++++--------- src/shared/constants/api.ts | 1 - src/shared/constants/cards.ts | 15 +++++++++++++++ 3 files changed, 33 insertions(+), 10 deletions(-) create mode 100644 src/shared/constants/cards.ts diff --git a/src/components/Card.tsx b/src/components/Card.tsx index a847cce..90e4490 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -1,4 +1,5 @@ import { Component } from 'react'; +import { CARD_TEXT } from '@/shared/constants/cards'; import type { Character } from '@/types/character'; type CardProps = { @@ -14,11 +15,11 @@ export class Card extends Component { if (value === 'unknown') { switch (key) { case 'status': - return 'Glitchy life status'; + return CARD_TEXT.fallback.status; case 'gender': - return 'Weird identity'; + return CARD_TEXT.fallback.gender; default: - return `${key}: Rick broke it!`; + return CARD_TEXT.fallback.default(key); } } return value; @@ -37,17 +38,25 @@ export class Card extends Component { />

{name}

- Species Code: {species} - Live Status: {this.getValue('status', status)} - Identity: {this.getValue('gender', gender)} + + {CARD_TEXT.label.species}: {species} + + + {CARD_TEXT.label.status}: {this.getValue('status', status)} + + + {CARD_TEXT.label.gender}: {this.getValue('gender', gender)} + {origin.name !== 'unknown' ? ( - Native Dimension: {origin.name} + + {CARD_TEXT.label.origin}: {origin.name} + ) : ( - Origin lost, last spotted:{' '} + {CARD_TEXT.fallback.originFallback}:{' '} {location?.name && location.name !== 'unknown' ? location.name - : 'somewhere in the multiverse'} + : CARD_TEXT.fallback.locationUnknown} )}
diff --git a/src/shared/constants/api.ts b/src/shared/constants/api.ts index 69d8b34..92b30ee 100644 --- a/src/shared/constants/api.ts +++ b/src/shared/constants/api.ts @@ -1,2 +1 @@ export const BASE_API_URL = 'https://rickandmortyapi.com/api'; -export const API_ERROR_404 = 'Seriously? 404, multiverse not found!'; diff --git a/src/shared/constants/cards.ts b/src/shared/constants/cards.ts new file mode 100644 index 0000000..1d91309 --- /dev/null +++ b/src/shared/constants/cards.ts @@ -0,0 +1,15 @@ +export const CARD_TEXT = { + label: { + species: 'Species Code', + status: 'Live Status', + gender: 'Identity', + origin: 'Native Dimension', + }, + fallback: { + status: 'Glitchy life status', + gender: 'Weird identity', + originFallback: 'Origin lost, last spotted', + locationUnknown: 'somewhere in the multiverse', + default: (key: string) => `${key}: Rick broke it!`, + }, +} as const; From c8e1c57e4051e5a408be86223a2c5d72a042d9bf Mon Sep 17 00:00:00 2001 From: maiano Date: Mon, 14 Jul 2025 23:38:42 +0200 Subject: [PATCH 05/20] refactor: move fallback UI to separate component --- src/components/ErrorBoundary.tsx | 27 +++----------------------- src/components/FallBack.tsx | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 24 deletions(-) create mode 100644 src/components/FallBack.tsx diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index c5714d1..3d9c904 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -1,7 +1,6 @@ import { Component, type ReactNode, type ErrorInfo } from 'react'; -import summer from '@/assets/Rick-And-Morty-PNG-Pic-Background.png'; -import { Button } from '@/components/Button'; -import { ERROR_UI_STRINGS } from '@/shared/constants/errors'; +import summerImage from '@/assets/Rick-And-Morty-PNG-Pic-Background.png'; +import { FallBack } from '@/components/FallBack'; type Props = { children: ReactNode; @@ -26,27 +25,7 @@ class ErrorBoundary extends Component { render(): ReactNode { if (this.state.hasError) { - return ( -
- {ERROR_UI_STRINGS.imageAlt} -

- {ERROR_UI_STRINGS.heading} -

-

- {ERROR_UI_STRINGS.description} -

- -
- ); + return ; } return this.props.children; diff --git a/src/components/FallBack.tsx b/src/components/FallBack.tsx new file mode 100644 index 0000000..71f37c3 --- /dev/null +++ b/src/components/FallBack.tsx @@ -0,0 +1,33 @@ +import { Component, type ReactNode } from 'react'; +import { Button } from '@/components/Button'; +import { ERROR_UI_STRINGS } from '@/shared/constants/errors'; + +type Props = { + imageSrc: string; +}; + +export class FallBack extends Component { + render(): ReactNode { + return ( +
+ {ERROR_UI_STRINGS.imageAlt} +

+ {ERROR_UI_STRINGS.heading} +

+

+ {ERROR_UI_STRINGS.description} +

+ +
+ ); + } +} From 011cbed3af936a0b4db751b6ba1cea76419b0d21 Mon Sep 17 00:00:00 2001 From: maiano Date: Tue, 15 Jul 2025 00:25:03 +0200 Subject: [PATCH 06/20] refactor: move search term persistence to HomePage and rename string variables --- src/components/Header.tsx | 3 ++- src/components/SearchBar/SearchBar.tsx | 24 ++++++++++----------- src/pages/HomePage.tsx | 30 +++++++++++++++----------- 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 3b82b85..6d7c81c 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,5 +1,6 @@ import { Component, type ReactNode } from 'react'; import logo from '@/assets/rick-and-morty-sticker-b-w.webp'; +import { UI_STRINGS } from '@/shared/constants/ui-strings'; class Header extends Component { render(): ReactNode { @@ -7,7 +8,7 @@ class Header extends Component {
Logo

- Rick and Morty + {UI_STRINGS.title}

); diff --git a/src/components/SearchBar/SearchBar.tsx b/src/components/SearchBar/SearchBar.tsx index 6635deb..81f6c0c 100644 --- a/src/components/SearchBar/SearchBar.tsx +++ b/src/components/SearchBar/SearchBar.tsx @@ -3,33 +3,31 @@ import { Button } from '@/components/Button'; import { Input } from '@/components/Input'; import { UI_STRINGS } from '@/shared/constants/ui-strings'; import { debug } from '@/shared/utils/debug-log'; -import { searchStorage } from '@/shared/utils/local-storage'; type Props = { onSearch: (text: string) => void; - loading?: boolean; - term: string; + isLoading?: boolean; + searchQuery: string; }; type State = { - searchText: string; + inputValue: string; }; export class SearchBar extends Component { state: State = { - searchText: this.props.term, + inputValue: this.props.searchQuery, }; handleInputChange = (e: React.ChangeEvent) => { - this.setState({ searchText: e.target.value }); + this.setState({ inputValue: e.target.value }); }; handleSearch = () => { - const text = this.state.searchText.trim(); - searchStorage.set(text); + const text = this.state.inputValue.trim(); debug('search string:', text); this.props.onSearch(text); - this.setState({ searchText: text }); + this.setState({ inputValue: text }); }; handleSubmit = (e: React.FormEvent) => { @@ -38,7 +36,7 @@ export class SearchBar extends Component { }; render(): ReactNode { - const { loading } = this.props; + const { isLoading } = this.props; return (
@@ -50,16 +48,16 @@ export class SearchBar extends Component { name="search" id="search" autoFocus - value={this.state.searchText} + value={this.state.inputValue} onChange={this.handleInputChange} type="text" placeholder={UI_STRINGS.searchPlaceholder} - disabled={loading} + disabled={isLoading} /> diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 0216cd5..d051645 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -11,29 +11,29 @@ import type { ApiInfo, Character } from '@/types/character'; type State = { info: ApiInfo | null; characters: Character[]; - loading: boolean; + isLoading: boolean; error: string | null; page: number; - term: string; + searchQuery: string; }; class HomePage extends Component { state: State = { info: null, characters: [], - loading: false, + isLoading: false, error: null, page: 1, - term: searchStorage.get(), + searchQuery: searchStorage.get(), }; componentDidMount(): void { - this.fetchCharacters(this.state.term); + this.fetchCharacters(this.state.searchQuery); } fetchCharacters = async (search: string, page = 1) => { try { - this.setState({ loading: true, error: null }); + this.setState({ isLoading: true, error: null }); const data = await fetchCharacters(search, page); @@ -49,12 +49,13 @@ class HomePage extends Component { }); } finally { setTimeout(() => { - this.setState({ loading: false }); + this.setState({ isLoading: false }); }, 200); } }; handleSearch = (text: string) => { + searchStorage.set(text); this.fetchCharacters(text); }; @@ -62,22 +63,27 @@ class HomePage extends Component { this.fetchCharacters(searchStorage.get(), page); render(): ReactNode { - const { characters, loading, error, page, info, term } = this.state; + const { characters, isLoading, error, page, info, searchQuery } = + this.state; return (
- + Loading - - {loading || error ? ( + + {isLoading || error ? (

{error}

) : ( )} - {!loading && !error && ( + {!isLoading && !error && ( Date: Tue, 15 Jul 2025 00:40:29 +0200 Subject: [PATCH 07/20] refactor: move alt texts to UI_STRINGS constants --- src/components/Header.tsx | 6 +++++- src/pages/HomePage.tsx | 7 ++++++- src/shared/constants/ui-strings.ts | 2 ++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 6d7c81c..5400a87 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -6,7 +6,11 @@ class Header extends Component { render(): ReactNode { return (
- Logo + {UI_STRINGS.altLogo}

{UI_STRINGS.title}

diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index d051645..a7222d9 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -4,6 +4,7 @@ import { CardList } from '@/components/CardList'; import { LoadingOverlay } from '@/components/LoadingOverlay'; import { Pagination } from '@/components/Pagination'; import { SearchBar } from '@/components/SearchBar'; +import { UI_STRINGS } from '@/shared/constants/ui-strings'; import { fetchCharacters } from '@/shared/utils/fetch-сharacters'; import { searchStorage } from '@/shared/utils/local-storage'; import type { ApiInfo, Character } from '@/types/character'; @@ -69,7 +70,11 @@ class HomePage extends Component { return (
- Loading + {UI_STRINGS.altLoading} Date: Tue, 15 Jul 2025 00:52:59 +0200 Subject: [PATCH 08/20] fix: tests for SearchBar component --- src/components/SearchBar/SearchBar.test.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/SearchBar/SearchBar.test.tsx b/src/components/SearchBar/SearchBar.test.tsx index cc1c753..c8fb762 100644 --- a/src/components/SearchBar/SearchBar.test.tsx +++ b/src/components/SearchBar/SearchBar.test.tsx @@ -6,7 +6,12 @@ describe('SearchBar component', () => { const setup = (props = {}) => { const onSearch = vi.fn(); const utils = render( - , + , ); const input = screen.getByPlaceholderText(/pickle/i); @@ -27,7 +32,7 @@ describe('SearchBar component', () => { }); it('renders initial term in input', () => { - const { input } = setup({ term: 'Morty' }); + const { input } = setup({ searchQuery: 'Morty' }); expect(input).toHaveValue('Morty'); }); @@ -45,7 +50,7 @@ describe('SearchBar component', () => { }); it('disables input and button when loading is true', () => { - const { input, button } = setup({ loading: true }); + const { input, button } = setup({ isLoading: true }); expect(input).toBeDisabled(); expect(button).toBeDisabled(); }); From 32d26b11c365f4786c54c7c6bda6f4add7de6cdf Mon Sep 17 00:00:00 2001 From: maiano Date: Tue, 15 Jul 2025 16:37:11 +0200 Subject: [PATCH 09/20] test: add tests for CardList component --- README.md | 2 +- src/components/CardList/CardList.test.tsx | 28 +++++++++++++++++++ src/components/{ => CardList}/CardList.tsx | 2 +- src/components/CardList/index.ts | 1 + .../mockCharacters.ts} | 0 5 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 src/components/CardList/CardList.test.tsx rename src/components/{ => CardList}/CardList.tsx (93%) create mode 100644 src/components/CardList/index.ts rename src/{mocks/characters.ts => test-utils/mockCharacters.ts} (100%) diff --git a/README.md b/README.md index 8d4fea0..c453d85 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ > [!NOTE] > To get started with the development: > -> 1. Clone the repository using `git clone https://github.com/maiano/eCommerce-Application.git`. +> 1. Clone the repository using `git clone https://github.com/maiano/react-2025.git`. > 2. Install dependencies using `npm install`. > 3. Run the development server using `npm run dev`. > 4. Build the project using `npm run build`. diff --git a/src/components/CardList/CardList.test.tsx b/src/components/CardList/CardList.test.tsx new file mode 100644 index 0000000..e961e9f --- /dev/null +++ b/src/components/CardList/CardList.test.tsx @@ -0,0 +1,28 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import { CardList } from './CardList'; +import { mockCharacters } from '@/test-utils/mockCharacters'; +import type { Character } from '@/types/character'; + +vi.mock('@/components/Card.tsx', () => ({ + Card: ({ character }: { character: Character }) => ( +
{character.name}
+ ), +})); + +describe('test CardList', () => { + const mockItems = mockCharacters.results.slice(1, 3) as Character[]; + + it('renders correct number of cards', () => { + render(); + const cards = screen.getAllByTestId('card'); + expect(cards).toHaveLength(2); + expect(cards[0]).toHaveTextContent('Black Rick'); + expect(cards[1]).toHaveTextContent('Cool Rick'); + }); + + it('renders "no results"', () => { + render(); + expect(screen.getByText(/no results/i)).toBeInTheDocument(); + }); +}); diff --git a/src/components/CardList.tsx b/src/components/CardList/CardList.tsx similarity index 93% rename from src/components/CardList.tsx rename to src/components/CardList/CardList.tsx index c0ed1d2..c1dde9a 100644 --- a/src/components/CardList.tsx +++ b/src/components/CardList/CardList.tsx @@ -1,5 +1,5 @@ import { Component } from 'react'; -import { Card } from './Card'; +import { Card } from '@/components/Card'; import type { Character } from '@/types/character'; type CardListProps = { diff --git a/src/components/CardList/index.ts b/src/components/CardList/index.ts new file mode 100644 index 0000000..c6cc75c --- /dev/null +++ b/src/components/CardList/index.ts @@ -0,0 +1 @@ +export { CardList } from './CardList'; diff --git a/src/mocks/characters.ts b/src/test-utils/mockCharacters.ts similarity index 100% rename from src/mocks/characters.ts rename to src/test-utils/mockCharacters.ts From 5683628cdb65753e13bee8d387500222fee1e205 Mon Sep 17 00:00:00 2001 From: maiano Date: Tue, 15 Jul 2025 18:32:33 +0200 Subject: [PATCH 10/20] test: add tests for Card component --- src/components/Card/Card.test.tsx | 61 +++++++++++++++++++++++ src/components/{ => Card}/Card.tsx | 5 +- src/components/Card/CardText.tsx | 3 ++ src/components/Card/index.ts | 1 + src/components/CardList/CardList.test.tsx | 17 +++---- 5 files changed, 72 insertions(+), 15 deletions(-) create mode 100644 src/components/Card/Card.test.tsx rename src/components/{ => Card}/Card.tsx (92%) create mode 100644 src/components/Card/CardText.tsx create mode 100644 src/components/Card/index.ts diff --git a/src/components/Card/Card.test.tsx b/src/components/Card/Card.test.tsx new file mode 100644 index 0000000..e38bf01 --- /dev/null +++ b/src/components/Card/Card.test.tsx @@ -0,0 +1,61 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import { Card } from './Card'; +import { CARD_TEXT } from '@/shared/constants/cards'; +import { mockCharacters } from '@/test-utils/mockCharacters'; +import type { Character } from '@/types/character'; + +const baseCharacter = mockCharacters.results[0] as Character; + +describe('Card', () => { + it('renders character name and basic info', () => { + render(); + expect(screen.getByText(baseCharacter.name)).toBeInTheDocument(); + expect( + screen.getByText(`${CARD_TEXT.label.species}: ${baseCharacter.species}`), + ).toBeInTheDocument(); + expect( + screen.getByText(`${CARD_TEXT.label.status}: ${baseCharacter.status}`), + ).toBeInTheDocument(); + expect( + screen.getByText(`${CARD_TEXT.label.gender}: ${baseCharacter.gender}`), + ).toBeInTheDocument(); + expect( + screen.getByText( + `${CARD_TEXT.label.origin}: ${baseCharacter.origin.name}`, + ), + ).toBeInTheDocument(); + }); + + it('handles unknown status and gender gracefully', () => { + const unknownCharacter = { + ...baseCharacter, + status: 'unknown', + gender: 'unknown', + } as Character; + render(); + expect( + screen.getByText( + `${CARD_TEXT.label.status}: ${CARD_TEXT.fallback.status}`, + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + `${CARD_TEXT.label.gender}: ${CARD_TEXT.fallback.gender}`, + ), + ).toBeInTheDocument(); + }); + + it('falls back to location when origin is unknown', () => { + const noOriginCharacter = { + ...baseCharacter, + origin: { name: 'unknown', url: '' }, + }; + render(); + expect( + screen.getByText( + `${CARD_TEXT.fallback.originFallback}: ${baseCharacter.location.name}`, + ), + ).toBeInTheDocument(); + }); +}); diff --git a/src/components/Card.tsx b/src/components/Card/Card.tsx similarity index 92% rename from src/components/Card.tsx rename to src/components/Card/Card.tsx index 90e4490..4260883 100644 --- a/src/components/Card.tsx +++ b/src/components/Card/Card.tsx @@ -1,4 +1,5 @@ import { Component } from 'react'; +import { CardText } from './CardText'; import { CARD_TEXT } from '@/shared/constants/cards'; import type { Character } from '@/types/character'; @@ -6,10 +7,6 @@ type CardProps = { character: Character; }; -const CardText = ({ children }: { children: React.ReactNode }) => ( -

{children}

-); - export class Card extends Component { getValue(key: string, value: string): string { if (value === 'unknown') { diff --git a/src/components/Card/CardText.tsx b/src/components/Card/CardText.tsx new file mode 100644 index 0000000..6c419fd --- /dev/null +++ b/src/components/Card/CardText.tsx @@ -0,0 +1,3 @@ +export const CardText = ({ children }: { children: React.ReactNode }) => ( +

{children}

+); diff --git a/src/components/Card/index.ts b/src/components/Card/index.ts new file mode 100644 index 0000000..1e15afd --- /dev/null +++ b/src/components/Card/index.ts @@ -0,0 +1 @@ +export { Card } from './Card'; diff --git a/src/components/CardList/CardList.test.tsx b/src/components/CardList/CardList.test.tsx index e961e9f..c458473 100644 --- a/src/components/CardList/CardList.test.tsx +++ b/src/components/CardList/CardList.test.tsx @@ -1,24 +1,19 @@ import { render, screen } from '@testing-library/react'; -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { CardList } from './CardList'; import { mockCharacters } from '@/test-utils/mockCharacters'; import type { Character } from '@/types/character'; -vi.mock('@/components/Card.tsx', () => ({ - Card: ({ character }: { character: Character }) => ( -
{character.name}
- ), -})); - describe('test CardList', () => { const mockItems = mockCharacters.results.slice(1, 3) as Character[]; it('renders correct number of cards', () => { render(); - const cards = screen.getAllByTestId('card'); - expect(cards).toHaveLength(2); - expect(cards[0]).toHaveTextContent('Black Rick'); - expect(cards[1]).toHaveTextContent('Cool Rick'); + + expect(screen.getByText('Black Rick')).toBeInTheDocument(); + expect(screen.getByText('Cool Rick')).toBeInTheDocument(); + + expect(screen.getAllByText(/species/i)).toHaveLength(2); }); it('renders "no results"', () => { From 4d2e4c8ebe2c940fd7d98fb18f44937e8b8ce109 Mon Sep 17 00:00:00 2001 From: maiano Date: Tue, 15 Jul 2025 23:15:55 +0200 Subject: [PATCH 11/20] refactor: split error into hasError and errorMessage --- src/pages/HomePage.tsx | 31 +++++++++++++++++++++---------- src/shared/constants/errors.ts | 1 + 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index a7222d9..edf8a9f 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -4,6 +4,7 @@ import { CardList } from '@/components/CardList'; import { LoadingOverlay } from '@/components/LoadingOverlay'; import { Pagination } from '@/components/Pagination'; import { SearchBar } from '@/components/SearchBar'; +import { ERROR_UI_STRINGS } from '@/shared/constants/errors'; import { UI_STRINGS } from '@/shared/constants/ui-strings'; import { fetchCharacters } from '@/shared/utils/fetch-сharacters'; import { searchStorage } from '@/shared/utils/local-storage'; @@ -13,7 +14,8 @@ type State = { info: ApiInfo | null; characters: Character[]; isLoading: boolean; - error: string | null; + hasError: boolean; + errorMessage: string | null; page: number; searchQuery: string; }; @@ -23,7 +25,8 @@ class HomePage extends Component { info: null, characters: [], isLoading: false, - error: null, + hasError: false, + errorMessage: null, page: 1, searchQuery: searchStorage.get(), }; @@ -33,9 +36,9 @@ class HomePage extends Component { } fetchCharacters = async (search: string, page = 1) => { - try { - this.setState({ isLoading: true, error: null }); + this.setState({ isLoading: true, hasError: false, errorMessage: null }); + try { const data = await fetchCharacters(search, page); this.setState({ @@ -45,7 +48,8 @@ class HomePage extends Component { }); } catch (error) { this.setState({ - error: (error as Error).message, + hasError: true, + errorMessage: (error as Error).message, characters: [], }); } finally { @@ -64,8 +68,15 @@ class HomePage extends Component { this.fetchCharacters(searchStorage.get(), page); render(): ReactNode { - const { characters, isLoading, error, page, info, searchQuery } = - this.state; + const { + characters, + isLoading, + hasError, + errorMessage, + page, + info, + searchQuery, + } = this.state; return (
@@ -81,14 +92,14 @@ class HomePage extends Component { onSearch={this.handleSearch} isLoading={isLoading} /> - {isLoading || error ? ( + {isLoading ? null : hasError ? (

- {error} + {errorMessage ?? ERROR_UI_STRINGS.unknownError}

) : ( )} - {!isLoading && !error && ( + {!isLoading && !hasError && ( Date: Wed, 16 Jul 2025 11:29:21 +0200 Subject: [PATCH 12/20] test: add tests for HomePage --- src/App.tsx | 2 +- src/components/Card/Card.test.tsx | 2 +- src/components/CardList/CardList.test.tsx | 2 +- src/components/ErrorBoundary.tsx | 5 +- src/components/FallBack.tsx | 9 +- src/components/SearchBar/SearchBar.tsx | 6 + src/main.tsx | 3 +- src/pages/HomePage/HomePage.test.tsx | 71 ++++ src/pages/{ => HomePage}/HomePage.tsx | 4 +- src/pages/HomePage/index.ts | 1 + src/test-utils/mockCharacters.ts | 468 ---------------------- src/tests/mockCharacters.ts | 168 ++++++++ vite.config.ts | 4 +- 13 files changed, 259 insertions(+), 486 deletions(-) create mode 100644 src/pages/HomePage/HomePage.test.tsx rename src/pages/{ => HomePage}/HomePage.tsx (97%) create mode 100644 src/pages/HomePage/index.ts delete mode 100644 src/test-utils/mockCharacters.ts create mode 100644 src/tests/mockCharacters.ts diff --git a/src/App.tsx b/src/App.tsx index c047bf2..f785afa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import { Component, type ReactNode } from 'react'; import Footer from '@/components/Footer'; import Header from '@/components/Header'; -import HomePage from '@/pages/HomePage'; +import { HomePage } from '@/pages/HomePage'; type AppState = { wouldThrow: boolean; diff --git a/src/components/Card/Card.test.tsx b/src/components/Card/Card.test.tsx index e38bf01..5502e7a 100644 --- a/src/components/Card/Card.test.tsx +++ b/src/components/Card/Card.test.tsx @@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react'; import { describe, it, expect } from 'vitest'; import { Card } from './Card'; import { CARD_TEXT } from '@/shared/constants/cards'; -import { mockCharacters } from '@/test-utils/mockCharacters'; +import { mockCharacters } from '@/tests/mockCharacters'; import type { Character } from '@/types/character'; const baseCharacter = mockCharacters.results[0] as Character; diff --git a/src/components/CardList/CardList.test.tsx b/src/components/CardList/CardList.test.tsx index c458473..c22abec 100644 --- a/src/components/CardList/CardList.test.tsx +++ b/src/components/CardList/CardList.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import { describe, it, expect } from 'vitest'; import { CardList } from './CardList'; -import { mockCharacters } from '@/test-utils/mockCharacters'; +import { mockCharacters } from '@/tests/mockCharacters'; import type { Character } from '@/types/character'; describe('test CardList', () => { diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 3d9c904..796e58e 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -1,9 +1,8 @@ import { Component, type ReactNode, type ErrorInfo } from 'react'; -import summerImage from '@/assets/Rick-And-Morty-PNG-Pic-Background.png'; -import { FallBack } from '@/components/FallBack'; type Props = { children: ReactNode; + fallback?: React.ReactElement; }; type State = { @@ -25,7 +24,7 @@ class ErrorBoundary extends Component { render(): ReactNode { if (this.state.hasError) { - return ; + return this.props.fallback ?? null; } return this.props.children; diff --git a/src/components/FallBack.tsx b/src/components/FallBack.tsx index 71f37c3..f4572b4 100644 --- a/src/components/FallBack.tsx +++ b/src/components/FallBack.tsx @@ -1,17 +1,14 @@ import { Component, type ReactNode } from 'react'; +import summerImage from '@/assets/Rick-And-Morty-PNG-Pic-Background.png'; import { Button } from '@/components/Button'; import { ERROR_UI_STRINGS } from '@/shared/constants/errors'; -type Props = { - imageSrc: string; -}; - -export class FallBack extends Component { +export class FallBack extends Component { render(): ReactNode { return (
{ERROR_UI_STRINGS.imageAlt} diff --git a/src/components/SearchBar/SearchBar.tsx b/src/components/SearchBar/SearchBar.tsx index 81f6c0c..fb394b4 100644 --- a/src/components/SearchBar/SearchBar.tsx +++ b/src/components/SearchBar/SearchBar.tsx @@ -19,6 +19,12 @@ export class SearchBar extends Component { inputValue: this.props.searchQuery, }; + componentDidUpdate(prevProps: Props) { + if (prevProps.searchQuery !== this.props.searchQuery) { + this.setState({ inputValue: this.props.searchQuery }); + } + } + handleInputChange = (e: React.ChangeEvent) => { this.setState({ inputValue: e.target.value }); }; diff --git a/src/main.tsx b/src/main.tsx index 4f44b04..59d2066 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,11 +3,12 @@ import { createRoot } from 'react-dom/client'; import './index.css'; import App from './App.tsx'; import ErrorBoundary from '@/components/ErrorBoundary.tsx'; +import { FallBack } from '@/components/FallBack.tsx'; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion createRoot(document.getElementById('root')!).render( - + }> , diff --git a/src/pages/HomePage/HomePage.test.tsx b/src/pages/HomePage/HomePage.test.tsx new file mode 100644 index 0000000..a779017 --- /dev/null +++ b/src/pages/HomePage/HomePage.test.tsx @@ -0,0 +1,71 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { HomePage } from './HomePage'; +import { fetchCharacters } from '@/shared/utils/fetch-сharacters'; +import { searchStorage } from '@/shared/utils/local-storage'; +import { mockCharacters } from '@/tests/mockCharacters'; +import type { CharacterApiResponse } from '@/types/character'; + +vi.mock('@/shared/utils/fetch-сharacters'); +vi.mock('@/shared/utils/local-storage', () => ({ + searchStorage: { + get: vi.fn(() => ''), + set: vi.fn(), + }, +})); + +const mockResponse = mockCharacters as CharacterApiResponse; + +const renderHomePage = () => render(); +const mockSuccessResponse = () => + vi.mocked(fetchCharacters).mockResolvedValue(mockResponse); + +describe('HomePage tests', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders characters from API on mount', async () => { + mockSuccessResponse(); + renderHomePage(); + + expect(screen.getByAltText(/loading/i)).toBeInTheDocument(); + + await waitFor(() => { + expect( + screen.getByText(mockResponse.results[0].name), + ).toBeInTheDocument(); + }); + }); + + it('handles API error', async () => { + vi.mocked(fetchCharacters).mockRejectedValue(new Error('network error')); + + renderHomePage(); + + await waitFor(() => { + expect(screen.getByText(/network error/i)).toBeInTheDocument(); + }); + }); + + it('shows spinner', async () => { + mockSuccessResponse(); + renderHomePage(); + + expect(screen.getByAltText(/loading/i)).toBeInTheDocument(); + + await waitFor(() => { + expect(screen.queryByAltText(/loading/i)).not.toBeInTheDocument(); + }); + }); + + it('loads initial search query from localStorage', async () => { + vi.mocked(searchStorage.get).mockReturnValue('Summer'); + mockSuccessResponse(); + renderHomePage(); + + await waitFor(() => { + expect(fetchCharacters).toHaveBeenCalledWith('Summer', 1); + }); + }); +}); diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage/HomePage.tsx similarity index 97% rename from src/pages/HomePage.tsx rename to src/pages/HomePage/HomePage.tsx index edf8a9f..ebf509f 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage/HomePage.tsx @@ -20,7 +20,7 @@ type State = { searchQuery: string; }; -class HomePage extends Component { +export class HomePage extends Component { state: State = { info: null, characters: [], @@ -111,5 +111,3 @@ class HomePage extends Component { ); } } - -export default HomePage; diff --git a/src/pages/HomePage/index.ts b/src/pages/HomePage/index.ts new file mode 100644 index 0000000..0799f47 --- /dev/null +++ b/src/pages/HomePage/index.ts @@ -0,0 +1 @@ +export { HomePage } from './HomePage'; diff --git a/src/test-utils/mockCharacters.ts b/src/test-utils/mockCharacters.ts deleted file mode 100644 index d4ddb31..0000000 --- a/src/test-utils/mockCharacters.ts +++ /dev/null @@ -1,468 +0,0 @@ -export const mockCharacters = { - info: { - count: 29, - pages: 2, - next: 'https://rickandmortyapi.com/api/character/?page=2&name=rick&status=alive', - prev: null, - }, - results: [ - { - id: 1, - name: 'Rick Sanchez', - status: 'Alive', - species: 'Human', - type: '', - gender: 'Male', - origin: { - name: 'Earth (C-137)', - url: 'https://rickandmortyapi.com/api/location/1', - }, - location: { - name: 'Citadel of Ricks', - url: 'https://rickandmortyapi.com/api/location/3', - }, - image: 'https://rickandmortyapi.com/api/character/avatar/1.jpeg', - episode: [ - 'https://rickandmortyapi.com/api/episode/1', - 'https://rickandmortyapi.com/api/episode/2', - 'https://rickandmortyapi.com/api/episode/3', - 'https://rickandmortyapi.com/api/episode/4', - 'https://rickandmortyapi.com/api/episode/5', - 'https://rickandmortyapi.com/api/episode/6', - 'https://rickandmortyapi.com/api/episode/7', - 'https://rickandmortyapi.com/api/episode/8', - 'https://rickandmortyapi.com/api/episode/9', - 'https://rickandmortyapi.com/api/episode/10', - 'https://rickandmortyapi.com/api/episode/11', - 'https://rickandmortyapi.com/api/episode/12', - 'https://rickandmortyapi.com/api/episode/13', - 'https://rickandmortyapi.com/api/episode/14', - 'https://rickandmortyapi.com/api/episode/15', - 'https://rickandmortyapi.com/api/episode/16', - 'https://rickandmortyapi.com/api/episode/17', - 'https://rickandmortyapi.com/api/episode/18', - 'https://rickandmortyapi.com/api/episode/19', - 'https://rickandmortyapi.com/api/episode/20', - 'https://rickandmortyapi.com/api/episode/21', - 'https://rickandmortyapi.com/api/episode/22', - 'https://rickandmortyapi.com/api/episode/23', - 'https://rickandmortyapi.com/api/episode/24', - 'https://rickandmortyapi.com/api/episode/25', - 'https://rickandmortyapi.com/api/episode/26', - 'https://rickandmortyapi.com/api/episode/27', - 'https://rickandmortyapi.com/api/episode/28', - 'https://rickandmortyapi.com/api/episode/29', - 'https://rickandmortyapi.com/api/episode/30', - 'https://rickandmortyapi.com/api/episode/31', - 'https://rickandmortyapi.com/api/episode/32', - 'https://rickandmortyapi.com/api/episode/33', - 'https://rickandmortyapi.com/api/episode/34', - 'https://rickandmortyapi.com/api/episode/35', - 'https://rickandmortyapi.com/api/episode/36', - 'https://rickandmortyapi.com/api/episode/37', - 'https://rickandmortyapi.com/api/episode/38', - 'https://rickandmortyapi.com/api/episode/39', - 'https://rickandmortyapi.com/api/episode/40', - 'https://rickandmortyapi.com/api/episode/41', - 'https://rickandmortyapi.com/api/episode/42', - 'https://rickandmortyapi.com/api/episode/43', - 'https://rickandmortyapi.com/api/episode/44', - 'https://rickandmortyapi.com/api/episode/45', - 'https://rickandmortyapi.com/api/episode/46', - 'https://rickandmortyapi.com/api/episode/47', - 'https://rickandmortyapi.com/api/episode/48', - 'https://rickandmortyapi.com/api/episode/49', - 'https://rickandmortyapi.com/api/episode/50', - 'https://rickandmortyapi.com/api/episode/51', - ], - url: 'https://rickandmortyapi.com/api/character/1', - created: '2017-11-04T18:48:46.250Z', - }, - { - id: 48, - name: 'Black Rick', - status: 'Alive', - species: 'Human', - type: '', - gender: 'Male', - origin: { - name: 'unknown', - url: '', - }, - location: { - name: 'Citadel of Ricks', - url: 'https://rickandmortyapi.com/api/location/3', - }, - image: 'https://rickandmortyapi.com/api/character/avatar/48.jpeg', - episode: [ - 'https://rickandmortyapi.com/api/episode/22', - 'https://rickandmortyapi.com/api/episode/28', - ], - url: 'https://rickandmortyapi.com/api/character/48', - created: '2017-11-05T11:15:26.044Z', - }, - { - id: 72, - name: 'Cool Rick', - status: 'Alive', - species: 'Human', - type: '', - gender: 'Male', - origin: { - name: 'Earth (K-83)', - url: 'https://rickandmortyapi.com/api/location/26', - }, - location: { - name: 'Citadel of Ricks', - url: 'https://rickandmortyapi.com/api/location/3', - }, - image: 'https://rickandmortyapi.com/api/character/avatar/72.jpeg', - episode: ['https://rickandmortyapi.com/api/episode/28'], - url: 'https://rickandmortyapi.com/api/character/72', - created: '2017-11-30T11:41:11.542Z', - }, - { - id: 74, - name: 'Cop Rick', - status: 'Alive', - species: 'Human', - type: '', - gender: 'Male', - origin: { - name: 'unknown', - url: '', - }, - location: { - name: 'Citadel of Ricks', - url: 'https://rickandmortyapi.com/api/location/3', - }, - image: 'https://rickandmortyapi.com/api/character/avatar/74.jpeg', - episode: ['https://rickandmortyapi.com/api/episode/28'], - url: 'https://rickandmortyapi.com/api/character/74', - created: '2017-11-30T11:48:18.950Z', - }, - { - id: 78, - name: 'Cowboy Rick', - status: 'Alive', - species: 'Human', - type: '', - gender: 'Male', - origin: { - name: 'unknown', - url: '', - }, - location: { - name: 'Citadel of Ricks', - url: 'https://rickandmortyapi.com/api/location/3', - }, - image: 'https://rickandmortyapi.com/api/character/avatar/78.jpeg', - episode: [ - 'https://rickandmortyapi.com/api/episode/10', - 'https://rickandmortyapi.com/api/episode/28', - ], - url: 'https://rickandmortyapi.com/api/character/78', - created: '2017-11-30T14:15:18.347Z', - }, - { - id: 220, - name: 'Mega Fruit Farmer Rick', - status: 'Alive', - species: 'Human', - type: '', - gender: 'Male', - origin: { - name: 'unknown', - url: '', - }, - location: { - name: 'Citadel of Ricks', - url: 'https://rickandmortyapi.com/api/location/3', - }, - image: 'https://rickandmortyapi.com/api/character/avatar/220.jpeg', - episode: ['https://rickandmortyapi.com/api/episode/28'], - url: 'https://rickandmortyapi.com/api/character/220', - created: '2017-12-30T14:35:30.736Z', - }, - { - id: 265, - name: 'Pickle Rick', - status: 'Alive', - species: 'unknown', - type: 'Pickle', - gender: 'Male', - origin: { - name: 'Earth (C-137)', - url: 'https://rickandmortyapi.com/api/location/1', - }, - location: { - name: 'Earth (Replacement Dimension)', - url: 'https://rickandmortyapi.com/api/location/20', - }, - image: 'https://rickandmortyapi.com/api/character/avatar/265.jpeg', - episode: ['https://rickandmortyapi.com/api/episode/24'], - url: 'https://rickandmortyapi.com/api/character/265', - created: '2017-12-31T13:47:10.617Z', - }, - { - id: 267, - name: 'Plumber Rick', - status: 'Alive', - species: 'Human', - type: '', - gender: 'Male', - origin: { - name: 'unknown', - url: '', - }, - location: { - name: 'Citadel of Ricks', - url: 'https://rickandmortyapi.com/api/location/3', - }, - image: 'https://rickandmortyapi.com/api/character/avatar/267.jpeg', - episode: ['https://rickandmortyapi.com/api/episode/28'], - url: 'https://rickandmortyapi.com/api/character/267', - created: '2017-12-31T13:50:57.337Z', - }, - { - id: 288, - name: 'Rick D716-B', - status: 'Alive', - species: 'Human', - type: '', - gender: 'Male', - origin: { - name: 'Earth (D716-B)', - url: 'https://rickandmortyapi.com/api/location/60', - }, - location: { - name: 'Citadel of Ricks', - url: 'https://rickandmortyapi.com/api/location/3', - }, - image: 'https://rickandmortyapi.com/api/character/avatar/288.jpeg', - episode: ['https://rickandmortyapi.com/api/episode/28'], - url: 'https://rickandmortyapi.com/api/character/288', - created: '2017-12-31T19:55:25.101Z', - }, - { - id: 289, - name: 'Rick D716-C', - status: 'Alive', - species: 'Human', - type: '', - gender: 'Male', - origin: { - name: 'Earth (D716-C)', - url: 'https://rickandmortyapi.com/api/location/61', - }, - location: { - name: 'Citadel of Ricks', - url: 'https://rickandmortyapi.com/api/location/3', - }, - image: 'https://rickandmortyapi.com/api/character/avatar/289.jpeg', - episode: ['https://rickandmortyapi.com/api/episode/28'], - url: 'https://rickandmortyapi.com/api/character/289', - created: '2017-12-31T19:57:36.546Z', - }, - { - id: 291, - name: 'Rick J-22', - status: 'Alive', - species: 'Human', - type: '', - gender: 'Male', - origin: { - name: 'Earth (J-22)', - url: 'https://rickandmortyapi.com/api/location/62', - }, - location: { - name: 'Citadel of Ricks', - url: 'https://rickandmortyapi.com/api/location/3', - }, - image: 'https://rickandmortyapi.com/api/character/avatar/291.jpeg', - episode: ['https://rickandmortyapi.com/api/episode/28'], - url: 'https://rickandmortyapi.com/api/character/291', - created: '2017-12-31T20:16:52.337Z', - }, - { - id: 292, - name: 'Rick K-22', - status: 'Alive', - species: 'Human', - type: '', - gender: 'Male', - origin: { - name: 'Earth (K-22)', - url: 'https://rickandmortyapi.com/api/location/52', - }, - location: { - name: 'Earth (Replacement Dimension)', - url: 'https://rickandmortyapi.com/api/location/20', - }, - image: 'https://rickandmortyapi.com/api/character/avatar/292.jpeg', - episode: ['https://rickandmortyapi.com/api/episode/28'], - url: 'https://rickandmortyapi.com/api/character/292', - created: '2017-12-31T20:20:40.484Z', - }, - { - id: 328, - name: 'Slow Rick', - status: 'Alive', - species: 'Human', - type: '', - gender: 'Male', - origin: { - name: 'unknown', - url: '', - }, - location: { - name: 'Citadel of Ricks', - url: 'https://rickandmortyapi.com/api/location/3', - }, - image: 'https://rickandmortyapi.com/api/character/avatar/328.jpeg', - episode: ['https://rickandmortyapi.com/api/episode/28'], - url: 'https://rickandmortyapi.com/api/character/328', - created: '2018-01-10T16:14:16.331Z', - }, - { - id: 345, - name: 'Teacher Rick', - status: 'Alive', - species: 'Human', - type: '', - gender: 'Male', - origin: { - name: 'unknown', - url: '', - }, - location: { - name: 'Citadel of Ricks', - url: 'https://rickandmortyapi.com/api/location/3', - }, - image: 'https://rickandmortyapi.com/api/character/avatar/345.jpeg', - episode: ['https://rickandmortyapi.com/api/episode/28'], - url: 'https://rickandmortyapi.com/api/character/345', - created: '2018-01-10T17:33:23.437Z', - }, - { - id: 381, - name: 'Woman Rick', - status: 'Alive', - species: 'Alien', - type: 'Chair', - gender: 'Female', - origin: { - name: 'unknown', - url: '', - }, - location: { - name: 'unknown', - url: '', - }, - image: 'https://rickandmortyapi.com/api/character/avatar/381.jpeg', - episode: ['https://rickandmortyapi.com/api/episode/10'], - url: 'https://rickandmortyapi.com/api/character/381', - created: '2018-01-10T19:46:00.622Z', - }, - { - id: 472, - name: 'Baby Rick', - status: 'Alive', - species: 'Human', - type: 'Clone', - gender: 'Male', - origin: { - name: 'Citadel of Ricks', - url: 'https://rickandmortyapi.com/api/location/3', - }, - location: { - name: 'Citadel of Ricks', - url: 'https://rickandmortyapi.com/api/location/3', - }, - image: 'https://rickandmortyapi.com/api/character/avatar/472.jpeg', - episode: ['https://rickandmortyapi.com/api/episode/28'], - url: 'https://rickandmortyapi.com/api/character/472', - created: '2018-05-22T17:11:53.084Z', - }, - { - id: 477, - name: 'Hairdresser Rick', - status: 'Alive', - species: 'Human', - type: '', - gender: 'Male', - origin: { - name: 'unknown', - url: '', - }, - location: { - name: 'Citadel of Ricks', - url: 'https://rickandmortyapi.com/api/location/3', - }, - image: 'https://rickandmortyapi.com/api/character/avatar/477.jpeg', - episode: ['https://rickandmortyapi.com/api/episode/28'], - url: 'https://rickandmortyapi.com/api/character/477', - created: '2018-05-22T17:19:36.127Z', - }, - { - id: 478, - name: 'Journalist Rick', - status: 'Alive', - species: 'Human', - type: '', - gender: 'Male', - origin: { - name: 'unknown', - url: '', - }, - location: { - name: 'Citadel of Ricks', - url: 'https://rickandmortyapi.com/api/location/3', - }, - image: 'https://rickandmortyapi.com/api/character/avatar/478.jpeg', - episode: ['https://rickandmortyapi.com/api/episode/28'], - url: 'https://rickandmortyapi.com/api/character/478', - created: '2018-05-22T17:22:18.417Z', - }, - { - id: 482, - name: 'Secret Service Rick', - status: 'Alive', - species: 'Human', - type: '', - gender: 'Male', - origin: { - name: 'unknown', - url: '', - }, - location: { - name: 'Citadel of Ricks', - url: 'https://rickandmortyapi.com/api/location/3', - }, - image: 'https://rickandmortyapi.com/api/character/avatar/482.jpeg', - episode: ['https://rickandmortyapi.com/api/episode/28'], - url: 'https://rickandmortyapi.com/api/character/482', - created: '2018-05-22T17:32:32.561Z', - }, - { - id: 483, - name: 'Steve Jobs Rick', - status: 'Alive', - species: 'Human', - type: '', - gender: 'Male', - origin: { - name: 'unknown', - url: '', - }, - location: { - name: 'Citadel of Ricks', - url: 'https://rickandmortyapi.com/api/location/3', - }, - image: 'https://rickandmortyapi.com/api/character/avatar/483.jpeg', - episode: ['https://rickandmortyapi.com/api/episode/28'], - url: 'https://rickandmortyapi.com/api/character/483', - created: '2018-05-22T17:33:33.815Z', - }, - ], -}; diff --git a/src/tests/mockCharacters.ts b/src/tests/mockCharacters.ts new file mode 100644 index 0000000..6bc041e --- /dev/null +++ b/src/tests/mockCharacters.ts @@ -0,0 +1,168 @@ +export const mockCharacters = { + info: { + count: 29, + pages: 2, + next: 'https://rickandmortyapi.com/api/character/?page=2&name=rick&status=alive', + prev: null, + }, + results: [ + { + id: 1, + name: 'Rick Sanchez', + status: 'Alive', + species: 'Human', + type: '', + gender: 'Male', + origin: { + name: 'Earth (C-137)', + url: 'https://rickandmortyapi.com/api/location/1', + }, + location: { + name: 'Citadel of Ricks', + url: 'https://rickandmortyapi.com/api/location/3', + }, + image: 'https://rickandmortyapi.com/api/character/avatar/1.jpeg', + episode: [ + 'https://rickandmortyapi.com/api/episode/1', + 'https://rickandmortyapi.com/api/episode/2', + 'https://rickandmortyapi.com/api/episode/3', + 'https://rickandmortyapi.com/api/episode/4', + 'https://rickandmortyapi.com/api/episode/5', + 'https://rickandmortyapi.com/api/episode/6', + 'https://rickandmortyapi.com/api/episode/7', + 'https://rickandmortyapi.com/api/episode/8', + 'https://rickandmortyapi.com/api/episode/9', + 'https://rickandmortyapi.com/api/episode/10', + 'https://rickandmortyapi.com/api/episode/11', + 'https://rickandmortyapi.com/api/episode/12', + 'https://rickandmortyapi.com/api/episode/13', + 'https://rickandmortyapi.com/api/episode/14', + 'https://rickandmortyapi.com/api/episode/15', + 'https://rickandmortyapi.com/api/episode/16', + 'https://rickandmortyapi.com/api/episode/17', + 'https://rickandmortyapi.com/api/episode/18', + 'https://rickandmortyapi.com/api/episode/19', + 'https://rickandmortyapi.com/api/episode/20', + 'https://rickandmortyapi.com/api/episode/21', + 'https://rickandmortyapi.com/api/episode/22', + 'https://rickandmortyapi.com/api/episode/23', + 'https://rickandmortyapi.com/api/episode/24', + 'https://rickandmortyapi.com/api/episode/25', + 'https://rickandmortyapi.com/api/episode/26', + 'https://rickandmortyapi.com/api/episode/27', + 'https://rickandmortyapi.com/api/episode/28', + 'https://rickandmortyapi.com/api/episode/29', + 'https://rickandmortyapi.com/api/episode/30', + 'https://rickandmortyapi.com/api/episode/31', + 'https://rickandmortyapi.com/api/episode/32', + 'https://rickandmortyapi.com/api/episode/33', + 'https://rickandmortyapi.com/api/episode/34', + 'https://rickandmortyapi.com/api/episode/35', + 'https://rickandmortyapi.com/api/episode/36', + 'https://rickandmortyapi.com/api/episode/37', + 'https://rickandmortyapi.com/api/episode/38', + 'https://rickandmortyapi.com/api/episode/39', + 'https://rickandmortyapi.com/api/episode/40', + 'https://rickandmortyapi.com/api/episode/41', + 'https://rickandmortyapi.com/api/episode/42', + 'https://rickandmortyapi.com/api/episode/43', + 'https://rickandmortyapi.com/api/episode/44', + 'https://rickandmortyapi.com/api/episode/45', + 'https://rickandmortyapi.com/api/episode/46', + 'https://rickandmortyapi.com/api/episode/47', + 'https://rickandmortyapi.com/api/episode/48', + 'https://rickandmortyapi.com/api/episode/49', + 'https://rickandmortyapi.com/api/episode/50', + 'https://rickandmortyapi.com/api/episode/51', + ], + url: 'https://rickandmortyapi.com/api/character/1', + created: '2017-11-04T18:48:46.250Z', + }, + { + id: 48, + name: 'Black Rick', + status: 'Alive', + species: 'Human', + type: '', + gender: 'Male', + origin: { + name: 'unknown', + url: '', + }, + location: { + name: 'Citadel of Ricks', + url: 'https://rickandmortyapi.com/api/location/3', + }, + image: 'https://rickandmortyapi.com/api/character/avatar/48.jpeg', + episode: [ + 'https://rickandmortyapi.com/api/episode/22', + 'https://rickandmortyapi.com/api/episode/28', + ], + url: 'https://rickandmortyapi.com/api/character/48', + created: '2017-11-05T11:15:26.044Z', + }, + { + id: 72, + name: 'Cool Rick', + status: 'Alive', + species: 'Human', + type: '', + gender: 'Male', + origin: { + name: 'Earth (K-83)', + url: 'https://rickandmortyapi.com/api/location/26', + }, + location: { + name: 'Citadel of Ricks', + url: 'https://rickandmortyapi.com/api/location/3', + }, + image: 'https://rickandmortyapi.com/api/character/avatar/72.jpeg', + episode: ['https://rickandmortyapi.com/api/episode/28'], + url: 'https://rickandmortyapi.com/api/character/72', + created: '2017-11-30T11:41:11.542Z', + }, + { + id: 74, + name: 'Cop Rick', + status: 'Alive', + species: 'Human', + type: '', + gender: 'Male', + origin: { + name: 'unknown', + url: '', + }, + location: { + name: 'Citadel of Ricks', + url: 'https://rickandmortyapi.com/api/location/3', + }, + image: 'https://rickandmortyapi.com/api/character/avatar/74.jpeg', + episode: ['https://rickandmortyapi.com/api/episode/28'], + url: 'https://rickandmortyapi.com/api/character/74', + created: '2017-11-30T11:48:18.950Z', + }, + { + id: 78, + name: 'Cowboy Rick', + status: 'Alive', + species: 'Human', + type: '', + gender: 'Male', + origin: { + name: 'unknown', + url: '', + }, + location: { + name: 'Citadel of Ricks', + url: 'https://rickandmortyapi.com/api/location/3', + }, + image: 'https://rickandmortyapi.com/api/character/avatar/78.jpeg', + episode: [ + 'https://rickandmortyapi.com/api/episode/10', + 'https://rickandmortyapi.com/api/episode/28', + ], + url: 'https://rickandmortyapi.com/api/character/78', + created: '2017-11-30T14:15:18.347Z', + }, + ], +}; diff --git a/vite.config.ts b/vite.config.ts index 8be5f52..d948d07 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -16,7 +16,7 @@ export default defineConfig({ coverage: { all: true, exclude: [ - 'src/test/**', + 'src/tests/**', '**/types/**', '**/*.d.ts', 'src/**/index.ts', @@ -41,7 +41,7 @@ export default defineConfig({ modules: { classNameStrategy: 'non-scoped' }, }, exclude: ['**/node_modules/**', '**/e2e/**'], - include: ['**/*.{test,spec}.{ts,tsx}'], + include: ['src/**/*.{test,spec}.{ts,tsx}'], environment: 'jsdom', globals: true, maxConcurrency: 4, From 319e966edf9c8b387992f1511669bfa6f0917d25 Mon Sep 17 00:00:00 2001 From: maiano Date: Wed, 16 Jul 2025 13:16:12 +0200 Subject: [PATCH 13/20] test: add tests for local-storage and fetch-characters utils --- src/shared/utils/fetch-characters.test.ts | 36 +++++++++++++++++++++++ src/shared/utils/local-storage.test.ts | 20 +++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/shared/utils/fetch-characters.test.ts create mode 100644 src/shared/utils/local-storage.test.ts diff --git a/src/shared/utils/fetch-characters.test.ts b/src/shared/utils/fetch-characters.test.ts new file mode 100644 index 0000000..d9f534c --- /dev/null +++ b/src/shared/utils/fetch-characters.test.ts @@ -0,0 +1,36 @@ +import { describe, beforeEach, vi, it, expect } from 'vitest'; +import { fetchCharacters } from './fetch-сharacters'; +import { ERROR_UI_STRINGS } from '@/shared/constants/errors'; +import { mockCharacters } from '@/tests/mockCharacters'; +import type { CharacterApiResponse } from '@/types/character'; + +const mockResponse = mockCharacters as CharacterApiResponse; + +describe('fetchCharacters', () => { + beforeEach(() => { + global.fetch = vi.fn(); + }); + + it('fetches characters', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + json: async () => mockResponse, + } as Response); + + const result = await fetchCharacters('Rick'); + expect(result).toEqual(mockResponse); + expect(fetch).toHaveBeenCalledWith(expect.stringContaining('name=Rick')); + }); + + it('throws error', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: false, + status: 404, + json: async () => ({}), + } as Response); + + await expect(fetchCharacters('unknown')).rejects.toThrow( + ERROR_UI_STRINGS.notFound, + ); + }); +}); diff --git a/src/shared/utils/local-storage.test.ts b/src/shared/utils/local-storage.test.ts new file mode 100644 index 0000000..37d1120 --- /dev/null +++ b/src/shared/utils/local-storage.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { searchStorage } from './local-storage'; + +const TEST_KEY = 'rick-and-morty-search'; + +describe('searchStorage tests', () => { + beforeEach(() => { + localStorage.clear(); + }); + + it('returns empty string', () => { + expect(searchStorage.get()).toBe(''); + }); + + it('set and get value', () => { + searchStorage.set('Summer'); + expect(localStorage.getItem(TEST_KEY)).toBe('Summer'); + expect(searchStorage.get()).toBe('Summer'); + }); +}); From 1ef1625791a69f61dea24a56f706d169af697a8e Mon Sep 17 00:00:00 2001 From: maiano Date: Wed, 16 Jul 2025 14:48:23 +0200 Subject: [PATCH 14/20] test: add tests for App ErrorBoundary and Fallback --- src/App.test.tsx | 11 +++++ .../ErrorBoundary/ErrorBoundary.test.tsx | 41 +++++++++++++++++++ .../{ => ErrorBoundary}/ErrorBoundary.tsx | 4 +- src/components/ErrorBoundary/index.ts | 1 + src/components/Fallback/FallBack.test.tsx | 15 +++++++ src/components/{ => Fallback}/FallBack.tsx | 0 src/components/Fallback/index.ts | 1 + 7 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 src/App.test.tsx create mode 100644 src/components/ErrorBoundary/ErrorBoundary.test.tsx rename src/components/{ => ErrorBoundary}/ErrorBoundary.tsx (86%) create mode 100644 src/components/ErrorBoundary/index.ts create mode 100644 src/components/Fallback/FallBack.test.tsx rename src/components/{ => Fallback}/FallBack.tsx (100%) create mode 100644 src/components/Fallback/index.ts diff --git a/src/App.test.tsx b/src/App.test.tsx new file mode 100644 index 0000000..da7ffb4 --- /dev/null +++ b/src/App.test.tsx @@ -0,0 +1,11 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import App from './App'; +import { UI_STRINGS } from '@/shared/constants/ui-strings'; + +describe('App component', () => { + it('renders components', () => { + render(); + expect(screen.getByText(UI_STRINGS.title)).toBeInTheDocument(); + }); +}); diff --git a/src/components/ErrorBoundary/ErrorBoundary.test.tsx b/src/components/ErrorBoundary/ErrorBoundary.test.tsx new file mode 100644 index 0000000..1b4cb82 --- /dev/null +++ b/src/components/ErrorBoundary/ErrorBoundary.test.tsx @@ -0,0 +1,41 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest'; +import { ErrorBoundary } from './ErrorBoundary'; + +const ThrowComponent = () => { + throw new Error('Test error'); +}; + +const Fallback = () =>
unexpected error
; + +describe('ErrorBoundary tests', () => { + const originalError = console.error; + beforeAll(() => { + console.error = vi.fn(); + }); + + afterAll(() => { + console.error = originalError; + }); + + it('renders fallback on error', () => { + render( + }> + + , + ); + + expect(screen.getByText(/unexpected error/i)).toBeInTheDocument(); + }); + + it('renders children', () => { + const NO_ERROR = 'there is no error'; + render( + }> +
{NO_ERROR}
+
, + ); + + expect(screen.getByText(NO_ERROR)).toBeInTheDocument(); + }); +}); diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary/ErrorBoundary.tsx similarity index 86% rename from src/components/ErrorBoundary.tsx rename to src/components/ErrorBoundary/ErrorBoundary.tsx index 796e58e..4041334 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -9,7 +9,7 @@ type State = { hasError: boolean; }; -class ErrorBoundary extends Component { +export class ErrorBoundary extends Component { state: State = { hasError: false, }; @@ -30,5 +30,3 @@ class ErrorBoundary extends Component { return this.props.children; } } - -export default ErrorBoundary; diff --git a/src/components/ErrorBoundary/index.ts b/src/components/ErrorBoundary/index.ts new file mode 100644 index 0000000..8d337a3 --- /dev/null +++ b/src/components/ErrorBoundary/index.ts @@ -0,0 +1 @@ +export { ErrorBoundary } from './ErrorBoundary'; diff --git a/src/components/Fallback/FallBack.test.tsx b/src/components/Fallback/FallBack.test.tsx new file mode 100644 index 0000000..07b29db --- /dev/null +++ b/src/components/Fallback/FallBack.test.tsx @@ -0,0 +1,15 @@ +import { render, screen } from '@testing-library/react'; +import { expect, it } from 'vitest'; +import { FallBack } from '@/components/Fallback'; +import { ERROR_UI_STRINGS } from '@/shared/constants/errors'; + +it('renders fallback content', () => { + render(); + + expect( + screen.getByRole('heading', { name: ERROR_UI_STRINGS.heading }), + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: ERROR_UI_STRINGS.buttonText }), + ).toBeInTheDocument(); +}); diff --git a/src/components/FallBack.tsx b/src/components/Fallback/FallBack.tsx similarity index 100% rename from src/components/FallBack.tsx rename to src/components/Fallback/FallBack.tsx diff --git a/src/components/Fallback/index.ts b/src/components/Fallback/index.ts new file mode 100644 index 0000000..1e04069 --- /dev/null +++ b/src/components/Fallback/index.ts @@ -0,0 +1 @@ +export { FallBack } from './FallBack'; From d7af943518a9a2f77cab31aa372a0962cd811f2b Mon Sep 17 00:00:00 2001 From: maiano Date: Wed, 16 Jul 2025 15:45:19 +0200 Subject: [PATCH 15/20] test: clean up test structure and naming --- src/components/Card/Card.test.tsx | 36 +++++++++++------------ src/components/CardList/CardList.test.tsx | 5 ++-- src/main.tsx | 4 +-- src/pages/HomePage/HomePage.tsx | 2 +- 4 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/components/Card/Card.test.tsx b/src/components/Card/Card.test.tsx index 5502e7a..842eeaa 100644 --- a/src/components/Card/Card.test.tsx +++ b/src/components/Card/Card.test.tsx @@ -5,56 +5,54 @@ import { CARD_TEXT } from '@/shared/constants/cards'; import { mockCharacters } from '@/tests/mockCharacters'; import type { Character } from '@/types/character'; -const baseCharacter = mockCharacters.results[0] as Character; +const rickCharacter = mockCharacters.results[0] as Character; -describe('Card', () => { +describe('Card Component', () => { it('renders character name and basic info', () => { - render(); - expect(screen.getByText(baseCharacter.name)).toBeInTheDocument(); + const { label } = CARD_TEXT; + + render(); + expect(screen.getByText(rickCharacter.name)).toBeInTheDocument(); expect( - screen.getByText(`${CARD_TEXT.label.species}: ${baseCharacter.species}`), + screen.getByText(`${label.species}: ${rickCharacter.species}`), ).toBeInTheDocument(); expect( - screen.getByText(`${CARD_TEXT.label.status}: ${baseCharacter.status}`), + screen.getByText(`${label.status}: ${rickCharacter.status}`), ).toBeInTheDocument(); expect( - screen.getByText(`${CARD_TEXT.label.gender}: ${baseCharacter.gender}`), + screen.getByText(`${label.gender}: ${rickCharacter.gender}`), ).toBeInTheDocument(); expect( - screen.getByText( - `${CARD_TEXT.label.origin}: ${baseCharacter.origin.name}`, - ), + screen.getByText(`${label.origin}: ${rickCharacter.origin.name}`), ).toBeInTheDocument(); }); it('handles unknown status and gender gracefully', () => { + const { label, fallback } = CARD_TEXT; + const unknownCharacter = { - ...baseCharacter, + ...rickCharacter, status: 'unknown', gender: 'unknown', } as Character; render(); expect( - screen.getByText( - `${CARD_TEXT.label.status}: ${CARD_TEXT.fallback.status}`, - ), + screen.getByText(`${label.status}: ${fallback.status}`), ).toBeInTheDocument(); expect( - screen.getByText( - `${CARD_TEXT.label.gender}: ${CARD_TEXT.fallback.gender}`, - ), + screen.getByText(`${label.gender}: ${fallback.gender}`), ).toBeInTheDocument(); }); it('falls back to location when origin is unknown', () => { const noOriginCharacter = { - ...baseCharacter, + ...rickCharacter, origin: { name: 'unknown', url: '' }, }; render(); expect( screen.getByText( - `${CARD_TEXT.fallback.originFallback}: ${baseCharacter.location.name}`, + `${CARD_TEXT.fallback.originFallback}: ${rickCharacter.location.name}`, ), ).toBeInTheDocument(); }); diff --git a/src/components/CardList/CardList.test.tsx b/src/components/CardList/CardList.test.tsx index c22abec..93134a7 100644 --- a/src/components/CardList/CardList.test.tsx +++ b/src/components/CardList/CardList.test.tsx @@ -4,7 +4,7 @@ import { CardList } from './CardList'; import { mockCharacters } from '@/tests/mockCharacters'; import type { Character } from '@/types/character'; -describe('test CardList', () => { +describe('CardList component', () => { const mockItems = mockCharacters.results.slice(1, 3) as Character[]; it('renders correct number of cards', () => { @@ -16,8 +16,9 @@ describe('test CardList', () => { expect(screen.getAllByText(/species/i)).toHaveLength(2); }); - it('renders "no results"', () => { + it('renders "no results" with empty array', () => { render(); + expect(screen.queryByText('Black Rick')).not.toBeInTheDocument(); expect(screen.getByText(/no results/i)).toBeInTheDocument(); }); }); diff --git a/src/main.tsx b/src/main.tsx index 59d2066..ca36fb3 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,8 +2,8 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import './index.css'; import App from './App.tsx'; -import ErrorBoundary from '@/components/ErrorBoundary.tsx'; -import { FallBack } from '@/components/FallBack.tsx'; +import { ErrorBoundary } from '@/components/ErrorBoundary'; +import { FallBack } from '@/components/Fallback'; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion createRoot(document.getElementById('root')!).render( diff --git a/src/pages/HomePage/HomePage.tsx b/src/pages/HomePage/HomePage.tsx index ebf509f..7cb58a5 100644 --- a/src/pages/HomePage/HomePage.tsx +++ b/src/pages/HomePage/HomePage.tsx @@ -55,7 +55,7 @@ export class HomePage extends Component { } finally { setTimeout(() => { this.setState({ isLoading: false }); - }, 200); + }, 0); } }; From d8996d35d039012972714d0c556db8e6afeadd55 Mon Sep 17 00:00:00 2001 From: maiano Date: Thu, 17 Jul 2025 15:26:25 +0200 Subject: [PATCH 16/20] refactor: remove timeout from loading state cleanup --- src/pages/HomePage/HomePage.tsx | 4 +--- vite.config.ts | 5 +++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pages/HomePage/HomePage.tsx b/src/pages/HomePage/HomePage.tsx index 7cb58a5..9f45853 100644 --- a/src/pages/HomePage/HomePage.tsx +++ b/src/pages/HomePage/HomePage.tsx @@ -53,9 +53,7 @@ export class HomePage extends Component { characters: [], }); } finally { - setTimeout(() => { - this.setState({ isLoading: false }); - }, 0); + this.setState({ isLoading: false }); } }; diff --git a/vite.config.ts b/vite.config.ts index d948d07..d5aa583 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -16,11 +16,12 @@ export default defineConfig({ coverage: { all: true, exclude: [ + 'src/**/*.{test,spec}.{js,jsx,ts,tsx}', 'src/tests/**', '**/types/**', '**/*.d.ts', - 'src/**/index.ts', - 'src/main.tsx', + 'src/**/index.{js,jsx,ts,tsx}', + 'src/main.{js,jsx,ts,tsx}', ], extension: ['.ts', '.tsx'], include: ['src/**/*'], From 46dc40cb6491c992bd1cb5c192092381719361f8 Mon Sep 17 00:00:00 2001 From: maiano Date: Thu, 17 Jul 2025 15:29:40 +0200 Subject: [PATCH 17/20] chore: config pre-push hook --- .husky/pre-push | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.husky/pre-push b/.husky/pre-push index 6c1fe76..cf22276 100644 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,4 +1,4 @@ echo 'Running test...' echo 'If there are no tests, you need to write them' - +npm test From f46c47e9807883dc95c62a6a2735f8e47ae95f14 Mon Sep 17 00:00:00 2001 From: maiano Date: Thu, 17 Jul 2025 18:23:09 +0200 Subject: [PATCH 18/20] test: add test for the Pagination component --- src/components/Pagination.test.tsx | 18 ++++++++++++++++++ src/pages/HomePage/HomePage.test.tsx | 22 +++++++++++++++++++--- src/shared/constants/cards.ts | 2 +- 3 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 src/components/Pagination.test.tsx diff --git a/src/components/Pagination.test.tsx b/src/components/Pagination.test.tsx new file mode 100644 index 0000000..c8a7f82 --- /dev/null +++ b/src/components/Pagination.test.tsx @@ -0,0 +1,18 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import { Pagination } from './Pagination'; + +describe('Pagination', () => { + it('render button if total ≤ MAX_PAGES', () => { + const onChange = vi.fn(); + render(); + expect(screen.getByText('1')).toBeInTheDocument(); + expect(screen.getByText('5')).toBeInTheDocument(); + }); + + it('render text if > MAX_PAGES', () => { + const onChange = vi.fn(); + render(); + expect(screen.getByText(/Dimension 4 of 10/)).toBeInTheDocument(); + }); +}); diff --git a/src/pages/HomePage/HomePage.test.tsx b/src/pages/HomePage/HomePage.test.tsx index a779017..1ab1fbc 100644 --- a/src/pages/HomePage/HomePage.test.tsx +++ b/src/pages/HomePage/HomePage.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, waitFor } from '@testing-library/react'; +import { render, screen, waitFor, fireEvent } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { HomePage } from './HomePage'; import { fetchCharacters } from '@/shared/utils/fetch-сharacters'; @@ -23,6 +23,7 @@ const mockSuccessResponse = () => describe('HomePage tests', () => { beforeEach(() => { vi.clearAllMocks(); + vi.mocked(searchStorage.get).mockReturnValue('test'); }); it('renders characters from API on mount', async () => { @@ -60,12 +61,27 @@ describe('HomePage tests', () => { }); it('loads initial search query from localStorage', async () => { - vi.mocked(searchStorage.get).mockReturnValue('Summer'); mockSuccessResponse(); renderHomePage(); await waitFor(() => { - expect(fetchCharacters).toHaveBeenCalledWith('Summer', 1); + expect(fetchCharacters).toHaveBeenCalledWith('test', 1); }); }); + + it('page navigation calls fetchCharacters', async () => { + const fetchMock = vi + .mocked(fetchCharacters) + .mockResolvedValue(mockResponse); + + vi.mocked(searchStorage.get).mockReturnValue('test'); + renderHomePage(); + await waitFor(() => expect(fetchMock).toHaveBeenCalled()); + + await waitFor(() => { + fireEvent.click(screen.getByText('2')); + }); + + expect(fetchMock).toHaveBeenCalledWith('test', 2); + }); }); diff --git a/src/shared/constants/cards.ts b/src/shared/constants/cards.ts index 1d91309..ce30675 100644 --- a/src/shared/constants/cards.ts +++ b/src/shared/constants/cards.ts @@ -10,6 +10,6 @@ export const CARD_TEXT = { gender: 'Weird identity', originFallback: 'Origin lost, last spotted', locationUnknown: 'somewhere in the multiverse', - default: (key: string) => `${key}: Rick broke it!`, + default: 'Rick broke it!', }, } as const; From 3ccd9952999153020ee1302aeb95da79af341260 Mon Sep 17 00:00:00 2001 From: maiano Date: Thu, 17 Jul 2025 18:58:08 +0200 Subject: [PATCH 19/20] test: add test for the App --- src/App.test.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/App.test.tsx b/src/App.test.tsx index da7ffb4..1c3bb12 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,6 +1,7 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, fireEvent } from '@testing-library/react'; import { describe, it, expect } from 'vitest'; import App from './App'; +import { ErrorBoundary } from '@/components/ErrorBoundary'; import { UI_STRINGS } from '@/shared/constants/ui-strings'; describe('App component', () => { @@ -8,4 +9,18 @@ describe('App component', () => { render(); expect(screen.getByText(UI_STRINGS.title)).toBeInTheDocument(); }); + + it('throw the error with the button', () => { + render( + App down
}> + + , + ); + + const errorButton = screen.getByText(UI_STRINGS.errorButton); + + fireEvent.click(errorButton); + + expect(screen.getByText(/App down/i)).toBeInTheDocument(); + }); }); From c36216f4e0e5c83d2322ebb292ebae65bac5cb10 Mon Sep 17 00:00:00 2001 From: maiano Date: Thu, 17 Jul 2025 19:52:32 +0200 Subject: [PATCH 20/20] refactor: remove unused code --- src/components/Card/Card.tsx | 2 +- src/components/SearchBar/SearchBar.tsx | 2 -- src/shared/utils/debug-log.ts | 6 ------ 3 files changed, 1 insertion(+), 9 deletions(-) delete mode 100644 src/shared/utils/debug-log.ts diff --git a/src/components/Card/Card.tsx b/src/components/Card/Card.tsx index 4260883..b594590 100644 --- a/src/components/Card/Card.tsx +++ b/src/components/Card/Card.tsx @@ -16,7 +16,7 @@ export class Card extends Component { case 'gender': return CARD_TEXT.fallback.gender; default: - return CARD_TEXT.fallback.default(key); + return CARD_TEXT.fallback.default; } } return value; diff --git a/src/components/SearchBar/SearchBar.tsx b/src/components/SearchBar/SearchBar.tsx index fb394b4..c10900a 100644 --- a/src/components/SearchBar/SearchBar.tsx +++ b/src/components/SearchBar/SearchBar.tsx @@ -2,7 +2,6 @@ import { Component, type ReactNode } from 'react'; import { Button } from '@/components/Button'; import { Input } from '@/components/Input'; import { UI_STRINGS } from '@/shared/constants/ui-strings'; -import { debug } from '@/shared/utils/debug-log'; type Props = { onSearch: (text: string) => void; @@ -31,7 +30,6 @@ export class SearchBar extends Component { handleSearch = () => { const text = this.state.inputValue.trim(); - debug('search string:', text); this.props.onSearch(text); this.setState({ inputValue: text }); }; diff --git a/src/shared/utils/debug-log.ts b/src/shared/utils/debug-log.ts deleted file mode 100644 index ea2a50b..0000000 --- a/src/shared/utils/debug-log.ts +++ /dev/null @@ -1,6 +0,0 @@ -const isDebugMode = import.meta.env.MODE === 'development'; - -export const debug = (namespace: string, ...args: unknown[]) => { - if (!isDebugMode) return; - console.log(`[debug:${namespace}]`, ...args); -};