diff --git a/eslint.config.js b/eslint.config.js index ea36dd3..27d7035 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -5,7 +5,7 @@ import reactRefresh from 'eslint-plugin-react-refresh' import { defineConfig, globalIgnores } from 'eslint/config' export default defineConfig([ - globalIgnores(['dist']), + globalIgnores(['dist', 'ignore']), { files: ['**/*.{js,jsx}'], extends: [ diff --git a/package-lock.json b/package-lock.json index e9d706c..383577d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,8 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/react": "^16.3.3", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", "@vitejs/plugin-react": "^6.1.0", @@ -22,7 +24,69 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.4", "globals": "^17.11.0", - "vite": "^8.2.2" + "jsdom": "^30.1.0", + "vite": "^8.2.2", + "vitest": "^5.0.1" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-7.0.0.tgz", + "integrity": "sha512-IxddtGnnoidVSO6O/aiTueptbTThHvi2qGHu9/AWuD1J7i86Ox+jl+Y5SXcDH1RixzQlP2cjH/PjnlImZiAurQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.3.0", + "@csstools/css-color-parser": "^4.2.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-9.2.1.tgz", + "integrity": "sha512-NT4s3yZLjovPpliRpTvdsdzyPjqRqiCZj9MxnarBihbaY5MbAG7DWAJcrLlhmC7xKTNCXsTELcqB8YsqpLnUFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bidi-js": "^1.1.0", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.3" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" } }, "node_modules/@babel/code-frame": { @@ -217,6 +281,16 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", @@ -265,6 +339,19 @@ "node": ">=6.9.0" } }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, "node_modules/@cacheable/memory": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", @@ -289,6 +376,146 @@ "keyv": "^5.6.0" } }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", + "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.0.tgz", + "integrity": "sha512-XQKj5B7QiZcHiegCOCAzcAOJdhGgWOHbbu62h5e5mkHnn8lWcfiJhllkqWmxu5zWR9jucPHuo1iTB56P033hcg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.3.tgz", + "integrity": "sha512-y4LpL+lmpuyKDiEFq2PnZUVFdAjsoB/qQJod79yLNokXyW7jewi+/WJ69EfItj8A2unWtxXnGjw6LYXgXu5ZjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.1", + "@csstools/css-calc": "^3.4.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.14.tgz", + "integrity": "sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.1.tgz", + "integrity": "sha512-bPlN9S9O1A0euCpEWE4qnvB5YDuyYVsUTrxSgmAM1Is0j4tICHoVyOVAXfWMP/kS9ZrjvyIXWV2PmomiAXXqOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", @@ -417,6 +644,24 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -829,6 +1074,117 @@ "dev": true, "license": "MIT" }, + "node_modules/@testing-library/dom": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz", + "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", + "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", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.1.tgz", + "integrity": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=22", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": ">=10 <11", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "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/@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/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "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/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -900,6 +1256,44 @@ } } }, + "node_modules/@vitest/mocker": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.1.tgz", + "integrity": "sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.1", + "estree-walker": "^3.0.3", + "magic-string": "^1.2.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.1.tgz", + "integrity": "sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", @@ -952,6 +1346,51 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "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/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/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/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/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -993,6 +1432,16 @@ "node": ">=6.0.0" } }, + "node_modules/bidi-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", + "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/brace-expansion": { "version": "5.0.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", @@ -1088,6 +1537,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1122,6 +1581,27 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1129,6 +1609,35 @@ "dev": true, "license": "MIT" }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1146,6 +1655,13 @@ } } }, + "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-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1162,6 +1678,16 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1172,6 +1698,14 @@ "node": ">=8" } }, + "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/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1193,6 +1727,19 @@ "dev": true, "license": "ISC" }, + "node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1211,6 +1758,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", @@ -1436,6 +1990,16 @@ "node": ">=4.0" } }, + "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/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -1443,7 +2007,17 @@ "dev": true, "license": "BSD-2-Clause", "engines": { - "node": ">=0.10.0" + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" } }, "node_modules/fast-deep-equal": { @@ -1752,6 +2326,19 @@ "dev": true, "license": "MIT" }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -1785,6 +2372,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1808,6 +2405,13 @@ "node": ">=0.10.0" } }, + "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/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1822,6 +2426,56 @@ "dev": true, "license": "MIT" }, + "node_modules/jsdom": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.1.0.tgz", + "integrity": "sha512-h/Q+H+KmlGf49ITw3TrykSVbg04S75S2O7ZtJc9gMGOWBrQRZmx5AJIKLtJ01e6Kszl+/tpFthCVqxNifxhoqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^7.0.0", + "@asamuzakjp/dom-selector": "^9.1.2", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.13", + "@exodus/bytes": "^1.15.1", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "tough-cookie": "^6.0.2", + "undici": "^8.10.2", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.1.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "canvas": "^3.2.3" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -2173,6 +2827,27 @@ "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": "1.4.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.1.tgz", + "integrity": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2182,6 +2857,13 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -2203,6 +2885,16 @@ "node": ">= 0.6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", @@ -2261,6 +2953,20 @@ "node": ">=18" } }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2311,6 +3017,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2390,6 +3109,22 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "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/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -2459,6 +3194,38 @@ "react": "*" } }, + "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/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rolldown": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.9.tgz", @@ -2493,6 +3260,19 @@ "@rolldown/binding-win32-x64-msvc": "1.2.9" } }, + "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.28.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", @@ -2532,6 +3312,13 @@ "node": ">=8" } }, + "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/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2542,6 +3329,53 @@ "node": ">=0.10.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": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinybench": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.4.tgz", + "integrity": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -2559,6 +3393,52 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tldts": { + "version": "7.4.13", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.13.tgz", + "integrity": "sha512-iHtaIWWIbMDkCeJdTBzZFGgbluE5J+oHlb2g7+oAz1S1gpuVpabRZdQyd471Vl8UUkcz2vXSL8xZH2kyCe8tfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.13" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.13", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.13.tgz", + "integrity": "sha512-mbYsrih5FRtGxs3Usvl/PqwJsNpp+jsmrdFviiK02teHDG0/HebBG/pqCylje3kzgXYzuLoHJF/0mz9W53t8Xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -2572,6 +3452,16 @@ "node": ">= 0.8.0" } }, + "node_modules/undici": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.2.tgz", + "integrity": "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", @@ -2691,6 +3581,137 @@ } } }, + "node_modules/vitest": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.1.tgz", + "integrity": "sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.1", + "chai": "^6.2.2", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.2.3", + "obug": "^2.1.4", + "picomatch": "^4.0.7", + "std-env": "^4.2.0", + "tinybench": "6.1.4", + "tinyexec": "1.3.0", + "tinyglobby": "^0.2.17", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.1", + "@vitest/browser-preview": "5.0.1", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.1", + "@vitest/coverage-v8": "5.0.1", + "@vitest/ui": "5.0.1", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "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": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.1.tgz", + "integrity": "sha512-ohjk1mdUebJVadRt3bAhQhx8lSnISq+GDttK79LFl8EHQkAPvzwctoasC4hs8tBt6kLAncBWWyq1N52qEfKvDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -2707,6 +3728,23 @@ "node": ">= 8" } }, + "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", @@ -2717,6 +3755,23 @@ "node": ">=0.10.0" } }, + "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/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index e53069e..c9dca8d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "dev": "vite", "build": "vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "axios": "^1.20.0", @@ -17,6 +19,8 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/react": "^16.3.3", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", "@vitejs/plugin-react": "^6.1.0", @@ -24,6 +28,8 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.4", "globals": "^17.11.0", - "vite": "^8.2.2" + "jsdom": "^30.1.0", + "vite": "^8.2.2", + "vitest": "^5.0.1" } } diff --git a/src/components/Document/DocumentItem/DocumentItem.jsx b/src/components/Document/DocumentItem/DocumentItem.jsx index 158a114..5fc384f 100644 --- a/src/components/Document/DocumentItem/DocumentItem.jsx +++ b/src/components/Document/DocumentItem/DocumentItem.jsx @@ -15,11 +15,11 @@ function formatarData(dataISO) { } function DocumentItem({ document, onView }) { - const typeClass = document.type + const typeClass = document.setor ?.toLowerCase() .replace(/\s+/g, "-"); - const accessClass = document.accessLevel + const accessClass = document.nivel ?.toLowerCase() .replace(/\s+/g, "-"); @@ -59,4 +59,4 @@ function DocumentItem({ document, onView }) { ); } -export default DocumentItem; \ No newline at end of file +export default DocumentItem; diff --git a/src/components/Document/DocumentItem/DocumentItem.test.jsx b/src/components/Document/DocumentItem/DocumentItem.test.jsx new file mode 100644 index 0000000..dc92c43 --- /dev/null +++ b/src/components/Document/DocumentItem/DocumentItem.test.jsx @@ -0,0 +1,87 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import DocumentItem from "./DocumentItem"; + +const documentData = { + id_documento: 1, + tipo_arquivo: "pdf", + nome: "Manual do sistema.pdf", + setor: "Engenharia", + data_atualizacao: "2026-09-20T12:00:00Z", + nivel: "Publico", +}; + +describe("DocumentItem", () => { + it("exibe os dados do documento", () => { + render(); + + expect(screen.getByText(documentData.nome)).toBeInTheDocument(); + expect(screen.getByText(documentData.setor)).toBeInTheDocument(); + expect(screen.getByText(documentData.nivel)).toBeInTheDocument(); + expect(screen.getByText(documentData.tipo_arquivo)).toBeInTheDocument(); + expect(screen.getByText("Visualizar")).toBeInTheDocument(); + }); + + it("formata a data de atualizacao no padrao pt-BR", () => { + render(); + + expect(screen.getByText("20/09/2026")).toBeInTheDocument(); + }); + + it("exibe '-' quando nao ha data de atualizacao", () => { + render( + + ); + + expect(screen.getByText("-")).toBeInTheDocument(); + }); + + it("exibe a data original quando ela e invalida", () => { + render( + + ); + + expect(screen.getByText("data-invalida")).toBeInTheDocument(); + }); + + it("gera as classes css a partir do setor e do nivel", () => { + const { container } = render( + + ); + + expect(container.querySelector(".type-engenharia")).toBeInTheDocument(); + expect(container.querySelector(".access-publico")).toBeInTheDocument(); + }); + + it("nao quebra quando setor e nivel estao ausentes", () => { + const { container } = render( + + ); + + expect(container.querySelector(".type-badge")).toBeInTheDocument(); + expect(container.querySelector(".access-badge")).toBeInTheDocument(); + }); + + it("chama onView com o documento ao clicar em Visualizar", () => { + const onView = vi.fn(); + render(); + + fireEvent.click(screen.getByRole("button", { name: "Visualizar" })); + + expect(onView).toHaveBeenCalledWith(documentData); + }); +}); diff --git a/src/components/Document/DocumentList/DocumentList.jsx b/src/components/Document/DocumentList/DocumentList.jsx index 4d74e42..afa4e73 100644 --- a/src/components/Document/DocumentList/DocumentList.jsx +++ b/src/components/Document/DocumentList/DocumentList.jsx @@ -20,7 +20,7 @@ function DocumentList({ documents, onView }) { {documents.length > 0 ? ( documents.map((document) => ( @@ -35,4 +35,4 @@ function DocumentList({ documents, onView }) { ); } -export default DocumentList; \ No newline at end of file +export default DocumentList; diff --git a/src/components/Document/DocumentModal/DocumentModal.css b/src/components/Document/DocumentModal/DocumentModal.css index 29d595c..05df264 100644 --- a/src/components/Document/DocumentModal/DocumentModal.css +++ b/src/components/Document/DocumentModal/DocumentModal.css @@ -370,6 +370,11 @@ font-size: 12px; } +.tags-empty { + color: #806d75; + font-size: 12px; +} + /*AÇÕES*/ .modal-actions { @@ -435,4 +440,9 @@ .modal-main-info { align-items: stretch; } -} \ No newline at end of file +} + +.modal-view-button:disabled { + cursor: not-allowed; + opacity: 0.6; +} diff --git a/src/components/Document/DocumentModal/DocumentModal.jsx b/src/components/Document/DocumentModal/DocumentModal.jsx index 0aa6771..382c51d 100644 --- a/src/components/Document/DocumentModal/DocumentModal.jsx +++ b/src/components/Document/DocumentModal/DocumentModal.jsx @@ -1,161 +1,134 @@ -import { FiEye } from "react-icons/fi"; +import { FiEye, FiX } from "react-icons/fi"; import "./DocumentModal.css"; -function DocumentModal({ document, onClose, onViewPdf }) { - if (!document) { - return null; +function formatDate(value) { + if (!value) return "-"; + + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + + return date.toLocaleDateString("pt-BR", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); +} + +function DocumentModal({ document, onClose, onViewPdf, loading, error, onRetry }) { + + const handleOverlayClick = (event) => { + if (event.target === event.currentTarget) { + onClose(); + } + }; + + const handleKeyDown = (event) => { + if (event.key === "Escape") { + onClose(); + } + }; + + if (loading || error) { + return ( +
+
+ + + {loading ? ( +

Carregando detalhes do documento...

+ ) : ( +
+

{error}

+ +
+ )} +
+
+ ); } - const tags = [ - "Materiais metálicos", - "Manutenção", - "MIL-HDBK", - "Engenharia", - "Handbook", - ]; + if (!document) return null; + + const tags = document.etiquetas || []; return ( -
-
event.stopPropagation()} - > +
- - Código da norma - + Arquivo
- {document.code || document.name} - + {document.nome} - PDF + {document.tipo_arquivo?.toUpperCase() || "ARQUIVO"}
-
-
- - Título do arquivo - - -

- {document.name} -

-
-
- - Setor/Tipo - - - - {document.type} - + Setor + {document.setor}
- - Nível de Acesso - - - - {document.accessLevel} - -
- -
- - Categoria - - - - {document.category || document.type} - + Nivel de acesso + {document.nivel}
- - Dados do arquivo - + Dados do arquivo
- ↻ Revisão: - - - {document.revision || "5J"} - -
- -
- ◷ Data de Atualização: - - - {document.updatedAt} - + Tipo: + {document.tipo_arquivo || "-"}
- ▤ Tamanho do arquivo: - - - {document.fileSize || "66.8 MB"} - + Data de atualizacao: + {formatDate(document.data_atualizacao)}
- - 🏷 Tags - + Tags
- {tags.map((tag) => ( - - {tag} - - ))} + {tags.length > 0 ? ( + tags.map((tag) => ( + + {tag.nome} + + )) + ) : ( + Nenhuma etiqueta atribuida. + )}
@@ -163,9 +136,10 @@ function DocumentModal({ document, onClose, onViewPdf }) {
@@ -174,4 +148,4 @@ function DocumentModal({ document, onClose, onViewPdf }) { ); } -export default DocumentModal; \ No newline at end of file +export default DocumentModal; diff --git a/src/components/Document/DocumentModal/DocumentModal.test.jsx b/src/components/Document/DocumentModal/DocumentModal.test.jsx new file mode 100644 index 0000000..54cf0be --- /dev/null +++ b/src/components/Document/DocumentModal/DocumentModal.test.jsx @@ -0,0 +1,136 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import DocumentModal from "./DocumentModal"; + +const documentData = { + id_documento: 1, + tipo_arquivo: "pdf", + nome: "Manual de teste.pdf", + setor: "Engenharia", + data_atualizacao: "2026-09-20T12:00:00Z", + nivel: "Publico", + data: "http://localhost:8000/media/documentos/manual.pdf", + etiquetas: [ + { id_etiqueta: 1, nome: "Tecnico" }, + { id_etiqueta: 2, nome: "Aprovado" }, + ], +}; + +function renderModal(props = {}) { + return render( + + ); +} + +describe("DocumentModal", () => { + it("exibe os metadados e as etiquetas retornados pela API", () => { + renderModal(); + + expect(screen.getByText("Manual de teste.pdf")).toBeInTheDocument(); + expect(screen.getByText("Engenharia")).toBeInTheDocument(); + expect(screen.getByText("Publico")).toBeInTheDocument(); + expect(screen.getByText("Tecnico")).toBeInTheDocument(); + expect(screen.getByText("Aprovado")).toBeInTheDocument(); + }); + + it("informa quando o documento nao possui etiquetas", () => { + renderModal({ document: { ...documentData, etiquetas: [] } }); + + expect(screen.getByText("Nenhuma etiqueta atribuida.")).toBeInTheDocument(); + }); + + it("fecha pelo botao e pelo clique no overlay, mas nao pelo conteudo", () => { + const onClose = vi.fn(); + renderModal({ onClose }); + + fireEvent.click(screen.getByText("Manual de teste.pdf")); + expect(onClose).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByLabelText("Fechar")); + expect(onClose).toHaveBeenCalledTimes(1); + + fireEvent.click(globalThis.document.querySelector(".document-modal-overlay")); + expect(onClose).toHaveBeenCalledTimes(2); + }); + + it("encaminha o documento com a URL do PDF ao visualizar", () => { + const onViewPdf = vi.fn(); + renderModal({ onViewPdf }); + + fireEvent.click(screen.getByRole("button", { name: "Visualizar" })); + + expect(onViewPdf).toHaveBeenCalledWith(documentData); + }); + + it("desabilita a visualizacao sem URL de arquivo", () => { + renderModal({ document: { ...documentData, data: "" } }); + + expect(screen.getByRole("button", { name: "Visualizar" })).toBeDisabled(); + }); + + it("exibe o estado de carregamento", () => { + renderModal({ loading: true }); + + expect( + screen.getByText("Carregando detalhes do documento...") + ).toBeInTheDocument(); + expect(screen.queryByText("Manual de teste.pdf")).not.toBeInTheDocument(); + }); + + it("exibe o erro e permite tentar novamente", () => { + const onRetry = vi.fn(); + renderModal({ error: "Falha ao carregar detalhes.", onRetry }); + + expect(screen.getByRole("alert")).toHaveTextContent( + "Falha ao carregar detalhes." + ); + + fireEvent.click(screen.getByText("Tentar novamente")); + expect(onRetry).toHaveBeenCalledTimes(1); + }); + + it("permite fechar o modal durante o carregamento", () => { + const onClose = vi.fn(); + renderModal({ loading: true, onClose }); + + fireEvent.click(screen.getByLabelText("Fechar")); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it("nao renderiza nada sem documento", () => { + const { container } = renderModal({ document: null }); + + expect(container).toBeEmptyDOMElement(); + }); + + it("usa o badge padrao quando o tipo de arquivo esta ausente", () => { + renderModal({ document: { ...documentData, tipo_arquivo: undefined } }); + + expect(screen.getByText("ARQUIVO")).toBeInTheDocument(); + }); + + it("formata a data de atualizacao e trata valores invalidos", () => { + renderModal({ document: { ...documentData, data_atualizacao: "2026-01-05T10:00:00Z" } }); + expect(screen.getByText("05/01/2026")).toBeInTheDocument(); + + renderModal({ document: { ...documentData, data_atualizacao: "data-invalida" } }); + expect(screen.getByText("data-invalida")).toBeInTheDocument(); + + renderModal({ document: { ...documentData, data_atualizacao: "" } }); + expect(screen.getByText("-")).toBeInTheDocument(); + }); + + it("fecha pelo clique no overlay mesmo em estado de erro", () => { + const onClose = vi.fn(); + renderModal({ error: "Falha", onClose }); + + fireEvent.click(globalThis.document.querySelector(".document-modal-overlay")); + expect(onClose).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/pages/Home/Home.jsx b/src/pages/Home/Home.jsx index c9ad150..9b4cb1d 100644 --- a/src/pages/Home/Home.jsx +++ b/src/pages/Home/Home.jsx @@ -1,12 +1,12 @@ import { FiFileText, FiFilter, FiAlertCircle } from "react-icons/fi"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import SearchBar from "../../components/SearchBar/SearchBar"; import DocumentList from "../../components/Document/DocumentList/DocumentList"; import Pagination from "../../components/Pagination/Pagination"; import DocumentModal from "../../components/Document/DocumentModal/DocumentModal"; -import { fetchDocuments } from "../../services/documentService"; +import { fetchDocumentById, fetchDocuments } from "../../services/documentService"; import "./Home.css"; @@ -18,6 +18,9 @@ function Home() { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [selectedDocument, setSelectedDocument] = useState(null); + const [documentDetailsLoading, setDocumentDetailsLoading] = useState(false); + const [documentDetailsError, setDocumentDetailsError] = useState(null); + const detailRequestId = useRef(0); useEffect(() => { const timer = setTimeout(async () => { @@ -57,13 +60,55 @@ function Home() { } function handleViewPdf(document) { - if (document.pdfUrl) { - window.open(document.pdfUrl, "_blank"); + if (document.data) { + window.open(document.data, "_blank"); return; } console.log("PDF ainda não disponível:", document.nome); } + async function loadDocumentDetails(idDocumento) { + const requestId = ++detailRequestId.current; + + setDocumentDetailsLoading(true); + setDocumentDetailsError(null); + + try { + const document = await fetchDocumentById(idDocumento); + + if (requestId === detailRequestId.current) { + setSelectedDocument(document); + } + } catch (err) { + if (requestId === detailRequestId.current) { + console.error("Erro ao buscar detalhes do documento:", err); + setDocumentDetailsError("Não foi possível carregar os detalhes do documento."); + } + } finally { + if (requestId === detailRequestId.current) { + setDocumentDetailsLoading(false); + } + } + } + + function handleOpenDocument(document) { + setSelectedDocument(document); + loadDocumentDetails(document.id_documento); + } + + function handleCloseDocumentModal() { + detailRequestId.current += 1; + setSelectedDocument(null); + setDocumentDetailsLoading(false); + setDocumentDetailsError(null); + } + + function handleRetryDocumentDetails() { + if (selectedDocument?.id_documento) { + loadDocumentDetails(selectedDocument.id_documento); + } + } + return (
@@ -100,7 +145,7 @@ function Home() { <> setSelectedDocument(null)} + loading={documentDetailsLoading} + error={documentDetailsError} + onClose={handleCloseDocumentModal} + onRetry={handleRetryDocumentDetails} onViewPdf={handleViewPdf} />
); } -export default Home; \ No newline at end of file +export default Home; diff --git a/src/pages/Home/Home.test.jsx b/src/pages/Home/Home.test.jsx new file mode 100644 index 0000000..004c6c6 --- /dev/null +++ b/src/pages/Home/Home.test.jsx @@ -0,0 +1,265 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import Home from "./Home"; +import { fetchDocumentById, fetchDocuments } from "../../services/documentService"; + +vi.mock("../../services/documentService", () => ({ + fetchDocuments: vi.fn(), + fetchDocumentById: vi.fn(), +})); + +const documentsPage = { + results: [ + { + id_documento: 1, + tipo_arquivo: "pdf", + nome: "Manual do sistema.pdf", + setor: "Engenharia", + data_atualizacao: "2026-09-20T12:00:00Z", + nivel: "Publico", + }, + { + id_documento: 2, + tipo_arquivo: "docx", + nome: "Ata de reuniao.docx", + setor: "Administrativo", + data_atualizacao: "2026-08-01T09:00:00Z", + nivel: "Interno", + }, + ], + pages: 2, +}; + +const documentDetail = { + id_documento: 1, + nome: "Manual do sistema.pdf", + tipo_arquivo: "pdf", + setor: "Engenharia", + data_atualizacao: "2026-09-20T12:00:00Z", + nivel: "Publico", + data: "http://localhost:8000/media/documentos/manual.pdf", + etiquetas: [{ id_etiqueta: 1, nome: "Tecnico" }], +}; + +function getModal() { + return globalThis.document.querySelector(".document-modal"); +} + +function openFirstDocument() { + fireEvent.click(screen.getAllByRole("button", { name: "Visualizar" })[0]); +} + +beforeEach(() => { + vi.mocked(fetchDocuments).mockResolvedValue(documentsPage); + vi.mocked(fetchDocumentById).mockResolvedValue(documentDetail); +}); + +describe("Home", () => { + it("carrega e exibe a lista de documentos", async () => { + render(); + + await waitFor(() => { + expect(screen.getByText("Manual do sistema.pdf")).toBeInTheDocument(); + }); + expect(screen.getByText("Ata de reuniao.docx")).toBeInTheDocument(); + expect(fetchDocuments).toHaveBeenCalledWith({ nome: "", page: 1 }); + }); + + it("exibe mensagem de erro quando a busca falha", async () => { + vi.mocked(fetchDocuments).mockRejectedValue(new Error("falha")); + + render(); + + await waitFor(() => { + expect(screen.getByText("Erro ao carregar")).toBeInTheDocument(); + }); + expect( + screen.getByText("Não foi possível carregar os documentos.") + ).toBeInTheDocument(); + expect(screen.queryByText("Manual do sistema.pdf")).not.toBeInTheDocument(); + }); + + it("busca novamente ao digitar na barra de pesquisa", async () => { + render(); + + await waitFor(() => { + expect(screen.getByText("Manual do sistema.pdf")).toBeInTheDocument(); + }); + + fireEvent.change(screen.getByPlaceholderText("Pesquisar documentos..."), { + target: { value: "manual" }, + }); + + await waitFor(() => { + expect(fetchDocuments).toHaveBeenLastCalledWith({ nome: "manual", page: 1 }); + }); + }); + + it("muda de pagina pela paginacao", async () => { + render(); + + await waitFor(() => { + expect(screen.getByText("Manual do sistema.pdf")).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByRole("button", { name: "Próxima página" })); + + await waitFor(() => { + expect(fetchDocuments).toHaveBeenLastCalledWith({ nome: "", page: 2 }); + }); + }); + + it("ignora mudanca de pagina fora dos limites", async () => { + render(); + + await waitFor(() => { + expect(screen.getByText("Manual do sistema.pdf")).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByRole("button", { name: "Página anterior" })); + + expect(fetchDocuments).toHaveBeenCalledTimes(1); + }); + + it("abre o modal com os detalhes do documento selecionado", async () => { + render(); + + await waitFor(() => { + expect(screen.getByText("Manual do sistema.pdf")).toBeInTheDocument(); + }); + + openFirstDocument(); + + expect(screen.getByText("Carregando detalhes do documento...")).toBeInTheDocument(); + + await waitFor(() => { + expect(getModal()).toBeInTheDocument(); + }); + expect(fetchDocumentById).toHaveBeenCalledWith(1); + expect(screen.getByText("Tecnico")).toBeInTheDocument(); + }); + + it("abre o modal direto quando ja existe documento selecionado", async () => { + render(); + + await waitFor(() => { + expect(screen.getByText("Manual do sistema.pdf")).toBeInTheDocument(); + }); + + openFirstDocument(); + + await waitFor(() => { + expect(screen.getByText("Tecnico")).toBeInTheDocument(); + }); + expect(getModal()).toBeInTheDocument(); + }); + + it("exibe erro nos detalhes e permite tentar novamente", async () => { + vi.mocked(fetchDocumentById).mockRejectedValue(new Error("falha")); + + render(); + + await waitFor(() => { + expect(screen.getByText("Manual do sistema.pdf")).toBeInTheDocument(); + }); + + openFirstDocument(); + + await waitFor(() => { + expect(screen.getByRole("alert")).toBeInTheDocument(); + }); + expect( + screen.getByText("Não foi possível carregar os detalhes do documento.") + ).toBeInTheDocument(); + + vi.mocked(fetchDocumentById).mockResolvedValue(documentDetail); + fireEvent.click(screen.getByText("Tentar novamente")); + + await waitFor(() => { + expect(screen.getByText("Tecnico")).toBeInTheDocument(); + }); + }); + + it("fecha o modal pelo botao e pelo overlay", async () => { + render(); + + await waitFor(() => { + expect(screen.getByText("Manual do sistema.pdf")).toBeInTheDocument(); + }); + + openFirstDocument(); + + await waitFor(() => { + expect(screen.getByText("Arquivo")).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByLabelText("Fechar")); + expect(getModal()).not.toBeInTheDocument(); + + openFirstDocument(); + await waitFor(() => { + expect(getModal()).toBeInTheDocument(); + }); + + fireEvent.click(globalThis.document.querySelector(".document-modal-overlay")); + expect(getModal()).not.toBeInTheDocument(); + }); + + it("abre o PDF em nova aba ao visualizar pelo modal", async () => { + const openSpy = vi + .spyOn(window, "open") + .mockImplementation(() => null); + + render(); + + await waitFor(() => { + expect(screen.getByText("Manual do sistema.pdf")).toBeInTheDocument(); + }); + + openFirstDocument(); + + await waitFor(() => { + expect(screen.getByText("Tecnico")).toBeInTheDocument(); + }); + + fireEvent.click(getModal().querySelector("footer .modal-view-button")); + + expect(openSpy).toHaveBeenCalledWith(documentDetail.data, "_blank"); + + openSpy.mockRestore(); + }); + + it("nao abre o PDF quando o documento nao tem URL de arquivo", async () => { + vi.mocked(fetchDocumentById).mockResolvedValue({ + ...documentDetail, + data: null, + }); + const openSpy = vi + .spyOn(window, "open") + .mockImplementation(() => null); + const consoleSpy = vi + .spyOn(console, "log") + .mockImplementation(() => {}); + + render(); + + await waitFor(() => { + expect(screen.getByText("Manual do sistema.pdf")).toBeInTheDocument(); + }); + + openFirstDocument(); + + await waitFor(() => { + expect(getModal()).toBeInTheDocument(); + }); + + const viewButton = getModal().querySelector("footer .modal-view-button"); + + expect(viewButton).toBeDisabled(); + expect(openSpy).not.toHaveBeenCalled(); + + openSpy.mockRestore(); + consoleSpy.mockRestore(); + }); +}); diff --git a/src/services/documentService.js b/src/services/documentService.js index 469ecf6..c8f3462 100644 --- a/src/services/documentService.js +++ b/src/services/documentService.js @@ -9,4 +9,10 @@ export async function fetchDocuments({ nome = "", page = 1 } = {}) { }); return response.data; -} \ No newline at end of file +} + +export async function fetchDocumentById(idDocumento) { + const response = await api.get(`/api/documentos/${idDocumento}/`); + + return response.data; +} diff --git a/src/services/documentService.test.js b/src/services/documentService.test.js new file mode 100644 index 0000000..f510601 --- /dev/null +++ b/src/services/documentService.test.js @@ -0,0 +1,24 @@ +import { describe, expect, it, vi } from 'vitest' + +import api from './api' +import { fetchDocumentById } from './documentService' + +vi.mock('./api', () => ({ + default: { + get: vi.fn(), + }, +})) + +describe('fetchDocumentById', () => { + it('busca o detalhe do documento pelo id e retorna o payload da API', async () => { + const document = { + id_documento: 42, + nome: 'Manual.pdf', + etiquetas: [{ id_etiqueta: 7, nome: 'Importante' }], + } + api.get.mockResolvedValue({ data: document }) + + await expect(fetchDocumentById(42)).resolves.toEqual(document) + expect(api.get).toHaveBeenCalledWith('/api/documentos/42/') + }) +}) diff --git a/src/tests/coverage/DocumentoList.test.jsx b/src/tests/coverage/DocumentoList.test.jsx index ed82aa7..3c7bc94 100644 --- a/src/tests/coverage/DocumentoList.test.jsx +++ b/src/tests/coverage/DocumentoList.test.jsx @@ -1,13 +1,12 @@ -import { render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; +import { fireEvent, render, screen } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; -import DocumentList from "./DocumentList"; +import DocumentList from "../../components/Document/DocumentList/DocumentList"; -vi.mock("./DocumentItem", () => ({ +vi.mock("../../components/Document/DocumentItem/DocumentItem", () => ({ default: ({ document, onView }) => (
- {document.name} + {document.nome}
), @@ -26,8 +25,8 @@ describe("DocumentList", () => { it("renderiza os documentos recebidos", () => { const documents = [ - { id: 1, name: "Documento 1" }, - { id: 2, name: "Documento 2" }, + { id_documento: 1, nome: "Documento 1" }, + { id_documento: 2, nome: "Documento 2" }, ]; render(); @@ -47,12 +46,12 @@ describe("DocumentList", () => { it("chama onView ao visualizar um documento", async () => { const onView = vi.fn(); - const document = { id: 1, name: "Documento 1" }; + const document = { id_documento: 1, nome: "Documento 1" }; render(); - await userEvent.click(screen.getByRole("button", { name: "Visualizar" })); + fireEvent.click(screen.getByRole("button", { name: "Visualizar" })); expect(onView).toHaveBeenCalledWith(document); }); -}); \ No newline at end of file +}); diff --git a/src/tests/coverage/Home.test.jsx b/src/tests/coverage/Home.test.jsx index e123c46..09f722d 100644 --- a/src/tests/coverage/Home.test.jsx +++ b/src/tests/coverage/Home.test.jsx @@ -1,14 +1,18 @@ import { act, fireEvent, render, screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import Home from "./Home"; -import { fetchDocuments } from "../services/documentService"; - -vi.mock("../services/documentService", () => ({ +import Home from "../../pages/Home/Home"; +import { + fetchDocumentById, + fetchDocuments, +} from "../../services/documentService"; + +vi.mock("../../services/documentService", () => ({ + fetchDocumentById: vi.fn(), fetchDocuments: vi.fn(), })); -vi.mock("../components/SearchBar/SearchBar", () => ({ +vi.mock("../../components/SearchBar/SearchBar", () => ({ default: ({ value, onChange }) => ( ({ ), })); -vi.mock("../components/Document/DocumentList", () => ({ +vi.mock("../../components/Document/DocumentList/DocumentList", () => ({ default: ({ documents, onView }) => (
{documents.map((document) => ( - ))} @@ -30,7 +34,7 @@ vi.mock("../components/Document/DocumentList", () => ({ ), })); -vi.mock("../components/Pagination/Pagination", () => ({ +vi.mock("../../components/Pagination/Pagination", () => ({ default: ({ currentPage, totalPages, onPageChange }) => ( +vi.mock("../../components/Document/DocumentModal/DocumentModal", () => ({ + default: ({ document, error, loading, onClose, onRetry, onViewPdf }) => + document || error || loading ? ( +
+ {loading && Carregando detalhes} + {error && } + {document && !loading && !error && ( + <> + Detalhe: {document.nome} + + + )} + +
) : null, })); @@ -63,8 +77,6 @@ describe("Home", () => { render(); - expect(screen.getByText("Carregando documentos...")).toBeInTheDocument(); - await act(async () => { await vi.advanceTimersByTimeAsync(400); }); @@ -117,21 +129,23 @@ describe("Home", () => { }); }); - it("abre o PDF do documento selecionado", async () => { - const windowOpen = vi - .spyOn(window, "open") - .mockImplementation(() => null); - + it("busca e exibe os detalhes do documento selecionado", async () => { + const windowOpen = vi.spyOn(window, "open").mockImplementation(() => null); fetchDocuments.mockResolvedValue({ results: [ { - id: 1, + id_documento: 1, nome: "Documento 1", - pdfUrl: "https://exemplo.com/documento.pdf", }, ], pages: 1, }); + let resolveDocumentDetails; + fetchDocumentById.mockReturnValue( + new Promise((resolve) => { + resolveDocumentDetails = resolve; + }) + ); render(); @@ -140,13 +154,80 @@ describe("Home", () => { }); fireEvent.click(screen.getByText("Documento 1")); - fireEvent.click(screen.getByText("Abrir PDF")); + expect(fetchDocumentById).toHaveBeenCalledWith(1); + expect(screen.getByText("Carregando detalhes")).toBeInTheDocument(); + await act(async () => { + resolveDocumentDetails({ + id_documento: 1, + nome: "Documento completo", + data: "http://localhost:8000/media/documentos/manual.pdf", + }); + }); + + expect(screen.getByText("Detalhe: Documento completo")).toBeInTheDocument(); + fireEvent.click(screen.getByText("Abrir PDF")); expect(windowOpen).toHaveBeenCalledWith( - "https://exemplo.com/documento.pdf", + "http://localhost:8000/media/documentos/manual.pdf", "_blank" ); windowOpen.mockRestore(); }); -}); \ No newline at end of file + + it("permite tentar novamente ao falhar ao buscar os detalhes", async () => { + fetchDocuments.mockResolvedValue({ + results: [{ id_documento: 1, nome: "Documento 1" }], + pages: 1, + }); + fetchDocumentById + .mockRejectedValueOnce(new Error("Erro na API")) + .mockResolvedValueOnce({ id_documento: 1, nome: "Documento completo" }); + + render(); + + await act(async () => { + await vi.advanceTimersByTimeAsync(400); + }); + + fireEvent.click(screen.getByText("Documento 1")); + + await act(async () => {}); + + expect(screen.getByText("Tentar novamente")).toBeInTheDocument(); + fireEvent.click(screen.getByText("Tentar novamente")); + + await act(async () => {}); + + expect(fetchDocumentById).toHaveBeenCalledTimes(2); + expect(screen.getByText("Detalhe: Documento completo")).toBeInTheDocument(); + }); + + it("ignora a resposta de detalhes depois que o modal é fechado", async () => { + fetchDocuments.mockResolvedValue({ + results: [{ id_documento: 1, nome: "Documento 1" }], + pages: 1, + }); + let resolveDocumentDetails; + fetchDocumentById.mockReturnValue( + new Promise((resolve) => { + resolveDocumentDetails = resolve; + }) + ); + + render(); + + await act(async () => { + await vi.advanceTimersByTimeAsync(400); + }); + + fireEvent.click(screen.getByText("Documento 1")); + fireEvent.click(screen.getByText("Fechar modal")); + + await act(async () => { + resolveDocumentDetails({ id_documento: 1, nome: "Documento completo" }); + }); + + expect(screen.queryByText("Detalhe: Documento completo")).not.toBeInTheDocument(); + }); +}); diff --git a/src/tests/setup.js b/src/tests/setup.js new file mode 100644 index 0000000..e87ca3f --- /dev/null +++ b/src/tests/setup.js @@ -0,0 +1,7 @@ +import '@testing-library/jest-dom/vitest' +import { cleanup } from '@testing-library/react' +import { afterEach } from 'vitest' + +afterEach(() => { + cleanup() +}) diff --git a/vite.config.js b/vite.config.js index 9982072..84e47e7 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,7 +1,12 @@ import react from '@vitejs/plugin-react' -import { defineConfig } from 'vite' +import { defineConfig } from 'vitest/config' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + test: { + environment: 'jsdom', + include: ['src/**/*.test.{js,jsx}'], + setupFiles: './src/tests/setup.js', + }, })