From 46257b7d1510a8ca1a81d55ad60ed73021b699b7 Mon Sep 17 00:00:00 2001 From: Daniel Sendreti Date: Sun, 20 Sep 2026 19:28:29 -0300 Subject: [PATCH 01/10] feat(26): add document detail service and test setup --- eslint.config.js | 2 +- package-lock.json | 1059 ++++++++++++++++++++- package.json | 10 +- src/services/documentService.js | 8 +- src/services/documentService.test.js | 24 + src/tests/coverage/DocumentoList.test.jsx | 19 +- src/tests/coverage/Home.test.jsx | 18 +- src/tests/setup.js | 7 + vite.config.js | 7 +- 9 files changed, 1127 insertions(+), 27 deletions(-) create mode 100644 src/services/documentService.test.js create mode 100644 src/tests/setup.js 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/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..e6afe28 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: 1, nome: "Documento 1" }, + { id: 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: 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..8068fc1 100644 --- a/src/tests/coverage/Home.test.jsx +++ b/src/tests/coverage/Home.test.jsx @@ -1,14 +1,14 @@ 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"; +import Home from "../../pages/Home/Home"; +import { fetchDocuments } from "../../services/documentService"; -vi.mock("../services/documentService", () => ({ +vi.mock("../../services/documentService", () => ({ 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 +30,7 @@ vi.mock("../components/Document/DocumentList", () => ({ ), })); -vi.mock("../components/Pagination/Pagination", () => ({ +vi.mock("../../components/Pagination/Pagination", () => ({ default: ({ currentPage, totalPages, onPageChange }) => ( @@ -63,8 +63,6 @@ describe("Home", () => { render(); - expect(screen.getByText("Carregando documentos...")).toBeInTheDocument(); - await act(async () => { await vi.advanceTimersByTimeAsync(400); }); @@ -149,4 +147,4 @@ describe("Home", () => { windowOpen.mockRestore(); }); -}); \ No newline at end of file +}); 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', + }, }) From 727d9ae29220676c319fcd79d77a41af55c38b5d Mon Sep 17 00:00:00 2001 From: Daniel Sendreti Date: Sun, 20 Sep 2026 20:32:19 -0300 Subject: [PATCH 02/10] feat(26): load document details in modal --- .../Document/DocumentItem/DocumentItem.jsx | 6 +- .../Document/DocumentList/DocumentList.jsx | 4 +- .../Document/DocumentModal/DocumentModal.jsx | 30 +++++- src/pages/Home/Home.jsx | 58 +++++++++- src/tests/coverage/DocumentoList.test.jsx | 6 +- src/tests/coverage/Home.test.jsx | 102 +++++++++++++++--- 6 files changed, 174 insertions(+), 32 deletions(-) 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/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.jsx b/src/components/Document/DocumentModal/DocumentModal.jsx index 0aa6771..e1888ce 100644 --- a/src/components/Document/DocumentModal/DocumentModal.jsx +++ b/src/components/Document/DocumentModal/DocumentModal.jsx @@ -1,7 +1,33 @@ import { FiEye } from "react-icons/fi"; import "./DocumentModal.css"; -function DocumentModal({ document, onClose, onViewPdf }) { +function DocumentModal({ document, onClose, onViewPdf, loading, error, onRetry }) { + if (loading || error) { + return ( +
+
event.stopPropagation()} + > + + + {loading ? ( +

Carregando detalhes do documento...

+ ) : ( +
+

{error}

+ +
+ )} +
+
+ ); + } + if (!document) { return null; } @@ -174,4 +200,4 @@ function DocumentModal({ document, onClose, onViewPdf }) { ); } -export default DocumentModal; \ No newline at end of file +export default DocumentModal; diff --git a/src/pages/Home/Home.jsx b/src/pages/Home/Home.jsx index c9ad150..ae1c42b 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 () => { @@ -64,6 +67,48 @@ function Home() { 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/tests/coverage/DocumentoList.test.jsx b/src/tests/coverage/DocumentoList.test.jsx index e6afe28..3c7bc94 100644 --- a/src/tests/coverage/DocumentoList.test.jsx +++ b/src/tests/coverage/DocumentoList.test.jsx @@ -25,8 +25,8 @@ describe("DocumentList", () => { it("renderiza os documentos recebidos", () => { const documents = [ - { id: 1, nome: "Documento 1" }, - { id: 2, nome: "Documento 2" }, + { id_documento: 1, nome: "Documento 1" }, + { id_documento: 2, nome: "Documento 2" }, ]; render(); @@ -46,7 +46,7 @@ describe("DocumentList", () => { it("chama onView ao visualizar um documento", async () => { const onView = vi.fn(); - const document = { id: 1, nome: "Documento 1" }; + const document = { id_documento: 1, nome: "Documento 1" }; render(); diff --git a/src/tests/coverage/Home.test.jsx b/src/tests/coverage/Home.test.jsx index 8068fc1..8b92314 100644 --- a/src/tests/coverage/Home.test.jsx +++ b/src/tests/coverage/Home.test.jsx @@ -2,9 +2,13 @@ import { act, fireEvent, render, screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import Home from "../../pages/Home/Home"; -import { fetchDocuments } from "../../services/documentService"; +import { + fetchDocumentById, + fetchDocuments, +} from "../../services/documentService"; vi.mock("../../services/documentService", () => ({ + fetchDocumentById: vi.fn(), fetchDocuments: vi.fn(), })); @@ -22,7 +26,7 @@ vi.mock("../../components/Document/DocumentList/DocumentList", () => ({ default: ({ documents, onView }) => (
{documents.map((document) => ( - ))} @@ -43,9 +47,16 @@ vi.mock("../../components/Pagination/Pagination", () => ({ })); vi.mock("../../components/Document/DocumentModal/DocumentModal", () => ({ - default: ({ document, onViewPdf }) => - document ? ( - + default: ({ document, error, loading, onClose, onRetry }) => + document || error || loading ? ( +
+ {loading && Carregando detalhes} + {error && } + {document && !loading && !error && ( + Detalhe: {document.nome} + )} + +
) : null, })); @@ -115,21 +126,48 @@ 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 () => { 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(); + + await act(async () => { + await vi.advanceTimersByTimeAsync(400); + }); + + fireEvent.click(screen.getByText("Documento 1")); + expect(fetchDocumentById).toHaveBeenCalledWith(1); + expect(screen.getByText("Carregando detalhes")).toBeInTheDocument(); + + await act(async () => { + resolveDocumentDetails({ id_documento: 1, nome: "Documento completo" }); + }); + + expect(screen.getByText("Detalhe: Documento completo")).toBeInTheDocument(); + }); + + 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(); @@ -138,13 +176,43 @@ describe("Home", () => { }); fireEvent.click(screen.getByText("Documento 1")); - fireEvent.click(screen.getByText("Abrir PDF")); - expect(windowOpen).toHaveBeenCalledWith( - "https://exemplo.com/documento.pdf", - "_blank" + 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; + }) ); - windowOpen.mockRestore(); + 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(); }); }); From 9f9ff805ed5257982cc27dbf345fb0b9936ae366 Mon Sep 17 00:00:00 2001 From: Daniel Sendreti Date: Sun, 20 Sep 2026 20:45:03 -0300 Subject: [PATCH 03/10] feat(26): display document details and tags in modal --- .../Document/DocumentModal/DocumentModal.css | 12 +- .../Document/DocumentModal/DocumentModal.jsx | 165 +++++------------- .../DocumentModal/DocumentModal.test.jsx | 76 ++++++++ src/pages/Home/Home.jsx | 4 +- src/tests/coverage/Home.test.jsx | 21 ++- 5 files changed, 151 insertions(+), 127 deletions(-) create mode 100644 src/components/Document/DocumentModal/DocumentModal.test.jsx 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 e1888ce..88998c0 100644 --- a/src/components/Document/DocumentModal/DocumentModal.jsx +++ b/src/components/Document/DocumentModal/DocumentModal.jsx @@ -1,16 +1,26 @@ import { FiEye } from "react-icons/fi"; import "./DocumentModal.css"; +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 }) { if (loading || error) { return (
-
event.stopPropagation()} - > +
event.stopPropagation()}> {loading ? ( @@ -28,160 +38,72 @@ function DocumentModal({ document, onClose, onViewPdf, loading, error, onRetry } ); } - if (!document) { - return null; - } + if (!document) return null; - const tags = [ - "Materiais metálicos", - "Manutenção", - "MIL-HDBK", - "Engenharia", - "Handbook", - ]; + const tags = document.etiquetas || []; return ( -
-
event.stopPropagation()} - > +
+
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} - -
- -
- - Nível de Acesso - - - - {document.accessLevel} - + Setor + {document.setor}
- - Categoria - - - - {document.category || document.type} - + Nivel de acesso + {document.nivel}
- - Dados do arquivo - + Dados do arquivo
- ↻ Revisão: - - - {document.revision || "5J"} - + Tipo: + {document.tipo_arquivo || "-"}
- ◷ Data de Atualização: - - - {document.updatedAt} - -
- -
- ▤ 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. + )}
@@ -189,6 +111,7 @@ function DocumentModal({ document, onClose, onViewPdf, loading, error, onRetry }
} {document && !loading && !error && ( - Detalhe: {document.nome} + <> + Detalhe: {document.nome} + + )}
@@ -127,6 +130,7 @@ describe("Home", () => { }); it("busca e exibe os detalhes do documento selecionado", async () => { + const windowOpen = vi.spyOn(window, "open").mockImplementation(() => null); fetchDocuments.mockResolvedValue({ results: [ { @@ -154,10 +158,21 @@ describe("Home", () => { expect(screen.getByText("Carregando detalhes")).toBeInTheDocument(); await act(async () => { - resolveDocumentDetails({ id_documento: 1, nome: "Documento completo" }); + 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( + "http://localhost:8000/media/documentos/manual.pdf", + "_blank" + ); + + windowOpen.mockRestore(); }); it("permite tentar novamente ao falhar ao buscar os detalhes", async () => { From 548d2effbc292b48f1b9fa17e8431a46a7d189d5 Mon Sep 17 00:00:00 2001 From: Daniel Sendreti Date: Tue, 22 Sep 2026 20:50:34 -0300 Subject: [PATCH 04/10] build(26): configure vitest coverage and fix sonarcloud quality gate --- .github/workflows/ci-frontend.yml | 18 +++++- .gitignore | 2 + package-lock.json | 104 ++++++++++++++++++++++++++++++ package.json | 4 +- sonar-project.properties | 5 ++ vite.config.js | 11 +++- 6 files changed, 141 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-frontend.yml b/.github/workflows/ci-frontend.yml index 7d48809..fb85135 100644 --- a/.github/workflows/ci-frontend.yml +++ b/.github/workflows/ci-frontend.yml @@ -46,6 +46,16 @@ jobs: - name: Gerar build de produção run: npm run build + - name: Executar testes e gerar coverage + run: npm run test:coverage + + - name: Fazer upload do relatório de cobertura + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage/ + retention-days: 1 + secondary-build: name: "Estágio 2: análise no SonarCloud" runs-on: ubuntu-latest @@ -61,6 +71,12 @@ jobs: with: fetch-depth: 0 + - name: Fazer download do relatório de cobertura + uses: actions/download-artifact@v4 + with: + name: coverage-report + path: coverage/ + - name: Executar análise no SonarCloud if: env.SONAR_TOKEN != '' uses: SonarSource/sonarqube-scan-action@7006c4492b2e0ee0f816d36501671557c97f5995 # v8.1.0 @@ -70,4 +86,4 @@ jobs: - name: Informar ausência do token do SonarCloud if: env.SONAR_TOKEN == '' - run: echo "::notice::SonarCloud não executado porque o secret SONAR_TOKEN não está disponível." + run: echo "::notice::SonarCloud não executado porque o secret SONAR_TOKEN não está disponível." \ No newline at end of file diff --git a/.gitignore b/.gitignore index a547bf3..3252172 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +coverage/ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 383577d..1289a57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", "@vitejs/plugin-react": "^6.1.0", + "@vitest/coverage-v8": "^5.0.1", "eslint": "^10.9.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.4", @@ -339,6 +340,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/@bramus/specificity": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", @@ -1256,6 +1267,58 @@ } } }, + "node_modules/@vitest/coverage-v8": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-5.0.1.tgz", + "integrity": "sha512-FRC8ACiudC3dI6MTplzRSYWHDRnIv2IPfbzs4FdoJNsMal/35sWV8hwIfV8ZcqzSPy+uXHeMVONt9CEqtOU17w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/istanbul-lib-coverage": "^1.0.0", + "@vitest/istanbul-lib-report": "^1.0.0", + "ast-v8-to-istanbul": "^1.0.5", + "magicast": "^0.5.4", + "obug": "^2.1.4", + "std-env": "^4.2.0", + "tinyrainbow": "^3.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "5.0.1", + "vitest": "5.0.1" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/istanbul-lib-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-coverage/-/istanbul-lib-coverage-1.0.1.tgz", + "integrity": "sha512-k3DJZ8LhMBK9NS4SclF1ASD3OgXEWDorbIcPTRDK0/Zae6fRvu+fJRxtFdLfHsa9Y24beCdPnoNZ4LviTNstfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@vitest/istanbul-lib-report": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-report/-/istanbul-lib-report-1.0.1.tgz", + "integrity": "sha512-1EOLRfsTMnyAr3+kEAsP4o9dhaDlGPpD7H5iLBBeq//YpNB1VIahkPhB+eRp9N2Dkfw8oySROjE3yf9XDeaIkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/istanbul-lib-coverage": "1.0.1" + }, + "engines": { + "node": ">=22" + } + }, "node_modules/@vitest/mocker": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.1.tgz", @@ -1391,6 +1454,25 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.7.tgz", + "integrity": "sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2848,6 +2930,18 @@ "@jridgewell/sourcemap-codec": "^1.6.0" } }, + "node_modules/magicast": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.5.tgz", + "integrity": "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3393,6 +3487,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tldts": { "version": "7.4.13", "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.13.tgz", diff --git a/package.json b/package.json index c9dca8d..7142330 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "lint": "eslint .", "preview": "vite preview", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" }, "dependencies": { "axios": "^1.20.0", @@ -24,6 +25,7 @@ "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", "@vitejs/plugin-react": "^6.1.0", + "@vitest/coverage-v8": "^5.0.1", "eslint": "^10.9.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.4", diff --git a/sonar-project.properties b/sonar-project.properties index 07db20a..da5b19d 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -12,3 +12,8 @@ sonar.sources=src # Encoding of the source code. Default is default system encoding sonar.sourceEncoding=UTF-8 + +sonar.javascript.lcov.reportPaths=coverage/lcov.info +sonar.exclusions=**/*.test.jsx, **/*.test.js, src/tests/**/*, src/tests/setup.js, vite.config.js +sonar.tests=src +sonar.test.inclusions=**/*.test.jsx, **/*.test.js \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index 84e47e7..7986c80 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,12 +1,21 @@ import react from '@vitejs/plugin-react' 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', + coverage: { + provider: 'v8', + reporter: ['text', 'lcov'], + include: ['src/**/*.{js,jsx}'], + exclude: [ + 'src/**/*.test.{js,jsx}', + 'src/tests/**/*', + 'src/tests/setup.js' + ], + }, }, }) From 221e839dff0e028105392eedc5b0f8f6fb349ec3 Mon Sep 17 00:00:00 2001 From: Daniel Sendreti Date: Tue, 22 Sep 2026 20:59:56 -0300 Subject: [PATCH 05/10] test(26): add coverage for DocumentItem, DocumentModal and Home --- .../DocumentItem/DocumentItem.test.jsx | 87 ++++++ .../DocumentModal/DocumentModal.test.jsx | 60 ++++ src/pages/Home/Home.test.jsx | 265 ++++++++++++++++++ 3 files changed, 412 insertions(+) create mode 100644 src/components/Document/DocumentItem/DocumentItem.test.jsx create mode 100644 src/pages/Home/Home.test.jsx 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/DocumentModal/DocumentModal.test.jsx b/src/components/Document/DocumentModal/DocumentModal.test.jsx index 6fa9b3a..54cf0be 100644 --- a/src/components/Document/DocumentModal/DocumentModal.test.jsx +++ b/src/components/Document/DocumentModal/DocumentModal.test.jsx @@ -73,4 +73,64 @@ describe("DocumentModal", () => { 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.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(); + }); +}); From ed7c1ec1c7d6444df0b31b615d6d926cd9c0d69b Mon Sep 17 00:00:00 2001 From: Daniel Sendreti Date: Tue, 22 Sep 2026 21:06:22 -0300 Subject: [PATCH 06/10] refactor(26): replace close button text with FiX icon in DocumentModal --- src/components/Document/DocumentModal/DocumentModal.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Document/DocumentModal/DocumentModal.jsx b/src/components/Document/DocumentModal/DocumentModal.jsx index 88998c0..e986168 100644 --- a/src/components/Document/DocumentModal/DocumentModal.jsx +++ b/src/components/Document/DocumentModal/DocumentModal.jsx @@ -1,4 +1,4 @@ -import { FiEye } from "react-icons/fi"; +import { FiEye, FiX } from "react-icons/fi"; import "./DocumentModal.css"; function formatDate(value) { @@ -20,7 +20,7 @@ function DocumentModal({ document, onClose, onViewPdf, loading, error, onRetry }
event.stopPropagation()}> {loading ? ( @@ -58,7 +58,7 @@ function DocumentModal({ document, onClose, onViewPdf, loading, error, onRetry }
From 25ae11fc9c66000551cab1d28fb013a054179418 Mon Sep 17 00:00:00 2001 From: d-broder Date: Wed, 23 Sep 2026 13:08:09 -0300 Subject: [PATCH 07/10] build(26): reverts "build(26): configure vitest coverage and fix sonarcloud quality gate" --- .github/workflows/ci-frontend.yml | 18 +----- .gitignore | 2 - package-lock.json | 104 ------------------------------ package.json | 4 +- sonar-project.properties | 5 -- vite.config.js | 11 +--- 6 files changed, 3 insertions(+), 141 deletions(-) diff --git a/.github/workflows/ci-frontend.yml b/.github/workflows/ci-frontend.yml index fb85135..7d48809 100644 --- a/.github/workflows/ci-frontend.yml +++ b/.github/workflows/ci-frontend.yml @@ -46,16 +46,6 @@ jobs: - name: Gerar build de produção run: npm run build - - name: Executar testes e gerar coverage - run: npm run test:coverage - - - name: Fazer upload do relatório de cobertura - uses: actions/upload-artifact@v4 - with: - name: coverage-report - path: coverage/ - retention-days: 1 - secondary-build: name: "Estágio 2: análise no SonarCloud" runs-on: ubuntu-latest @@ -71,12 +61,6 @@ jobs: with: fetch-depth: 0 - - name: Fazer download do relatório de cobertura - uses: actions/download-artifact@v4 - with: - name: coverage-report - path: coverage/ - - name: Executar análise no SonarCloud if: env.SONAR_TOKEN != '' uses: SonarSource/sonarqube-scan-action@7006c4492b2e0ee0f816d36501671557c97f5995 # v8.1.0 @@ -86,4 +70,4 @@ jobs: - name: Informar ausência do token do SonarCloud if: env.SONAR_TOKEN == '' - run: echo "::notice::SonarCloud não executado porque o secret SONAR_TOKEN não está disponível." \ No newline at end of file + run: echo "::notice::SonarCloud não executado porque o secret SONAR_TOKEN não está disponível." diff --git a/.gitignore b/.gitignore index 3252172..a547bf3 100644 --- a/.gitignore +++ b/.gitignore @@ -22,5 +22,3 @@ dist-ssr *.njsproj *.sln *.sw? - -coverage/ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1289a57..383577d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,6 @@ "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", "@vitejs/plugin-react": "^6.1.0", - "@vitest/coverage-v8": "^5.0.1", "eslint": "^10.9.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.4", @@ -340,16 +339,6 @@ "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/@bramus/specificity": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", @@ -1267,58 +1256,6 @@ } } }, - "node_modules/@vitest/coverage-v8": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-5.0.1.tgz", - "integrity": "sha512-FRC8ACiudC3dI6MTplzRSYWHDRnIv2IPfbzs4FdoJNsMal/35sWV8hwIfV8ZcqzSPy+uXHeMVONt9CEqtOU17w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^1.0.2", - "@vitest/istanbul-lib-coverage": "^1.0.0", - "@vitest/istanbul-lib-report": "^1.0.0", - "ast-v8-to-istanbul": "^1.0.5", - "magicast": "^0.5.4", - "obug": "^2.1.4", - "std-env": "^4.2.0", - "tinyrainbow": "^3.1.1" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@vitest/browser": "5.0.1", - "vitest": "5.0.1" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } - } - }, - "node_modules/@vitest/istanbul-lib-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-coverage/-/istanbul-lib-coverage-1.0.1.tgz", - "integrity": "sha512-k3DJZ8LhMBK9NS4SclF1ASD3OgXEWDorbIcPTRDK0/Zae6fRvu+fJRxtFdLfHsa9Y24beCdPnoNZ4LviTNstfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=22" - } - }, - "node_modules/@vitest/istanbul-lib-report": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-report/-/istanbul-lib-report-1.0.1.tgz", - "integrity": "sha512-1EOLRfsTMnyAr3+kEAsP4o9dhaDlGPpD7H5iLBBeq//YpNB1VIahkPhB+eRp9N2Dkfw8oySROjE3yf9XDeaIkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/istanbul-lib-coverage": "1.0.1" - }, - "engines": { - "node": ">=22" - } - }, "node_modules/@vitest/mocker": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.1.tgz", @@ -1454,25 +1391,6 @@ "node": ">=12" } }, - "node_modules/ast-v8-to-istanbul": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.7.tgz", - "integrity": "sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "estree-walker": "^3.0.3", - "js-tokens": "^10.0.0" - } - }, - "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "dev": true, - "license": "MIT" - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2930,18 +2848,6 @@ "@jridgewell/sourcemap-codec": "^1.6.0" } }, - "node_modules/magicast": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.5.tgz", - "integrity": "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7", - "source-map-js": "^1.2.1" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3487,16 +3393,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyrainbow": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", - "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/tldts": { "version": "7.4.13", "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.13.tgz", diff --git a/package.json b/package.json index 7142330..c9dca8d 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,7 @@ "lint": "eslint .", "preview": "vite preview", "test": "vitest run", - "test:watch": "vitest", - "test:coverage": "vitest run --coverage" + "test:watch": "vitest" }, "dependencies": { "axios": "^1.20.0", @@ -25,7 +24,6 @@ "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", "@vitejs/plugin-react": "^6.1.0", - "@vitest/coverage-v8": "^5.0.1", "eslint": "^10.9.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.4", diff --git a/sonar-project.properties b/sonar-project.properties index da5b19d..07db20a 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -12,8 +12,3 @@ sonar.sources=src # Encoding of the source code. Default is default system encoding sonar.sourceEncoding=UTF-8 - -sonar.javascript.lcov.reportPaths=coverage/lcov.info -sonar.exclusions=**/*.test.jsx, **/*.test.js, src/tests/**/*, src/tests/setup.js, vite.config.js -sonar.tests=src -sonar.test.inclusions=**/*.test.jsx, **/*.test.js \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index 7986c80..84e47e7 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,21 +1,12 @@ import react from '@vitejs/plugin-react' 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', - coverage: { - provider: 'v8', - reporter: ['text', 'lcov'], - include: ['src/**/*.{js,jsx}'], - exclude: [ - 'src/**/*.test.{js,jsx}', - 'src/tests/**/*', - 'src/tests/setup.js' - ], - }, }, }) From 3fd7f9b1e60d6a85a0fe9d266fc4d48406e81f49 Mon Sep 17 00:00:00 2001 From: d-broder Date: Wed, 23 Sep 2026 15:03:28 -0300 Subject: [PATCH 08/10] fix(26): add aria-hidden attribute to icons in DocumentModal --- src/components/Document/DocumentModal/DocumentModal.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Document/DocumentModal/DocumentModal.jsx b/src/components/Document/DocumentModal/DocumentModal.jsx index e986168..8a04de2 100644 --- a/src/components/Document/DocumentModal/DocumentModal.jsx +++ b/src/components/Document/DocumentModal/DocumentModal.jsx @@ -20,7 +20,7 @@ function DocumentModal({ document, onClose, onViewPdf, loading, error, onRetry }
event.stopPropagation()}> {loading ? ( @@ -58,7 +58,7 @@ function DocumentModal({ document, onClose, onViewPdf, loading, error, onRetry }
@@ -114,7 +114,7 @@ function DocumentModal({ document, onClose, onViewPdf, loading, error, onRetry } disabled={!document.data} onClick={() => onViewPdf(document)} > - +