diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index da4cbd3..c028f23 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -24,7 +24,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 cache: "pnpm" - name: Install dependencies @@ -65,7 +65,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 cache: "pnpm" - name: Install dependencies diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..50c551d --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,41 @@ +name: Unit Tests + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + test: + name: Run Unit Tests + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run unit tests + run: pnpm test + + - name: Run unit tests with coverage + run: pnpm run test:coverage + if: github.event_name == 'pull_request' + + - name: Upload coverage reports + uses: actions/upload-artifact@v4 + if: github.event_name == 'pull_request' + with: + name: coverage-report + path: coverage/ + retention-days: 7 diff --git a/package.json b/package.json index 1953e18..a49ee7e 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,9 @@ "format": "prettier --write .", "format:check": "prettier --check .", "preview": "vite preview", - "test": "vitest --config vite.config.test.ts", - "test:ui": "vitest --ui --config vite.config.test.ts", - "test:coverage": "vitest run --coverage --config vite.config.test.ts", + "test": "vitest", + "test:ui": "vitest --ui", + "test:coverage": "vitest run --coverage", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", "test:e2e:headed": "playwright test --headed", @@ -91,7 +91,6 @@ "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", "vaul": "^0.9.3", - "vite-plugin-pwa": "^1.0.1", "workbox-background-sync": "^7.3.0", "workbox-precaching": "^7.3.0", "workbox-routing": "^7.3.0", @@ -99,14 +98,20 @@ "zod": "^3.23.8" }, "devDependencies": { + "vite-plugin-pwa": "^1.2.0", "@playwright/test": "^1.54.1", "@tailwindcss/typography": "^0.5.15", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", "@types/node": "^22.5.5", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react-swc": "^3.5.0", - "@vitest/ui": "^3.2.4", + "@vitejs/plugin-react-swc": "^4.2.2", + "@vitest/coverage-v8": "^4.0.15", + "@vitest/ui": "^4.0.15", "autoprefixer": "^10.4.20", + "baseline-browser-mapping": "^2.9.5", "globals": "^15.9.0", "husky": "^9.1.7", "jsdom": "^27.0.0", @@ -118,8 +123,8 @@ "tailwindcss": "^3.4.11", "tsx": "^4.20.3", "typescript": "^5.5.3", - "vite": "^5.4.1", - "vitest": "^3.2.4" + "vite": "^7.2.7", + "vitest": "^4.0.15" }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3223399..08c084d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -197,9 +197,6 @@ importers: vaul: specifier: ^0.9.3 version: 0.9.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - vite-plugin-pwa: - specifier: ^1.0.1 - version: 1.0.3(vite@5.4.20(@types/node@22.18.6)(terser@5.44.0))(workbox-build@7.3.0)(workbox-window@7.3.0) workbox-background-sync: specifier: ^7.3.0 version: 7.3.0 @@ -222,6 +219,15 @@ importers: '@tailwindcss/typography': specifier: ^0.5.15 version: 0.5.19(tailwindcss@3.4.17) + '@testing-library/jest-dom': + specifier: ^6.9.1 + version: 6.9.1 + '@testing-library/react': + specifier: ^16.3.0 + version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@testing-library/user-event': + specifier: ^14.6.1 + version: 14.6.1(@testing-library/dom@10.4.1) '@types/node': specifier: ^22.5.5 version: 22.18.6 @@ -232,14 +238,20 @@ importers: specifier: ^18.3.0 version: 18.3.7(@types/react@18.3.24) '@vitejs/plugin-react-swc': - specifier: ^3.5.0 - version: 3.11.0(vite@5.4.20(@types/node@22.18.6)(terser@5.44.0)) + specifier: ^4.2.2 + version: 4.2.2(vite@7.2.7(@types/node@22.18.6)(jiti@1.21.7)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/coverage-v8': + specifier: ^4.0.15 + version: 4.0.15(vitest@4.0.15) '@vitest/ui': - specifier: ^3.2.4 - version: 3.2.4(vitest@3.2.4) + specifier: ^4.0.15 + version: 4.0.15(vitest@4.0.15) autoprefixer: specifier: ^10.4.20 version: 10.4.21(postcss@8.5.6) + baseline-browser-mapping: + specifier: ^2.9.5 + version: 2.9.5 globals: specifier: ^15.9.0 version: 15.15.0 @@ -274,14 +286,20 @@ importers: specifier: ^5.5.3 version: 5.9.2 vite: - specifier: ^5.4.1 - version: 5.4.20(@types/node@22.18.6)(terser@5.44.0) + specifier: ^7.2.7 + version: 7.2.7(@types/node@22.18.6)(jiti@1.21.7)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite-plugin-pwa: + specifier: ^1.2.0 + version: 1.2.0(vite@7.2.7(@types/node@22.18.6)(jiti@1.21.7)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(workbox-build@7.3.0)(workbox-window@7.3.0) vitest: - specifier: ^3.2.4 - version: 3.2.4(@types/node@22.18.6)(@vitest/ui@3.2.4)(jsdom@27.0.0(postcss@8.5.6))(terser@5.44.0) + specifier: ^4.0.15 + version: 4.0.15(@types/node@22.18.6)(@vitest/ui@4.0.15)(jiti@1.21.7)(jsdom@27.0.0(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) packages: + '@adobe/css-tools@4.4.4': + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -388,8 +406,8 @@ packages: resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} '@babel/helper-validator-option@7.27.1': @@ -404,8 +422,8 @@ packages: resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} engines: {node: '>=6.9.0'} - '@babel/parser@7.28.4': - resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -792,10 +810,14 @@ packages: resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.4': - resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + '@csstools/color-helpers@5.1.0': resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} engines: {node: '>=18'} @@ -833,204 +855,102 @@ packages: '@date-fns/tz@1.4.1': resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.25.10': resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.25.10': resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.25.10': resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.25.10': resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.25.10': resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.25.10': resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.25.10': resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.25.10': resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.25.10': resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.25.10': resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.25.10': resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.25.10': resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.25.10': resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.25.10': resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.25.10': resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.25.10': resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.25.10': resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} engines: {node: '>=18'} @@ -1043,12 +963,6 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.25.10': resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} engines: {node: '>=18'} @@ -1061,12 +975,6 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.25.10': resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} engines: {node: '>=18'} @@ -1079,48 +987,24 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.25.10': resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.25.10': resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.25.10': resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.25.10': resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} engines: {node: '>=18'} @@ -1870,8 +1754,8 @@ packages: resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==} engines: {node: '>=14.0.0'} - '@rolldown/pluginutils@1.0.0-beta.27': - resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rolldown/pluginutils@1.0.0-beta.47': + resolution: {integrity: sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==} '@rollup/plugin-babel@5.3.1': resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} @@ -2032,6 +1916,9 @@ packages: cpu: [x64] os: [win32] + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@supabase/auth-js@2.81.1': resolution: {integrity: sha512-K20GgiSm9XeRLypxYHa5UCnybWc2K0ok0HLbqCej/wRxDpJxToXNOwKt0l7nO8xI1CyQ+GrNfU6bcRzvdbeopQ==} engines: {node: '>=20.0.0'} @@ -2173,6 +2060,38 @@ packages: peerDependencies: react: ^18 || ^19 + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.0': + resolution: {integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==} + 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 + + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} @@ -2267,44 +2186,54 @@ packages: vue-router: optional: true - '@vitejs/plugin-react-swc@3.11.0': - resolution: {integrity: sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==} + '@vitejs/plugin-react-swc@4.2.2': + resolution: {integrity: sha512-x+rE6tsxq/gxrEJN3Nv3dIV60lFflPj94c90b+NNo6n1QV1QQUTLoL0MpaOVasUZ0zqVBn7ead1B5ecx1JAGfA==} + engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: vite: ^4 || ^5 || ^6 || ^7 - '@vitest/expect@3.2.4': - resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + '@vitest/coverage-v8@4.0.15': + resolution: {integrity: sha512-FUJ+1RkpTFW7rQITdgTi93qOCWJobWhBirEPCeXh2SW2wsTlFxy51apDz5gzG+ZEYt/THvWeNmhdAoS9DTwpCw==} + peerDependencies: + '@vitest/browser': 4.0.15 + vitest: 4.0.15 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@4.0.15': + resolution: {integrity: sha512-Gfyva9/GxPAWXIWjyGDli9O+waHDC0Q0jaLdFP1qPAUUfo1FEXPXUfUkp3eZA0sSq340vPycSyOlYUeM15Ft1w==} - '@vitest/mocker@3.2.4': - resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + '@vitest/mocker@4.0.15': + resolution: {integrity: sha512-CZ28GLfOEIFkvCFngN8Sfx5h+Se0zN+h4B7yOsPVCcgtiO7t5jt9xQh2E1UkFep+eb9fjyMfuC5gBypwb07fvQ==} peerDependencies: msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + vite: ^6.0.0 || ^7.0.0-0 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@3.2.4': - resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + '@vitest/pretty-format@4.0.15': + resolution: {integrity: sha512-SWdqR8vEv83WtZcrfLNqlqeQXlQLh2iilO1Wk1gv4eiHKjEzvgHb2OVc3mIPyhZE6F+CtfYjNlDJwP5MN6Km7A==} - '@vitest/runner@3.2.4': - resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + '@vitest/runner@4.0.15': + resolution: {integrity: sha512-+A+yMY8dGixUhHmNdPUxOh0la6uVzun86vAbuMT3hIDxMrAOmn5ILBHm8ajrqHE0t8R9T1dGnde1A5DTnmi3qw==} - '@vitest/snapshot@3.2.4': - resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + '@vitest/snapshot@4.0.15': + resolution: {integrity: sha512-A7Ob8EdFZJIBjLjeO0DZF4lqR6U7Ydi5/5LIZ0xcI+23lYlsYJAfGn8PrIWTYdZQRNnSRlzhg0zyGu37mVdy5g==} - '@vitest/spy@3.2.4': - resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + '@vitest/spy@4.0.15': + resolution: {integrity: sha512-+EIjOJmnY6mIfdXtE/bnozKEvTC4Uczg19yeZ2vtCz5Yyb0QQ31QWVQ8hswJ3Ysx/K2EqaNsVanjr//2+P3FHw==} - '@vitest/ui@3.2.4': - resolution: {integrity: sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==} + '@vitest/ui@4.0.15': + resolution: {integrity: sha512-sxSyJMaKp45zI0u+lHrPuZM1ZJQ8FaVD35k+UxVrha1yyvQ+TZuUYllUixwvQXlB7ixoDc7skf3lQPopZIvaQw==} peerDependencies: - vitest: 3.2.4 + vitest: 4.0.15 - '@vitest/utils@3.2.4': - resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@vitest/utils@4.0.15': + resolution: {integrity: sha512-HXjPW2w5dxhTD0dLwtYHDnelK3j8sR8cWIaLxr22evTyY6q8pRCjZSmhRWVjBaOVXChQd6AwMzi9pucorXCPZA==} acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} @@ -2334,6 +2263,10 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + ansi-styles@6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} @@ -2352,6 +2285,13 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} @@ -2360,9 +2300,8 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} + ast-v8-to-istanbul@0.3.8: + resolution: {integrity: sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==} async-function@1.0.0: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} @@ -2404,8 +2343,8 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - baseline-browser-mapping@2.8.7: - resolution: {integrity: sha512-bxxN2M3a4d1CRoQC//IqsR5XrLh0IJ8TCv2x6Y9N0nckNz/rTjZB3//GGscZziZOxmjP55rzxg/ze7usFI9FqQ==} + baseline-browser-mapping@2.9.5: + resolution: {integrity: sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA==} hasBin: true bidi-js@1.0.3: @@ -2437,10 +2376,6 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -2460,14 +2395,10 @@ packages: caniuse-lite@1.0.30001745: resolution: {integrity: sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==} - chai@5.3.3: - resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + chai@6.2.1: + resolution: {integrity: sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==} engines: {node: '>=18'} - check-error@2.1.1: - resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} - engines: {node: '>= 16'} - chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -2553,6 +2484,9 @@ packages: resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -2655,10 +2589,6 @@ packages: decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} - deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} - deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -2671,6 +2601,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} @@ -2680,6 +2614,12 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} @@ -2755,11 +2695,6 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} - hasBin: true - esbuild@0.25.10: resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} engines: {node: '>=18'} @@ -2965,6 +2900,10 @@ packages: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} @@ -2988,6 +2927,9 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -3015,6 +2957,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -3175,6 +3121,22 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -3262,9 +3224,6 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - loupe@3.2.1: - resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -3280,11 +3239,22 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.25.9: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} - magic-string@0.30.19: - resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.5.1: + resolution: {integrity: sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} marked@16.3.0: resolution: {integrity: sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w==} @@ -3310,6 +3280,10 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -3407,6 +3381,9 @@ packages: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} engines: {node: '>= 0.4'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -3452,10 +3429,6 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - pathval@2.0.1: - resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} - engines: {node: '>= 14.16'} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3566,6 +3539,10 @@ packages: resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} engines: {node: ^14.13.1 || >=16.0.0} + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + proc-log@6.0.0: resolution: {integrity: sha512-KG/XsTDN901PNfPfAMmj6N/Ywg9tM+bHK8pAz+27fS4N4Pcr+4zoYBOcGSBu6ceXYNPxkLpa4ohtfxV1XcLAfA==} engines: {node: ^20.17.0 || >=22.9.0} @@ -3611,6 +3588,9 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -3700,6 +3680,10 @@ packages: react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -3794,6 +3778,11 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -3883,8 +3872,8 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@3.9.0: - resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} @@ -3942,8 +3931,9 @@ packages: resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} engines: {node: '>=10'} - strip-literal@3.0.0: - resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} sucrase@3.35.0: resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} @@ -3955,6 +3945,10 @@ packages: engines: {npm: '>=8'} hasBin: true + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -4005,23 +3999,16 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tinypool@1.1.1: - resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} - engines: {node: ^18.0.0 || >=20.0.0} - - tinyrainbow@2.0.0: - resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} - engines: {node: '>=14.0.0'} - - tinyspy@4.0.4: - resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + tinyrainbow@3.0.3: + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} tldts-core@7.0.16: @@ -4167,39 +4154,39 @@ packages: victory-vendor@36.9.2: resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} - vite-node@3.2.4: - resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - - vite-plugin-pwa@1.0.3: - resolution: {integrity: sha512-/OpqIpUldALGxcsEnv/ekQiQ5xHkQ53wcoN5ewX4jiIDNGs3W+eNcI1WYZeyOLmzoEjg09D7aX0O89YGjen1aw==} + vite-plugin-pwa@1.2.0: + resolution: {integrity: sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw==} engines: {node: '>=16.0.0'} peerDependencies: '@vite-pwa/assets-generator': ^1.0.0 vite: ^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - workbox-build: ^7.3.0 - workbox-window: ^7.3.0 + workbox-build: ^7.4.0 + workbox-window: ^7.4.0 peerDependenciesMeta: '@vite-pwa/assets-generator': optional: true - vite@5.4.20: - resolution: {integrity: sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==} - engines: {node: ^18.0.0 || >=20.0.0} + vite@7.2.7: + resolution: {integrity: sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 peerDependenciesMeta: '@types/node': optional: true + jiti: + optional: true less: optional: true lightningcss: @@ -4214,27 +4201,37 @@ packages: optional: true terser: optional: true + tsx: + optional: true + yaml: + optional: true - vitest@3.2.4: - resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + vitest@4.0.15: + resolution: {integrity: sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true 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 + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.15 + '@vitest/browser-preview': 4.0.15 + '@vitest/browser-webdriverio': 4.0.15 + '@vitest/ui': 4.0.15 happy-dom: '*' jsdom: '*' peerDependenciesMeta: '@edge-runtime/vm': optional: true - '@types/debug': + '@opentelemetry/api': optional: true '@types/node': optional: true - '@vitest/browser': + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': optional: true '@vitest/ui': optional: true @@ -4406,6 +4403,8 @@ packages: snapshots: + '@adobe/css-tools@4.4.4': {} + '@alloc/quick-lru@5.2.0': {} '@apideck/better-ajv-errors@0.3.6(ajv@8.17.1)': @@ -4435,7 +4434,7 @@ snapshots: '@babel/code-frame@7.27.1': dependencies: - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 picocolors: 1.1.1 @@ -4448,10 +4447,10 @@ snapshots: '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) '@babel/helpers': 7.28.4 - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -4463,15 +4462,15 @@ snapshots: '@babel/generator@7.28.3': dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@babel/helper-compilation-targets@7.27.2': dependencies: @@ -4517,14 +4516,14 @@ snapshots: '@babel/helper-member-expression-to-functions@7.27.1': dependencies: '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@7.27.1': dependencies: '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -4532,14 +4531,14 @@ snapshots: dependencies: '@babel/core': 7.28.4 '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@babel/helper-plugin-utils@7.27.1': {} @@ -4564,13 +4563,13 @@ snapshots: '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} '@babel/helper-validator-option@7.27.1': {} @@ -4578,18 +4577,18 @@ snapshots: dependencies: '@babel/template': 7.27.2 '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color '@babel/helpers@7.28.4': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 - '@babel/parser@7.28.4': + '@babel/parser@7.28.5': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.28.4)': dependencies: @@ -4819,7 +4818,7 @@ snapshots: '@babel/core': 7.28.4 '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color @@ -5059,7 +5058,7 @@ snapshots: dependencies: '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 esutils: 2.0.3 '@babel/runtime@7.28.4': {} @@ -5067,25 +5066,27 @@ snapshots: '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 '@babel/traverse@7.28.4': dependencies: '@babel/code-frame': 7.27.1 '@babel/generator': 7.28.3 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.28.4': + '@babel/types@7.28.5': dependencies: '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@1.0.2': {} '@csstools/color-helpers@5.1.0': {} @@ -5113,150 +5114,81 @@ snapshots: '@date-fns/tz@1.4.1': {} - '@esbuild/aix-ppc64@0.21.5': - optional: true - '@esbuild/aix-ppc64@0.25.10': optional: true - '@esbuild/android-arm64@0.21.5': - optional: true - '@esbuild/android-arm64@0.25.10': optional: true - '@esbuild/android-arm@0.21.5': - optional: true - '@esbuild/android-arm@0.25.10': optional: true - '@esbuild/android-x64@0.21.5': - optional: true - '@esbuild/android-x64@0.25.10': optional: true - '@esbuild/darwin-arm64@0.21.5': - optional: true - '@esbuild/darwin-arm64@0.25.10': optional: true - '@esbuild/darwin-x64@0.21.5': - optional: true - '@esbuild/darwin-x64@0.25.10': optional: true - '@esbuild/freebsd-arm64@0.21.5': - optional: true - '@esbuild/freebsd-arm64@0.25.10': optional: true - '@esbuild/freebsd-x64@0.21.5': - optional: true - '@esbuild/freebsd-x64@0.25.10': optional: true - '@esbuild/linux-arm64@0.21.5': - optional: true - '@esbuild/linux-arm64@0.25.10': optional: true - '@esbuild/linux-arm@0.21.5': - optional: true - '@esbuild/linux-arm@0.25.10': optional: true - '@esbuild/linux-ia32@0.21.5': - optional: true - '@esbuild/linux-ia32@0.25.10': optional: true - '@esbuild/linux-loong64@0.21.5': - optional: true - '@esbuild/linux-loong64@0.25.10': optional: true - '@esbuild/linux-mips64el@0.21.5': - optional: true - '@esbuild/linux-mips64el@0.25.10': optional: true - '@esbuild/linux-ppc64@0.21.5': - optional: true - '@esbuild/linux-ppc64@0.25.10': optional: true - '@esbuild/linux-riscv64@0.21.5': - optional: true - '@esbuild/linux-riscv64@0.25.10': optional: true - '@esbuild/linux-s390x@0.21.5': - optional: true - '@esbuild/linux-s390x@0.25.10': optional: true - '@esbuild/linux-x64@0.21.5': - optional: true - '@esbuild/linux-x64@0.25.10': optional: true '@esbuild/netbsd-arm64@0.25.10': optional: true - '@esbuild/netbsd-x64@0.21.5': - optional: true - '@esbuild/netbsd-x64@0.25.10': optional: true '@esbuild/openbsd-arm64@0.25.10': optional: true - '@esbuild/openbsd-x64@0.21.5': - optional: true - '@esbuild/openbsd-x64@0.25.10': optional: true '@esbuild/openharmony-arm64@0.25.10': optional: true - '@esbuild/sunos-x64@0.21.5': - optional: true - '@esbuild/sunos-x64@0.25.10': optional: true - '@esbuild/win32-arm64@0.21.5': - optional: true - '@esbuild/win32-arm64@0.25.10': optional: true - '@esbuild/win32-ia32@0.21.5': - optional: true - '@esbuild/win32-ia32@0.25.10': optional: true - '@esbuild/win32-x64@0.21.5': - optional: true - '@esbuild/win32-x64@0.25.10': optional: true @@ -6040,7 +5972,7 @@ snapshots: '@remix-run/router@1.23.0': {} - '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rolldown/pluginutils@1.0.0-beta.47': {} '@rollup/plugin-babel@5.3.1(@babel/core@7.28.4)(rollup@2.79.2)': dependencies: @@ -6156,6 +6088,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.52.2': optional: true + '@standard-schema/spec@1.0.0': {} + '@supabase/auth-js@2.81.1': dependencies: tslib: 2.8.1 @@ -6291,6 +6225,42 @@ snapshots: '@tanstack/query-core': 5.90.2 react: 18.3.1 + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/runtime': 7.28.4 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.4.4 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@testing-library/dom': 10.4.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': + dependencies: + '@testing-library/dom': 10.4.1 + + '@types/aria-query@5.0.4': {} + '@types/chai@5.2.2': dependencies: '@types/deep-eql': 4.0.2 @@ -6360,66 +6330,80 @@ snapshots: optionalDependencies: react: 18.3.1 - '@vitejs/plugin-react-swc@3.11.0(vite@5.4.20(@types/node@22.18.6)(terser@5.44.0))': + '@vitejs/plugin-react-swc@4.2.2(vite@7.2.7(@types/node@22.18.6)(jiti@1.21.7)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: - '@rolldown/pluginutils': 1.0.0-beta.27 + '@rolldown/pluginutils': 1.0.0-beta.47 '@swc/core': 1.13.19 - vite: 5.4.20(@types/node@22.18.6)(terser@5.44.0) + vite: 7.2.7(@types/node@22.18.6)(jiti@1.21.7)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - '@swc/helpers' - '@vitest/expect@3.2.4': + '@vitest/coverage-v8@4.0.15(vitest@4.0.15)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.0.15 + ast-v8-to-istanbul: 0.3.8 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magicast: 0.5.1 + obug: 2.1.1 + std-env: 3.10.0 + tinyrainbow: 3.0.3 + vitest: 4.0.15(@types/node@22.18.6)(@vitest/ui@4.0.15)(jiti@1.21.7)(jsdom@27.0.0(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@4.0.15': dependencies: + '@standard-schema/spec': 1.0.0 '@types/chai': 5.2.2 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.3.3 - tinyrainbow: 2.0.0 + '@vitest/spy': 4.0.15 + '@vitest/utils': 4.0.15 + chai: 6.2.1 + tinyrainbow: 3.0.3 - '@vitest/mocker@3.2.4(vite@5.4.20(@types/node@22.18.6)(terser@5.44.0))': + '@vitest/mocker@4.0.15(vite@7.2.7(@types/node@22.18.6)(jiti@1.21.7)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: - '@vitest/spy': 3.2.4 + '@vitest/spy': 4.0.15 estree-walker: 3.0.3 - magic-string: 0.30.19 + magic-string: 0.30.21 optionalDependencies: - vite: 5.4.20(@types/node@22.18.6)(terser@5.44.0) + vite: 7.2.7(@types/node@22.18.6)(jiti@1.21.7)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - '@vitest/pretty-format@3.2.4': + '@vitest/pretty-format@4.0.15': dependencies: - tinyrainbow: 2.0.0 + tinyrainbow: 3.0.3 - '@vitest/runner@3.2.4': + '@vitest/runner@4.0.15': dependencies: - '@vitest/utils': 3.2.4 + '@vitest/utils': 4.0.15 pathe: 2.0.3 - strip-literal: 3.0.0 - '@vitest/snapshot@3.2.4': + '@vitest/snapshot@4.0.15': dependencies: - '@vitest/pretty-format': 3.2.4 - magic-string: 0.30.19 + '@vitest/pretty-format': 4.0.15 + magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@3.2.4': - dependencies: - tinyspy: 4.0.4 + '@vitest/spy@4.0.15': {} - '@vitest/ui@3.2.4(vitest@3.2.4)': + '@vitest/ui@4.0.15(vitest@4.0.15)': dependencies: - '@vitest/utils': 3.2.4 + '@vitest/utils': 4.0.15 fflate: 0.8.2 flatted: 3.3.3 pathe: 2.0.3 sirv: 3.0.2 tinyglobby: 0.2.15 - tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@22.18.6)(@vitest/ui@3.2.4)(jsdom@27.0.0(postcss@8.5.6))(terser@5.44.0) + tinyrainbow: 3.0.3 + vitest: 4.0.15(@types/node@22.18.6)(@vitest/ui@4.0.15)(jiti@1.21.7)(jsdom@27.0.0(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - '@vitest/utils@3.2.4': + '@vitest/utils@4.0.15': dependencies: - '@vitest/pretty-format': 3.2.4 - loupe: 3.2.1 - tinyrainbow: 2.0.0 + '@vitest/pretty-format': 4.0.15 + tinyrainbow: 3.0.3 acorn@8.15.0: {} @@ -6444,6 +6428,8 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@5.2.0: {} + ansi-styles@6.2.3: {} any-promise@1.3.0: {} @@ -6459,6 +6445,12 @@ snapshots: dependencies: tslib: 2.8.1 + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.4 @@ -6474,7 +6466,11 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 - assertion-error@2.0.1: {} + ast-v8-to-istanbul@0.3.8: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 9.0.1 async-function@1.0.0: {} @@ -6522,7 +6518,7 @@ snapshots: balanced-match@1.0.2: {} - baseline-browser-mapping@2.8.7: {} + baseline-browser-mapping@2.9.5: {} bidi-js@1.0.3: dependencies: @@ -6553,7 +6549,7 @@ snapshots: browserslist@4.26.2: dependencies: - baseline-browser-mapping: 2.8.7 + baseline-browser-mapping: 2.9.5 caniuse-lite: 1.0.30001745 electron-to-chromium: 1.5.224 node-releases: 2.0.21 @@ -6561,8 +6557,6 @@ snapshots: buffer-from@1.1.2: {} - cac@6.7.14: {} - call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -6584,15 +6578,7 @@ snapshots: caniuse-lite@1.0.30001745: {} - chai@5.3.3: - dependencies: - assertion-error: 2.0.1 - check-error: 2.1.1 - deep-eql: 5.0.2 - loupe: 3.2.1 - pathval: 2.0.1 - - check-error@2.1.1: {} + chai@6.2.1: {} chokidar@3.6.0: dependencies: @@ -6678,6 +6664,8 @@ snapshots: mdn-data: 2.12.2 source-map-js: 1.2.1 + css.escape@1.5.1: {} + cssesc@3.0.0: {} cssstyle@5.3.1(postcss@8.5.6): @@ -6769,8 +6757,6 @@ snapshots: decimal.js@10.6.0: {} - deep-eql@5.0.2: {} - deepmerge@4.3.1: {} define-data-property@1.1.4: @@ -6785,12 +6771,18 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + dequal@2.0.3: {} + detect-node-es@1.1.0: {} didyoumean@1.2.2: {} dlv@1.1.3: {} + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + dom-helpers@5.2.1: dependencies: '@babel/runtime': 7.28.4 @@ -6912,32 +6904,6 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild@0.21.5: - optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 - esbuild@0.25.10: optionalDependencies: '@esbuild/aix-ppc64': 0.25.10 @@ -7159,6 +7125,8 @@ snapshots: has-bigints@1.1.0: {} + has-flag@4.0.0: {} + has-property-descriptors@1.0.2: dependencies: es-define-property: 1.0.1 @@ -7181,6 +7149,8 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 + html-escaper@2.0.2: {} + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -7207,6 +7177,8 @@ snapshots: imurmurhash@0.1.4: {} + indent-string@4.0.0: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -7362,6 +7334,27 @@ snapshots: isexe@2.0.0: {} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -7467,8 +7460,6 @@ snapshots: dependencies: js-tokens: 4.0.0 - loupe@3.2.1: {} - lru-cache@10.4.3: {} lru-cache@11.2.2: {} @@ -7481,14 +7472,26 @@ snapshots: dependencies: react: 18.3.1 + lz-string@1.5.0: {} + magic-string@0.25.9: dependencies: sourcemap-codec: 1.4.8 - magic-string@0.30.19: + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.5.1: + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.3 + marked@16.3.0: {} math-intrinsics@1.1.0: {} @@ -7504,6 +7507,8 @@ snapshots: mimic-function@5.0.1: {} + min-indent@1.0.1: {} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -7582,6 +7587,8 @@ snapshots: has-symbols: 1.1.0 object-keys: 1.1.1 + obug@2.1.1: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -7626,8 +7633,6 @@ snapshots: pathe@2.0.3: {} - pathval@2.0.1: {} - picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -7708,6 +7713,12 @@ snapshots: pretty-bytes@6.1.1: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + proc-log@6.0.0: {} prop-types@15.8.1: @@ -7752,6 +7763,8 @@ snapshots: react-is@16.13.1: {} + react-is@17.0.2: {} + react-is@18.3.1: {} react-remove-scroll-bar@2.3.8(@types/react@18.3.24)(react@18.3.1): @@ -7846,6 +7859,11 @@ snapshots: tiny-invariant: 1.3.3 victory-vendor: 36.9.2 + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -7977,6 +7995,8 @@ snapshots: semver@6.3.1: {} + semver@7.7.3: {} + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -8078,7 +8098,7 @@ snapshots: stackback@0.0.2: {} - std-env@3.9.0: {} + std-env@3.10.0: {} stop-iteration-iterator@1.1.0: dependencies: @@ -8165,9 +8185,9 @@ snapshots: strip-comments@2.0.1: {} - strip-literal@3.0.0: + strip-indent@3.0.0: dependencies: - js-tokens: 9.0.1 + min-indent: 1.0.1 sucrase@3.35.0: dependencies: @@ -8188,6 +8208,10 @@ snapshots: transitivePeerDependencies: - supports-color + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} symbol-tree@3.2.4: {} @@ -8261,18 +8285,14 @@ snapshots: tinybench@2.9.0: {} - tinyexec@0.3.2: {} + tinyexec@1.0.2: {} tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - tinypool@1.1.1: {} - - tinyrainbow@2.0.0: {} - - tinyspy@4.0.4: {} + tinyrainbow@3.0.3: {} tldts-core@7.0.16: {} @@ -8432,75 +8452,61 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite-node@3.2.4(@types/node@22.18.6)(terser@5.44.0): - dependencies: - cac: 6.7.14 - debug: 4.4.3 - es-module-lexer: 1.7.0 - pathe: 2.0.3 - vite: 5.4.20(@types/node@22.18.6)(terser@5.44.0) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - vite-plugin-pwa@1.0.3(vite@5.4.20(@types/node@22.18.6)(terser@5.44.0))(workbox-build@7.3.0)(workbox-window@7.3.0): + vite-plugin-pwa@1.2.0(vite@7.2.7(@types/node@22.18.6)(jiti@1.21.7)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(workbox-build@7.3.0)(workbox-window@7.3.0): dependencies: debug: 4.4.3 pretty-bytes: 6.1.1 tinyglobby: 0.2.15 - vite: 5.4.20(@types/node@22.18.6)(terser@5.44.0) + vite: 7.2.7(@types/node@22.18.6)(jiti@1.21.7)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) workbox-build: 7.3.0 workbox-window: 7.3.0 transitivePeerDependencies: - supports-color - vite@5.4.20(@types/node@22.18.6)(terser@5.44.0): + vite@7.2.7(@types/node@22.18.6)(jiti@1.21.7)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: - esbuild: 0.21.5 + esbuild: 0.25.10 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 postcss: 8.5.6 rollup: 4.52.2 + tinyglobby: 0.2.15 optionalDependencies: '@types/node': 22.18.6 fsevents: 2.3.3 + jiti: 1.21.7 terser: 5.44.0 + tsx: 4.20.6 + yaml: 2.8.1 - vitest@3.2.4(@types/node@22.18.6)(@vitest/ui@3.2.4)(jsdom@27.0.0(postcss@8.5.6))(terser@5.44.0): + vitest@4.0.15(@types/node@22.18.6)(@vitest/ui@4.0.15)(jiti@1.21.7)(jsdom@27.0.0(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: - '@types/chai': 5.2.2 - '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@5.4.20(@types/node@22.18.6)(terser@5.44.0)) - '@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.3.3 - debug: 4.4.3 + '@vitest/expect': 4.0.15 + '@vitest/mocker': 4.0.15(vite@7.2.7(@types/node@22.18.6)(jiti@1.21.7)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/pretty-format': 4.0.15 + '@vitest/runner': 4.0.15 + '@vitest/snapshot': 4.0.15 + '@vitest/spy': 4.0.15 + '@vitest/utils': 4.0.15 + es-module-lexer: 1.7.0 expect-type: 1.2.2 - magic-string: 0.30.19 + magic-string: 0.30.21 + obug: 2.1.1 pathe: 2.0.3 picomatch: 4.0.3 - std-env: 3.9.0 + std-env: 3.10.0 tinybench: 2.9.0 - tinyexec: 0.3.2 + tinyexec: 1.0.2 tinyglobby: 0.2.15 - tinypool: 1.1.1 - tinyrainbow: 2.0.0 - vite: 5.4.20(@types/node@22.18.6)(terser@5.44.0) - vite-node: 3.2.4(@types/node@22.18.6)(terser@5.44.0) + tinyrainbow: 3.0.3 + vite: 7.2.7(@types/node@22.18.6)(jiti@1.21.7)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.18.6 - '@vitest/ui': 3.2.4(vitest@3.2.4) + '@vitest/ui': 4.0.15(vitest@4.0.15) jsdom: 27.0.0(postcss@8.5.6) transitivePeerDependencies: + - jiti - less - lightningcss - msw @@ -8508,8 +8514,9 @@ snapshots: - sass-embedded - stylus - sugarss - - supports-color - terser + - tsx + - yaml w3c-xmlserializer@5.0.0: dependencies: diff --git a/src/components/GenreBadge.test.tsx b/src/components/GenreBadge.test.tsx new file mode 100644 index 0000000..8077010 --- /dev/null +++ b/src/components/GenreBadge.test.tsx @@ -0,0 +1,126 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { GenreBadge } from "./GenreBadge"; +import * as useGenresModule from "@/hooks/queries/genres/useGenres"; + +vi.mock("@/hooks/queries/genres/useGenres"); + +describe("GenreBadge", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders genre name when genre is found", () => { + vi.spyOn(useGenresModule, "useGenres").mockReturnValue({ + genres: [ + { id: "1", name: "Rock" }, + { id: "2", name: "Pop" }, + ], + loading: false, + error: null, + }); + + render(); + expect(screen.getByText("Rock")).toBeInTheDocument(); + }); + + it("renders null when loading", () => { + vi.spyOn(useGenresModule, "useGenres").mockReturnValue({ + genres: [], + loading: true, + error: null, + }); + + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it("renders null when error", () => { + vi.spyOn(useGenresModule, "useGenres").mockReturnValue({ + genres: [], + loading: false, + error: new Error("Failed to load"), + }); + + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it("renders null when genre is not found", () => { + vi.spyOn(useGenresModule, "useGenres").mockReturnValue({ + genres: [ + { id: "1", name: "Rock" }, + { id: "2", name: "Pop" }, + ], + loading: false, + error: null, + }); + + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it("renders with default size", () => { + vi.spyOn(useGenresModule, "useGenres").mockReturnValue({ + genres: [{ id: "1", name: "Rock" }], + loading: false, + error: null, + }); + + const { container } = render(); + const badge = container.querySelector("div"); + expect(badge).not.toHaveClass("text-xs", "px-2", "py-1"); + }); + + it("renders with small size", () => { + vi.spyOn(useGenresModule, "useGenres").mockReturnValue({ + genres: [{ id: "1", name: "Rock" }], + loading: false, + error: null, + }); + + const { container } = render(); + const badge = container.querySelector("div"); + expect(badge).toHaveClass("text-xs", "px-2", "py-1"); + }); + + it("has correct styling classes", () => { + vi.spyOn(useGenresModule, "useGenres").mockReturnValue({ + genres: [{ id: "1", name: "Rock" }], + loading: false, + error: null, + }); + + const { container } = render(); + const badge = container.querySelector("div"); + expect(badge).toHaveClass("bg-purple-600/50", "text-purple-100"); + }); + + it("finds correct genre from multiple genres", () => { + vi.spyOn(useGenresModule, "useGenres").mockReturnValue({ + genres: [ + { id: "1", name: "Rock" }, + { id: "2", name: "Pop" }, + { id: "3", name: "Jazz" }, + ], + loading: false, + error: null, + }); + + render(); + expect(screen.getByText("Pop")).toBeInTheDocument(); + expect(screen.queryByText("Rock")).not.toBeInTheDocument(); + expect(screen.queryByText("Jazz")).not.toBeInTheDocument(); + }); + + it("renders when genres list is empty", () => { + vi.spyOn(useGenresModule, "useGenres").mockReturnValue({ + genres: [], + loading: false, + error: null, + }); + + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); +}); diff --git a/src/components/StageBadge.test.tsx b/src/components/StageBadge.test.tsx new file mode 100644 index 0000000..c9740ef --- /dev/null +++ b/src/components/StageBadge.test.tsx @@ -0,0 +1,107 @@ +import { describe, expect, it } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { StageBadge } from "./StageBadge"; + +describe("StageBadge", () => { + it("renders stage name", () => { + render(); + expect(screen.getByText("Main Stage")).toBeInTheDocument(); + }); + + it("renders with custom color", () => { + const { container } = render( + , + ); + const badge = container.querySelector("div"); + expect(badge).toHaveStyle({ + backgroundColor: "#ff000080", + borderColor: "#ff0000", + }); + }); + + it("renders with default color when no color provided", () => { + const { container } = render(); + const badge = container.querySelector("div"); + expect(badge).toHaveStyle({ + backgroundColor: "#7c3aed80", + borderColor: "#7c3aed", + }); + }); + + it("renders small size by default", () => { + const { container } = render(); + const badge = container.querySelector("div"); + expect(badge).toHaveClass("text-xs", "px-2", "py-1", "gap-1"); + }); + + it("renders medium size when specified", () => { + const { container } = render( + , + ); + const badge = container.querySelector("div"); + expect(badge).toHaveClass("text-sm", "px-3", "py-1.5", "gap-2"); + }); + + it("shows icon by default", () => { + const { container } = render(); + const icon = container.querySelector("svg"); + expect(icon).toBeInTheDocument(); + }); + + it("hides icon when showIcon is false", () => { + const { container } = render( + , + ); + const icon = container.querySelector("svg"); + expect(icon).not.toBeInTheDocument(); + }); + + it("applies correct icon size for small badge", () => { + const { container } = render( + , + ); + const icon = container.querySelector("svg"); + expect(icon).toHaveClass("h-3", "w-3"); + }); + + it("applies correct icon size for medium badge", () => { + const { container } = render( + , + ); + const icon = container.querySelector("svg"); + expect(icon).toHaveClass("h-4", "w-4"); + }); + + it("renders with all custom props", () => { + const { container } = render( + , + ); + expect(screen.getByText("Custom Stage")).toBeInTheDocument(); + const badge = container.querySelector("div"); + expect(badge).toHaveStyle({ + backgroundColor: "#00ff0080", + borderColor: "#00ff00", + }); + expect(badge).toHaveClass("text-sm"); + expect(container.querySelector("svg")).toBeInTheDocument(); + }); + + it("has correct base classes", () => { + const { container } = render(); + const badge = container.querySelector("div"); + expect(badge).toHaveClass( + "inline-flex", + "items-center", + "rounded-full", + "backdrop-blur-sm", + "border", + "text-white", + "font-medium", + ); + }); +}); diff --git a/src/components/StagePin.test.tsx b/src/components/StagePin.test.tsx new file mode 100644 index 0000000..5ef4f6c --- /dev/null +++ b/src/components/StagePin.test.tsx @@ -0,0 +1,123 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { StagePin } from "./StagePin"; +import * as useStageQueryModule from "@/hooks/queries/stages/useStageQuery"; + +type StageQueryResult = ReturnType; + +vi.mock("@/hooks/queries/stages/useStageQuery"); + +describe("StagePin", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders stage name when data is available", () => { + vi.spyOn(useStageQueryModule, "useStageQuery").mockReturnValue({ + data: { id: "1", name: "Main Stage", color: "#ff0000", archived: false }, + isLoading: false, + error: null, + } as StageQueryResult); + + render(); + expect(screen.getByText("Main Stage")).toBeInTheDocument(); + }); + + it("renders MapPin icon when data is available", () => { + vi.spyOn(useStageQueryModule, "useStageQuery").mockReturnValue({ + data: { id: "1", name: "Main Stage", color: "#ff0000", archived: false }, + isLoading: false, + error: null, + } as StageQueryResult); + + const { container } = render(); + const icon = container.querySelector("svg"); + expect(icon).toBeInTheDocument(); + }); + + it("renders null when no data", () => { + vi.spyOn(useStageQueryModule, "useStageQuery").mockReturnValue({ + data: null, + isLoading: false, + error: null, + } as StageQueryResult); + + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it("renders null when stageId is null", () => { + vi.spyOn(useStageQueryModule, "useStageQuery").mockReturnValue({ + data: null, + isLoading: false, + error: null, + } as StageQueryResult); + + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it("renders null when loading", () => { + vi.spyOn(useStageQueryModule, "useStageQuery").mockReturnValue({ + data: null, + isLoading: true, + error: null, + } as unknown as StageQueryResult); + + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it("has correct container classes", () => { + vi.spyOn(useStageQueryModule, "useStageQuery").mockReturnValue({ + data: { id: "1", name: "Main Stage", color: "#ff0000", archived: false }, + isLoading: false, + error: null, + } as StageQueryResult); + + const { container } = render(); + const wrapper = container.querySelector("div"); + expect(wrapper).toHaveClass("flex", "items-center", "gap-2"); + }); + + it("has correct icon size", () => { + vi.spyOn(useStageQueryModule, "useStageQuery").mockReturnValue({ + data: { id: "1", name: "Main Stage", color: "#ff0000", archived: false }, + isLoading: false, + error: null, + } as StageQueryResult); + + const { container } = render(); + const icon = container.querySelector("svg"); + expect(icon).toHaveClass("h-4", "w-4"); + }); + + it("has correct text size", () => { + vi.spyOn(useStageQueryModule, "useStageQuery").mockReturnValue({ + data: { id: "1", name: "Main Stage", color: "#ff0000", archived: false }, + isLoading: false, + error: null, + } as StageQueryResult); + + const { container } = render(); + const text = container.querySelector("span"); + expect(text).toHaveClass("text-sm"); + }); + + it("renders different stage names correctly", () => { + vi.spyOn(useStageQueryModule, "useStageQuery").mockReturnValue({ + data: { + id: "2", + name: "Electronic Stage", + color: "#00ff00", + archived: false, + }, + isLoading: false, + error: null, + } as StageQueryResult); + + render(); + expect(screen.getByText("Electronic Stage")).toBeInTheDocument(); + expect(screen.queryByText("Main Stage")).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/ui/badge.test.tsx b/src/components/ui/badge.test.tsx new file mode 100644 index 0000000..8889dbe --- /dev/null +++ b/src/components/ui/badge.test.tsx @@ -0,0 +1,86 @@ +import { describe, expect, it } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { Badge } from "./badge"; + +describe("Badge", () => { + it("renders children content", () => { + render(Test Badge); + expect(screen.getByText("Test Badge")).toBeInTheDocument(); + }); + + it("renders with default variant", () => { + const { container } = render(Default); + const badge = container.querySelector("div"); + expect(badge).toHaveClass("bg-primary", "text-primary-foreground"); + }); + + it("renders with secondary variant", () => { + const { container } = render(Secondary); + const badge = container.querySelector("div"); + expect(badge).toHaveClass("bg-secondary", "text-secondary-foreground"); + }); + + it("renders with destructive variant", () => { + const { container } = render( + Destructive, + ); + const badge = container.querySelector("div"); + expect(badge).toHaveClass("bg-destructive", "text-destructive-foreground"); + }); + + it("renders with outline variant", () => { + const { container } = render(Outline); + const badge = container.querySelector("div"); + expect(badge).toHaveClass("text-foreground"); + }); + + it("applies custom className", () => { + const { container } = render( + Custom, + ); + const badge = container.querySelector("div"); + expect(badge).toHaveClass("custom-class"); + }); + + it("has base classes", () => { + const { container } = render(Base); + const badge = container.querySelector("div"); + expect(badge).toHaveClass( + "inline-flex", + "items-center", + "rounded-full", + "border", + "px-2.5", + "py-0.5", + "text-xs", + "font-semibold", + ); + }); + + it("passes through HTML attributes", () => { + render( + + Test + , + ); + const badge = screen.getByTestId("custom-badge"); + expect(badge).toHaveAttribute("title", "Badge Title"); + }); + + it("renders as div element", () => { + const { container } = render(Test); + const badge = container.querySelector("div"); + expect(badge?.tagName).toBe("DIV"); + }); + + it("combines custom className with variant classes", () => { + const { container } = render( + + Combined + , + ); + const badge = container.querySelector("div"); + expect(badge).toHaveClass("bg-secondary"); + expect(badge).toHaveClass("my-custom-class"); + }); +}); diff --git a/src/components/ui/button.test.tsx b/src/components/ui/button.test.tsx new file mode 100644 index 0000000..d0a0075 --- /dev/null +++ b/src/components/ui/button.test.tsx @@ -0,0 +1,154 @@ +import { describe, expect, it, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Button } from "./button"; + +describe("Button", () => { + it("renders children content", () => { + render(); + expect( + screen.getByRole("button", { name: "Click me" }), + ).toBeInTheDocument(); + }); + + it("renders with default variant", () => { + const { container } = render(); + const button = container.querySelector("button"); + expect(button).toHaveClass("bg-primary", "text-primary-foreground"); + }); + + it("renders with destructive variant", () => { + const { container } = render(); + const button = container.querySelector("button"); + expect(button).toHaveClass("bg-destructive", "text-destructive-foreground"); + }); + + it("renders with outline variant", () => { + const { container } = render(); + const button = container.querySelector("button"); + expect(button).toHaveClass("border", "border-input", "bg-background"); + }); + + it("renders with secondary variant", () => { + const { container } = render( + , + ); + const button = container.querySelector("button"); + expect(button).toHaveClass("bg-secondary", "text-secondary-foreground"); + }); + + it("renders with ghost variant", () => { + const { container } = render(); + const button = container.querySelector("button"); + expect(button).toHaveClass("hover:bg-accent"); + }); + + it("renders with link variant", () => { + const { container } = render(); + const button = container.querySelector("button"); + expect(button).toHaveClass("text-primary", "underline-offset-4"); + }); + + it("renders with default size", () => { + const { container } = render(); + const button = container.querySelector("button"); + expect(button).toHaveClass("h-10", "px-4", "py-2"); + }); + + it("renders with small size", () => { + const { container } = render(); + const button = container.querySelector("button"); + expect(button).toHaveClass("h-9", "px-3"); + }); + + it("renders with large size", () => { + const { container } = render(); + const button = container.querySelector("button"); + expect(button).toHaveClass("h-11", "px-8"); + }); + + it("renders with icon size", () => { + const { container } = render(); + const button = container.querySelector("button"); + expect(button).toHaveClass("h-10", "w-10"); + }); + + it("handles click events", async () => { + const user = userEvent.setup(); + const handleClick = vi.fn(); + render(); + await user.click(screen.getByRole("button")); + expect(handleClick).toHaveBeenCalledTimes(1); + }); + + it("is disabled when disabled prop is true", () => { + render(); + const button = screen.getByRole("button"); + expect(button).toBeDisabled(); + }); + + it("does not trigger click when disabled", async () => { + const user = userEvent.setup(); + const handleClick = vi.fn(); + render( + , + ); + await user.click(screen.getByRole("button")); + expect(handleClick).not.toHaveBeenCalled(); + }); + + it("applies custom className", () => { + const { container } = render( + , + ); + const button = container.querySelector("button"); + expect(button).toHaveClass("custom-class"); + }); + + it("has base classes", () => { + const { container } = render(); + const button = container.querySelector("button"); + expect(button).toHaveClass( + "inline-flex", + "items-center", + "justify-center", + "rounded-md", + "text-sm", + "font-medium", + ); + }); + + it("passes through HTML attributes", () => { + render( + , + ); + const button = screen.getByTestId("submit-btn"); + expect(button).toHaveAttribute("type", "submit"); + }); + + it("renders as button element by default", () => { + render(); + expect(screen.getByRole("button")).toBeTruthy(); + }); + + it("combines variant and size classes", () => { + const { container } = render( + , + ); + const button = container.querySelector("button"); + expect(button).toHaveClass("bg-destructive"); + expect(button).toHaveClass("h-11", "px-8"); + }); + + it("forwards ref to button element", () => { + const ref = { current: null }; + render(); + expect(ref.current).toBeInstanceOf(HTMLButtonElement); + }); +}); diff --git a/src/components/ui/card.test.tsx b/src/components/ui/card.test.tsx new file mode 100644 index 0000000..14b02e6 --- /dev/null +++ b/src/components/ui/card.test.tsx @@ -0,0 +1,228 @@ +import { describe, expect, it } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + CardFooter, +} from "./card"; + +describe("Card", () => { + it("renders children content", () => { + render(Card Content); + expect(screen.getByText("Card Content")).toBeInTheDocument(); + }); + + it("has base classes", () => { + const { container } = render(Test); + const card = container.firstChild as HTMLElement; + expect(card).toHaveClass( + "rounded-lg", + "border", + "bg-card", + "text-card-foreground", + "shadow-sm", + ); + }); + + it("applies custom className", () => { + const { container } = render(Test); + const card = container.firstChild as HTMLElement; + expect(card).toHaveClass("custom-class"); + }); + + it("forwards ref", () => { + const ref = { current: null }; + render(Test); + expect(ref.current).toBeInstanceOf(HTMLDivElement); + }); + + it("passes through HTML attributes", () => { + render(Test); + expect(screen.getByTestId("custom-card")).toBeInTheDocument(); + }); +}); + +describe("CardHeader", () => { + it("renders children content", () => { + render(Header Content); + expect(screen.getByText("Header Content")).toBeInTheDocument(); + }); + + it("has base classes", () => { + const { container } = render(Test); + const header = container.firstChild as HTMLElement; + expect(header).toHaveClass("flex", "flex-col", "space-y-1.5", "p-6"); + }); + + it("applies custom className", () => { + const { container } = render( + Test, + ); + const header = container.firstChild as HTMLElement; + expect(header).toHaveClass("custom-header"); + }); + + it("forwards ref", () => { + const ref = { current: null }; + render(Test); + expect(ref.current).toBeInstanceOf(HTMLDivElement); + }); +}); + +describe("CardTitle", () => { + it("renders children content", () => { + render(Title Text); + expect(screen.getByText("Title Text")).toBeInTheDocument(); + }); + + it("renders as h3 element", () => { + render(Title); + expect(screen.getByRole("heading", { level: 3 })).toBeInTheDocument(); + }); + + it("has base classes", () => { + const { container } = render(Test); + const title = container.querySelector("h3"); + expect(title).toHaveClass( + "text-2xl", + "font-semibold", + "leading-none", + "tracking-tight", + ); + }); + + it("applies custom className", () => { + const { container } = render( + Test, + ); + const title = container.querySelector("h3"); + expect(title).toHaveClass("custom-title"); + }); + + it("forwards ref", () => { + const ref = { current: null }; + render(Test); + expect(ref.current).toBeInstanceOf(HTMLHeadingElement); + }); +}); + +describe("CardDescription", () => { + it("renders children content", () => { + render(Description Text); + expect(screen.getByText("Description Text")).toBeInTheDocument(); + }); + + it("has base classes", () => { + const { container } = render(Test); + const description = container.firstChild as HTMLElement; + expect(description).toHaveClass("text-sm", "text-muted-foreground"); + }); + + it("applies custom className", () => { + const { container } = render( + Test, + ); + const description = container.firstChild as HTMLElement; + expect(description).toHaveClass("custom-desc"); + }); + + it("forwards ref", () => { + const ref = { current: null }; + render(Test); + expect(ref.current).toBeInstanceOf(HTMLDivElement); + }); +}); + +describe("CardContent", () => { + it("renders children content", () => { + render(Content Text); + expect(screen.getByText("Content Text")).toBeInTheDocument(); + }); + + it("has base classes", () => { + const { container } = render(Test); + const content = container.firstChild as HTMLElement; + expect(content).toHaveClass("p-6", "pt-0"); + }); + + it("applies custom className", () => { + const { container } = render( + Test, + ); + const content = container.firstChild as HTMLElement; + expect(content).toHaveClass("custom-content"); + }); + + it("forwards ref", () => { + const ref = { current: null }; + render(Test); + expect(ref.current).toBeInstanceOf(HTMLDivElement); + }); +}); + +describe("CardFooter", () => { + it("renders children content", () => { + render(Footer Content); + expect(screen.getByText("Footer Content")).toBeInTheDocument(); + }); + + it("has base classes", () => { + const { container } = render(Test); + const footer = container.firstChild as HTMLElement; + expect(footer).toHaveClass("flex", "items-center", "p-6", "pt-0"); + }); + + it("applies custom className", () => { + const { container } = render( + Test, + ); + const footer = container.firstChild as HTMLElement; + expect(footer).toHaveClass("custom-footer"); + }); + + it("forwards ref", () => { + const ref = { current: null }; + render(Test); + expect(ref.current).toBeInstanceOf(HTMLDivElement); + }); +}); + +describe("Card composition", () => { + it("renders a complete card with all components", () => { + render( + + + Test Title + Test Description + + Test Content + Test Footer + , + ); + + expect(screen.getByText("Test Title")).toBeInTheDocument(); + expect(screen.getByText("Test Description")).toBeInTheDocument(); + expect(screen.getByText("Test Content")).toBeInTheDocument(); + expect(screen.getByText("Test Footer")).toBeInTheDocument(); + }); + + it("maintains proper hierarchy", () => { + const { container } = render( + + + Title + + , + ); + + const card = container.firstChild as HTMLElement; + const header = card.querySelector("div"); + const title = header?.querySelector("h3") ?? null; + + expect(card).toContainElement(header); + expect(header).toContainElement(title); + }); +}); diff --git a/src/hooks/use-mobile.test.tsx b/src/hooks/use-mobile.test.tsx new file mode 100644 index 0000000..6e37166 --- /dev/null +++ b/src/hooks/use-mobile.test.tsx @@ -0,0 +1,209 @@ +import { describe, expect, it, beforeEach, afterEach, vi } from "vitest"; +import { renderHook, act, waitFor } from "@testing-library/react"; +import { useIsMobile } from "./use-mobile"; + +describe("useIsMobile", () => { + let matchMediaMock: { + matches: boolean; + addEventListener: ReturnType; + removeEventListener: ReturnType; + }; + + beforeEach(() => { + matchMediaMock = { + matches: false, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + }; + + Object.defineProperty(window, "matchMedia", { + writable: true, + value: vi.fn().mockImplementation(() => matchMediaMock), + }); + + Object.defineProperty(window, "innerWidth", { + writable: true, + configurable: true, + value: 1024, + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("returns false for desktop viewport (>= 768px)", () => { + Object.defineProperty(window, "innerWidth", { + writable: true, + configurable: true, + value: 1024, + }); + + const { result } = renderHook(() => useIsMobile()); + expect(result.current).toBe(false); + }); + + it("returns true for mobile viewport (< 768px)", () => { + Object.defineProperty(window, "innerWidth", { + writable: true, + configurable: true, + value: 375, + }); + + const { result } = renderHook(() => useIsMobile()); + expect(result.current).toBe(true); + }); + + it("returns true for 767px viewport (just below breakpoint)", () => { + Object.defineProperty(window, "innerWidth", { + writable: true, + configurable: true, + value: 767, + }); + + const { result } = renderHook(() => useIsMobile()); + expect(result.current).toBe(true); + }); + + it("returns false for 768px viewport (at breakpoint)", () => { + Object.defineProperty(window, "innerWidth", { + writable: true, + configurable: true, + value: 768, + }); + + const { result } = renderHook(() => useIsMobile()); + expect(result.current).toBe(false); + }); + + it("sets up matchMedia listener on mount", () => { + renderHook(() => useIsMobile()); + expect(window.matchMedia).toHaveBeenCalledWith("(max-width: 767px)"); + expect(matchMediaMock.addEventListener).toHaveBeenCalledWith( + "change", + expect.any(Function), + ); + }); + + it("removes matchMedia listener on unmount", () => { + const { unmount } = renderHook(() => useIsMobile()); + unmount(); + expect(matchMediaMock.removeEventListener).toHaveBeenCalledWith( + "change", + expect.any(Function), + ); + }); + + it("updates when window is resized to mobile", async () => { + Object.defineProperty(window, "innerWidth", { + writable: true, + configurable: true, + value: 1024, + }); + + const { result } = renderHook(() => useIsMobile()); + expect(result.current).toBe(false); + + act(() => { + Object.defineProperty(window, "innerWidth", { + writable: true, + configurable: true, + value: 375, + }); + + const changeHandler = matchMediaMock.addEventListener.mock.calls[0][1]; + changeHandler(); + }); + + await waitFor(() => { + expect(result.current).toBe(true); + }); + }); + + it("updates when window is resized to desktop", async () => { + Object.defineProperty(window, "innerWidth", { + writable: true, + configurable: true, + value: 375, + }); + + const { result } = renderHook(() => useIsMobile()); + expect(result.current).toBe(true); + + act(() => { + Object.defineProperty(window, "innerWidth", { + writable: true, + configurable: true, + value: 1024, + }); + + const changeHandler = matchMediaMock.addEventListener.mock.calls[0][1]; + changeHandler(); + }); + + await waitFor(() => { + expect(result.current).toBe(false); + }); + }); + + it("starts with undefined and initializes correctly", () => { + Object.defineProperty(window, "innerWidth", { + writable: true, + configurable: true, + value: 1024, + }); + + const { result } = renderHook(() => useIsMobile()); + expect(result.current).toBe(false); + }); + + it("handles multiple resize events", async () => { + Object.defineProperty(window, "innerWidth", { + writable: true, + configurable: true, + value: 1024, + }); + + const { result } = renderHook(() => useIsMobile()); + const changeHandler = matchMediaMock.addEventListener.mock.calls[0][1]; + + act(() => { + Object.defineProperty(window, "innerWidth", { + writable: true, + configurable: true, + value: 375, + }); + changeHandler(); + }); + + await waitFor(() => { + expect(result.current).toBe(true); + }); + + act(() => { + Object.defineProperty(window, "innerWidth", { + writable: true, + configurable: true, + value: 1024, + }); + changeHandler(); + }); + + await waitFor(() => { + expect(result.current).toBe(false); + }); + + act(() => { + Object.defineProperty(window, "innerWidth", { + writable: true, + configurable: true, + value: 320, + }); + changeHandler(); + }); + + await waitFor(() => { + expect(result.current).toBe(true); + }); + }); +}); diff --git a/src/hooks/use-toast.test.ts b/src/hooks/use-toast.test.ts new file mode 100644 index 0000000..81bcb83 --- /dev/null +++ b/src/hooks/use-toast.test.ts @@ -0,0 +1,237 @@ +import { describe, expect, it, afterEach, vi } from "vitest"; +import { renderHook, act } from "@testing-library/react"; +import { useToast, toast } from "./use-toast"; + +describe("useToast", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("initializes with empty toasts array", () => { + const { result } = renderHook(() => useToast()); + expect(result.current.toasts).toEqual([]); + }); + + it("adds a toast when toast is called", () => { + const { result } = renderHook(() => useToast()); + + act(() => { + result.current.toast({ title: "Test Toast" }); + }); + + expect(result.current.toasts).toHaveLength(1); + expect(result.current.toasts[0].title).toBe("Test Toast"); + }); + + it("returns toast with id, dismiss, and update methods", () => { + const { result } = renderHook(() => useToast()); + + type ToastReturn = ReturnType; + let toastResult: ToastReturn | undefined; + act(() => { + toastResult = result.current.toast({ title: "Test" }); + }); + + expect(toastResult).toHaveProperty("id"); + expect(toastResult).toHaveProperty("dismiss"); + expect(toastResult).toHaveProperty("update"); + expect(typeof toastResult?.id).toBe("string"); + expect(typeof toastResult?.dismiss).toBe("function"); + expect(typeof toastResult?.update).toBe("function"); + }); + + it("generates unique IDs for toasts", () => { + const { result } = renderHook(() => useToast()); + + let toast1: { id: string } | undefined; + let toast2: { id: string } | undefined; + act(() => { + toast1 = result.current.toast({ title: "Toast 1" }); + toast2 = result.current.toast({ title: "Toast 2" }); + }); + + expect(toast1?.id).not.toBe(toast2?.id); + }); + + it("limits toasts to TOAST_LIMIT (1)", () => { + const { result } = renderHook(() => useToast()); + + act(() => { + result.current.toast({ title: "Toast 1" }); + result.current.toast({ title: "Toast 2" }); + result.current.toast({ title: "Toast 3" }); + }); + + expect(result.current.toasts).toHaveLength(1); + expect(result.current.toasts[0].title).toBe("Toast 3"); + }); + + it("sets toast as open by default", () => { + const { result } = renderHook(() => useToast()); + + act(() => { + result.current.toast({ title: "Test" }); + }); + + expect(result.current.toasts[0].open).toBe(true); + }); + + it("dismisses a toast by ID", () => { + const { result } = renderHook(() => useToast()); + + let toastId: string; + act(() => { + const toastResult = result.current.toast({ title: "Test" }); + toastId = toastResult.id; + }); + + act(() => { + result.current.dismiss(toastId); + }); + + expect(result.current.toasts[0].open).toBe(false); + }); + + it("dismisses all toasts when no ID provided", () => { + const { result } = renderHook(() => useToast()); + + act(() => { + result.current.toast({ title: "Toast 1" }); + }); + + act(() => { + result.current.dismiss(); + }); + + expect(result.current.toasts[0].open).toBe(false); + }); + + it("updates toast with new props", () => { + const { result } = renderHook(() => useToast()); + + type ToastReturn = ReturnType; + let toastResult: ToastReturn | undefined; + act(() => { + toastResult = result.current.toast({ title: "Original" }); + }); + + act(() => { + if (toastResult) { + toastResult.update({ id: toastResult.id, title: "Updated" }); + } + }); + + expect(result.current.toasts[0].title).toBe("Updated"); + }); + + it("toast dismiss method works", () => { + const { result } = renderHook(() => useToast()); + + type ToastReturn = ReturnType; + let toastResult: ToastReturn | undefined; + act(() => { + toastResult = result.current.toast({ title: "Test" }); + }); + + act(() => { + toastResult?.dismiss(); + }); + + expect(result.current.toasts[0].open).toBe(false); + }); + + it("handles description prop", () => { + const { result } = renderHook(() => useToast()); + + act(() => { + result.current.toast({ + title: "Title", + description: "Description text", + }); + }); + + expect(result.current.toasts[0].description).toBe("Description text"); + }); + + it("handles variant prop", () => { + const { result } = renderHook(() => useToast()); + + act(() => { + result.current.toast({ + title: "Test", + variant: "destructive", + }); + }); + + expect(result.current.toasts[0].variant).toBe("destructive"); + }); + + it("can use toast function directly without hook", () => { + const result = toast({ title: "Direct Toast" }); + + expect(result).toHaveProperty("id"); + expect(result).toHaveProperty("dismiss"); + expect(result).toHaveProperty("update"); + }); + + it("maintains toast state across multiple hook instances", () => { + const { result: result1 } = renderHook(() => useToast()); + const { result: result2 } = renderHook(() => useToast()); + + act(() => { + result1.current.toast({ title: "Shared Toast" }); + }); + + expect(result1.current.toasts).toHaveLength(1); + expect(result2.current.toasts).toHaveLength(1); + expect(result1.current.toasts[0].id).toBe(result2.current.toasts[0].id); + }); + + it("handles onOpenChange callback", () => { + const { result } = renderHook(() => useToast()); + + act(() => { + result.current.toast({ title: "Test" }); + }); + + const toastItem = result.current.toasts[0]; + expect(toastItem.onOpenChange).toBeDefined(); + expect(typeof toastItem.onOpenChange).toBe("function"); + + act(() => { + toastItem.onOpenChange?.(false); + }); + + expect(result.current.toasts[0].open).toBe(false); + }); + + it("increments ID counter correctly", () => { + const { result } = renderHook(() => useToast()); + + let id1 = ""; + let id2 = ""; + let id3 = ""; + act(() => { + const t1 = result.current.toast({ title: "1" }); + const t2 = result.current.toast({ title: "2" }); + const t3 = result.current.toast({ title: "3" }); + id1 = t1.id; + id2 = t2.id; + id3 = t3.id; + }); + + expect(parseInt(id1)).toBeLessThan(parseInt(id2)); + expect(parseInt(id2)).toBeLessThan(parseInt(id3)); + }); + + it("keeps most recent toast when limit is reached", () => { + const { result } = renderHook(() => useToast()); + + act(() => { + result.current.toast({ title: "First" }); + result.current.toast({ title: "Second" }); + }); + + expect(result.current.toasts[0].title).toBe("Second"); + }); +}); diff --git a/src/hooks/useCookieConsent.test.ts b/src/hooks/useCookieConsent.test.ts new file mode 100644 index 0000000..baaf8ef --- /dev/null +++ b/src/hooks/useCookieConsent.test.ts @@ -0,0 +1,362 @@ +import { describe, expect, it, beforeEach, vi } from "vitest"; +import { renderHook, act, waitFor } from "@testing-library/react"; +import { useCookieConsent } from "./useCookieConsent"; +import * as CrossDomainStorageModule from "@/lib/crossDomainStorage"; + +vi.mock("@/lib/crossDomainStorage", () => ({ + CrossDomainStorage: { + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + }, +})); + +describe("useCookieConsent", () => { + beforeEach(() => { + vi.clearAllMocks(); + localStorage.clear(); + }); + + it("initializes with null consent and showBanner true when no saved consent", async () => { + vi.spyOn( + CrossDomainStorageModule.CrossDomainStorage, + "getItem", + ).mockReturnValue(null); + + const { result } = renderHook(() => useCookieConsent()); + + await waitFor(() => { + expect(result.current.consent).toBeNull(); + expect(result.current.showBanner).toBe(true); + }); + }); + + it("loads saved consent from storage", async () => { + const savedConsent = { + essential: true, + analytics: true, + preferences: false, + marketing: false, + version: "1.0", + timestamp: Date.now(), + }; + + vi.spyOn( + CrossDomainStorageModule.CrossDomainStorage, + "getItem", + ).mockReturnValue(JSON.stringify(savedConsent)); + + const { result } = renderHook(() => useCookieConsent()); + + await waitFor(() => { + expect(result.current.consent).toEqual(savedConsent); + expect(result.current.showBanner).toBe(false); + }); + }); + + it("shows banner when saved consent has wrong version", async () => { + const savedConsent = { + essential: true, + analytics: true, + preferences: false, + marketing: false, + version: "0.9", + timestamp: Date.now(), + }; + + vi.spyOn( + CrossDomainStorageModule.CrossDomainStorage, + "getItem", + ).mockReturnValue(JSON.stringify(savedConsent)); + + const { result } = renderHook(() => useCookieConsent()); + + await waitFor(() => { + expect(result.current.showBanner).toBe(true); + }); + }); + + it("shows banner when saved consent is invalid JSON", async () => { + vi.spyOn( + CrossDomainStorageModule.CrossDomainStorage, + "getItem", + ).mockReturnValue("invalid json"); + + const { result } = renderHook(() => useCookieConsent()); + + await waitFor(() => { + expect(result.current.showBanner).toBe(true); + }); + }); + + it("saveConsent updates consent and hides banner", () => { + vi.spyOn( + CrossDomainStorageModule.CrossDomainStorage, + "getItem", + ).mockReturnValue(null); + const setItemSpy = vi.spyOn( + CrossDomainStorageModule.CrossDomainStorage, + "setItem", + ); + + const { result } = renderHook(() => useCookieConsent()); + + act(() => { + result.current.saveConsent({ analytics: true }); + }); + + expect(result.current.consent).toMatchObject({ + essential: true, + analytics: true, + }); + expect(result.current.showBanner).toBe(false); + expect(setItemSpy).toHaveBeenCalled(); + }); + + it("acceptAll enables all preferences", () => { + vi.spyOn( + CrossDomainStorageModule.CrossDomainStorage, + "getItem", + ).mockReturnValue(null); + + const { result } = renderHook(() => useCookieConsent()); + + act(() => { + result.current.acceptAll(); + }); + + expect(result.current.consent).toMatchObject({ + essential: true, + analytics: true, + preferences: true, + marketing: true, + }); + expect(result.current.showBanner).toBe(false); + }); + + it("acceptEssential only enables essential cookies", () => { + vi.spyOn( + CrossDomainStorageModule.CrossDomainStorage, + "getItem", + ).mockReturnValue(null); + + const { result } = renderHook(() => useCookieConsent()); + + act(() => { + result.current.acceptEssential(); + }); + + expect(result.current.consent).toMatchObject({ + essential: true, + analytics: false, + preferences: false, + marketing: false, + }); + expect(result.current.showBanner).toBe(false); + }); + + it("updateConsent merges with existing consent", () => { + const savedConsent = { + essential: true, + analytics: true, + preferences: false, + marketing: false, + version: "1.0", + timestamp: Date.now(), + }; + + vi.spyOn( + CrossDomainStorageModule.CrossDomainStorage, + "getItem", + ).mockReturnValue(JSON.stringify(savedConsent)); + + const { result } = renderHook(() => useCookieConsent()); + + act(() => { + result.current.updateConsent({ marketing: true }); + }); + + expect(result.current.consent).toMatchObject({ + essential: true, + analytics: true, + preferences: false, + marketing: true, + }); + }); + + it("revokeConsent clears consent and shows banner", () => { + const savedConsent = { + essential: true, + analytics: true, + preferences: false, + marketing: false, + version: "1.0", + timestamp: Date.now(), + }; + + vi.spyOn( + CrossDomainStorageModule.CrossDomainStorage, + "getItem", + ).mockReturnValue(JSON.stringify(savedConsent)); + const removeItemSpy = vi.spyOn( + CrossDomainStorageModule.CrossDomainStorage, + "removeItem", + ); + + const { result } = renderHook(() => useCookieConsent()); + + act(() => { + result.current.revokeConsent(); + }); + + expect(result.current.consent).toBeNull(); + expect(result.current.showBanner).toBe(true); + expect(removeItemSpy).toHaveBeenCalledWith("gdpr-consent"); + }); + + it("canUseCookie returns true when permission granted", () => { + const savedConsent = { + essential: true, + analytics: true, + preferences: false, + marketing: false, + version: "1.0", + timestamp: Date.now(), + }; + + vi.spyOn( + CrossDomainStorageModule.CrossDomainStorage, + "getItem", + ).mockReturnValue(JSON.stringify(savedConsent)); + + const { result } = renderHook(() => useCookieConsent()); + + expect(result.current.canUseCookie("analytics")).toBe(true); + expect(result.current.canUseCookie("essential")).toBe(true); + }); + + it("canUseCookie returns false when permission not granted", () => { + const savedConsent = { + essential: true, + analytics: false, + preferences: false, + marketing: false, + version: "1.0", + timestamp: Date.now(), + }; + + vi.spyOn( + CrossDomainStorageModule.CrossDomainStorage, + "getItem", + ).mockReturnValue(JSON.stringify(savedConsent)); + + const { result } = renderHook(() => useCookieConsent()); + + expect(result.current.canUseCookie("analytics")).toBe(false); + expect(result.current.canUseCookie("marketing")).toBe(false); + }); + + it("canUseCookie returns false when no consent", () => { + vi.spyOn( + CrossDomainStorageModule.CrossDomainStorage, + "getItem", + ).mockReturnValue(null); + + const { result } = renderHook(() => useCookieConsent()); + + expect(result.current.canUseCookie("analytics")).toBe(false); + }); + + it("setShowBanner updates banner visibility", () => { + vi.spyOn( + CrossDomainStorageModule.CrossDomainStorage, + "getItem", + ).mockReturnValue(null); + + const { result } = renderHook(() => useCookieConsent()); + + act(() => { + result.current.setShowBanner(false); + }); + + expect(result.current.showBanner).toBe(false); + + act(() => { + result.current.setShowBanner(true); + }); + + expect(result.current.showBanner).toBe(true); + }); + + it("saveConsent sets timestamp and version", () => { + vi.spyOn( + CrossDomainStorageModule.CrossDomainStorage, + "getItem", + ).mockReturnValue(null); + const setItemSpy = vi.spyOn( + CrossDomainStorageModule.CrossDomainStorage, + "setItem", + ); + + const { result } = renderHook(() => useCookieConsent()); + + act(() => { + result.current.saveConsent({ analytics: true }); + }); + + expect(result.current.consent?.version).toBe("1.0"); + expect(result.current.consent?.timestamp).toBeDefined(); + expect(typeof result.current.consent?.timestamp).toBe("number"); + expect(setItemSpy).toHaveBeenCalledWith( + "gdpr-consent", + expect.stringContaining('"version":"1.0"'), + ); + }); + + it("revokeConsent clears sidebar state when preferences not granted", () => { + const savedConsent = { + essential: true, + analytics: false, + preferences: false, + marketing: false, + version: "1.0", + timestamp: Date.now(), + }; + + vi.spyOn( + CrossDomainStorageModule.CrossDomainStorage, + "getItem", + ).mockReturnValue(JSON.stringify(savedConsent)); + + localStorage.setItem("sidebar:state", "collapsed"); + const { result } = renderHook(() => useCookieConsent()); + + act(() => { + result.current.revokeConsent(); + }); + + expect(localStorage.getItem("sidebar:state")).toBeNull(); + }); + + it("defaults to essential true", () => { + vi.spyOn( + CrossDomainStorageModule.CrossDomainStorage, + "getItem", + ).mockReturnValue(null); + + const { result } = renderHook(() => useCookieConsent()); + + act(() => { + result.current.acceptEssential(); + }); + + expect(result.current.consent?.essential).toBe(true); + + act(() => { + result.current.saveConsent({ analytics: true }); + }); + + expect(result.current.consent?.essential).toBe(true); + expect(result.current.consent?.analytics).toBe(true); + }); +}); diff --git a/src/hooks/useOnlineStatus.test.ts b/src/hooks/useOnlineStatus.test.ts new file mode 100644 index 0000000..7cea0e0 --- /dev/null +++ b/src/hooks/useOnlineStatus.test.ts @@ -0,0 +1,187 @@ +import { describe, expect, it, beforeEach, afterEach, vi } from "vitest"; +import { renderHook, act, waitFor } from "@testing-library/react"; +import { useOnlineStatus } from "./useOnlineStatus"; + +describe("useOnlineStatus", () => { + let onlineListener: ((event: Event) => void) | null = null; + let offlineListener: ((event: Event) => void) | null = null; + + beforeEach(() => { + Object.defineProperty(navigator, "onLine", { + writable: true, + configurable: true, + value: true, + }); + + vi.spyOn(window, "addEventListener").mockImplementation( + (event, handler) => { + if (event === "online") { + onlineListener = handler as (event: Event) => void; + } else if (event === "offline") { + offlineListener = handler as (event: Event) => void; + } + }, + ); + + vi.spyOn(window, "removeEventListener").mockImplementation(() => {}); + }); + + afterEach(() => { + vi.clearAllMocks(); + onlineListener = null; + offlineListener = null; + }); + + it("returns true when navigator.onLine is true", () => { + Object.defineProperty(navigator, "onLine", { + writable: true, + configurable: true, + value: true, + }); + + const { result } = renderHook(() => useOnlineStatus()); + expect(result.current).toBe(true); + }); + + it("returns false when navigator.onLine is false", () => { + Object.defineProperty(navigator, "onLine", { + writable: true, + configurable: true, + value: false, + }); + + const { result } = renderHook(() => useOnlineStatus()); + expect(result.current).toBe(false); + }); + + it("sets up online event listener on mount", () => { + renderHook(() => useOnlineStatus()); + expect(window.addEventListener).toHaveBeenCalledWith( + "online", + expect.any(Function), + ); + }); + + it("sets up offline event listener on mount", () => { + renderHook(() => useOnlineStatus()); + expect(window.addEventListener).toHaveBeenCalledWith( + "offline", + expect.any(Function), + ); + }); + + it("removes event listeners on unmount", () => { + const { unmount } = renderHook(() => useOnlineStatus()); + unmount(); + expect(window.removeEventListener).toHaveBeenCalledWith( + "online", + expect.any(Function), + ); + expect(window.removeEventListener).toHaveBeenCalledWith( + "offline", + expect.any(Function), + ); + }); + + it("updates to true when online event is fired", async () => { + Object.defineProperty(navigator, "onLine", { + writable: true, + configurable: true, + value: false, + }); + + const { result } = renderHook(() => useOnlineStatus()); + expect(result.current).toBe(false); + + act(() => { + if (onlineListener) { + onlineListener(new Event("online")); + } + }); + + await waitFor(() => { + expect(result.current).toBe(true); + }); + }); + + it("updates to false when offline event is fired", async () => { + Object.defineProperty(navigator, "onLine", { + writable: true, + configurable: true, + value: true, + }); + + const { result } = renderHook(() => useOnlineStatus()); + expect(result.current).toBe(true); + + act(() => { + if (offlineListener) { + offlineListener(new Event("offline")); + } + }); + + await waitFor(() => { + expect(result.current).toBe(false); + }); + }); + + it("handles multiple online/offline transitions", async () => { + const { result } = renderHook(() => useOnlineStatus()); + + act(() => { + if (offlineListener) { + offlineListener(new Event("offline")); + } + }); + + await waitFor(() => { + expect(result.current).toBe(false); + }); + + act(() => { + if (onlineListener) { + onlineListener(new Event("online")); + } + }); + + await waitFor(() => { + expect(result.current).toBe(true); + }); + + act(() => { + if (offlineListener) { + offlineListener(new Event("offline")); + } + }); + + await waitFor(() => { + expect(result.current).toBe(false); + }); + }); + + it("maintains state across re-renders", async () => { + const { result, rerender } = renderHook(() => useOnlineStatus()); + expect(result.current).toBe(true); + + rerender(); + expect(result.current).toBe(true); + + act(() => { + if (offlineListener) { + offlineListener(new Event("offline")); + } + }); + + await waitFor(() => { + expect(result.current).toBe(false); + }); + + rerender(); + expect(result.current).toBe(false); + }); + + it("cleans up listeners on unmount without errors", () => { + const { unmount } = renderHook(() => useOnlineStatus()); + expect(() => unmount()).not.toThrow(); + }); +}); diff --git a/src/lib/constants/stages.test.ts b/src/lib/constants/stages.test.ts new file mode 100644 index 0000000..5d72bac --- /dev/null +++ b/src/lib/constants/stages.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { DEFAULT_STAGE_COLOR } from "./stages"; + +describe("DEFAULT_STAGE_COLOR", () => { + it("is defined", () => { + expect(DEFAULT_STAGE_COLOR).toBeDefined(); + }); + + it("is a valid hex color", () => { + expect(DEFAULT_STAGE_COLOR).toMatch(/^#[0-9a-fA-F]{6}$/); + }); + + it("has the correct value", () => { + expect(DEFAULT_STAGE_COLOR).toBe("#6b7280"); + }); + + it("is a string", () => { + expect(typeof DEFAULT_STAGE_COLOR).toBe("string"); + }); + + it("starts with hash symbol", () => { + expect(DEFAULT_STAGE_COLOR).toMatch(/^#/); + }); + + it("has 7 characters total", () => { + expect(DEFAULT_STAGE_COLOR).toHaveLength(7); + }); +}); diff --git a/src/lib/markdown.test.ts b/src/lib/markdown.test.ts new file mode 100644 index 0000000..69f9e13 --- /dev/null +++ b/src/lib/markdown.test.ts @@ -0,0 +1,152 @@ +import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; +import { parseMarkdown } from "./markdown"; + +describe("parseMarkdown", () => { + let consoleErrorSpy: ReturnType; + + beforeEach(() => { + consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + }); + + afterEach(() => { + consoleErrorSpy.mockRestore(); + }); + + it("returns empty string for empty input", () => { + expect(parseMarkdown("")).toBe(""); + }); + + it("parses basic markdown text", () => { + const result = parseMarkdown("Hello world"); + expect(result).toContain("Hello world"); + }); + + it("parses bold text", () => { + const result = parseMarkdown("**bold text**"); + expect(result).toContain(""); + expect(result).toContain("bold text"); + expect(result).toContain(""); + }); + + it("parses italic text", () => { + const result = parseMarkdown("*italic text*"); + expect(result).toContain(""); + expect(result).toContain("italic text"); + expect(result).toContain(""); + }); + + it("parses links", () => { + const result = parseMarkdown("[link text](https://example.com)"); + expect(result).toContain(''); + expect(result).toContain("link text"); + expect(result).toContain(""); + }); + + it("parses headings", () => { + const result = parseMarkdown("# Heading 1"); + expect(result).toContain("

"); + expect(result).toContain("Heading 1"); + expect(result).toContain("

"); + }); + + it("parses multiple heading levels", () => { + const result = parseMarkdown("## Heading 2\n### Heading 3"); + expect(result).toContain("

"); + expect(result).toContain("

"); + }); + + it("parses unordered lists", () => { + const result = parseMarkdown("- Item 1\n- Item 2"); + expect(result).toContain("
    "); + expect(result).toContain("
  • "); + expect(result).toContain("Item 1"); + expect(result).toContain("Item 2"); + expect(result).toContain("
"); + }); + + it("parses ordered lists", () => { + const result = parseMarkdown("1. First\n2. Second"); + expect(result).toContain("
    "); + expect(result).toContain("
  1. "); + expect(result).toContain("First"); + expect(result).toContain("Second"); + expect(result).toContain("
"); + }); + + it("converts line breaks with breaks: true option", () => { + const result = parseMarkdown("Line 1\nLine 2"); + expect(result).toContain("
"); + }); + + it("parses code blocks", () => { + const result = parseMarkdown("`code here`"); + expect(result).toContain(""); + expect(result).toContain("code here"); + expect(result).toContain(""); + }); + + it("parses fenced code blocks", () => { + const result = parseMarkdown("```\ncode block\n```"); + expect(result).toContain("
");
+    expect(result).toContain("");
+    expect(result).toContain("code block");
+  });
+
+  it("parses blockquotes", () => {
+    const result = parseMarkdown("> Quote here");
+    expect(result).toContain("
"); + expect(result).toContain("Quote here"); + expect(result).toContain("
"); + }); + + it("handles mixed markdown elements", () => { + const markdown = "# Title\n\n**Bold** and *italic* text\n\n- List item"; + const result = parseMarkdown(markdown); + expect(result).toContain("

"); + expect(result).toContain(""); + expect(result).toContain(""); + expect(result).toContain("
    "); + }); + + it("supports GitHub Flavored Markdown (GFM)", () => { + const result = parseMarkdown("~~strikethrough~~"); + expect(result).toContain("strikethrough"); + }); + + it("handles tables (GFM feature)", () => { + const markdown = "| Header |\n| ------ |\n| Cell |"; + const result = parseMarkdown(markdown); + expect(result).toContain(""); + expect(result).toContain("Header"); + expect(result).toContain("Cell"); + }); + + it("returns plain text with line breaks on parse error", () => { + expect(parseMarkdown("Normal text\nWith newline")).toBeTruthy(); + }); + + it("handles special characters", () => { + const result = parseMarkdown("Text with & and < and >"); + expect(result).toContain("&"); + expect(result).toContain("<"); + expect(result).toContain(">"); + }); + + it("handles URLs in text", () => { + const result = parseMarkdown("Visit https://example.com"); + expect(result).toBeTruthy(); + }); + + it("parses nested markdown", () => { + const result = parseMarkdown("**bold with *italic* inside**"); + expect(result).toContain(""); + expect(result).toContain(""); + }); + + it("handles multiple paragraphs", () => { + const result = parseMarkdown("Paragraph 1\n\nParagraph 2"); + expect(result).toContain("

    "); + expect(result).toContain("Paragraph 1"); + expect(result).toContain("Paragraph 2"); + }); +}); diff --git a/src/lib/__tests__/slug.test.ts b/src/lib/slug.test.ts similarity index 98% rename from src/lib/__tests__/slug.test.ts rename to src/lib/slug.test.ts index 23ba59f..ba22ce1 100644 --- a/src/lib/__tests__/slug.test.ts +++ b/src/lib/slug.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { generateSlug, isValidSlug, sanitizeSlug } from "../slug"; +import { generateSlug, isValidSlug, sanitizeSlug } from "./slug"; describe("generateSlug", () => { it("converts basic text to lowercase slug", () => { diff --git a/src/lib/stageUtils.test.ts b/src/lib/stageUtils.test.ts new file mode 100644 index 0000000..b16c9dd --- /dev/null +++ b/src/lib/stageUtils.test.ts @@ -0,0 +1,164 @@ +import { describe, expect, it } from "vitest"; +import { sortStagesByOrder } from "./stageUtils"; + +describe("sortStagesByOrder", () => { + it("sorts stages with order > 0 by their order value", () => { + const stages = [ + { name: "Stage C", stage_order: 3 }, + { name: "Stage A", stage_order: 1 }, + { name: "Stage B", stage_order: 2 }, + ]; + + const sorted = sortStagesByOrder(stages); + + expect(sorted[0].name).toBe("Stage A"); + expect(sorted[1].name).toBe("Stage B"); + expect(sorted[2].name).toBe("Stage C"); + }); + + it("places stages with order 0 at the end, sorted alphabetically", () => { + const stages = [ + { name: "Stage B", stage_order: 0 }, + { name: "Stage A", stage_order: 0 }, + { name: "Stage C", stage_order: 0 }, + ]; + + const sorted = sortStagesByOrder(stages); + + expect(sorted[0].name).toBe("Stage A"); + expect(sorted[1].name).toBe("Stage B"); + expect(sorted[2].name).toBe("Stage C"); + }); + + it("places ordered stages before unordered stages", () => { + const stages = [ + { name: "Zebra Stage", stage_order: 0 }, + { name: "Main Stage", stage_order: 1 }, + { name: "Second Stage", stage_order: 2 }, + { name: "Alpha Stage", stage_order: 0 }, + ]; + + const sorted = sortStagesByOrder(stages); + + expect(sorted[0].name).toBe("Main Stage"); + expect(sorted[1].name).toBe("Second Stage"); + expect(sorted[2].name).toBe("Alpha Stage"); + expect(sorted[3].name).toBe("Zebra Stage"); + }); + + it("handles null stage_order as 0", () => { + const stages: Array<{ name: string; stage_order: number | null }> = [ + { name: "Stage B", stage_order: null }, + { name: "Stage A", stage_order: 1 }, + { name: "Stage C", stage_order: null }, + ]; + + const sorted = sortStagesByOrder( + stages as Array<{ name: string; stage_order: number }>, + ); + + expect(sorted[0].name).toBe("Stage A"); + expect(sorted[1].name).toBe("Stage B"); + expect(sorted[2].name).toBe("Stage C"); + }); + + it("handles undefined stage_order as 0", () => { + const stages: Array<{ name: string; stage_order: number | undefined }> = [ + { name: "Stage B", stage_order: undefined }, + { name: "Stage A", stage_order: 1 }, + { name: "Stage C", stage_order: undefined }, + ]; + + const sorted = sortStagesByOrder( + stages as Array<{ name: string; stage_order: number }>, + ); + + expect(sorted[0].name).toBe("Stage A"); + expect(sorted[1].name).toBe("Stage B"); + expect(sorted[2].name).toBe("Stage C"); + }); + + it("handles mixed ordered and unordered stages", () => { + const stages = [ + { name: "Unordered Z", stage_order: 0 }, + { name: "Ordered 3", stage_order: 3 }, + { name: "Unordered A", stage_order: 0 }, + { name: "Ordered 1", stage_order: 1 }, + { name: "Ordered 2", stage_order: 2 }, + { name: "Unordered M", stage_order: 0 }, + ]; + + const sorted = sortStagesByOrder(stages); + + expect(sorted[0].name).toBe("Ordered 1"); + expect(sorted[1].name).toBe("Ordered 2"); + expect(sorted[2].name).toBe("Ordered 3"); + expect(sorted[3].name).toBe("Unordered A"); + expect(sorted[4].name).toBe("Unordered M"); + expect(sorted[5].name).toBe("Unordered Z"); + }); + + it("handles empty array", () => { + const stages: Array<{ name: string; stage_order: number | null }> = []; + const sorted = sortStagesByOrder( + stages as Array<{ name: string; stage_order: number }>, + ); + expect(sorted).toEqual([]); + }); + + it("handles single stage", () => { + const stages = [{ name: "Only Stage", stage_order: 1 }]; + const sorted = sortStagesByOrder(stages); + expect(sorted).toHaveLength(1); + expect(sorted[0].name).toBe("Only Stage"); + }); + + it("preserves other properties of stages", () => { + const stages = [ + { name: "Stage B", stage_order: 2, color: "#ff0000", id: "b" }, + { name: "Stage A", stage_order: 1, color: "#00ff00", id: "a" }, + ]; + + const sorted = sortStagesByOrder(stages); + + expect(sorted[0]).toEqual({ + name: "Stage A", + stage_order: 1, + color: "#00ff00", + id: "a", + }); + expect(sorted[1]).toEqual({ + name: "Stage B", + stage_order: 2, + color: "#ff0000", + id: "b", + }); + }); + + it("is stable for stages with same order", () => { + const stages = [ + { name: "Stage C", stage_order: 1 }, + { name: "Stage A", stage_order: 1 }, + { name: "Stage B", stage_order: 1 }, + ]; + + const sorted = sortStagesByOrder(stages); + + expect(sorted).toHaveLength(3); + expect(sorted.every((s) => s.stage_order === 1)).toBe(true); + }); + + it("handles case-sensitive alphabetical sorting", () => { + const stages = [ + { name: "zebra", stage_order: 0 }, + { name: "Apple", stage_order: 0 }, + { name: "banana", stage_order: 0 }, + ]; + + const sorted = sortStagesByOrder(stages); + + expect(sorted[0].name).toBe("Apple"); + expect(sorted[1].name).toBe("banana"); + expect(sorted[2].name).toBe("zebra"); + }); +}); diff --git a/src/lib/__tests__/textAlignment.test.ts b/src/lib/textAlignment.test.ts similarity index 97% rename from src/lib/__tests__/textAlignment.test.ts rename to src/lib/textAlignment.test.ts index 1094931..9d81c27 100644 --- a/src/lib/__tests__/textAlignment.test.ts +++ b/src/lib/textAlignment.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { detectTextAlignment, getTextAlignmentClasses } from "../textAlignment"; +import { detectTextAlignment, getTextAlignmentClasses } from "./textAlignment"; describe("textAlignment", () => { describe("detectTextAlignment", () => { diff --git a/src/lib/timeUtils.test.ts b/src/lib/timeUtils.test.ts new file mode 100644 index 0000000..e715eb4 --- /dev/null +++ b/src/lib/timeUtils.test.ts @@ -0,0 +1,295 @@ +import { describe, expect, it } from "vitest"; +import { + formatTimeRange, + formatDateTime, + formatTimeOnly, + toDatetimeLocal, + toISOString, + combineDateAndTime, + convertLocalTimeToUTC, +} from "./timeUtils"; + +describe("formatTimeRange", () => { + it("returns null when both times are null", () => { + expect(formatTimeRange(null, null)).toBeNull(); + }); + + it("formats start time only", () => { + const result = formatTimeRange("2024-12-15T14:00:00Z", null); + expect(result).toContain("Starts:"); + expect(result).toContain("Dec 15"); + }); + + it("formats end time only", () => { + const result = formatTimeRange(null, "2024-12-15T16:00:00Z"); + expect(result).toContain("Ends:"); + expect(result).toContain("Dec 15"); + }); + + it("formats time range on same day in 12-hour format", () => { + const result = formatTimeRange( + "2024-12-15T14:00:00Z", + "2024-12-15T16:00:00Z", + ); + expect(result).toBeTruthy(); + expect(result).toContain("-"); + }); + + it("formats time range on same day in 24-hour format", () => { + const result = formatTimeRange( + "2024-12-15T14:00:00Z", + "2024-12-15T16:00:00Z", + true, + ); + expect(result).toBeTruthy(); + expect(result).toContain("-"); + expect(result).toMatch(/\d{2}:\d{2}/); + }); + + it("formats time range across different days", () => { + const result = formatTimeRange( + "2024-12-15T10:00:00Z", + "2024-12-16T14:00:00Z", + ); + expect(result).toContain("Dec 15"); + expect(result).toContain("Dec 16"); + }); + + it("returns null for invalid start time", () => { + const result = formatTimeRange("invalid", "2024-12-15T16:00:00Z"); + expect(result).toContain("Ends:"); + }); + + it("returns null for invalid end time", () => { + const result = formatTimeRange("2024-12-15T14:00:00Z", "invalid"); + expect(result).toContain("Starts:"); + }); + + it("returns null for both invalid times", () => { + expect(formatTimeRange("invalid", "also-invalid")).toBeNull(); + }); +}); + +describe("formatDateTime", () => { + it("returns null for null input", () => { + expect(formatDateTime(null)).toBeNull(); + }); + + it("formats date and time in 12-hour format", () => { + const result = formatDateTime("2024-12-15T14:30:00Z"); + expect(result).toContain("Dec 15"); + }); + + it("formats date and time in 24-hour format", () => { + const result = formatDateTime("2024-12-15T14:30:00Z", true); + expect(result).toContain("Dec 15"); + expect(result).toMatch(/\d{2}:\d{2}/); + }); + + it("returns null for invalid date", () => { + expect(formatDateTime("invalid-date")).toBeNull(); + }); + + it("handles different months", () => { + const jan = formatDateTime("2024-01-15T14:00:00Z"); + const jun = formatDateTime("2024-06-15T14:00:00Z"); + expect(jan).toContain("Jan"); + expect(jun).toContain("Jun"); + }); +}); + +describe("formatTimeOnly", () => { + it("returns null for null start time", () => { + expect(formatTimeOnly(null, null)).toBeNull(); + }); + + it("formats single time in 12-hour format", () => { + const result = formatTimeOnly("2024-12-15T14:30:00Z", null); + expect(result).toBeTruthy(); + }); + + it("formats single time in 24-hour format", () => { + const result = formatTimeOnly("2024-12-15T14:30:00Z", null, true); + expect(result).toMatch(/\d{2}:\d{2}/); + }); + + it("formats time range in 12-hour format", () => { + const result = formatTimeOnly( + "2024-12-15T14:00:00Z", + "2024-12-15T16:00:00Z", + ); + expect(result).toContain("-"); + }); + + it("formats time range in 24-hour format", () => { + const result = formatTimeOnly( + "2024-12-15T14:00:00Z", + "2024-12-15T16:00:00Z", + true, + ); + expect(result).toContain("-"); + expect(result).toMatch(/\d{2}:\d{2}/); + }); + + it("returns null for invalid start time", () => { + expect(formatTimeOnly("invalid", null)).toBeNull(); + }); + + it("handles invalid end time gracefully", () => { + const result = formatTimeOnly("2024-12-15T14:00:00Z", "invalid"); + expect(result).toBeTruthy(); + expect(result).not.toContain("-"); + }); +}); + +describe("toDatetimeLocal", () => { + it("returns empty string for null input", () => { + expect(toDatetimeLocal(null)).toBe(""); + }); + + it("returns empty string for empty string input", () => { + expect(toDatetimeLocal("")).toBe(""); + }); + + it("converts ISO string to datetime-local format", () => { + const result = toDatetimeLocal("2024-12-15T14:30:00Z"); + expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/); + }); + + it("handles different times", () => { + const morning = toDatetimeLocal("2024-12-15T08:00:00Z"); + const evening = toDatetimeLocal("2024-12-15T20:00:00Z"); + expect(morning).toBeTruthy(); + expect(evening).toBeTruthy(); + }); +}); + +describe("toISOString", () => { + it("returns empty string for empty input", () => { + expect(toISOString("")).toBe(""); + }); + + it("converts datetime-local format to ISO string", () => { + const result = toISOString("2024-12-15T14:30"); + expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); + }); + + it("handles different datetime values", () => { + const result1 = toISOString("2024-01-01T00:00"); + const result2 = toISOString("2024-12-31T23:59"); + expect(result1).toBeTruthy(); + expect(result2).toBeTruthy(); + }); +}); + +describe("combineDateAndTime", () => { + it("returns null when date is undefined", () => { + expect(combineDateAndTime(undefined, "14:30")).toBeNull(); + }); + + it("returns null when time is undefined", () => { + expect(combineDateAndTime("2024-12-15", undefined)).toBeNull(); + }); + + it("returns null when both are undefined", () => { + expect(combineDateAndTime(undefined, undefined)).toBeNull(); + }); + + it("combines date and time strings", () => { + const result = combineDateAndTime("2024-12-15", "14:30"); + expect(result).toBe("2024-12-15 14:30:00"); + }); + + it("pads single digit hours", () => { + const result = combineDateAndTime("2024-12-15", "8:30"); + expect(result).toBe("2024-12-15 08:30:00"); + }); + + it("handles time with seconds", () => { + const result = combineDateAndTime("2024-12-15", "14:30:45"); + expect(result).toBe("2024-12-15 14:30:45"); + }); + + it("pads time without seconds", () => { + const result = combineDateAndTime("2024-12-15", "14:30"); + expect(result).toBe("2024-12-15 14:30:00"); + }); + + it("trims whitespace from inputs", () => { + const result = combineDateAndTime(" 2024-12-15 ", " 14:30 "); + expect(result).toBe("2024-12-15 14:30:00"); + }); + + it("handles various time formats", () => { + expect(combineDateAndTime("2024-12-15", "8:00")).toBe( + "2024-12-15 08:00:00", + ); + expect(combineDateAndTime("2024-12-15", "08:00")).toBe( + "2024-12-15 08:00:00", + ); + expect(combineDateAndTime("2024-12-15", "8:00:00")).toBe( + "2024-12-15 08:00:00", + ); + expect(combineDateAndTime("2024-12-15", "08:00:00")).toBe( + "2024-12-15 08:00:00", + ); + }); +}); + +describe("convertLocalTimeToUTC", () => { + it("returns null for undefined input", () => { + expect(convertLocalTimeToUTC(undefined, "America/New_York")).toBeNull(); + }); + + it("returns null for empty string", () => { + expect(convertLocalTimeToUTC("", "America/New_York")).toBeNull(); + }); + + it("converts local time to UTC", () => { + const result = convertLocalTimeToUTC( + "2024-12-15 14:30:00", + "America/New_York", + ); + expect(result).toBeTruthy(); + if (result) { + expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); + } + }); + + it("handles different timezones", () => { + const resultNY = convertLocalTimeToUTC( + "2024-12-15 14:30:00", + "America/New_York", + ); + const resultLA = convertLocalTimeToUTC( + "2024-12-15 14:30:00", + "America/Los_Angeles", + ); + expect(resultNY).toBeTruthy(); + expect(resultLA).toBeTruthy(); + expect(resultNY).not.toBe(resultLA); + }); + + it("handles UTC timezone", () => { + const result = convertLocalTimeToUTC("2024-12-15 14:30:00", "UTC"); + expect(result).toBeTruthy(); + }); + + it("returns null for invalid date string", () => { + const result = convertLocalTimeToUTC("invalid-date", "America/New_York"); + expect(result).toBeNull(); + }); + + it("handles different date formats", () => { + const result1 = convertLocalTimeToUTC( + "2024-12-15T14:30:00", + "America/New_York", + ); + const result2 = convertLocalTimeToUTC( + "2024-12-15 14:30:00", + "America/New_York", + ); + expect(result1).toBeTruthy(); + expect(result2).toBeTruthy(); + }); +}); diff --git a/src/lib/voteConfig.test.ts b/src/lib/voteConfig.test.ts new file mode 100644 index 0000000..72f6661 --- /dev/null +++ b/src/lib/voteConfig.test.ts @@ -0,0 +1,167 @@ +import { describe, expect, it } from "vitest"; +import { + VOTE_CONFIG, + VOTES_TYPES, + getVoteConfig, + getVoteValue, + type VoteType, +} from "./voteConfig"; +import { Star, Heart, X } from "lucide-react"; + +describe("VOTE_CONFIG", () => { + it("has correct structure for mustGo", () => { + expect(VOTE_CONFIG.mustGo).toBeDefined(); + expect(VOTE_CONFIG.mustGo.value).toBe(2); + expect(VOTE_CONFIG.mustGo.label).toBe("Must Go"); + expect(VOTE_CONFIG.mustGo.icon).toBe(Star); + }); + + it("has correct structure for interested", () => { + expect(VOTE_CONFIG.interested).toBeDefined(); + expect(VOTE_CONFIG.interested.value).toBe(1); + expect(VOTE_CONFIG.interested.label).toBe("Interested"); + expect(VOTE_CONFIG.interested.icon).toBe(Heart); + }); + + it("has correct structure for wontGo", () => { + expect(VOTE_CONFIG.wontGo).toBeDefined(); + expect(VOTE_CONFIG.wontGo.value).toBe(-1); + expect(VOTE_CONFIG.wontGo.label).toBe("Won't Go"); + expect(VOTE_CONFIG.wontGo.icon).toBe(X); + }); + + it("has consistent properties across all vote types", () => { + const requiredProps = [ + "value", + "label", + "icon", + "bgColor", + "iconColor", + "textColor", + "descColor", + "circleColor", + "buttonSelected", + "buttonUnselected", + "spinnerColor", + "description", + ]; + + VOTES_TYPES.forEach((voteType) => { + requiredProps.forEach((prop) => { + expect(VOTE_CONFIG[voteType]).toHaveProperty(prop); + }); + }); + }); + + it("has unique values for each vote type", () => { + const values = VOTES_TYPES.map((type) => VOTE_CONFIG[type].value); + const uniqueValues = new Set(values); + expect(uniqueValues.size).toBe(VOTES_TYPES.length); + }); + + it("has valid color classes", () => { + VOTES_TYPES.forEach((voteType) => { + const config = VOTE_CONFIG[voteType]; + expect(config.bgColor).toMatch(/^bg-/); + expect(config.iconColor).toMatch(/^text-/); + expect(config.textColor).toMatch(/^text-/); + expect(config.circleColor).toMatch(/^bg-/); + }); + }); +}); + +describe("VOTES_TYPES", () => { + it("contains all vote types", () => { + expect(VOTES_TYPES).toEqual(["mustGo", "interested", "wontGo"]); + }); + + it("is a readonly array", () => { + expect(VOTES_TYPES).toHaveLength(3); + }); +}); + +describe("getVoteConfig", () => { + it("returns correct vote type for value 2", () => { + expect(getVoteConfig(2)).toBe("mustGo"); + }); + + it("returns correct vote type for value 1", () => { + expect(getVoteConfig(1)).toBe("interested"); + }); + + it("returns correct vote type for value -1", () => { + expect(getVoteConfig(-1)).toBe("wontGo"); + }); + + it("returns undefined for invalid values", () => { + expect(getVoteConfig(0)).toBeUndefined(); + expect(getVoteConfig(3)).toBeUndefined(); + expect(getVoteConfig(-2)).toBeUndefined(); + expect(getVoteConfig(999)).toBeUndefined(); + }); + + it("returns undefined for non-numeric values", () => { + expect(getVoteConfig(NaN)).toBeUndefined(); + expect(getVoteConfig(Infinity)).toBeUndefined(); + expect(getVoteConfig(-Infinity)).toBeUndefined(); + }); + + it("returns correct vote type for all valid values", () => { + const validMappings: Array<[number, VoteType]> = [ + [2, "mustGo"], + [1, "interested"], + [-1, "wontGo"], + ]; + + validMappings.forEach(([value, expectedType]) => { + expect(getVoteConfig(value)).toBe(expectedType); + }); + }); +}); + +describe("getVoteValue", () => { + it("returns 2 for mustGo", () => { + expect(getVoteValue("mustGo")).toBe(2); + }); + + it("returns 1 for interested", () => { + expect(getVoteValue("interested")).toBe(1); + }); + + it("returns -1 for wontGo", () => { + expect(getVoteValue("wontGo")).toBe(-1); + }); + + it("returns correct values for all vote types", () => { + const expectedValues: Record = { + mustGo: 2, + interested: 1, + wontGo: -1, + }; + + VOTES_TYPES.forEach((voteType) => { + expect(getVoteValue(voteType)).toBe(expectedValues[voteType]); + }); + }); +}); + +describe("getVoteConfig and getVoteValue integration", () => { + it("should be inverse operations for valid values", () => { + const validValues = [2, 1, -1]; + + validValues.forEach((value) => { + const voteType = getVoteConfig(value); + expect(voteType).toBeDefined(); + if (voteType) { + expect(getVoteValue(voteType)).toBe(value); + } + }); + }); + + it("should be inverse operations for valid types", () => { + VOTES_TYPES.forEach((voteType) => { + const value = getVoteValue(voteType); + expect(getVoteConfig(value)).toBe(voteType); + }); + }); +}); diff --git a/src/test/setup.ts b/src/test/setup.ts new file mode 100644 index 0000000..0c6b74b --- /dev/null +++ b/src/test/setup.ts @@ -0,0 +1,35 @@ +import "@testing-library/jest-dom/vitest"; + +// Polyfill for ArrayBuffer.prototype.resizable and SharedArrayBuffer.prototype.growable +// These are needed by webidl-conversions package +if (typeof ArrayBuffer !== "undefined" && !Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, "resizable")) { + Object.defineProperty(ArrayBuffer.prototype, "resizable", { + get() { + return false; + }, + configurable: true, + }); +} + +if (typeof SharedArrayBuffer !== "undefined" && !Object.getOwnPropertyDescriptor(SharedArrayBuffer.prototype, "growable")) { + Object.defineProperty(SharedArrayBuffer.prototype, "growable", { + get() { + return false; + }, + configurable: true, + }); +} + +// Polyfill for webidl-conversions and whatwg-url +if (typeof global.Set === "undefined") { + global.Set = Set; +} +if (typeof global.Map === "undefined") { + global.Map = Map; +} +if (typeof global.WeakMap === "undefined") { + global.WeakMap = WeakMap; +} +if (typeof global.WeakSet === "undefined") { + global.WeakSet = WeakSet; +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index e11a5ee..ddaf447 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -7,8 +7,10 @@ interface ViteTypeOptions { } interface ImportMetaEnv { - readonly SUPABASE_URL: string; - readonly SUPABASE_PUBLISHABLE_KEY: string; + readonly VITE_SUPABASE_URL: string; + readonly VITE_SUPABASE_PUBLISHABLE_KEY: string; + readonly VITE_PUBLIC_POSTHOG_KEY: string; + readonly VITE_PUBLIC_POSTHOG_HOST: string; // more env variables... } diff --git a/tsconfig.node.json b/tsconfig.node.json index 3133162..a826964 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -4,19 +4,17 @@ "lib": ["ES2023"], "module": "ESNext", "skipLibCheck": true, - /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, - /* Linting */ "strict": true, "noUnusedLocals": false, "noUnusedParameters": false, "noFallthroughCasesInSwitch": true }, - "include": ["vite.config.ts"] + "include": ["vite.config.ts", "vitest.config.ts"] } diff --git a/vite.config.test.ts b/vitest.config.ts similarity index 85% rename from vite.config.test.ts rename to vitest.config.ts index 21908a4..e8ed08e 100644 --- a/vite.config.test.ts +++ b/vitest.config.ts @@ -7,6 +7,9 @@ export default defineConfig({ test: { globals: true, environment: "jsdom", + globalSetup: "./vitest.global-setup.ts", + setupFiles: ["./src/test/setup.ts"], + pool: "forks", exclude: [ "**/node_modules/**", "**/dist/**", diff --git a/vitest.global-setup.ts b/vitest.global-setup.ts new file mode 100644 index 0000000..7d4774a --- /dev/null +++ b/vitest.global-setup.ts @@ -0,0 +1,21 @@ +export default function setup() { + // Polyfill for ArrayBuffer.prototype.resizable and SharedArrayBuffer.prototype.growable + // These are needed by webidl-conversions package which is loaded before test setup files + if (typeof ArrayBuffer !== "undefined" && !Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, "resizable")) { + Object.defineProperty(ArrayBuffer.prototype, "resizable", { + get() { + return false; + }, + configurable: true, + }); + } + + if (typeof SharedArrayBuffer !== "undefined" && !Object.getOwnPropertyDescriptor(SharedArrayBuffer.prototype, "growable")) { + Object.defineProperty(SharedArrayBuffer.prototype, "growable", { + get() { + return false; + }, + configurable: true, + }); + } +}