diff --git a/package-lock.json b/package-lock.json index 2dcc855..730be53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,9 @@ "devDependencies": { "@eslint/js": "^9.15.0", "@react-router/dev": "^7.5.1", + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", "@types/jsonwebtoken": "^9.0.9", "@types/node": "^22.10.2", "@types/pg": "^8.15.2", @@ -50,7 +53,9 @@ "eslint-plugin-import": "^2.31.0", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", + "global-jsdom": "^26.0.0", "globals": "^15.12.0", + "jsdom": "^26.1.0", "postcss": "^8.5.3", "react-router-devtools": "^1.1.10", "tailwindcss": "^3.4.17", @@ -58,9 +63,17 @@ "typescript": "~5.6.2", "typescript-eslint": "^8.15.0", "vite": "^6.0.1", - "vite-tsconfig-paths": "^5.1.4" + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.2.1" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.3.tgz", + "integrity": "sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -87,6 +100,27 @@ "node": ">=6.0.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -642,6 +676,121 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", + "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", @@ -3207,6 +3356,96 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", + "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "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.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -3239,6 +3478,13 @@ "optional": true, "peer": 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" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3284,6 +3530,16 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, "node_modules/@types/d3-hierarchy": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-1.1.11.tgz", @@ -3291,6 +3547,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -4094,6 +4357,134 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, + "node_modules/@vitest/expect": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.1.tgz", + "integrity": "sha512-FqS/BnDOzV6+IpxrTg5GQRyLOCtcJqkwMwcS8qGCI2IyRVDwPAtutztaf1CjtPHlZlWtl1yUPCd7HM0cNiDOYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.1", + "@vitest/utils": "3.2.1", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.1.tgz", + "integrity": "sha512-OXxMJnx1lkB+Vl65Re5BrsZEHc90s5NMjD23ZQ9NlU7f7nZiETGoX4NeKZSmsKjseuMq2uOYXdLOeoM0pJU+qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.1", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.1.tgz", + "integrity": "sha512-xBh1X2GPlOGBupp6E1RcUQWIxw0w/hRLd3XyBS6H+dMdKTAqHDNsIR2AnJwPA3yYe9DFy3VUKTe3VRTrAiQ01g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.1.tgz", + "integrity": "sha512-kygXhNTu/wkMYbwYpS3z/9tBe0O8qpdBuC3dD/AW9sWa0LE/DAZEjnHtWA9sIad7lpD4nFW1yQ+zN7mEKNH3yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.1", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.1.tgz", + "integrity": "sha512-5xko/ZpW2Yc65NVK9Gpfg2y4BFvcF+At7yRT5AHUpTg9JvZ4xZoyuRY4ASlmNcBZjMslV08VRLDrBOmUe2YX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/spy": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.1.tgz", + "integrity": "sha512-Nbfib34Z2rfcJGSetMxjDCznn4pCYPZOtQYox2kzebIJcgH75yheIKd5QYSFmR8DIZf2M8fwOm66qSDIfRFFfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.1.tgz", + "integrity": "sha512-KkHlGhePEKZSub5ViknBcN5KEF+u7dSUr9NW8QsVICusUojrgrOnnY3DEWWO877ax2Pyopuk2qHmt+gkNKnBVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.1", + "loupe": "^3.1.3", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/abbrev": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", @@ -4163,6 +4554,16 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4251,6 +4652,16 @@ "node": ">=10" } }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", @@ -4377,6 +4788,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -4787,10 +5208,27 @@ ], "license": "CC-BY-4.0" }, - "node_modules/chain-function": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/chain-function/-/chain-function-1.0.1.tgz", - "integrity": "sha512-SxltgMwL9uCko5/ZCLiyG2B7R9fY4pDZUw7hJ4MhirdjBLosoDqkWABi3XMucddHdLiFJMb7PD2MZifZriuMTg==", + "node_modules/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/chain-function": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chain-function/-/chain-function-1.0.1.tgz", + "integrity": "sha512-SxltgMwL9uCko5/ZCLiyG2B7R9fY4pDZUw7hJ4MhirdjBLosoDqkWABi3XMucddHdLiFJMb7PD2MZifZriuMTg==", "dev": true, "license": "MIT" }, @@ -4811,6 +5249,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -5090,6 +5538,13 @@ "node": ">= 8" } }, + "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/cssbeautify": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/cssbeautify/-/cssbeautify-0.3.1.tgz", @@ -5114,6 +5569,20 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.3.1.tgz", + "integrity": "sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.1.2", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -5259,6 +5728,20 @@ "node": ">=12" } }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -5325,9 +5808,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5354,6 +5837,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/decimal.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "dev": true, + "license": "MIT" + }, "node_modules/dedent": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", @@ -5369,6 +5859,16 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -5482,6 +5982,13 @@ "node": ">=0.10.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, "node_modules/dom-helpers": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", @@ -5629,6 +6136,19 @@ "node": ">= 0.8" } }, + "node_modules/entities": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", + "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -6128,6 +6648,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", @@ -6160,6 +6690,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/expect-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", + "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -6707,6 +7247,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/global-jsdom": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/global-jsdom/-/global-jsdom-26.0.0.tgz", + "integrity": "sha512-BqXpTNZFjP40N+s4k8Bk9HS8GFVPJB/+TKtwcShM84wLv6C5dH9o1dydI3pL6potanhfDiIAVDbaaGj/uSdRSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "jsdom": ">=26 <27" + } + }, "node_modules/globals": { "version": "15.15.0", "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", @@ -6908,6 +7461,19 @@ "html": "bin/html.js" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -6924,6 +7490,34 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -6973,6 +7567,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/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -7267,6 +7871,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -7510,6 +8121,46 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -7762,6 +8413,13 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -7781,6 +8439,26 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -7908,6 +8586,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": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -8216,6 +8904,13 @@ "node": ">=10" } }, + "node_modules/nwsapi": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", + "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -8471,6 +9166,19 @@ "dev": true, "license": "MIT" }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -8550,6 +9258,16 @@ "dev": true, "license": "MIT" }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/pg": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.0.tgz", @@ -8910,6 +9628,51 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, "node_modules/proc-log": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", @@ -9400,6 +10163,20 @@ "node": ">=8.10.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -9550,6 +10327,13 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -9654,6 +10438,19 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", @@ -9887,6 +10684,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -9972,6 +10776,13 @@ "node": ">= 10.x" } }, + "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/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -9981,6 +10792,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, "node_modules/stream-slice": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/stream-slice/-/stream-slice-0.1.2.tgz", @@ -10169,6 +10987,19 @@ "node": ">=4" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -10236,6 +11067,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", @@ -10313,6 +11151,115 @@ "node": ">=0.8" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz", + "integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.0.tgz", + "integrity": "sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -10334,6 +11281,32 @@ "node": ">=0.6" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -11066,6 +12039,135 @@ } } }, + "node_modules/vitest": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.1.tgz", + "integrity": "sha512-VZ40MBnlE1/V5uTgdqY3DmjUgZtIzsYq758JGlyQrv5syIsaYcabkfPkEuWML49Ph0D/SoqpVFd0dyVTr551oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.1", + "@vitest/mocker": "3.2.1", + "@vitest/pretty-format": "^3.2.1", + "@vitest/runner": "3.2.1", + "@vitest/snapshot": "3.2.1", + "@vitest/spy": "3.2.1", + "@vitest/utils": "3.2.1", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.0", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.1", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.1", + "@vitest/ui": "3.2.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest/node_modules/vite-node": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.1.tgz", + "integrity": "sha512-V4EyKQPxquurNJPtQJRZo8hKOoKNBRIhxcDbQFPFig0JdoWcUhwRgK8yoCXXrfYVPKS6XwirGHPszLnR8FbjCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/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/warning": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", @@ -11076,6 +12178,66 @@ "loose-envify": "^1.0.0" } }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -11180,6 +12342,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -11278,6 +12457,45 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 2c2cc02..e34667e 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "migrate:dev": "tsx src/db/scripts/migrate.ts", "reset-db:dev": "npm run drop-db:dev && npm run create-db:dev && npm run migrate:dev && npm run seed:dev", "seed": "node dist/db/scripts/seed.js", - "seed:dev": "tsx src/db/scripts/seed.ts" + "seed:dev": "tsx src/db/scripts/seed.ts", + "test": "vitest" }, "dependencies": { "@hookform/resolvers": "^4.1.3", @@ -48,6 +49,9 @@ "devDependencies": { "@eslint/js": "^9.15.0", "@react-router/dev": "^7.5.1", + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", "@types/jsonwebtoken": "^9.0.9", "@types/node": "^22.10.2", "@types/pg": "^8.15.2", @@ -61,7 +65,9 @@ "eslint-plugin-import": "^2.31.0", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", + "global-jsdom": "^26.0.0", "globals": "^15.12.0", + "jsdom": "^26.1.0", "postcss": "^8.5.3", "react-router-devtools": "^1.1.10", "tailwindcss": "^3.4.17", @@ -69,6 +75,7 @@ "typescript": "~5.6.2", "typescript-eslint": "^8.15.0", "vite": "^6.0.1", - "vite-tsconfig-paths": "^5.1.4" + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.2.1" } } diff --git a/src/components/ui/button/button.test.tsx b/src/components/ui/button/button.test.tsx new file mode 100644 index 0000000..7152101 --- /dev/null +++ b/src/components/ui/button/button.test.tsx @@ -0,0 +1,30 @@ +import { render, screen } from "@testing-library/react"; +import { describe, it, expect } from "vitest"; + +import { Button } from "@/components/ui"; + +describe("Button Component", () => { + it("render correctly", () => { + render(); + + const button = screen.queryByRole("button", { + name: /click me/i, + }); + + expect(button).toBeInTheDocument(); + }); + + it("render secondary variant correctly", () => { + render(); + + const button = screen.queryByRole("button", { + name: /click me/i, + }); + + expect(button).toHaveClass( + "bg-secondary", + "text-secondary-foreground", + "hover:bg-secondary-hover" + ); + }); +}); diff --git a/src/lib/utils.tests.ts b/src/lib/utils.tests.ts new file mode 100644 index 0000000..827733f --- /dev/null +++ b/src/lib/utils.tests.ts @@ -0,0 +1,75 @@ +import { vi } from "vitest"; + +import type { Category } from "@/models/category.model"; +import type { Product } from "@/models/product.model"; +import type { User } from "@/models/user.model"; + +import type { Session } from "react-router"; + +type TestRequestConfig = { + url?: string; + headers?: HeadersInit; +}; + +// Helper functions for creating commonly used test objects +export const createTestUser = (overrides?: Partial): User => ({ + id: 1, + email: "", + name: null, + password: null, + isGuest: false, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + ...overrides, +}); + +export const createTestRequest = (overrides?: TestRequestConfig): Request => { + const defaultConfig: TestRequestConfig = { + url: "http://localhost/test", + headers: { Cookie: "session=mock-session-id" }, + }; + + const config = { ...defaultConfig, ...overrides }; + return new Request(config.url!, { + headers: { ...defaultConfig.headers, ...config.headers }, + }); +}; + +export const createMockSession = (userId: number | null): Session => ({ + id: "mock-session-id", + data: {}, + has: vi.fn(), + get: vi.fn().mockReturnValue(userId), // Default userId in session + set: vi.fn(), + flash: vi.fn(), + unset: vi.fn(), +}); + +export const createTestProduct = (overrides?: Partial): Product => ({ + id: 1, + title: "Test Product", + imgSrc: "/test-image.jpg", + alt: "Test alt text", + price: 100, + description: "Test description", + categoryId: 1, + isOnSale: false, + features: ["Feature 1", "Feature 2"], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + ...overrides, +}); + +export const createTestCategory = ( + overrides?: Partial +): Category => ({ + id: 1, + title: "Polos", + slug: "polos", + imgSrc: "/images/polos.jpg", + alt: "Colección de polos para programadores", + description: "Explora nuestra colección de polos para programadores", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + ...overrides, +}); diff --git a/src/routes/not-found/index.tsx b/src/routes/not-found/index.tsx index 5148f87..e596126 100644 --- a/src/routes/not-found/index.tsx +++ b/src/routes/not-found/index.tsx @@ -5,14 +5,16 @@ import { Button, Container, Section } from "@/components/ui"; export default function NotFound() { return ( -
-
-

404

-

Página no encontrada

-

+

+
+

404

+

+ Página no encontrada +

+

No pudimos encontrar la página que estás buscando.

-
diff --git a/src/routes/order-confirmation/order-confirmation.loader.test.ts b/src/routes/order-confirmation/order-confirmation.loader.test.ts new file mode 100644 index 0000000..a8475b1 --- /dev/null +++ b/src/routes/order-confirmation/order-confirmation.loader.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; + +import { loader } from "."; + +describe("OrderConfirmation loader", () => { + // Helper function to create loader arguments + const createLoaderArgs = (orderId: string) => ({ + params: { orderId }, + request: new Request(`http://localhost/order-confirmation/${orderId}`), + context: {}, + }); + + it("should return orderId from params", async () => { + // Step 1: Setup - Create test data + const testOrderId = "testOrderId-123"; // Example order ID + + // Step 2: Mock - Not needed as loader has no dependencies + + // Step 3: Call service function + const result = await loader(createLoaderArgs(testOrderId)); + + // Step 4: Verify expected behavior + expect(result).toEqual({ + orderId: testOrderId, + }); + }); +}); diff --git a/src/routes/order-confirmation/order-confirmation.test.tsx b/src/routes/order-confirmation/order-confirmation.test.tsx new file mode 100644 index 0000000..becd17c --- /dev/null +++ b/src/routes/order-confirmation/order-confirmation.test.tsx @@ -0,0 +1,50 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import OrderConfirmation from "."; +import type { Route } from "./+types"; + +// Creates minimal test props for OrderConfirmation component +const createTestProps = (orderId = "test-123"): Route.ComponentProps => ({ + loaderData: { orderId }, + params: vi.fn() as any, + matches: vi.fn() as any, +}); + +describe("OrderConfirmation", () => { + describe("Success Messages Display", () => { + it("should display all success messages correctly", () => { + // Step 1: Setup - Create test props + const props = createTestProps(); + // Step 2: Mock + // Step 3: Call - Render component + render(); + // Step 4: Verify - Check all success messages + const expectedMessages = [ + "¡Muchas gracias por tu compra!", + "Tu orden está en camino", + "Llegaremos a la puerta de tu domicilio lo antes posible", + ]; + expectedMessages.forEach((message) => { + expect(screen.queryByText(message)).toBeInTheDocument(); + }); + }); + }); + + describe("Order Tracking Information", () => { + it("should display correct tracking code section", () => { + // Step 1: Setup - Create test props with a specific order ID + const testOrderId = "order-456"; + const props = createTestProps(testOrderId); + // Step 2: Mock + // Step 3: Call - Render component + render(); + // Step 4: Verify - Check tracking code section + const trackingCodeLabel = screen.queryByText("Código de seguimiento"); + expect(trackingCodeLabel).toBeInTheDocument(); + + const trackingCode = screen.queryByText(testOrderId); + expect(trackingCode).toBeInTheDocument(); + }); + }); +}); diff --git a/src/routes/product/product.loader.test.ts b/src/routes/product/product.loader.test.ts new file mode 100644 index 0000000..794b99b --- /dev/null +++ b/src/routes/product/product.loader.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it, vi } from "vitest"; + +import { createTestProduct } from "@/lib/utils.tests"; +import * as productService from "@/services/product.service"; + +import { loader } from "."; + +// Mock the product service +vi.mock("@/services/product.service", () => ({ + getProductById: vi.fn(), // mock function +})); + +const mockGetProductById = vi.mocked(productService.getProductById); + +describe("Product loader", () => { + const createLoaderArgs = (id: string) => ({ + params: { id }, + request: new Request(`http://localhost/products/${id}`), + context: {}, + }); + + it("returns a product when it exists", async () => { + const mockProduct = createTestProduct(); + + mockGetProductById.mockResolvedValue(mockProduct); + + const result = await loader(createLoaderArgs("1")); + + expect(result.product).toBeDefined(); + expect(result.product).toEqual(mockProduct); + expect(mockGetProductById).toHaveBeenCalledWith(1); + }); + + it("returns empty object when product does not exist", async () => { + mockGetProductById.mockRejectedValue(new Error("Product not found")); + + const result = await loader(createLoaderArgs("999")); + + expect(result).toEqual({}); + expect(mockGetProductById).toHaveBeenCalledWith(999); + }); + + it("handles invalid product id", async () => { + mockGetProductById.mockRejectedValue(new Error("Invalid ID")); + + const result = await loader(createLoaderArgs("invalid")); + + expect(result).toEqual({}); + expect(mockGetProductById).toHaveBeenCalledWith(NaN); + }); +}); diff --git a/src/routes/product/product.test.tsx b/src/routes/product/product.test.tsx new file mode 100644 index 0000000..3d0bbee --- /dev/null +++ b/src/routes/product/product.test.tsx @@ -0,0 +1,162 @@ +import { render, screen } from "@testing-library/react"; +import { useNavigation } from "react-router"; +import { describe, expect, it, vi } from "vitest"; + +import { createTestProduct } from "@/lib/utils.tests"; +import type { Product as ProductType } from "@/models/product.model"; + +import Product from "."; + +import type { Route } from "./+types"; + +// Helper function to create a test navigation object +const createTestNavigation = (overrides = {}) => ({ + state: "idle", + location: undefined, + formMethod: undefined, + formAction: undefined, + formEncType: undefined, + formData: undefined, + json: undefined, + text: undefined, + ...overrides, +}); + +// Mock de react-router +vi.mock("react-router", () => ({ + Form: vi.fn(({ children }) =>
{children}
), + useNavigation: vi.fn(() => createTestNavigation()), + Link: vi.fn(({ children, ...props }) => {children}), +})); + +const createTestProps = ( + productData: Partial = {} +): Route.ComponentProps => ({ + loaderData: { product: createTestProduct(productData) }, + params: vi.fn() as any, + matches: vi.fn() as any, +}); + +describe("Product Component", () => { + describe("Rendering with valid product data", () => { + it("should render product title correctly", () => { + // Step 1: Setup - Create test props + const props = createTestProps({ title: "Awesome Product" }); + // Step 2: Mock - Component mocks already set up above + // Step 3: Call - Render component + render(); + // Step 4: Verify - Check title is rendered correctly + const titleElement = screen.getByRole("heading", { level: 1 }); + expect(titleElement).toHaveTextContent("Awesome Product"); + }); + + it("should render product price with dollar sign", () => { + // Step 1: Setup - Create test props + const props = createTestProps({ price: 150.99 }); + // Step 2: Mock - Component mocks already set up above + // Step 3: Call - Render component + render(); + // Step 4: Verify - Check price is rendered correctly + expect(screen.queryByText("$150.99")).toBeInTheDocument(); + }); + + it("should render product description", () => { + // Step 1: Setup - Create test props + const props = createTestProps({ + description: "Amazing product", + }); + // Step 2: Mock - Component mocks already set up above + // Step 3: Call - Render component + render(); + // Step 4: Verify - Check description is rendered + expect(screen.queryByText("Amazing product")).toBeInTheDocument(); + }); + + it("should render product image with correct src and alt attributes", () => { + // Step 1: Setup - Create test props + const props = createTestProps({ + imgSrc: "/test-image.jpg", + alt: "Test Product", + }); + // Step 2: Mock - Component mocks already set up above + // Step 3: Call - Render component + render(); + // Step 4: Verify - Check image attributes + const image = screen.getByRole("img"); + expect(image).toHaveAttribute("src", "/test-image.jpg"); + expect(image).toHaveAttribute("alt", "Test Product"); + }); + + it("should render all product features as list items", () => { + // Step 1: Setup - Create test props + const features = ["Feature 1", "Feature 2", "Feature 3"]; + const props = createTestProps({ features }); + // Step 2: Mock - Component mocks already set up above + // Step 3: Call - Render component + render(); + // Step 4: Verify - Check features are rendered + features.forEach((feature) => { + expect(screen.queryByText(feature)).toBeInTheDocument(); + }); + }); + + it('should render "Agregar al Carrito" button', () => { + // Step 1: Setup - Create test props + const props = createTestProps(); + // Step 2: Mock - Component mocks already set up above + // Step 3: Call - Render component + render(); + // Step 4: Verify - Check button is present + expect( + screen.queryByRole("button", { name: "Agregar al Carrito" }) + ).toBeInTheDocument(); + }); + }); + + describe("Form interactions", () => { + it("should include hidden redirectTo input with correct value", () => { + // Step 1: Setup + const productId = 123; + const props = createTestProps({ id: productId }); + // Step 2: Mock - Component mocks already set up above + // Step 3: Call + render(); + // Step 4: Verify + const redirectInput = screen.queryByDisplayValue( + `/products/${productId}` + ); + expect(redirectInput).toBeInTheDocument(); + }); + + it("should disable button when cart is loading", () => { + // Step 1: Setup + const props = createTestProps(); + const expectedNavigation = createTestNavigation({ state: "submitting" }); + // Step 2: Mock - Override navigation state to simulate loading + vi.mocked(useNavigation).mockReturnValue(expectedNavigation as any); + // Step 3: Call + render(); + // Step 4: Verify + const button = screen.getByRole("button"); + expect(button).toBeDisabled(); + expect(button).toHaveTextContent("Agregando..."); + }); + }); + + describe("Error handling", () => { + it("should render NotFound component when product is not provided", () => { + // Step 1: Setup - Create props without product + const props = createTestProps(); + props.loaderData.product = undefined; + + // Step 2: Mock - Mock NotFound component + // vi.mock("../not-found", () => ({ + // default: () =>
Not Found Page
, + // })); + // Step 3: Call + render(); + // Step 4: Verify + expect(screen.getByTestId("not-found")).toBeInTheDocument(); + }); + }); +}); diff --git a/src/routes/root/components/auth-nav/auth-nav.test.tsx b/src/routes/root/components/auth-nav/auth-nav.test.tsx new file mode 100644 index 0000000..09a5357 --- /dev/null +++ b/src/routes/root/components/auth-nav/auth-nav.test.tsx @@ -0,0 +1,69 @@ +import { render, screen } from "@testing-library/react"; +import { createMemoryRouter, RouterProvider } from "react-router"; +import { describe, it, expect } from "vitest"; + +import type { User } from "@/models/user.model"; +import AuthNav from "@/routes/root/components/auth-nav"; + +// Opción con Mock React Router components +// vi.mock("react-router", () => ({ +// Form: ({ children, ...props }: any) =>
{children}
, +// Link: ({ children, to, ...props }: any) => ( +// +// {children} +// +// ), +// })); + +const renderWithRouter = (component: React.ReactElement) => { + const router = createMemoryRouter([ + { + path: "/", + element: component, + }, + ]); + return render(); +}; + +describe("AuthNav Component", () => { + it("renders correctly when user doesn't exist", () => { + renderWithRouter(); + + expect(screen.queryByText("Iniciar sesión")).toBeInTheDocument(); + expect(screen.queryByText("Crear una cuenta")).toBeInTheDocument(); + }); + + it("renders correctly when user exists with name", () => { + const user: Omit = { + id: 1, + email: "testino@mail.com", + name: "Testino", + isGuest: false, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + + renderWithRouter(); + + expect(screen.queryByText(`Bienvenido ${user.name}`)).toBeInTheDocument(); + expect(screen.queryByText("Cerrar sesión")).toBeInTheDocument(); + expect(screen.queryByText("Iniciar sesión")).not.toBeInTheDocument(); + expect(screen.queryByText("Crear una cuenta")).not.toBeInTheDocument(); + }); + + it("renders correctly when user exists with email only", () => { + const user: Omit = { + id: 1, + email: "testino@mail.com", + name: null, + isGuest: false, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + + renderWithRouter(); + + expect(screen.queryByText(`Bienvenido ${user.email}`)).toBeInTheDocument(); + expect(screen.queryByText("Cerrar sesión")).toBeInTheDocument(); + }); +}); diff --git a/src/routes/signup/index.tsx b/src/routes/signup/index.tsx index 05804dc..8c35e4b 100644 --- a/src/routes/signup/index.tsx +++ b/src/routes/signup/index.tsx @@ -87,7 +87,7 @@ export async function action({ request }: Route.ActionArgs) { } export async function loader({ request }: Route.LoaderArgs) { - redirectIfAuthenticated(request); + await redirectIfAuthenticated(request); } export default function Signup({ actionData }: Route.ComponentProps) { diff --git a/src/routes/signup/signup.loader.test.ts b/src/routes/signup/signup.loader.test.ts new file mode 100644 index 0000000..5e5581d --- /dev/null +++ b/src/routes/signup/signup.loader.test.ts @@ -0,0 +1,67 @@ +import { redirect } from "react-router"; +import { describe, expect, it, vi, afterEach } from "vitest"; + +import { createTestRequest } from "@/lib/utils.tests"; +import * as AuthService from "@/services/auth.service"; + +import { loader } from "."; + +import type { Route } from "./+types"; + +vi.mock("@/services/auth.service", () => ({ + redirectIfAuthenticated: vi.fn(), +})); + +describe("signup.loader", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should call redirectIfAuthenticated with request", async () => { + // Step 1: Setup/Arrange + const request = createTestRequest(); + const loaderArgs: Route.LoaderArgs = { request, params: {}, context: {} }; + + // Step 2: Mock + vi.mocked(AuthService.redirectIfAuthenticated).mockResolvedValueOnce(null); + + // Step 3: Call/Act + await loader(loaderArgs); + + // Step 4: Verify/Assert + expect(AuthService.redirectIfAuthenticated).toHaveBeenCalledTimes(1); + expect(AuthService.redirectIfAuthenticated).toHaveBeenCalledWith(request); + }); + + it("should return undefined when user is not authenticated", async () => { + // Step 1: Setup/Arrange + const request = createTestRequest(); + const loaderArgs: Route.LoaderArgs = { request, params: {}, context: {} }; + + // Step 2: Mock + vi.mocked(AuthService.redirectIfAuthenticated).mockResolvedValueOnce(null); + + // Step 3: Call/Act + const result = await loader(loaderArgs); + + // Step 4: Verify/Assert + expect(result).toBeUndefined(); + }); + + it("should throw redirect when user is authenticated", async () => { + // Step 1: Setup/Arrange + const request = createTestRequest(); + const loaderArgs: Route.LoaderArgs = { request, params: {}, context: {} }; + const redirectResponse = redirect("/"); + + // Step 2: Mock + vi.mocked(AuthService.redirectIfAuthenticated).mockImplementationOnce( + () => { + throw redirectResponse; + } + ); + + // Step 3 & 4: Call and Verify + await expect(loader(loaderArgs)).rejects.toBe(redirectResponse); + }); +}); diff --git a/src/services/category.service.test.ts b/src/services/category.service.test.ts new file mode 100644 index 0000000..bb50a06 --- /dev/null +++ b/src/services/category.service.test.ts @@ -0,0 +1,77 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { createTestCategory } from "@/lib/utils.tests"; +import * as categoriesRepository from "@/repositories/category.repository"; +import { + getAllCategories, + getCategoryBySlug, +} from "@/services/category.service"; + +// Mock the repository +vi.mock("@/repositories/category.repository"); + +describe("Category Service", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("getAllCategories", () => { + it("should return all categories", async () => { + const mockCategories = [ + createTestCategory(), + createTestCategory({ + id: 2, + slug: "stickers", + title: "Stickers", + imgSrc: "/img/stickers.jpg", + alt: "Colección de stickers para programadores", + description: + "Explora nuestra colección de stickers para programadores", + }), + ]; + + vi.mocked(categoriesRepository.getAllCategories).mockResolvedValue( + mockCategories + ); + + const result = await getAllCategories(); + + expect(result).toEqual(mockCategories); + expect(categoriesRepository.getAllCategories).toHaveBeenCalledTimes(1); + }); + + it("should handle empty categories", async () => { + vi.mocked(categoriesRepository.getAllCategories).mockResolvedValue([]); + + const result = await getAllCategories(); + + expect(result).toEqual([]); + expect(categoriesRepository.getAllCategories).toHaveBeenCalledTimes(1); + }); + }); + + describe("getCategoryBySlug", () => { + it("should return category when found", async () => { + const mockCategory = createTestCategory(); + + vi.mocked(categoriesRepository.getCategoryBySlug).mockResolvedValue( + mockCategory + ); + + const result = await getCategoryBySlug("polos"); + + expect(result).toEqual(mockCategory); + expect(categoriesRepository.getCategoryBySlug).toHaveBeenCalledWith( + "polos" + ); + }); + + it("should throw error when category not found", async () => { + vi.mocked(categoriesRepository.getCategoryBySlug).mockResolvedValue(null); + + await expect(getCategoryBySlug("non-existent")).rejects.toThrow( + 'Category with slug "non-existent" not found' + ); + }); + }); +}); diff --git a/src/services/order.service.test.ts b/src/services/order.service.test.ts new file mode 100644 index 0000000..28e99ff --- /dev/null +++ b/src/services/order.service.test.ts @@ -0,0 +1,145 @@ +import { describe, expect, it, vi } from "vitest"; + +import { calculateTotal } from "@/lib/cart"; +import { + createMockSession, + createTestRequest, + createTestUser, +} from "@/lib/utils.tests"; +import type { CartItemInput } from "@/models/cart.model"; +import type { Order, OrderDetails } from "@/models/order.model"; +import * as orderRepository from "@/repositories/order.repository"; +import { getSession } from "@/session.server"; + +import { createOrder, getOrdersByUser } from "./order.service"; +import { getOrCreateUser } from "./user.service"; + +vi.mock("./user.service"); +vi.mock("@/lib/cart"); +vi.mock("@/repositories/order.repository"); +vi.mock("@/session.server"); + +describe("Order Service", () => { + const mockedItems: CartItemInput[] = [ + { + productId: 1, + quantity: 2, + title: "Test Product", + price: 19.99, + imgSrc: "test-product.jpg", + }, + { + productId: 2, + quantity: 1, + title: "Another Product", + price: 29.99, + imgSrc: "another-product.jpg", + }, + ]; + + const mockedFormData: OrderDetails = { + email: "test@test.com", + firstName: "", + lastName: "", + company: null, + address: "", + city: "", + country: "", + region: "", + zip: "", + phone: "", + }; + + const mockedUser = createTestUser(); + + const mockedOrder: Order = { + createdAt: "", + id: 1, + items: [ + { + ...mockedItems[0], + id: 2, + orderId: 1, + createdAt: "", + updatedAt: "", + }, + { + ...mockedItems[1], + id: 1, + orderId: 1, + createdAt: "", + updatedAt: "", + }, + ], + totalAmount: 200, + userId: 1, + updatedAt: "", + details: mockedFormData, + }; + + const mockedTotalAmount = 200; + + const mockedRequest = createTestRequest(); + + it("should create an order", async () => { + vi.mocked(getOrCreateUser).mockResolvedValue(mockedUser); + vi.mocked(calculateTotal).mockReturnValue(mockedTotalAmount); + vi.mocked(orderRepository.createOrderWithItems).mockResolvedValue( + mockedOrder + ); + + const order = await createOrder(mockedItems, mockedFormData); + + expect(orderRepository.createOrderWithItems).toBeCalledWith( + mockedUser.id, + mockedItems, + mockedFormData, + mockedTotalAmount + ); + expect(order).toEqual(mockedOrder); + }); + + it("should get orders by user", async () => { + const mockedOrders = [mockedOrder, { ...mockedOrder, id: 3 }]; + const mockedSession = createMockSession(mockedUser.id); // Simulate updated user ID in session + + vi.mocked(getSession).mockResolvedValue(mockedSession); + vi.mocked(orderRepository.getOrdersByUserId).mockResolvedValue( + mockedOrders + ); + + const orders = await getOrdersByUser(mockedRequest); + + expect(orderRepository.getOrdersByUserId).toBeCalledWith(mockedUser.id); + expect(orders).toEqual(mockedOrders); + }); + + it("should throw error if user is not authenticated", async () => { + const mockedSession = createMockSession(null); // Simulate updated user ID in session + + vi.mocked(getSession).mockResolvedValue(mockedSession); + + await expect(getOrdersByUser(mockedRequest)).rejects.toThrow( + "User not authenticated" + ); + + expect(getSession).toHaveBeenCalledWith("session=mock-session-id"); + }); + + it("should throw error if order is null", async () => { + vi.mocked(getOrCreateUser).mockResolvedValue(mockedUser); + vi.mocked(calculateTotal).mockReturnValue(mockedTotalAmount); + vi.mocked(orderRepository.createOrderWithItems).mockResolvedValue(null); + + await expect(createOrder(mockedItems, mockedFormData)).rejects.toThrow( + "Failed to create order" + ); + + expect(orderRepository.createOrderWithItems).toBeCalledWith( + mockedUser.id, + mockedItems, + mockedFormData, + mockedTotalAmount + ); + }); +}); diff --git a/src/services/order.service.ts b/src/services/order.service.ts index 6285ef7..54e365a 100644 --- a/src/services/order.service.ts +++ b/src/services/order.service.ts @@ -8,9 +8,9 @@ import { getOrCreateUser } from "./user.service"; export async function createOrder( items: CartItemInput[], - formData: Record + formData: OrderDetails ): Promise { - const shippingDetails = formData as unknown as OrderDetails; + const shippingDetails = formData; const user = await getOrCreateUser(shippingDetails.email); const totalAmount = calculateTotal(items); diff --git a/src/services/product.service.test.ts b/src/services/product.service.test.ts new file mode 100644 index 0000000..ea57513 --- /dev/null +++ b/src/services/product.service.test.ts @@ -0,0 +1,103 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { createTestCategory, createTestProduct } from "@/lib/utils.tests"; +import type { Category } from "@/models/category.model"; +import type { Product } from "@/models/product.model"; +import * as productRepository from "@/repositories/product.repository"; + +import { getCategoryBySlug } from "./category.service"; +import { getProductById, getProductsByCategorySlug } from "./product.service"; + +// Mock dependencies +vi.mock("@/repositories/product.repository"); +vi.mock("./category.service"); + +describe("Product Service", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("getProductsByCategorySlug", () => { + it("should return products for a valid category slug", async () => { + // Step 1: Setup - Create test data with valid category and products + const testCategory = createTestCategory(); + const mockedProducts: Product[] = [ + createTestProduct({ id: 1, categoryId: testCategory.id }), + createTestProduct({ + id: 2, + title: "Test Product 2", + categoryId: testCategory.id, + }), + ]; + + // Step 2: Mock - Configure repository responses + vi.mocked(getCategoryBySlug).mockResolvedValue(testCategory); + vi.mocked(productRepository.getProductsByCategory).mockResolvedValue( + mockedProducts + ); + + // Step 3: Call service function + const products = await getProductsByCategorySlug(testCategory.slug); + + // Step 4: Verify expected behavior + expect(getCategoryBySlug).toHaveBeenCalledWith(testCategory.slug); + expect(productRepository.getProductsByCategory).toHaveBeenCalledWith( + testCategory.id + ); + expect(products).toEqual(mockedProducts); + }); + + it("should throw error when category slug does not exist", async () => { + // Step 1: Setup - Create test data for non-existent category + const invalidSlug = "invalid-slug"; + + // Step 2: Mock - Configure error response + const errorMessage = `Category with slug "${invalidSlug}" not found`; + vi.mocked(getCategoryBySlug).mockRejectedValue(new Error(errorMessage)); + + // Step 3: Call service function + const getProducts = getProductsByCategorySlug( + invalidSlug as Category["slug"] + ); + + // Step 4: Verify expected behavior + await expect(getProducts).rejects.toThrow(errorMessage); + expect(productRepository.getProductsByCategory).not.toHaveBeenCalled(); + }); + }); + + describe("getProductById", () => { + it("should return product for valid ID", async () => { + // Step 1: Setup - Create test data for existing product + const testProduct = createTestProduct(); + + // Step 2: Mock - Configure repository response + vi.mocked(productRepository.getProductById).mockResolvedValue( + testProduct + ); + + // Step 3: Call service function + const result = await getProductById(testProduct.id); + + // Step 4: Verify expected behavior + expect(productRepository.getProductById).toHaveBeenCalledWith( + testProduct.id + ); + expect(result).toEqual(testProduct); + }); + + it("should throw error when product does not exist", async () => { + // Step 1: Setup - Configure ID for non-existent product + const nonExistentId = 999; + + // Step 2: Mock - Configure null response from repository + vi.mocked(productRepository.getProductById).mockResolvedValue(null); + + // Step 3: Call service function + const productPromise = getProductById(nonExistentId); + + // Step 4: Verify expected behavior + await expect(productPromise).rejects.toThrow("Product not found"); + }); + }); +}); diff --git a/src/services/user.service.test.ts b/src/services/user.service.test.ts new file mode 100644 index 0000000..ffe8965 --- /dev/null +++ b/src/services/user.service.test.ts @@ -0,0 +1,127 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { hashPassword } from "@/lib/security"; +import { + createMockSession, + createTestRequest, + createTestUser, +} from "@/lib/utils.tests"; +import * as userRepository from "@/repositories/user.repository"; +import { getSession } from "@/session.server"; + +import * as userService from "./user.service"; + +// Mocking dependencies for unit tests +vi.mock("@/session.server"); +vi.mock("@/repositories/user.repository"); +vi.mock("@/lib/security"); + +describe("user service", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("updateUser", () => { + it("should update user details", async () => { + // Setup - Create mocks (test data) + const updatedUser = createTestUser(); + const request = createTestRequest(); + const mockSession = createMockSession(updatedUser.id); // Simulate updated user ID in session + + // Mockeando las funciones que serán llamadas + vi.mocked(userRepository.updateUser).mockResolvedValue(updatedUser); + vi.mocked(getSession).mockResolvedValue(mockSession); + + // Llamando al servicio y verificando el resultado + expect(await userService.updateUser(updatedUser, request)).toEqual( + updatedUser + ); + }); + + it("should hash password if provided", async () => { + // Setup - Create mocks (test data) + const passwordBeforeHashing = "testing123"; + const updatedUser = createTestUser({ + id: 6, + password: passwordBeforeHashing, + }); + const request = createTestRequest(); + const mockSession = createMockSession(updatedUser.id); // Simulate updated user ID in session + + // Mockeando las funciones que serán llamadas + vi.mocked(getSession).mockResolvedValue(mockSession); + vi.mocked(hashPassword).mockResolvedValue("hashed-password"); + + // Llamando al servicio y verificando el resultado + await userService.updateUser(updatedUser, request); + + expect(hashPassword).toHaveBeenCalledWith(passwordBeforeHashing); // Verifica que se haya llamado a hashPassword con la contraseña original + expect(updatedUser.password).not.toBe(passwordBeforeHashing); // Verifica que la contraseña se haya actualizado + expect(updatedUser.password).toBe("hashed-password"); // Verifica que la contraseña se haya actualizado + }); + + it("should throw error if user is not authenticated", async () => { + // Setup - Create mocks (test data) + const updatedUser = createTestUser(); // No user ID provided + const request = createTestRequest(); + const mockSession = createMockSession(null); // Simulate no user ID in session + + // Mockeando las funciones que serán llamadas + vi.mocked(getSession).mockResolvedValue(mockSession); + + // Llamando al servicio y verificando el resultado + await expect( + userService.updateUser(updatedUser, request) + ).rejects.toThrow("User not authenticated"); + + expect(getSession).toHaveBeenCalledWith("session=mock-session-id"); + }); + }); + + describe("getOrCreateUser", () => { + it("should return existing user when email is found", async () => { + // Setup - Create mock data + const email = "test@example.com"; + const existingUser = createTestUser({ + email, + id: 10, + }); + + // Mock repository function to return existing user + vi.mocked(userRepository.getUserByEmail).mockResolvedValue(existingUser); + + // Call service function + const result = await userService.getOrCreateUser(email); + + // Verify results + expect(result).toEqual(existingUser); + expect(userRepository.getUserByEmail).toHaveBeenCalledWith(email); + expect(userRepository.createUser).not.toHaveBeenCalled(); + }); + + it("should create a new guest user when email is not found", async () => { + // Setup - Create mock data + const email = "test@example.com"; + const newUser = createTestUser({ + email, + id: 20, + isGuest: true, + }); + const createUserDTO = { + email, + password: null, + isGuest: true, + name: null, + }; + // Mock repository functions + vi.mocked(userRepository.getUserByEmail).mockResolvedValue(null); + vi.mocked(userRepository.createUser).mockResolvedValue(newUser); + // Call service function + const result = await userService.getOrCreateUser(email); + // Verify results + expect(result).toEqual(newUser); + expect(userRepository.getUserByEmail).toHaveBeenCalledWith(email); + expect(userRepository.createUser).toHaveBeenCalledWith(createUserDTO); + }); + }); +}); diff --git a/testing-strategies.md b/testing-strategies.md new file mode 100644 index 0000000..168f02c --- /dev/null +++ b/testing-strategies.md @@ -0,0 +1,257 @@ +# Testing Strategies + +## 1. Component Testing Strategy + +### UI Components + +Test your reusable UI components in isolation: + +```typescript +// Example: src/components/ui/button/button.test.tsx +import { render, screen } from "@testing-library/react"; +import { describe, it, expect } from "vitest"; +import { Button } from "./button"; + +describe("Button Component", () => { + it("renders correctly with different variants", () => { + render(); + const button = screen.getByRole("button"); + expect(button).toHaveClass("bg-secondary"); + }); +}); +``` + +### Route Components with Mock Router + +Create a test utility for rendering components with React Router context: + +```typescript +// src/test-utils/router-utils.tsx +import { createMemoryRouter, RouterProvider } from "react-router"; +import { render } from "@testing-library/react"; + +export const renderWithRouter = ( + component: React.ReactElement, + initialEntries = ["/"] +) => { + const router = createMemoryRouter( + [ + { + path: "*", + element: component, + }, + ], + { initialEntries } + ); + + return render(); +}; +``` + +## 2. Route Module Testing Strategy + +### Testing Loaders + +Test loaders independently from components: + +```typescript +// src/routes/product/product.loader.test.ts +import { describe, expect, it, vi } from "vitest"; +import { loader } from "./index"; +import * as productService from "@/services/product.service"; + +vi.mock("@/services/product.service"); + +describe("Product Loader", () => { + it("returns product data when product exists", async () => { + const mockProduct = { id: 1, title: "Test Product" }; + vi.mocked(productService.getProductById).mockResolvedValue(mockProduct); + + const result = await loader({ + params: { id: "1" }, + request: new Request("http://localhost/products/1"), + context: {}, + }); + + expect(result.product).toEqual(mockProduct); + }); +}); +``` + +### Testing Actions + +Test form actions and server-side logic: + +```typescript +// src/routes/cart/add-item/add-item.action.test.ts +import { describe, expect, it, vi } from "vitest"; +import { action } from "./index"; +import * as cartLib from "@/lib/cart"; + +vi.mock("@/lib/cart"); +vi.mock("@/session.server"); + +describe("Add Item Action", () => { + it("adds item to cart and redirects", async () => { + const formData = new FormData(); + formData.append("productId", "1"); + formData.append("quantity", "2"); + + const request = new Request("http://localhost", { + method: "POST", + body: formData, + }); + + const result = await action({ request, params: {}, context: {} }); + + expect(vi.mocked(cartLib.addToCart)).toHaveBeenCalledWith( + undefined, // userId + undefined, // sessionCartId + 1, // productId + 2 // quantity + ); + }); +}); +``` + +## 3. Integration Testing Strategy + +### Full Route Testing + +Test complete user flows with mocked services: + +```typescript +// src/routes/product/product.integration.test.tsx +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { createMemoryRouter, RouterProvider } from "react-router"; +import { describe, it, expect, vi } from "vitest"; + +import ProductRoute from "./index"; +import * as productService from "@/services/product.service"; + +vi.mock("@/services/product.service"); + +// Alternative to `renderWithRouter` +const createTestRouter = (loaderData: any) => { + return createMemoryRouter( + [ + { + path: "/products/:id", + element: , + }, + ], + { initialEntries: ["/products/1"] } + ); +}; + +describe("Product Route Integration", () => { + it("displays product and allows adding to cart", async () => { + const mockProduct = { + id: 1, + title: "Test Product", + price: 99.99, + description: "Test description", + }; + + const router = createTestRouter({ product: mockProduct }); + render(); + + expect(screen.getByText("Test Product")).toBeInTheDocument(); + + const addToCartButton = screen.getByText("Agregar al Carrito"); + await userEvent.click(addToCartButton); + + // Test form submission behavior + }); +}); +``` + +## 4. Session Testing Strategy + +Create utilities for testing session-dependent functionality: + +```typescript +// src/test-utils/session-utils.ts +export const createMockSession = (data: Partial = {}) => ({ + get: vi.fn((key: string) => data[key as keyof SessionData]), + set: vi.fn(), + unset: vi.fn(), + has: vi.fn((key: string) => key in data), +}); + +export const createMockRequest = ( + url = "http://localhost", + options: RequestInit = {}, + sessionData: Partial = {} +) => { + const request = new Request(url, options); + // Mock session data somehow - you might need to adjust based on your session implementation + return request; +}; +``` + +## 5. Service Layer Testing + +Test your service functions independently: + +```typescript +// src/services/cart.service.test.ts +import { describe, expect, it, vi } from "vitest"; +import { addToCart } from "@/lib/cart"; +import * as cartRepository from "@/repositories/cart.repository"; + +vi.mock("@/repositories/cart.repository"); + +describe("Cart Service", () => { + it("creates new cart for guest user", async () => { + vi.mocked(cartRepository.getCart).mockResolvedValue(null); + vi.mocked(cartRepository.createCart).mockResolvedValue({ + id: 1, + items: [], + sessionCartId: "test-session", + }); + + await addToCart(undefined, undefined, 1, 2); + + expect(cartRepository.createCart).toHaveBeenCalled(); + }); +}); +``` + +## 6. Mock Strategy + +Create mock objects to use on your tests. You can also create functions that create those objects + +```typescript +// src/test-utils/mocks.ts +export const mockUser = { + id: 1, + email: "test@example.com", + name: "Test User", + isGuest: false, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), +}; + +export const mockCart = { + id: 1, + items: [], + sessionCartId: "test-session", + userId: 1, +}; + +export const mockProduct = { + id: 1, + title: "Test Product", + price: 99.99, + description: "Test description", + imgSrc: "/test.jpg", + features: ["Feature 1"], + alt: "Test alt", + categoryId: 1, + isOnSale: false, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), +}; +``` diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..44c2a13 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,16 @@ +import path from "node:path"; + +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "jsdom", + setupFiles: ["./vitest.setup.ts"], + }, + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +}); diff --git a/vitest.setup.ts b/vitest.setup.ts new file mode 100644 index 0000000..d0de870 --- /dev/null +++ b/vitest.setup.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom";