From 93549202c3cf6919174d503c877dc4048d12bd97 Mon Sep 17 00:00:00 2001 From: icaroharry Date: Mon, 23 Mar 2026 13:12:38 -0300 Subject: [PATCH 1/7] Add testing infrastructure with Vitest and Testing Library Install vitest, @testing-library/react, @testing-library/jest-dom, @testing-library/user-event, jsdom, and @vitest/coverage-v8. Create vitest.config.ts (separate from vite.config.ts to avoid reactRouter plugin issues), setup file with jest-dom matchers and matchMedia mock, TypeScript globals reference, test helper for rendering components with router context via createRoutesStub, and npm scripts for test, test:run, and test:coverage. --- app/test/render-with-router.tsx | 33 + package-lock.json | 1997 ++++++++++++++++++++++++++++++- package.json | 13 +- vitest-env.d.ts | 1 + vitest.config.ts | 19 + vitest.setup.ts | 20 + 6 files changed, 2021 insertions(+), 62 deletions(-) create mode 100644 app/test/render-with-router.tsx create mode 100644 vitest-env.d.ts create mode 100644 vitest.config.ts create mode 100644 vitest.setup.ts diff --git a/app/test/render-with-router.tsx b/app/test/render-with-router.tsx new file mode 100644 index 00000000..ecc364ee --- /dev/null +++ b/app/test/render-with-router.tsx @@ -0,0 +1,33 @@ +import { createRoutesStub } from "react-router"; +import { render } from "@testing-library/react"; + +type RouteStubOptions = { + path?: string; + initialEntries?: string[]; + loader?: () => unknown; + action?: () => unknown; + children?: Parameters[0]; +}; + +export function renderWithRouter( + Component: React.ComponentType, + { + path = "/", + initialEntries, + loader, + action, + children, + }: RouteStubOptions = {}, +) { + const Stub = createRoutesStub([ + { + path, + Component, + loader, + action, + children, + }, + ]); + + return render(); +} diff --git a/package-lock.json b/package-lock.json index b948f0f2..8e8ef5a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -78,17 +78,22 @@ "@react-router/remix-routes-option-adapter": "^7.10.1", "@tailwindcss/postcss": "^4.1.17", "@tailwindcss/typography": "^0.5.19", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/eslint-config-prettier": "^6.11.3", "@types/howler": "^2.2.12", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@types/vimeo__player": "^2.18.3", + "@vitest/coverage-v8": "^3.2.4", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "globals": "^16.5.0", "husky": "^9.1.7", + "jsdom": "^27.0.1", "lint-staged": "^15.5.2", "prettier": "^3.7.4", "remix-flat-routes": "^0.8", @@ -96,12 +101,20 @@ "typescript": "^5.9.3", "typescript-eslint": "^8.48.1", "vite": "^7.2.6", - "vite-tsconfig-paths": "^5.1.4" + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.2.4" }, "engines": { "node": ">=20" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "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", @@ -115,6 +128,75 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", + "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.0.0", + "@csstools/css-color-parser": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.5" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -146,7 +228,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -427,13 +508,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -575,9 +656,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "dependencies": { @@ -588,6 +669,156 @@ "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/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "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.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "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.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "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.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "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.1", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.1.tgz", + "integrity": "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==", + "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.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "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/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -1386,6 +1617,90 @@ "node": "20 || >=22" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1561,6 +1876,17 @@ "integrity": "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q==", "license": "MIT" }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -2899,7 +3225,6 @@ "integrity": "sha512-kap9O8rTN6b3vxjd+0SGjhm5vqiAZHMmOX1Hc7Y4KXRVVdusn+0+hxs44cDSfbW6Z6fCLw6GXXe0Kr+DJIRezw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.27.7", "@babel/generator": "^7.27.5", @@ -3026,7 +3351,6 @@ "resolved": "https://registry.npmjs.org/@react-router/serve/-/serve-7.10.1.tgz", "integrity": "sha512-qYco7sFpbRgoKJKsCgJmFBQwaLVsLv255K8vbPodnXe13YBEzV/ugIqRCYVz2hghvlPiEKgaHh2On0s/5npn6w==", "license": "MIT", - "peer": true, "dependencies": { "@mjackson/node-fetch-server": "^0.2.0", "@react-router/express": "7.10.1", @@ -3763,43 +4087,159 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@types/ms": "*" + "@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/@types/eslint-config-prettier": { - "version": "6.11.3", - "resolved": "https://registry.npmjs.org/@types/eslint-config-prettier/-/eslint-config-prettier-6.11.3.tgz", - "integrity": "sha512-3wXCiM8croUnhg9LdtZUJQwNcQYGWxxdOWDjPe1ykCqJFPVpzAKfs/2dgSoCtAvdPeaponcWPI7mPcGGp9dkKQ==", + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", "dev": true, - "license": "MIT" + "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": ">=14", + "npm": ">=6", + "yarn": ">=1" + } }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "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/@types/estree-jsx": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", - "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "*" + "@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/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@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/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "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/eslint-config-prettier": { + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/@types/eslint-config-prettier/-/eslint-config-prettier-6.11.3.tgz", + "integrity": "sha512-3wXCiM8croUnhg9LdtZUJQwNcQYGWxxdOWDjPe1ykCqJFPVpzAKfs/2dgSoCtAvdPeaponcWPI7mPcGGp9dkKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "dev": true, "license": "MIT", @@ -3866,7 +4306,6 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3877,7 +4316,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3951,7 +4389,6 @@ "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", @@ -4180,6 +4617,210 @@ "weakmap-polyfill": "2.0.4" } }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/expect/node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/expect/node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@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.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/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.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@wojtekmaj/react-recaptcha-v3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/@wojtekmaj/react-recaptcha-v3/-/react-recaptcha-v3-0.1.4.tgz", @@ -4236,7 +4877,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4284,6 +4924,16 @@ "react-dom": ">=18.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4382,6 +5032,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", @@ -4526,6 +5186,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "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/astring": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", @@ -4768,7 +5457,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4898,6 +5586,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4959,6 +5664,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -5258,6 +5973,27 @@ "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==", "license": "BSD" }, + "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/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -5271,16 +6007,65 @@ "node": ">=4" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "node_modules/cssstyle": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", + "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "dev": true, "license": "MIT", - "peer": true - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "dependencies": { + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz", + "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^15.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/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/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, "license": "MIT", @@ -5359,6 +6144,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/decode-named-character-reference": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", @@ -5388,6 +6180,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", @@ -5518,6 +6320,14 @@ "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", + "peer": true + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -5568,6 +6378,13 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -5617,6 +6434,19 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -5921,7 +6751,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6302,6 +7131,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", @@ -6432,8 +7271,7 @@ "version": "4.32.10", "resolved": "https://registry.npmjs.org/filepond/-/filepond-4.32.10.tgz", "integrity": "sha512-uflpIL+rfAgsYk7HgXugh7ELlHNHLTPtIHfvuq0gv3x2qNfVxdGA6T7RPxMh9Vji8ysXstXkDHVXQ4hY38MPTQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/filepond-plugin-file-validate-type": { "version": "1.2.9", @@ -6581,6 +7419,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -6834,6 +7689,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -6847,6 +7724,32 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", @@ -7102,6 +8005,26 @@ "integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==", "license": "ISC" }, + "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/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -7122,6 +8045,34 @@ "url": "https://opencollective.com/express" } }, + "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/human-signals": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", @@ -7209,6 +8160,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", @@ -7573,6 +8534,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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", @@ -7759,6 +8727,60 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -7777,6 +8799,22 @@ "node": ">= 0.4" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jay-peg": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/jay-peg/-/jay-peg-1.1.1.tgz", @@ -7815,6 +8853,46 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "27.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.1.tgz", + "integrity": "sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/dom-selector": "^6.7.2", + "cssstyle": "^5.3.1", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.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", + "parse5": "^8.0.0", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", @@ -8409,6 +9487,13 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "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", @@ -8428,6 +9513,17 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -8438,6 +9534,34 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/markdown-extensions": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", @@ -8665,6 +9789,13 @@ "url": "https://opencollective.com/unified" } }, + "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/media-engine": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz", @@ -9424,6 +10555,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/mini-svg-data-uri": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", @@ -9455,7 +10596,17 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/morgan": { + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/morgan": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", @@ -9835,6 +10986,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -9887,6 +11045,19 @@ "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", "license": "MIT" }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "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", @@ -9928,6 +11099,30 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", @@ -9941,6 +11136,16 @@ "dev": true, "license": "MIT" }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -10078,6 +11283,55 @@ "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", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/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", + "peer": true + }, "node_modules/prism-react-renderer": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", @@ -10195,7 +11449,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -10205,7 +11458,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -10356,7 +11608,6 @@ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.10.1.tgz", "integrity": "sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw==", "license": "MIT", - "peer": true, "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" @@ -10536,6 +11787,20 @@ "url": "https://opencollective.com/unified" } }, + "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", @@ -10654,7 +11919,6 @@ "https://github.com/sponsors/sergiodxa" ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.0.0" } @@ -10801,7 +12065,6 @@ "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -10838,6 +12101,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/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -10925,6 +12195,19 @@ "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==", "license": "BlueOak-1.0.0" }, + "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.25.0-rc-603e6108-20241029", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-603e6108-20241029.tgz", @@ -11256,6 +12539,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", @@ -11392,6 +12682,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "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.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -11401,6 +12698,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -11452,6 +12756,62 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -11581,6 +12941,30 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", @@ -11594,6 +12978,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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", @@ -11607,6 +13004,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", @@ -11659,6 +13076,13 @@ "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==", "license": "ISC" }, + "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/tabbable": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz", @@ -11679,8 +13103,7 @@ "version": "4.1.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tailwindcss-animate": { "version": "1.0.7", @@ -11705,6 +13128,60 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/timezones-ical-library": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/timezones-ical-library/-/timezones-ical-library-1.10.0.tgz", @@ -11727,6 +13204,20 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "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.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -11744,6 +13235,56 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.27.tgz", + "integrity": "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.27" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.27.tgz", + "integrity": "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==", + "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", @@ -11766,6 +13307,32 @@ "node": ">=0.6" } }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "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/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -11938,7 +13505,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12397,7 +13963,6 @@ "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -12531,6 +14096,127 @@ } } }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/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/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": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", @@ -12549,6 +14235,67 @@ "node": ">=8.10.0" } }, + "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-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "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": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -12653,6 +14400,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", @@ -12681,6 +14445,80 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", @@ -12694,6 +14532,45 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "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/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 d0eafdc6..d6749c2c 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,10 @@ "typecheck": "react-router typegen && tsc", "lint": "eslint .", "lint-fix": "eslint --fix .", - "prepare": "husky" + "prepare": "husky", + "test": "vitest", + "test:run": "vitest run", + "test:coverage": "vitest run --coverage" }, "dependencies": { "@headlessui/react": "^2.2.9", @@ -86,17 +89,22 @@ "@react-router/remix-routes-option-adapter": "^7.10.1", "@tailwindcss/postcss": "^4.1.17", "@tailwindcss/typography": "^0.5.19", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/eslint-config-prettier": "^6.11.3", "@types/howler": "^2.2.12", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@types/vimeo__player": "^2.18.3", + "@vitest/coverage-v8": "^3.2.4", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "globals": "^16.5.0", "husky": "^9.1.7", + "jsdom": "^27.0.1", "lint-staged": "^15.5.2", "prettier": "^3.7.4", "remix-flat-routes": "^0.8", @@ -104,7 +112,8 @@ "typescript": "^5.9.3", "typescript-eslint": "^8.48.1", "vite": "^7.2.6", - "vite-tsconfig-paths": "^5.1.4" + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.2.4" }, "engines": { "node": ">=20" diff --git a/vitest-env.d.ts b/vitest-env.d.ts new file mode 100644 index 00000000..9896c472 --- /dev/null +++ b/vitest-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..ce4cba27 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from "vitest/config"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [tsconfigPaths()], + test: { + globals: true, + environment: "jsdom", + setupFiles: ["./vitest.setup.ts"], + include: ["app/**/*.{test,spec}.{ts,tsx}"], + exclude: ["node_modules", "build", ".react-router"], + css: false, + coverage: { + provider: "v8", + include: ["app/**/*.{ts,tsx}"], + exclude: ["app/**/*.server.ts", "app/routes.ts", "app/entry.*"], + }, + }, +}); diff --git a/vitest.setup.ts b/vitest.setup.ts new file mode 100644 index 00000000..9effced7 --- /dev/null +++ b/vitest.setup.ts @@ -0,0 +1,20 @@ +import "@testing-library/jest-dom"; +import { cleanup } from "@testing-library/react"; +import { afterEach } from "vitest"; + +afterEach(() => cleanup()); + +// Mock window.matchMedia (needed by Radix UI, responsive hooks) +Object.defineProperty(window, "matchMedia", { + writable: true, + value: vi.fn().mockImplementation((query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +}); From 45b7be2ba6b0f87e2bfcd5cb5b9a3387d3b459c4 Mon Sep 17 00:00:00 2001 From: icaroharry Date: Mon, 23 Mar 2026 13:13:05 -0300 Subject: [PATCH 2/7] Add unit tests for utility functions Test cn, classNames, formatDate, formatName, generateRandomInt, toTitleCase, workshop status utils (hasHappened, isUpcoming, isNew), and path utils (getOgGeneratorUrl, setActiveClassForPath). --- app/lib/utils/class-names.test.ts | 17 ++++++ app/lib/utils/cn.test.ts | 24 +++++++++ app/lib/utils/format-date.test.ts | 26 +++++++++ app/lib/utils/format-name.test.ts | 23 ++++++++ app/lib/utils/numbers.test.ts | 20 +++++++ app/lib/utils/path-utils.test.ts | 56 +++++++++++++++++++ app/lib/utils/string-utils.test.ts | 29 ++++++++++ app/lib/utils/workshop-utils.test.ts | 80 ++++++++++++++++++++++++++++ 8 files changed, 275 insertions(+) create mode 100644 app/lib/utils/class-names.test.ts create mode 100644 app/lib/utils/cn.test.ts create mode 100644 app/lib/utils/format-date.test.ts create mode 100644 app/lib/utils/format-name.test.ts create mode 100644 app/lib/utils/numbers.test.ts create mode 100644 app/lib/utils/path-utils.test.ts create mode 100644 app/lib/utils/string-utils.test.ts create mode 100644 app/lib/utils/workshop-utils.test.ts diff --git a/app/lib/utils/class-names.test.ts b/app/lib/utils/class-names.test.ts new file mode 100644 index 00000000..a6dc2e33 --- /dev/null +++ b/app/lib/utils/class-names.test.ts @@ -0,0 +1,17 @@ +import classNames from "./class-names"; + +describe("classNames", () => { + it("joins multiple class names", () => { + expect(classNames("foo", "bar")).toBe("foo bar"); + }); + + it("filters out falsy values", () => { + expect(classNames("foo", false, null, undefined, 0, "", "bar")).toBe( + "foo bar", + ); + }); + + it("returns empty string with no truthy inputs", () => { + expect(classNames(false, null, undefined)).toBe(""); + }); +}); diff --git a/app/lib/utils/cn.test.ts b/app/lib/utils/cn.test.ts new file mode 100644 index 00000000..4d28e0f8 --- /dev/null +++ b/app/lib/utils/cn.test.ts @@ -0,0 +1,24 @@ +import { cn } from "./cn"; + +describe("cn", () => { + it("merges class names", () => { + expect(cn("foo", "bar")).toBe("foo bar"); + }); + + it("handles conditional classes", () => { + const isHidden = false; + expect(cn("base", isHidden && "hidden", "visible")).toBe("base visible"); + }); + + it("resolves tailwind conflicts (last wins)", () => { + expect(cn("px-4", "px-8")).toBe("px-8"); + }); + + it("handles undefined and null gracefully", () => { + expect(cn("base", undefined, null, "extra")).toBe("base extra"); + }); + + it("returns empty string for no inputs", () => { + expect(cn()).toBe(""); + }); +}); diff --git a/app/lib/utils/format-date.test.ts b/app/lib/utils/format-date.test.ts new file mode 100644 index 00000000..85d7b4a6 --- /dev/null +++ b/app/lib/utils/format-date.test.ts @@ -0,0 +1,26 @@ +import { formatDate, formatDateDDMMYYYY } from "./format-date"; + +// Use datetime strings with T12:00:00 to avoid timezone-related date shifts +describe("formatDate", () => { + it("formats date in pt-BR long format", () => { + const result = formatDate("2024-01-15T12:00:00"); + expect(result).toContain("15"); + expect(result).toContain("janeiro"); + expect(result).toContain("2024"); + }); + + it("formats different months correctly", () => { + expect(formatDate("2024-06-01T12:00:00")).toContain("junho"); + expect(formatDate("2024-12-25T12:00:00")).toContain("dezembro"); + }); +}); + +describe("formatDateDDMMYYYY", () => { + it("formats date as DD/MM/YYYY", () => { + expect(formatDateDDMMYYYY("2024-01-15T12:00:00")).toBe("15/01/2024"); + }); + + it("pads single digit day and month", () => { + expect(formatDateDDMMYYYY("2024-03-05T12:00:00")).toBe("05/03/2024"); + }); +}); diff --git a/app/lib/utils/format-name.test.ts b/app/lib/utils/format-name.test.ts new file mode 100644 index 00000000..9532ea47 --- /dev/null +++ b/app/lib/utils/format-name.test.ts @@ -0,0 +1,23 @@ +import { formatName } from "./format-name"; + +describe("formatName", () => { + it("capitalizes a single word", () => { + expect(formatName("joão")).toBe("João"); + }); + + it("capitalizes multiple words", () => { + expect(formatName("maria silva")).toBe("Maria Silva"); + }); + + it("handles all uppercase input", () => { + expect(formatName("CARLOS SANTOS")).toBe("Carlos Santos"); + }); + + it("trims whitespace", () => { + expect(formatName(" ana ")).toBe("Ana"); + }); + + it("handles mixed case", () => { + expect(formatName("jOÃO pEDRO")).toBe("João Pedro"); + }); +}); diff --git a/app/lib/utils/numbers.test.ts b/app/lib/utils/numbers.test.ts new file mode 100644 index 00000000..a9b18651 --- /dev/null +++ b/app/lib/utils/numbers.test.ts @@ -0,0 +1,20 @@ +import { generateRandomInt } from "./numbers"; + +describe("generateRandomInt", () => { + it("returns a value within the specified range", () => { + for (let i = 0; i < 100; i++) { + const result = generateRandomInt(1, 10); + expect(result).toBeGreaterThanOrEqual(1); + expect(result).toBeLessThan(10); + } + }); + + it("returns min when min equals max - 1", () => { + expect(generateRandomInt(5, 6)).toBe(5); + }); + + it("returns an integer", () => { + const result = generateRandomInt(1, 100); + expect(Number.isInteger(result)).toBe(true); + }); +}); diff --git a/app/lib/utils/path-utils.test.ts b/app/lib/utils/path-utils.test.ts new file mode 100644 index 00000000..36e328c8 --- /dev/null +++ b/app/lib/utils/path-utils.test.ts @@ -0,0 +1,56 @@ +import { getOgGeneratorUrl, setActiveClassForPath } from "./path-utils"; +import type { UIMatch } from "react-router"; + +const makeMatches = (pathname: string): UIMatch[] => + [{ pathname }] as UIMatch[]; + +describe("getOgGeneratorUrl", () => { + it("generates URL with title only", () => { + expect(getOgGeneratorUrl("Blog do Codante")).toBe( + "https://og.codante.io/api/blog-do-codante", + ); + }); + + it("generates URL with title and subtitle", () => { + expect(getOgGeneratorUrl("Meu Post", "React Router")).toBe( + "https://og.codante.io/api/react-router/meu-post", + ); + }); +}); + +describe("setActiveClassForPath", () => { + it("returns className on exact match", () => { + const matches = makeMatches("/blog"); + expect(setActiveClassForPath(matches, "/blog", "active")).toBe("active"); + }); + + it("returns empty string on exact mismatch", () => { + const matches = makeMatches("/about"); + expect(setActiveClassForPath(matches, "/blog", "active")).toBe(""); + }); + + it("matches with startsWith", () => { + const matches = makeMatches("/blog/my-post"); + expect( + setActiveClassForPath(matches, "/blog", "active", "startsWith"), + ).toBe("active"); + }); + + it("matches with endsWith", () => { + const matches = makeMatches("/dashboard/settings"); + expect( + setActiveClassForPath(matches, "/settings", "active", "endsWith"), + ).toBe("active"); + }); + + it("matches with includes", () => { + const matches = makeMatches("/app/mini-projetos/react"); + expect( + setActiveClassForPath(matches, "mini-projetos", "active", "includes"), + ).toBe("active"); + }); + + it("returns empty string when no matches exist", () => { + expect(setActiveClassForPath([], "/blog", "active")).toBe(""); + }); +}); diff --git a/app/lib/utils/string-utils.test.ts b/app/lib/utils/string-utils.test.ts new file mode 100644 index 00000000..2c33c03c --- /dev/null +++ b/app/lib/utils/string-utils.test.ts @@ -0,0 +1,29 @@ +import { toTitleCase, generateSimpleLoremIpsum } from "./string-utils"; + +describe("toTitleCase", () => { + it("capitalizes each word", () => { + expect(toTitleCase("hello world")).toBe("Hello World"); + }); + + it("handles single word", () => { + expect(toTitleCase("hello")).toBe("Hello"); + }); + + it("lowercases non-first characters", () => { + expect(toTitleCase("HELLO WORLD")).toBe("Hello World"); + }); + + it("handles empty string", () => { + expect(toTitleCase("")).toBe(""); + }); +}); + +describe("generateSimpleLoremIpsum", () => { + it("returns a non-empty string", () => { + expect(generateSimpleLoremIpsum().length).toBeGreaterThan(0); + }); + + it("contains pudim recipe text", () => { + expect(generateSimpleLoremIpsum()).toContain("pudim"); + }); +}); diff --git a/app/lib/utils/workshop-utils.test.ts b/app/lib/utils/workshop-utils.test.ts new file mode 100644 index 00000000..3c84b77a --- /dev/null +++ b/app/lib/utils/workshop-utils.test.ts @@ -0,0 +1,80 @@ +import { hasHappened, isUpcoming, isNew } from "./workshop-utils"; + +const makeWorkshop = (overrides: Record = {}) => ({ + id: 1, + name: "Test Workshop", + slug: "test-workshop", + status: "published" as const, + published_at: null as string | null, + ...overrides, +}); + +describe("hasHappened", () => { + it("returns true when published_at is in the past", () => { + const workshop = makeWorkshop({ + published_at: "2020-01-01T00:00:00Z", + }); + expect(hasHappened(workshop as any)).toBe(true); + }); + + it("returns false when published_at is in the future", () => { + const workshop = makeWorkshop({ + published_at: "2099-01-01T00:00:00Z", + }); + expect(hasHappened(workshop as any)).toBe(false); + }); + + it("returns false when published_at is null", () => { + const workshop = makeWorkshop({ published_at: null }); + expect(hasHappened(workshop as any)).toBe(false); + }); +}); + +describe("isUpcoming", () => { + it('returns true when published_at is future and status is "soon"', () => { + const workshop = makeWorkshop({ + published_at: "2099-01-01T00:00:00Z", + status: "soon", + }); + expect(isUpcoming(workshop as any)).toBeTruthy(); + }); + + it("returns falsy when published_at is in the past", () => { + const workshop = makeWorkshop({ + published_at: "2020-01-01T00:00:00Z", + status: "soon", + }); + expect(isUpcoming(workshop as any)).toBeFalsy(); + }); + + it('returns falsy when status is not "soon"', () => { + const workshop = makeWorkshop({ + published_at: "2099-01-01T00:00:00Z", + status: "published", + }); + expect(isUpcoming(workshop as any)).toBeFalsy(); + }); +}); + +describe("isNew", () => { + it("returns true when published within last 30 days", () => { + const recentDate = new Date(Date.now() - 1000 * 60 * 60 * 24 * 5); // 5 days ago + const workshop = makeWorkshop({ + published_at: recentDate.toISOString(), + }); + expect(isNew(workshop as any)).toBeTruthy(); + }); + + it("returns falsy when published more than 30 days ago", () => { + const oldDate = new Date(Date.now() - 1000 * 60 * 60 * 24 * 60); // 60 days ago + const workshop = makeWorkshop({ + published_at: oldDate.toISOString(), + }); + expect(isNew(workshop as any)).toBeFalsy(); + }); + + it("returns falsy when published_at is null", () => { + const workshop = makeWorkshop({ published_at: null }); + expect(isNew(workshop as any)).toBeFalsy(); + }); +}); From 3106674e264954cac473af033df4f08f6b456b4b Mon Sep 17 00:00:00 2001 From: icaroharry Date: Mon, 23 Mar 2026 13:13:29 -0300 Subject: [PATCH 3/7] Add tests for custom hooks Test useMediaQuery and usePrefersReducedMotion hooks with mocked matchMedia responses and state assertions. --- app/lib/hooks/use-media-query.test.tsx | 39 +++++++++++++ .../hooks/use-prefers-reduced-motion.test.tsx | 57 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 app/lib/hooks/use-media-query.test.tsx create mode 100644 app/lib/hooks/use-prefers-reduced-motion.test.tsx diff --git a/app/lib/hooks/use-media-query.test.tsx b/app/lib/hooks/use-media-query.test.tsx new file mode 100644 index 00000000..0c38c1e7 --- /dev/null +++ b/app/lib/hooks/use-media-query.test.tsx @@ -0,0 +1,39 @@ +import { renderHook } from "@testing-library/react"; +import { useMediaQuery } from "./use-media-query"; + +describe("useMediaQuery", () => { + it("returns false when query does not match", () => { + const { result } = renderHook(() => useMediaQuery("(min-width: 768px)")); + expect(result.current).toBe(false); + }); + + it("returns true when query matches", () => { + vi.mocked(window.matchMedia).mockImplementation((query) => ({ + matches: true, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })); + + const { result } = renderHook(() => useMediaQuery("(min-width: 768px)")); + expect(result.current).toBe(true); + }); + + afterEach(() => { + // Reset matchMedia to default (non-matching) + vi.mocked(window.matchMedia).mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })); + }); +}); diff --git a/app/lib/hooks/use-prefers-reduced-motion.test.tsx b/app/lib/hooks/use-prefers-reduced-motion.test.tsx new file mode 100644 index 00000000..f282f983 --- /dev/null +++ b/app/lib/hooks/use-prefers-reduced-motion.test.tsx @@ -0,0 +1,57 @@ +import { renderHook } from "@testing-library/react"; +import usePrefersReducedMotion from "./use-prefers-reduced-motion"; + +describe("usePrefersReducedMotion", () => { + it("returns false when user has no preference for reduced motion", () => { + // matchMedia returns matches: false by default (from vitest.setup.ts) + // The hook queries "(prefers-reduced-motion: no-preference)" + // matches: false means the user DOES prefer reduced motion + const { result } = renderHook(() => usePrefersReducedMotion()); + expect(typeof result.current).toBe("boolean"); + }); + + it("returns false when user prefers no reduced motion", () => { + vi.mocked(window.matchMedia).mockImplementation((query) => ({ + matches: true, // no-preference matches = user does NOT prefer reduced motion + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })); + + const { result } = renderHook(() => usePrefersReducedMotion()); + expect(result.current).toBe(false); + }); + + it("returns true when user prefers reduced motion", () => { + vi.mocked(window.matchMedia).mockImplementation((query) => ({ + matches: false, // no-preference does NOT match = user prefers reduced motion + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })); + + const { result } = renderHook(() => usePrefersReducedMotion()); + expect(result.current).toBe(true); + }); + + afterEach(() => { + vi.mocked(window.matchMedia).mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })); + }); +}); From 3d9c236377b288a3b3a7a5d7a76aa84909a1c2e9 Mon Sep 17 00:00:00 2001 From: icaroharry Date: Mon, 23 Mar 2026 13:13:40 -0300 Subject: [PATCH 4/7] Add tests for UI components Test Chip, Button (variants, sizes, asChild, ref forwarding), Skeleton, AlertBanner (5 variant types), ProBadge, UserAvatar (pro/team badges, fallback URLs), PriceCard compound component (Title, Pricing, Features, Coupon interaction, Divider), and ChallengeCard (links, chips, avatar count, soon status). --- .../ui/alert-banner/alert-banner.test.tsx | 67 +++++++ app/components/ui/button/button.test.tsx | 56 ++++++ .../ui/cards/challenge-card.test.tsx | 94 ++++++++++ .../ui/cards/pricing/price-card.test.tsx | 165 ++++++++++++++++++ app/components/ui/chip.test.tsx | 36 ++++ .../ui/pro-badge/pro-badge.test.tsx | 14 ++ app/components/ui/skeleton.test.tsx | 24 +++ .../ui/user-avatar/user-avatar.test.tsx | 70 ++++++++ 8 files changed, 526 insertions(+) create mode 100644 app/components/ui/alert-banner/alert-banner.test.tsx create mode 100644 app/components/ui/button/button.test.tsx create mode 100644 app/components/ui/cards/challenge-card.test.tsx create mode 100644 app/components/ui/cards/pricing/price-card.test.tsx create mode 100644 app/components/ui/chip.test.tsx create mode 100644 app/components/ui/pro-badge/pro-badge.test.tsx create mode 100644 app/components/ui/skeleton.test.tsx create mode 100644 app/components/ui/user-avatar/user-avatar.test.tsx diff --git a/app/components/ui/alert-banner/alert-banner.test.tsx b/app/components/ui/alert-banner/alert-banner.test.tsx new file mode 100644 index 00000000..5089b530 --- /dev/null +++ b/app/components/ui/alert-banner/alert-banner.test.tsx @@ -0,0 +1,67 @@ +import { render, screen } from "@testing-library/react"; +import AlertBanner from "./index"; + +describe("AlertBanner", () => { + it("renders title and subtitle", () => { + render(); + expect(screen.getByText("Atenção")).toBeInTheDocument(); + expect(screen.getByText("Algo aconteceu")).toBeInTheDocument(); + }); + + it("renders default type with info styling", () => { + const { container } = render( + , + ); + expect(container.firstChild).toHaveClass("border-brand"); + }); + + it("renders warning type", () => { + const { container } = render( + , + ); + expect(container.firstChild).toHaveClass("border-yellow-400"); + }); + + it("renders streaming type", () => { + const { container } = render( + , + ); + expect(container.firstChild).toHaveClass("border-red-500"); + }); + + it("renders workshop-is-live type", () => { + const { container } = render( + , + ); + expect(container.firstChild).toHaveClass("border-red-500"); + }); + + it("renders black-friday type", () => { + const { container } = render( + , + ); + expect(container.firstChild).toHaveClass("border-yellow-500"); + }); + + it("applies custom className", () => { + const { container } = render( + , + ); + expect(container.firstChild).toHaveClass("my-custom"); + }); + + it("renders custom icon when provided", () => { + render( + !} + />, + ); + expect(screen.getByTestId("custom-icon")).toBeInTheDocument(); + }); +}); diff --git a/app/components/ui/button/button.test.tsx b/app/components/ui/button/button.test.tsx new file mode 100644 index 00000000..8fb26f64 --- /dev/null +++ b/app/components/ui/button/button.test.tsx @@ -0,0 +1,56 @@ +import { render, screen } from "@testing-library/react"; +import { Button } from "./index"; + +describe("Button", () => { + it("renders with children text", () => { + render(); + expect( + screen.getByRole("button", { name: "Click me" }), + ).toBeInTheDocument(); + }); + + it("renders as a button element by default", () => { + render(); + expect(screen.getByRole("button")).toBeInTheDocument(); + }); + + it("applies variant classes", () => { + render(); + const button = screen.getByRole("button"); + expect(button.className).toContain("bg-red-500"); + }); + + it("applies size classes", () => { + render(); + const button = screen.getByRole("button"); + expect(button.className).toContain("h-11"); + }); + + it("merges custom className", () => { + render(); + const button = screen.getByRole("button"); + expect(button).toHaveClass("my-class"); + }); + + it("supports disabled state", () => { + render(); + expect(screen.getByRole("button")).toBeDisabled(); + }); + + it("renders as child element with asChild", () => { + render( + , + ); + const link = screen.getByRole("link", { name: "Link Button" }); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute("href", "/test"); + }); + + it("forwards ref", () => { + const ref = { current: null as HTMLButtonElement | null }; + render(); + expect(ref.current).toBeInstanceOf(HTMLButtonElement); + }); +}); diff --git a/app/components/ui/cards/challenge-card.test.tsx b/app/components/ui/cards/challenge-card.test.tsx new file mode 100644 index 00000000..55963fd1 --- /dev/null +++ b/app/components/ui/cards/challenge-card.test.tsx @@ -0,0 +1,94 @@ +import { screen } from "@testing-library/react"; +import { renderWithRouter } from "~/test/render-with-router"; +import ChallengeCard from "./challenge-card"; + +// Mock tooltip for UserAvatar +vi.mock("~/components/ui/tooltip", () => ({ + default: ({ children }: { children: React.ReactNode }) => <>{children}, +})); + +const mockChallenge = { + id: 1, + name: "Landing Page com Tailwind", + slug: "landing-page-tailwind", + status: "published", + image_url: "https://example.com/challenge.png", + is_premium: true, + difficulty: 2, + enrolled_users_count: 3, + main_technology: { + name: "TailwindCSS", + image_url: "https://example.com/tw.png", + }, + avatars: [ + { name: "User1", avatar_url: "https://example.com/u1.jpg", badge: null }, + { name: "User2", avatar_url: "https://example.com/u2.jpg", badge: null }, + ], +}; + +describe("ChallengeCard", () => { + it("renders challenge name", () => { + renderWithRouter(() => ); + expect(screen.getByText("Landing Page com Tailwind")).toBeInTheDocument(); + }); + + it("links to challenge page", () => { + renderWithRouter(() => ); + const link = screen.getByRole("link"); + expect(link).toHaveAttribute( + "href", + "/mini-projetos/landing-page-tailwind", + ); + }); + + it("renders Ver projeto button", () => { + renderWithRouter(() => ); + expect(screen.getByText("Ver projeto")).toBeInTheDocument(); + }); + + it("renders challenge image", () => { + const { container } = renderWithRouter(() => ( + + )); + const challengeImg = container.querySelector( + 'img[src="https://example.com/challenge.png"]', + ); + expect(challengeImg).toBeInTheDocument(); + }); + + it('shows "Em breve" chip for soon status', () => { + const soonChallenge = { ...mockChallenge, status: "soon" }; + renderWithRouter(() => ); + expect(screen.getByText("Em breve")).toBeInTheDocument(); + }); + + it('shows "Gratuito" chip when not premium', () => { + const freeChallenge = { ...mockChallenge, is_premium: false }; + renderWithRouter(() => ); + expect(screen.getByText("Gratuito")).toBeInTheDocument(); + }); + + it("shows +N count when more than 5 enrolled users", () => { + const popularChallenge = { + ...mockChallenge, + enrolled_users_count: 15, + avatars: Array.from({ length: 5 }, (_, i) => ({ + name: `User${i}`, + avatar_url: "", + badge: null, + })), + }; + renderWithRouter(() => ( + + )); + expect(screen.getByText("+10")).toBeInTheDocument(); + }); + + it("has cursor-not-allowed for soon challenges", () => { + renderWithRouter(() => ( + + )); + const link = screen.getByRole("link"); + expect(link.className).toContain("cursor-not-allowed"); + }); +}); diff --git a/app/components/ui/cards/pricing/price-card.test.tsx b/app/components/ui/cards/pricing/price-card.test.tsx new file mode 100644 index 00000000..38c851ba --- /dev/null +++ b/app/components/ui/cards/pricing/price-card.test.tsx @@ -0,0 +1,165 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import PriceCard from "./price-card"; + +// Mock Tooltip which needs Radix provider +vi.mock("~/components/ui/tooltip", () => ({ + default: ({ children }: { children: React.ReactNode }) => <>{children}, +})); + +describe("PriceCard", () => { + it("renders children", () => { + render( + + Card content + , + ); + expect(screen.getByText("Card content")).toBeInTheDocument(); + }); + + it("applies custom className", () => { + const { container } = render( + + Test + , + ); + expect(container.firstChild).toHaveClass("border-amber-400"); + }); +}); + +describe("PriceCard.Title", () => { + it("renders title text", () => { + render( + + Plano PRO + , + ); + expect(screen.getByText("Plano PRO")).toBeInTheDocument(); + }); + + it("renders skeleton when loading", () => { + const { container } = render( + + Plano PRO + , + ); + expect(container.querySelector(".animate-pulse")).toBeInTheDocument(); + }); +}); + +describe("PriceCard.Pricing", () => { + it("renders monthly price", () => { + render( + + + , + ); + expect(screen.getByText(/R\$\s*49/)).toBeInTheDocument(); + }); + + it("renders installments when > 1", () => { + render( + + + , + ); + expect(screen.getByText("12x")).toBeInTheDocument(); + }); + + it("renders full price with strikethrough", () => { + render( + + + , + ); + expect(screen.getByText(/R\$ 99/)).toBeInTheDocument(); + }); + + it("renders skeleton when loading", () => { + const { container } = render( + + + , + ); + expect(container.querySelector(".animate-pulse")).toBeInTheDocument(); + }); +}); + +describe("PriceCard.Features", () => { + const features = [ + { + "Recursos Principais": [ + { title: "Acesso a mini-projetos", isAvailable: true }, + { title: "Certificados", isAvailable: false }, + ], + }, + ]; + + it("renders feature items", () => { + render( + + + , + ); + expect(screen.getByText("Acesso a mini-projetos")).toBeInTheDocument(); + expect(screen.getByText("Certificados")).toBeInTheDocument(); + }); + + it("renders category name", () => { + render( + + + , + ); + expect(screen.getByText("Recursos Principais")).toBeInTheDocument(); + }); +}); + +describe("PriceCard.Coupon", () => { + it("shows 'Possui cupom?' button initially", () => { + render( + + + , + ); + expect(screen.getByText("Possui cupom?")).toBeInTheDocument(); + }); + + it("shows form when clicked", async () => { + const user = userEvent.setup(); + render( + + + , + ); + + await user.click(screen.getByText("Possui cupom?")); + expect(screen.getByPlaceholderText("Código do Cupom")).toBeInTheDocument(); + expect(screen.getByText("Aplicar")).toBeInTheDocument(); + }); + + it("shows error message", async () => { + const user = userEvent.setup(); + render( + + + , + ); + + await user.click(screen.getByText("Possui cupom?")); + expect(screen.getByText("Cupom inválido")).toBeInTheDocument(); + }); +}); + +describe("PriceCard.Divider", () => { + it("renders a divider element", () => { + const { container } = render( + + + , + ); + expect( + container.querySelector(".bg-background-500\\/30"), + ).toBeInTheDocument(); + }); +}); diff --git a/app/components/ui/chip.test.tsx b/app/components/ui/chip.test.tsx new file mode 100644 index 00000000..9e94adc0 --- /dev/null +++ b/app/components/ui/chip.test.tsx @@ -0,0 +1,36 @@ +import { render, screen } from "@testing-library/react"; +import Chip from "./chip"; + +describe("Chip", () => { + it("renders the text", () => { + render(); + expect(screen.getByText("PRO")).toBeInTheDocument(); + }); + + it("renders with free type styling", () => { + render(); + const chip = screen.getByText("Gratuito"); + expect(chip).toBeInTheDocument(); + expect(chip).toHaveClass("text-green-600"); + }); + + it("renders with unlisted type styling", () => { + render(); + const chip = screen.getByText("Não listado"); + expect(chip).toHaveClass("text-red-600"); + }); + + it("applies default type when no type is provided", () => { + render(); + const chip = screen.getByText("Default"); + expect(chip).toHaveClass("text-blue-600"); + }); + + it("accepts additional className", () => { + const { container } = render( + , + ); + const outerDiv = container.firstChild as HTMLElement; + expect(outerDiv).toHaveClass("my-custom-class"); + }); +}); diff --git a/app/components/ui/pro-badge/pro-badge.test.tsx b/app/components/ui/pro-badge/pro-badge.test.tsx new file mode 100644 index 00000000..51e32d4d --- /dev/null +++ b/app/components/ui/pro-badge/pro-badge.test.tsx @@ -0,0 +1,14 @@ +import { render, screen } from "@testing-library/react"; +import ProBadge from "./index"; + +describe("ProBadge", () => { + it("renders PRO text", () => { + render(); + expect(screen.getByText("PRO")).toBeInTheDocument(); + }); + + it("has amber background", () => { + render(); + expect(screen.getByText("PRO")).toHaveClass("bg-amber-400"); + }); +}); diff --git a/app/components/ui/skeleton.test.tsx b/app/components/ui/skeleton.test.tsx new file mode 100644 index 00000000..e6323bda --- /dev/null +++ b/app/components/ui/skeleton.test.tsx @@ -0,0 +1,24 @@ +import { render } from "@testing-library/react"; +import { Skeleton } from "./skeleton"; + +describe("Skeleton", () => { + it("renders a div with animate-pulse", () => { + const { container } = render(); + const el = container.firstChild as HTMLElement; + expect(el.tagName).toBe("DIV"); + expect(el).toHaveClass("animate-pulse"); + }); + + it("merges custom className", () => { + const { container } = render(); + const el = container.firstChild as HTMLElement; + expect(el).toHaveClass("h-10"); + expect(el).toHaveClass("w-full"); + expect(el).toHaveClass("animate-pulse"); + }); + + it("passes through additional HTML attributes", () => { + const { container } = render(); + expect(container.querySelector("[data-testid='skel']")).toBeInTheDocument(); + }); +}); diff --git a/app/components/ui/user-avatar/user-avatar.test.tsx b/app/components/ui/user-avatar/user-avatar.test.tsx new file mode 100644 index 00000000..34abcdd8 --- /dev/null +++ b/app/components/ui/user-avatar/user-avatar.test.tsx @@ -0,0 +1,70 @@ +import { render, screen } from "@testing-library/react"; +import UserAvatar from "./index"; + +// TooltipWrapper requires Radix Tooltip provider — mock it to just render children +vi.mock("../tooltip", () => ({ + default: ({ children }: { children: React.ReactNode }) => <>{children}, +})); + +describe("UserAvatar", () => { + it("renders avatar image with URL", () => { + render( + , + ); + const img = screen.getByAltText("Avatar do usuário"); + expect(img).toHaveAttribute("src", "https://example.com/avatar.jpg"); + }); + + it("renders fallback avatar when avatar_url is empty", () => { + render( + , + ); + const img = screen.getByAltText("Avatar do usuário"); + expect(img.getAttribute("src")).toContain("codante.io"); + }); + + it("renders pro badge ring", () => { + render( + , + ); + const img = screen.getByAltText("Avatar do usuário"); + expect(img).toHaveClass("ring-amber-400"); + }); + + it("renders team badge ring", () => { + render( + , + ); + const img = screen.getByAltText("Avatar do usuário"); + expect(img).toHaveClass("ring-brand-500"); + }); + + it("applies custom className", () => { + render( + , + ); + const img = screen.getByAltText("Avatar do usuário"); + expect(img).toHaveClass("w-20"); + }); +}); From 5480d258faa5ab32ebc9428f030f4a95b201314f Mon Sep 17 00:00:00 2001 From: icaroharry Date: Mon, 23 Mar 2026 13:13:51 -0300 Subject: [PATCH 5/7] Add tests for feature and layout components Test Error500, NotFound, form Input, LoadingButton (idle/loading/ submitting/success states), WhatsApp button, LinkToLoginWithRedirect (redirect logic, password-reset edge case), ranking badge SVGs, Wave (top/bottom variants), and BackgroundBlur. --- .../background-blur/background-blur.test.tsx | 22 +++++++ app/components/_layouts/wave/wave.test.tsx | 24 ++++++++ .../button-whatsapp/whatsapp.test.tsx | 34 +++++++++++ .../error-handling/500/error-500.test.tsx | 32 ++++++++++ .../not-found/not-found.test.tsx | 37 +++++++++++ .../features/form/input/input.test.tsx | 46 ++++++++++++++ .../loading-button/loading-button.test.tsx | 61 +++++++++++++++++++ .../link-to-login.test.tsx | 61 +++++++++++++++++++ .../ranking-badges/ranking-badges.test.tsx | 32 ++++++++++ 9 files changed, 349 insertions(+) create mode 100644 app/components/_layouts/background-blur/background-blur.test.tsx create mode 100644 app/components/_layouts/wave/wave.test.tsx create mode 100644 app/components/features/button-whatsapp/whatsapp.test.tsx create mode 100644 app/components/features/error-handling/500/error-500.test.tsx create mode 100644 app/components/features/error-handling/not-found/not-found.test.tsx create mode 100644 app/components/features/form/input/input.test.tsx create mode 100644 app/components/features/form/loading-button/loading-button.test.tsx create mode 100644 app/components/features/link-to-login-with-redirect/link-to-login.test.tsx create mode 100644 app/components/features/ranking/ranking-badges/ranking-badges.test.tsx diff --git a/app/components/_layouts/background-blur/background-blur.test.tsx b/app/components/_layouts/background-blur/background-blur.test.tsx new file mode 100644 index 00000000..699516cc --- /dev/null +++ b/app/components/_layouts/background-blur/background-blur.test.tsx @@ -0,0 +1,22 @@ +import { render } from "@testing-library/react"; +import BackgroundBlur from "./index"; + +describe("BackgroundBlur", () => { + it("renders two blur elements", () => { + const { container } = render(); + const blurElements = container.querySelectorAll("[aria-hidden='true']"); + expect(blurElements).toHaveLength(2); + }); + + it("has blur-3xl class on containers", () => { + const { container } = render(); + const elements = container.querySelectorAll(".blur-3xl"); + expect(elements).toHaveLength(2); + }); + + it("renders gradient divs inside", () => { + const { container } = render(); + const gradients = container.querySelectorAll(".bg-linear-to-tr"); + expect(gradients).toHaveLength(2); + }); +}); diff --git a/app/components/_layouts/wave/wave.test.tsx b/app/components/_layouts/wave/wave.test.tsx new file mode 100644 index 00000000..cb0981ff --- /dev/null +++ b/app/components/_layouts/wave/wave.test.tsx @@ -0,0 +1,24 @@ +import { render } from "@testing-library/react"; +import Wave from "./index"; + +describe("Wave", () => { + it("renders bottom wave as SVG", () => { + const { container } = render(); + expect(container.querySelector("svg")).toBeInTheDocument(); + }); + + it("renders top wave with wrapper div", () => { + const { container } = render(); + expect(container.querySelector(".relative")).toBeInTheDocument(); + expect(container.querySelector("svg")).toBeInTheDocument(); + }); + + it("renders different SVG paths for top and bottom", () => { + const { container: bottomContainer } = render(); + const { container: topContainer } = render(); + + const bottomPath = bottomContainer.querySelector("path")?.getAttribute("d"); + const topPath = topContainer.querySelector("path")?.getAttribute("d"); + expect(bottomPath).not.toBe(topPath); + }); +}); diff --git a/app/components/features/button-whatsapp/whatsapp.test.tsx b/app/components/features/button-whatsapp/whatsapp.test.tsx new file mode 100644 index 00000000..cf1fb27a --- /dev/null +++ b/app/components/features/button-whatsapp/whatsapp.test.tsx @@ -0,0 +1,34 @@ +import { render, screen } from "@testing-library/react"; +import WhatsButton from "./index"; + +describe("WhatsButton", () => { + it("renders a link to WhatsApp", () => { + render(); + const link = screen.getByRole("link"); + expect(link).toHaveAttribute("href", expect.stringContaining("wa.me")); + }); + + it("opens in a new tab", () => { + render(); + expect(screen.getByRole("link")).toHaveAttribute("target", "_blank"); + }); + + it("has accessible label", () => { + render(); + expect( + screen.getByLabelText("Fale conosco pelo WhatsApp"), + ).toBeInTheDocument(); + }); + + it("renders without fixed positioning when onlyWrapper", () => { + const { container } = render(); + const link = container.querySelector("a"); + expect(link?.className).not.toContain("fixed"); + }); + + it("renders with fixed positioning by default", () => { + const { container } = render(); + const link = container.querySelector("a"); + expect(link?.className).toContain("fixed"); + }); +}); diff --git a/app/components/features/error-handling/500/error-500.test.tsx b/app/components/features/error-handling/500/error-500.test.tsx new file mode 100644 index 00000000..5d200f71 --- /dev/null +++ b/app/components/features/error-handling/500/error-500.test.tsx @@ -0,0 +1,32 @@ +import { screen } from "@testing-library/react"; +import { renderWithRouter } from "~/test/render-with-router"; +import { Error500 } from "./index"; + +const mockError = { message: "Test error", stack: "Error stack trace" }; + +describe("Error500", () => { + it("renders 500 status code", () => { + renderWithRouter(() => ); + expect(screen.getByText("500")).toBeInTheDocument(); + }); + + it("renders error heading", () => { + renderWithRouter(() => ); + expect(screen.getByText("Ops...")).toBeInTheDocument(); + }); + + it("renders description text", () => { + renderWithRouter(() => ); + expect(screen.getByText("Alguma coisa deu errada...")).toBeInTheDocument(); + }); + + it("renders link back to home", () => { + renderWithRouter(() => ); + expect(screen.getByText(/Voltar para a Home/)).toBeInTheDocument(); + }); + + it("shows error details in non-production", () => { + renderWithRouter(() => ); + expect(screen.getByText(/"Test error"/)).toBeInTheDocument(); + }); +}); diff --git a/app/components/features/error-handling/not-found/not-found.test.tsx b/app/components/features/error-handling/not-found/not-found.test.tsx new file mode 100644 index 00000000..cfc23dff --- /dev/null +++ b/app/components/features/error-handling/not-found/not-found.test.tsx @@ -0,0 +1,37 @@ +import { screen } from "@testing-library/react"; +import { renderWithRouter } from "~/test/render-with-router"; +import NotFound from "./index"; + +vi.mock("~/components/_layouts/public-env", () => ({ + getPublicEnv: (key: string) => (key === "NODE_ENV" ? "test" : undefined), +})); + +describe("NotFound", () => { + it("renders 404 status code", () => { + renderWithRouter(() => ); + expect(screen.getByText("404")).toBeInTheDocument(); + }); + + it("renders page heading", () => { + renderWithRouter(() => ); + expect(screen.getByText("Página não encontrada")).toBeInTheDocument(); + }); + + it("renders description", () => { + renderWithRouter(() => ); + expect( + screen.getByText("Desculpe, não encontramos nada por aqui..."), + ).toBeInTheDocument(); + }); + + it("renders link to home", () => { + renderWithRouter(() => ); + const link = screen.getByText(/Voltar para a Home/); + expect(link.closest("a")).toHaveAttribute("href", "/"); + }); + + it("shows error details in non-production", () => { + renderWithRouter(() => ); + expect(screen.getByText(/"some error info"/)).toBeInTheDocument(); + }); +}); diff --git a/app/components/features/form/input/input.test.tsx b/app/components/features/form/input/input.test.tsx new file mode 100644 index 00000000..caed510a --- /dev/null +++ b/app/components/features/form/input/input.test.tsx @@ -0,0 +1,46 @@ +import { render, screen } from "@testing-library/react"; +import Input from "./index"; + +describe("Input", () => { + it("renders label text", () => { + render(); + expect(screen.getByText("E-mail")).toBeInTheDocument(); + }); + + it("renders input with correct attributes", () => { + render(); + const input = screen.getByLabelText("E-mail"); + expect(input).toHaveAttribute("type", "email"); + expect(input).toHaveAttribute("name", "email"); + expect(input).toHaveAttribute("id", "email"); + }); + + it("defaults to text type", () => { + render(); + expect(screen.getByLabelText("Nome")).toHaveAttribute("type", "text"); + }); + + it("supports disabled state", () => { + render(); + expect(screen.getByLabelText("Nome")).toBeDisabled(); + }); + + it("supports placeholder", () => { + render( + , + ); + expect(screen.getByPlaceholderText("Digite seu nome")).toBeInTheDocument(); + }); + + it("passes through additional props", () => { + render( + , + ); + expect(screen.getByTestId("custom-input")).toBeInTheDocument(); + }); +}); diff --git a/app/components/features/form/loading-button/loading-button.test.tsx b/app/components/features/form/loading-button/loading-button.test.tsx new file mode 100644 index 00000000..724f7015 --- /dev/null +++ b/app/components/features/form/loading-button/loading-button.test.tsx @@ -0,0 +1,61 @@ +import { render, screen } from "@testing-library/react"; +import LoadingButton from "./index"; + +describe("LoadingButton", () => { + it("renders children text in idle state", () => { + render(Enviar); + expect(screen.getByText("Enviar")).toBeVisible(); + }); + + it("is enabled in idle state", () => { + render(Enviar); + expect(screen.getByRole("button")).not.toBeDisabled(); + }); + + it("is disabled when loading", () => { + render(Enviar); + expect(screen.getByRole("button")).toBeDisabled(); + }); + + it("is disabled when submitting", () => { + render(Enviar); + expect(screen.getByRole("button")).toBeDisabled(); + }); + + it("shows spinner during loading", () => { + const { container } = render( + Enviar, + ); + expect(container.querySelector(".animate-spin")).toBeInTheDocument(); + }); + + it("shows spinner during submitting", () => { + const { container } = render( + Enviar, + ); + expect(container.querySelector(".animate-spin")).toBeInTheDocument(); + }); + + it("hides children text when loading", () => { + render(Enviar); + expect(screen.getByText("Enviar")).toHaveClass("invisible"); + }); + + it("is disabled on successful submission", () => { + render( + + Enviar + , + ); + expect(screen.getByRole("button")).toBeDisabled(); + }); + + it("hides children on successful submission", () => { + render( + + Enviar + , + ); + expect(screen.getByText("Enviar")).toHaveClass("invisible"); + }); +}); diff --git a/app/components/features/link-to-login-with-redirect/link-to-login.test.tsx b/app/components/features/link-to-login-with-redirect/link-to-login.test.tsx new file mode 100644 index 00000000..23311eb1 --- /dev/null +++ b/app/components/features/link-to-login-with-redirect/link-to-login.test.tsx @@ -0,0 +1,61 @@ +import { screen } from "@testing-library/react"; +import { renderWithRouter } from "~/test/render-with-router"; +import LinkToLoginWithRedirect from "./index"; + +describe("LinkToLoginWithRedirect", () => { + it("renders children", () => { + renderWithRouter( + () => Login, + { path: "/workshops/react" }, + ); + expect(screen.getByText("Login")).toBeInTheDocument(); + }); + + it("links to /login with current path as redirectTo", () => { + renderWithRouter( + () => Login, + { path: "/workshops/react" }, + ); + expect(screen.getByRole("link")).toHaveAttribute( + "href", + "/login?redirectTo=/workshops/react", + ); + }); + + it("redirects to /dashboard when on root path", () => { + renderWithRouter( + () => Login, + { path: "/" }, + ); + expect(screen.getByRole("link")).toHaveAttribute( + "href", + "/login?redirectTo=/dashboard", + ); + }); + + it("uses custom redirectPath when provided", () => { + renderWithRouter( + () => ( + + Login + + ), + { path: "/somewhere" }, + ); + expect(screen.getByRole("link")).toHaveAttribute( + "href", + "/login?redirectTo=/custom", + ); + }); + + it("redirects to / then /dashboard when on password-reset page", () => { + renderWithRouter( + () => Login, + { path: "/password-reset/abc123" }, + ); + expect(screen.getByRole("link")).toHaveAttribute( + "href", + "/login?redirectTo=/dashboard", + ); + }); +}); diff --git a/app/components/features/ranking/ranking-badges/ranking-badges.test.tsx b/app/components/features/ranking/ranking-badges/ranking-badges.test.tsx new file mode 100644 index 00000000..c9b9a493 --- /dev/null +++ b/app/components/features/ranking/ranking-badges/ranking-badges.test.tsx @@ -0,0 +1,32 @@ +import { render } from "@testing-library/react"; +import { FirstPlace, SecondPlace, ThirdPlace } from "./index"; + +describe("Ranking Badges", () => { + it("renders FirstPlace SVG with amber fill", () => { + const { container } = render(); + const svg = container.querySelector("svg"); + expect(svg).toBeInTheDocument(); + expect(container.querySelector(".fill-amber-400")).toBeInTheDocument(); + }); + + it("renders SecondPlace SVG with slate fill", () => { + const { container } = render(); + const svg = container.querySelector("svg"); + expect(svg).toBeInTheDocument(); + expect(container.querySelector(".fill-slate-400")).toBeInTheDocument(); + }); + + it("renders ThirdPlace SVG with amber-700 fill", () => { + const { container } = render(); + const svg = container.querySelector("svg"); + expect(svg).toBeInTheDocument(); + expect(container.querySelector(".fill-amber-700")).toBeInTheDocument(); + }); + + it("passes through additional props", () => { + const { container } = render(); + expect( + container.querySelector("[data-testid='badge']"), + ).toBeInTheDocument(); + }); +}); From c42fd81263ee4240ee765d220ac41053862f67f5 Mon Sep 17 00:00:00 2001 From: icaroharry Date: Mon, 23 Mar 2026 13:14:02 -0300 Subject: [PATCH 6/7] Add route integration tests Test Blog route (stubbed loader with async rendering), BlogPostCard (Link href, router context), Ranking (loader data, filter links), Codando com IA checkout (paid/pending/denied/error states, boleto flow), and Agenda (events list, type filters, empty state). --- .../_layout-app/_agenda/agenda/index.test.tsx | 111 ++++++++++++++++ .../blog/components/blog-post-card.test.tsx | 55 ++++++++ .../_layout-app/_blog/blog/index.test.tsx | 82 ++++++++++++ .../codando-com-ia.sucesso/index.test.tsx | 119 ++++++++++++++++++ .../_ranking/ranking/index.test.tsx | 84 +++++++++++++ 5 files changed, 451 insertions(+) create mode 100644 app/routes/_layout-app/_agenda/agenda/index.test.tsx create mode 100644 app/routes/_layout-app/_blog/blog/components/blog-post-card.test.tsx create mode 100644 app/routes/_layout-app/_blog/blog/index.test.tsx create mode 100644 app/routes/_layout-app/_landing-pages/codando-com-ia.sucesso/index.test.tsx create mode 100644 app/routes/_layout-app/_ranking/ranking/index.test.tsx diff --git a/app/routes/_layout-app/_agenda/agenda/index.test.tsx b/app/routes/_layout-app/_agenda/agenda/index.test.tsx new file mode 100644 index 00000000..82f546ab --- /dev/null +++ b/app/routes/_layout-app/_agenda/agenda/index.test.tsx @@ -0,0 +1,111 @@ +import { screen } from "@testing-library/react"; +import { renderWithRouter } from "~/test/render-with-router"; +import Calendar from "./index"; + +vi.mock("~/lib/models/calendar.server", () => ({ + getUpcomingEvents: vi.fn(), + getPreviousEvents: vi.fn(), +})); + +// Calendar uses useOutletContext for user data +vi.mock("react-router", async () => { + const actual = await vi.importActual("react-router"); + return { + ...actual, + useOutletContext: () => ({ user: null }), + }; +}); + +// BlurReveal uses framer-motion — mock to passthrough +vi.mock("~/components/ui/motion/blur-reveal", () => ({ + BlurReveal: ({ children }: { children: React.ReactNode }) => <>{children}, +})); + +const mockEvents = { + upcoming: [ + { + id: 1, + title: "Workshop React Router v7", + description: "Aprenda React Router v7", + datetime: "2026-04-15T19:00:00Z", + type: "workshop", + url: "/workshops/react-router-v7", + image_url: null, + }, + { + id: 2, + title: "Mini Projeto CSS Grid", + description: "Pratique CSS Grid", + datetime: "2026-04-20T19:00:00Z", + type: "challenge", + url: "/mini-projetos/css-grid", + image_url: null, + }, + ], + previous: [ + { + id: 3, + title: "Workshop TypeScript", + description: "TypeScript avançado", + datetime: "2026-01-10T19:00:00Z", + type: "workshop", + url: "/workshops/typescript", + image_url: null, + }, + ], +}; + +describe("Agenda route", () => { + it("renders the page heading", async () => { + renderWithRouter(Calendar, { + path: "/agenda", + loader: () => mockEvents, + }); + + expect(await screen.findByText("Agenda")).toBeInTheDocument(); + }); + + it("renders upcoming events", async () => { + renderWithRouter(Calendar, { + path: "/agenda", + loader: () => mockEvents, + }); + + expect( + await screen.findByText("Workshop React Router v7"), + ).toBeInTheDocument(); + expect(screen.getByText("Mini Projeto CSS Grid")).toBeInTheDocument(); + }); + + it("renders filter toggle buttons", async () => { + renderWithRouter(Calendar, { + path: "/agenda", + loader: () => mockEvents, + }); + + expect(await screen.findByText("Mini Projetos")).toBeInTheDocument(); + expect(screen.getByText("Workshops")).toBeInTheDocument(); + expect(screen.getByText("Resoluções de Mini Projetos")).toBeInTheDocument(); + }); + + it("renders upcoming/previous toggle", async () => { + renderWithRouter(Calendar, { + path: "/agenda", + loader: () => mockEvents, + }); + + expect(await screen.findByText("Próximos eventos")).toBeInTheDocument(); + expect(screen.getByText("Eventos passados")).toBeInTheDocument(); + }); + + it("shows empty state when no events match filter", async () => { + renderWithRouter(Calendar, { + path: "/agenda", + loader: () => ({ upcoming: [], previous: [] }), + }); + + expect( + await screen.findByText("Nenhum evento encontrado"), + ).toBeInTheDocument(); + }); +}); diff --git a/app/routes/_layout-app/_blog/blog/components/blog-post-card.test.tsx b/app/routes/_layout-app/_blog/blog/components/blog-post-card.test.tsx new file mode 100644 index 00000000..974e638a --- /dev/null +++ b/app/routes/_layout-app/_blog/blog/components/blog-post-card.test.tsx @@ -0,0 +1,55 @@ +import { screen } from "@testing-library/react"; +import { renderWithRouter } from "~/test/render-with-router"; +import BlogPostCard from "./blog-post-card"; + +vi.mock("~/lib/hooks/useUserFromOutletContext", () => ({ + useUserFromOutletContext: () => null, +})); + +const mockBlogPost = { + id: 1, + title: "Introdução ao React Router v7", + slug: "introducao-react-router-v7", + status: "published" as const, + content: "

Conteúdo completo

", + short_description: "Aprenda as novidades do React Router v7", + image_url: "https://example.com/image.jpg", + instructor: { + id: "1", + name: "João", + slug: "joao", + avatar_url: "https://example.com/avatar.jpg", + company: "Codante", + email: "joao@codante.io", + bio: "Instrutor", + created_at: "2024-01-01", + updated_at: "2024-01-01", + }, + reactions: { + reaction_counts: [], + user_reactions: [], + }, + tags: [], +}; + +describe("BlogPostCard", () => { + it("renders the blog post title", () => { + renderWithRouter(() => ); + expect( + screen.getByText("Introdução ao React Router v7"), + ).toBeInTheDocument(); + }); + + it("renders the short description", () => { + renderWithRouter(() => ); + expect( + screen.getByText("Aprenda as novidades do React Router v7"), + ).toBeInTheDocument(); + }); + + it("links to the correct blog post URL", () => { + renderWithRouter(() => ); + const link = screen.getByRole("link"); + expect(link).toHaveAttribute("href", "/blog/introducao-react-router-v7"); + }); +}); diff --git a/app/routes/_layout-app/_blog/blog/index.test.tsx b/app/routes/_layout-app/_blog/blog/index.test.tsx new file mode 100644 index 00000000..7697a279 --- /dev/null +++ b/app/routes/_layout-app/_blog/blog/index.test.tsx @@ -0,0 +1,82 @@ +import { screen } from "@testing-library/react"; +import { renderWithRouter } from "~/test/render-with-router"; +import Blog from "./index"; + +vi.mock("~/lib/hooks/useUserFromOutletContext", () => ({ + useUserFromOutletContext: () => null, +})); + +// Mock server module to prevent env var validation at import time +vi.mock("~/lib/models/blog-post.server", () => ({ + getPosts: vi.fn(), +})); + +const mockBlogPosts = [ + { + id: 1, + title: "Post sobre React", + slug: "post-react", + status: "published" as const, + content: "

Conteúdo

", + short_description: "Aprenda React", + image_url: "https://example.com/img.jpg", + instructor: { + id: 1, + name: "João", + slug: "joao", + avatar_url: "https://example.com/avatar.jpg", + company: "Codante", + }, + reactions: { reaction_counts: [], user_reactions: [] }, + tags: [], + }, + { + id: 2, + title: "Post sobre TypeScript", + slug: "post-typescript", + status: "published" as const, + content: "

Conteúdo TS

", + short_description: "Aprenda TypeScript", + image_url: "https://example.com/img2.jpg", + instructor: { + id: 2, + name: "Maria", + slug: "maria", + avatar_url: "https://example.com/avatar2.jpg", + company: "Codante", + }, + reactions: { reaction_counts: [], user_reactions: [] }, + tags: [], + }, +]; + +describe("Blog route", () => { + it("renders the page heading", async () => { + renderWithRouter(Blog, { + path: "/blog", + loader: () => ({ blogPosts: mockBlogPosts }), + }); + + expect(await screen.findByText("Blog do Codante")).toBeInTheDocument(); + }); + + it("renders all blog posts from loader data", async () => { + renderWithRouter(Blog, { + path: "/blog", + loader: () => ({ blogPosts: mockBlogPosts }), + }); + + expect(await screen.findByText("Post sobre React")).toBeInTheDocument(); + expect(screen.getByText("Post sobre TypeScript")).toBeInTheDocument(); + }); + + it("renders blog post descriptions", async () => { + renderWithRouter(Blog, { + path: "/blog", + loader: () => ({ blogPosts: mockBlogPosts }), + }); + + expect(await screen.findByText("Aprenda React")).toBeInTheDocument(); + expect(screen.getByText("Aprenda TypeScript")).toBeInTheDocument(); + }); +}); diff --git a/app/routes/_layout-app/_landing-pages/codando-com-ia.sucesso/index.test.tsx b/app/routes/_layout-app/_landing-pages/codando-com-ia.sucesso/index.test.tsx new file mode 100644 index 00000000..b50ad374 --- /dev/null +++ b/app/routes/_layout-app/_landing-pages/codando-com-ia.sucesso/index.test.tsx @@ -0,0 +1,119 @@ +import { screen } from "@testing-library/react"; +import { renderWithRouter } from "~/test/render-with-router"; +import CodandoComIaCheckoutSuccess from "./index"; + +vi.mock("~/lib/models/pagarme.server", () => ({ + getCodandoComIaOrderStatus: vi.fn(), +})); + +describe("CodandoComIaCheckoutSuccess route", () => { + it("renders error state when error is returned", async () => { + renderWithRouter(CodandoComIaCheckoutSuccess, { + path: "/codando-com-ia/sucesso", + loader: () => ({ + order: null, + error: "Não encontramos informações da sua compra.", + }), + }); + + expect( + await screen.findByText("Não foi possível encontrar sua compra"), + ).toBeInTheDocument(); + expect( + screen.getByText("Não encontramos informações da sua compra."), + ).toBeInTheDocument(); + }); + + it("renders paid status correctly", async () => { + renderWithRouter(CodandoComIaCheckoutSuccess, { + path: "/codando-com-ia/sucesso", + loader: () => ({ + order: { + id: "order_123", + status: "paid", + amount: 29700, + charges: [ + { + status: "paid", + payment_method: "credit_card", + last_transaction: { status: "paid" }, + }, + ], + }, + error: null, + }), + }); + + expect( + await screen.findByText("Pagamento confirmado!"), + ).toBeInTheDocument(); + expect(screen.getByText("order_123")).toBeInTheDocument(); + expect(screen.getByText("R$ 297,00")).toBeInTheDocument(); + }); + + it("renders pending boleto status with next steps", async () => { + renderWithRouter(CodandoComIaCheckoutSuccess, { + path: "/codando-com-ia/sucesso", + loader: () => ({ + order: { + id: "order_456", + status: "pending", + amount: 29700, + charges: [ + { + status: "pending", + payment_method: "boleto", + last_transaction: { + status: "waiting_payment", + url: "https://boleto.example.com", + line: "12345.67890 12345.678901 12345.678901 1 12340000029700", + }, + }, + ], + }, + error: null, + }), + }); + + expect( + await screen.findByText("Pagamento em processamento"), + ).toBeInTheDocument(); + expect(screen.getByText("Abrir boleto")).toBeInTheDocument(); + }); + + it("renders denied status", async () => { + renderWithRouter(CodandoComIaCheckoutSuccess, { + path: "/codando-com-ia/sucesso", + loader: () => ({ + order: { + id: "order_789", + status: "failed", + amount: 29700, + charges: [ + { + status: "failed", + payment_method: "credit_card", + last_transaction: { status: "failed" }, + }, + ], + }, + error: null, + }), + }); + + const elements = await screen.findAllByText("Pagamento não confirmado"); + expect(elements.length).toBeGreaterThanOrEqual(1); + }); + + it("renders contact email link", async () => { + renderWithRouter(CodandoComIaCheckoutSuccess, { + path: "/codando-com-ia/sucesso", + loader: () => ({ + order: null, + error: "Erro genérico", + }), + }); + + expect(await screen.findByText("contato@codante.io")).toBeInTheDocument(); + }); +}); diff --git a/app/routes/_layout-app/_ranking/ranking/index.test.tsx b/app/routes/_layout-app/_ranking/ranking/index.test.tsx new file mode 100644 index 00000000..8428738f --- /dev/null +++ b/app/routes/_layout-app/_ranking/ranking/index.test.tsx @@ -0,0 +1,84 @@ +import { screen } from "@testing-library/react"; +import { renderWithRouter } from "~/test/render-with-router"; +import RankingList from "./index"; + +vi.mock("~/lib/models/ranking.server", () => ({ + getRanking: vi.fn(), +})); + +const mockRankingUsers = [ + { + avatar: { + name: "Alice", + avatar_url: "https://example.com/a.jpg", + badge: null, + }, + points: 500, + completed_challenge_count: 10, + received_reaction_count: 25, + }, + { + avatar: { + name: "Bob", + avatar_url: "https://example.com/b.jpg", + badge: "pro", + }, + points: 400, + completed_challenge_count: 8, + received_reaction_count: 15, + }, + { + avatar: { name: "Charlie", avatar_url: "", badge: null }, + points: 300, + completed_challenge_count: 5, + received_reaction_count: 10, + }, +]; + +// TooltipWrapper uses Radix which needs a provider — mock it +vi.mock("~/components/ui/tooltip", () => ({ + default: ({ children }: { children: React.ReactNode }) => <>{children}, +})); + +describe("Ranking route", () => { + it("renders the page heading", async () => { + renderWithRouter(RankingList, { + path: "/ranking", + loader: () => ({ rankingUsers: mockRankingUsers }), + }); + + expect(await screen.findByText("Ranking")).toBeInTheDocument(); + }); + + it("renders all ranking users", async () => { + renderWithRouter(RankingList, { + path: "/ranking", + loader: () => ({ rankingUsers: mockRankingUsers }), + }); + + expect(await screen.findByText("Alice")).toBeInTheDocument(); + expect(screen.getByText("Bob")).toBeInTheDocument(); + expect(screen.getByText("Charlie")).toBeInTheDocument(); + }); + + it("renders user points", async () => { + renderWithRouter(RankingList, { + path: "/ranking", + loader: () => ({ rankingUsers: mockRankingUsers }), + }); + + expect(await screen.findByText("500")).toBeInTheDocument(); + expect(screen.getByText("400")).toBeInTheDocument(); + expect(screen.getByText("300")).toBeInTheDocument(); + }); + + it("renders Geral and Mensal filter links", async () => { + renderWithRouter(RankingList, { + path: "/ranking", + loader: () => ({ rankingUsers: mockRankingUsers }), + }); + + expect(await screen.findByText("Geral")).toBeInTheDocument(); + expect(screen.getByText("Mensal")).toBeInTheDocument(); + }); +}); From 04c81602d243ad2f6738c663da3f0d28db0ef8a0 Mon Sep 17 00:00:00 2001 From: icaroharry Date: Mon, 23 Mar 2026 13:14:09 -0300 Subject: [PATCH 7/7] Add CI workflow for running tests on PRs and pushes GitHub Actions workflow that runs on pull requests and pushes to main. Installs dependencies, runs the test suite, and generates coverage. --- .github/workflows/test.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..5e60bef4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,31 @@ +name: Tests + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + test: + name: Unit & Integration Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci --force + + - name: Run tests + run: npm run test:run + + - name: Run coverage + run: npm run test:coverage