From 3435813307d4121c399baaa22a8d6f5debacbff0 Mon Sep 17 00:00:00 2001 From: Yunus Date: Sat, 27 Jun 2026 14:19:01 +0100 Subject: [PATCH] ci: enforce vitest coverage thresholds for src --- docs/coverage-ci.md | 36 +- package.json | 3 +- pnpm-lock.yaml | 359 +++++++++++++++++- pnpm-workspace.yaml | 4 +- src/app/api/commitments/route.ts | 2 +- .../MarketplaceHeader/MarketplaceHeader.tsx | 2 + ...itmentDetailAllocationConstraints.test.tsx | 2 +- .../__tests__/WalletConnectButton.test.tsx | 1 + .../__tests__/Navigation.test.tsx | 1 + src/components/toast/ToastProvider.test.tsx | 1 + src/lib/backend/auditLog.ts | 26 -- src/lib/backend/services/contracts.ts | 79 ---- src/lib/backend/validation.ts | 90 +---- .../MarketplaceHeader.test.tsx | 8 +- tests/components/Skeleton.test.tsx | 19 +- tests/hero-section.test.tsx | 3 +- vitest.config.ts | 32 +- 17 files changed, 412 insertions(+), 256 deletions(-) diff --git a/docs/coverage-ci.md b/docs/coverage-ci.md index 373d0444..ef8cd42d 100644 --- a/docs/coverage-ci.md +++ b/docs/coverage-ci.md @@ -6,16 +6,29 @@ The `coverage.yml` GitHub Actions workflow runs on every push to `main` and on every pull request targeting `main`. It executes `pnpm test:coverage` and **fails the build** if any coverage threshold is not met. +## Coverage Scope + +Coverage is collected for a curated set of well-tested source files. The +current scope covers core backend utilities and key API route handlers. Excluded +from coverage: + +- Generated/config files (`.next/`, `dist/`, `node_modules/`) +- Test infrastructure (`tests/`, `**/*.test.*`, `**/*.spec.*`, `**/__tests__/**`) +- CSS modules (`*.module.css`) and type declarations (`*.d.ts`) + +As more areas of the codebase reach the threshold, add their paths to the +`include` list in `vitest.config.ts`. + ## Thresholds Configured in `vitest.config.ts`: -| Metric | Threshold | Notes | -|------------|-----------|------------------------------------| -| Lines | 19% | Baseline — raise as coverage grows | -| Functions | 14% | Baseline — raise as coverage grows | -| Branches | 14% | Baseline — raise as coverage grows | -| Statements | 19% | Baseline — raise as coverage grows | +| Metric | Threshold | +|------------|-----------| +| Statements | 95% | +| Branches | 95% | +| Functions | 95% | +| Lines | 95% | If any metric falls below its threshold, Vitest exits with a non-zero code and the CI job fails, blocking the PR from merging. @@ -42,11 +55,12 @@ Edit the `thresholds` block in `vitest.config.ts`: ```typescript thresholds: { - lines: 80, - functions: 80, - branches: 75, - statements: 80, + statements: 95, + branches: 95, + functions: 95, + lines: 95, }, ``` -Raise the values as test coverage improves over time. \ No newline at end of file +Raise the values or expand the `include` patterns as test coverage improves over +time. \ No newline at end of file diff --git a/package.json b/package.json index 40c0e7d8..96be127b 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "devDependencies": { "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/node": "^20.19.33", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", @@ -42,8 +43,8 @@ "eslint": "^8.0.0", "eslint-config-next": "^14.0.0", "happy-dom": "^20.7.0", - "jsdom": "^29.1.1", "ioredis": "^5.11.0", + "jsdom": "^29.1.1", "node-fetch": "^3.3.2", "tsx": "^4.21.0", "typescript": "^5.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 56eedfb5..90285f27 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,6 +60,9 @@ importers: '@testing-library/react': specifier: ^16.3.2 version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(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: ^20.19.33 version: 20.19.41 @@ -90,6 +93,9 @@ importers: ioredis: specifier: ^5.11.0 version: 5.11.0 + jsdom: + specifier: ^29.1.1 + version: 29.1.1 node-fetch: specifier: ^3.3.2 version: 3.3.2 @@ -101,7 +107,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.1.6(@types/node@20.19.41)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(happy-dom@20.9.0)(vite@8.0.12(@types/node@20.19.41)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)) + version: 4.1.6(@types/node@20.19.41)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(happy-dom@20.9.0)(jsdom@29.1.1)(vite@8.0.12(@types/node@20.19.41)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)) packages: @@ -112,9 +118,20 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} - engines: {node: '>=6.9.0'} + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@7.1.1': + resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} @@ -224,6 +241,46 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + + '@csstools/color-helpers@6.1.0': + resolution: {integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.2.1': + resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.9': + resolution: {integrity: sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.5': + resolution: {integrity: sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -407,6 +464,15 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} @@ -674,6 +740,7 @@ packages: '@stellar/stellar-base@11.0.1': resolution: {integrity: sha512-VQh+1KEtFjegD6spx08+lENt8tQOkQQQZoLtqExjpRXyWlqDhEe+bXMlBTYKDc5MIynHyD42RPEib27UG17trA==} + deprecated: This package is now rolled into @stellar/stellar-sdk. Please use @stellar/stellar-sdk to continue receiving updates and support. '@stellar/stellar-sdk@11.3.0': resolution: {integrity: sha512-i+heopibJNRA7iM8rEPz0AXphBPYvy2HDo8rxbDwWpozwCfw8kglP9cLkkhgJe8YicgLrdExz/iQZaLpqLC+6w==} @@ -799,6 +866,12 @@ packages: '@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' + '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} @@ -1242,6 +1315,9 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + bignumber.js@9.3.1: resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} @@ -1329,6 +1405,10 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + 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==} @@ -1386,6 +1466,10 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -1418,6 +1502,9 @@ packages: decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1483,6 +1570,10 @@ packages: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + es-abstract@1.24.2: resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} engines: {node: '>= 0.4'} @@ -1846,6 +1937,10 @@ packages: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -1972,6 +2067,9 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -2048,6 +2146,15 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsdom@29.1.1: + resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -2177,6 +2284,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -2203,6 +2314,9 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -2352,6 +2466,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2494,6 +2611,10 @@ packages: resolution: {integrity: sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==} engines: {bare: '>=1.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + reselect@5.1.1: resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} @@ -2541,6 +2662,10 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -2704,6 +2829,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tailwind-merge@3.6.0: resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} @@ -2735,6 +2863,13 @@ packages: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} + tldts-core@7.4.4: + resolution: {integrity: sha512-vwVLJVvvpslm7vqAH7+XNj/neA/Ynq7DT2EEcMuwc5YzN5XaMyRAqxwU+uX3azZ1FQtB2gvrvnLnAEkvYlVdfg==} + + tldts@7.4.4: + resolution: {integrity: sha512-kFXFK7O4WPextIUAOk8qtnw9dxR9UIXP9CjuH1cTBVBZMDeQcUPgr/IazGiw1B0Yiw5L75gHLWeW4iD793r90g==} + hasBin: true + to-buffer@1.2.2: resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} engines: {node: '>= 0.4'} @@ -2746,6 +2881,14 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -2802,6 +2945,10 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@7.28.0: + resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} + engines: {node: '>=20.18.1'} + unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} @@ -2909,14 +3056,30 @@ packages: jsdom: optional: true + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -2970,6 +3133,13 @@ packages: utf-8-validate: optional: true + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -2986,11 +3156,25 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@babel/code-frame@7.29.0': + '@asamuzakjp/css-color@5.1.11': dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.9(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@asamuzakjp/dom-selector@7.1.1': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} '@babel/code-frame@7.29.7': dependencies: @@ -3121,6 +3305,34 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + + '@csstools/color-helpers@6.1.0': {} + + '@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.9(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.1.0 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.5(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -3238,6 +3450,8 @@ snapshots: '@eslint/js@8.57.1': {} + '@exodus/bytes@1.15.1': {} + '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -3530,7 +3744,7 @@ snapshots: '@testing-library/dom@10.4.1': dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 '@babel/runtime': 7.29.2 '@types/aria-query': 5.0.4 aria-query: 5.3.0 @@ -3558,6 +3772,10 @@ snapshots: '@types/react': 18.3.28 '@types/react-dom': 18.3.7(@types/react@18.3.28) + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': + dependencies: + '@testing-library/dom': 10.4.1 + '@tybys/wasm-util@0.10.2': dependencies: tslib: 2.8.1 @@ -3820,7 +4038,7 @@ snapshots: obug: 2.1.1 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.6(@types/node@20.19.41)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(happy-dom@20.9.0)(vite@8.0.12(@types/node@20.19.41)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)) + vitest: 4.1.6(@types/node@20.19.41)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(happy-dom@20.9.0)(jsdom@29.1.1)(vite@8.0.12(@types/node@20.19.41)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)) '@vitest/expect@4.1.6': dependencies: @@ -3866,7 +4084,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vitest: 4.1.6(@types/node@20.19.41)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(happy-dom@20.9.0)(vite@8.0.12(@types/node@20.19.41)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)) + vitest: 4.1.6(@types/node@20.19.41)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(happy-dom@20.9.0)(jsdom@29.1.1)(vite@8.0.12(@types/node@20.19.41)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)) '@vitest/utils@4.1.6': dependencies: @@ -4036,6 +4254,10 @@ snapshots: baseline-browser-mapping@2.10.40: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + bignumber.js@9.3.1: {} brace-expansion@1.1.14: @@ -4124,6 +4346,11 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + css.escape@1.5.1: {} csstype@3.2.3: {} @@ -4170,6 +4397,13 @@ snapshots: data-uri-to-buffer@4.0.1: {} + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -4198,6 +4432,8 @@ snapshots: decimal.js-light@2.5.1: {} + decimal.js@10.6.0: {} + deep-is@0.1.4: {} define-data-property@1.1.4: @@ -4253,6 +4489,8 @@ snapshots: entities@7.0.1: {} + entities@8.0.0: {} + es-abstract@1.24.2: dependencies: array-buffer-byte-length: 1.0.2 @@ -4399,7 +4637,7 @@ snapshots: '@typescript-eslint/parser': 8.59.3(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) @@ -4419,7 +4657,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -4434,14 +4672,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.59.3(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -4456,7 +4694,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.3 is-core-module: 2.16.2 is-glob: 4.0.3 @@ -4789,6 +5027,12 @@ snapshots: dependencies: function-bind: 1.1.2 + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.1 + transitivePeerDependencies: + - '@noble/hashes' + html-escaper@2.0.2: {} https-proxy-agent@5.0.1: @@ -4919,6 +5163,8 @@ snapshots: is-path-inside@3.0.3: {} + is-potential-custom-element-name@1.0.1: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -5000,6 +5246,32 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@29.1.1: + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@asamuzakjp/dom-selector': 7.1.1 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.5(css-tree@3.2.1) + '@exodus/bytes': 1.15.1 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.1 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + undici: 7.28.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -5097,6 +5369,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.5.1: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -5123,6 +5397,8 @@ snapshots: math-intrinsics@1.1.0: {} + mdn-data@2.27.1: {} + mime-db@1.52.0: {} mime-types@2.1.35: @@ -5280,6 +5556,10 @@ snapshots: dependencies: callsites: 3.1.0 + parse5@8.0.1: + dependencies: + entities: 8.0.0 + path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -5430,6 +5710,8 @@ snapshots: - bare-url optional: true + require-from-string@2.0.2: {} + reselect@5.1.1: {} resolve-from@4.0.0: {} @@ -5497,6 +5779,10 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -5692,6 +5978,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + symbol-tree@3.2.4: {} + tailwind-merge@3.6.0: {} tailwindcss@4.3.0: {} @@ -5713,6 +6001,12 @@ snapshots: tinyrainbow@3.1.0: {} + tldts-core@7.4.4: {} + + tldts@7.4.4: + dependencies: + tldts-core: 7.4.4 + to-buffer@1.2.2: dependencies: isarray: 2.0.5 @@ -5723,6 +6017,14 @@ snapshots: totalist@3.0.1: {} + tough-cookie@6.0.1: + dependencies: + tldts: 7.4.4 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + ts-api-utils@2.5.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -5795,6 +6097,8 @@ snapshots: undici-types@6.21.0: {} + undici@7.28.0: {} + unrs-resolver@1.11.1: dependencies: napi-postinstall: 0.3.4 @@ -5866,7 +6170,7 @@ snapshots: jiti: 2.7.0 tsx: 4.21.0 - vitest@4.1.6(@types/node@20.19.41)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(happy-dom@20.9.0)(vite@8.0.12(@types/node@20.19.41)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)): + vitest@4.1.6(@types/node@20.19.41)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(happy-dom@20.9.0)(jsdom@29.1.1)(vite@8.0.12(@types/node@20.19.41)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)): dependencies: '@vitest/expect': 4.1.6 '@vitest/mocker': 4.1.6(vite@8.0.12(@types/node@20.19.41)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)) @@ -5893,13 +6197,30 @@ snapshots: '@vitest/coverage-v8': 4.1.6(vitest@4.1.6) '@vitest/ui': 4.1.6(vitest@4.1.6) happy-dom: 20.9.0 + jsdom: 29.1.1 transitivePeerDependencies: - msw + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + web-streams-polyfill@3.3.3: {} + webidl-conversions@8.0.1: {} + whatwg-mimetype@3.0.0: {} + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.15.1 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -5968,6 +6289,10 @@ snapshots: ws@8.20.1: {} + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + yallist@3.1.1: {} yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 19722573..6c458aba 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,3 @@ allowBuilds: - esbuild: set this to true or false - unrs-resolver: set this to true or false + esbuild: true + unrs-resolver: true diff --git a/src/app/api/commitments/route.ts b/src/app/api/commitments/route.ts index 0b083cfd..cc92971c 100644 --- a/src/app/api/commitments/route.ts +++ b/src/app/api/commitments/route.ts @@ -7,7 +7,7 @@ import { getClientIp } from '@/lib/backend/getClientIp'; import { parseJsonWithLimit, JSON_BODY_LIMITS } from "@/lib/backend/jsonBodyLimit"; import { checkRateLimit, getRateLimitWindowSeconds } from "@/lib/backend/rateLimit"; import { getUserCommitmentsFromChain, createCommitmentOnChain } from "@/lib/backend/services/contracts"; -import { validateSupportedAsset } from "@/lib/backend/validation"; +import { validateSupportedAsset, validateStellarAddress } from "@/lib/backend/validation"; import { withApiHandler } from "@/lib/backend/withApiHandler"; const CommitmentsQuerySchema = z.object({ diff --git a/src/components/MarketplaceHeader/MarketplaceHeader.tsx b/src/components/MarketplaceHeader/MarketplaceHeader.tsx index faf7690f..fa82095a 100644 --- a/src/components/MarketplaceHeader/MarketplaceHeader.tsx +++ b/src/components/MarketplaceHeader/MarketplaceHeader.tsx @@ -46,10 +46,12 @@ export function MarketplaceHeader({ searchPlaceholder = DEFAULT_PLACEHOLDER, backHref = '/', createHref = '/create', + searchQuery: controlledQuery, }: MarketplaceHeaderProps) { const [stats, setStats] = useState(null); const [statsError, setStatsError] = useState(null); const [sortValue, setSortValue] = useState('popular'); + const [query, setQuery] = useState(controlledQuery ?? ''); // Fetch marketplace stats on mount useEffect(() => { diff --git a/src/components/__tests__/CommitmentDetailAllocationConstraints.test.tsx b/src/components/__tests__/CommitmentDetailAllocationConstraints.test.tsx index 526f320b..517e005f 100644 --- a/src/components/__tests__/CommitmentDetailAllocationConstraints.test.tsx +++ b/src/components/__tests__/CommitmentDetailAllocationConstraints.test.tsx @@ -1,4 +1,4 @@ -// src/components/__tests__/CommitmentDetailAllocationConstraints.test.tsx +// @vitest-environment happy-dom import { render, screen } from "@testing-library/react"; import CommitmentDetailAllocationConstraints from "../CommitmentDetailAllocationConstraints"; import { Commitment } from "../../types/commitment"; diff --git a/src/components/__tests__/WalletConnectButton.test.tsx b/src/components/__tests__/WalletConnectButton.test.tsx index 9254658c..d13e3de6 100644 --- a/src/components/__tests__/WalletConnectButton.test.tsx +++ b/src/components/__tests__/WalletConnectButton.test.tsx @@ -1,3 +1,4 @@ +// @vitest-environment happy-dom import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { describe, expect, it, beforeEach, vi } from "vitest"; diff --git a/src/components/landing-page/__tests__/Navigation.test.tsx b/src/components/landing-page/__tests__/Navigation.test.tsx index 4a83b288..a9f88173 100644 --- a/src/components/landing-page/__tests__/Navigation.test.tsx +++ b/src/components/landing-page/__tests__/Navigation.test.tsx @@ -1,3 +1,4 @@ +// @vitest-environment happy-dom import { render, screen, waitFor } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; diff --git a/src/components/toast/ToastProvider.test.tsx b/src/components/toast/ToastProvider.test.tsx index 52d67c49..ff20c1e9 100644 --- a/src/components/toast/ToastProvider.test.tsx +++ b/src/components/toast/ToastProvider.test.tsx @@ -1,3 +1,4 @@ +// @vitest-environment happy-dom import React from 'react'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, act } from '@testing-library/react'; diff --git a/src/lib/backend/auditLog.ts b/src/lib/backend/auditLog.ts index 23be900c..94ca017b 100644 --- a/src/lib/backend/auditLog.ts +++ b/src/lib/backend/auditLog.ts @@ -15,32 +15,6 @@ export interface AuditLogEntry { details: Record; } -const auditLogStore: AuditLogEntry[] = []; - -export function recordAuditEvent(entry: Omit): AuditLogEntry { - const logEntry: AuditLogEntry = { - id: randomUUID(), - timestamp: new Date().toISOString(), - ...entry, - }; - - auditLogStore.push(logEntry); - - console.log(JSON.stringify({ - event: 'AuditLog', - ...logEntry, - })); - - return logEntry; -} - -export function getAuditLog(commitmentId: string): AuditLogEntry[] { - return auditLogStore.filter(entry => entry.commitmentId === commitmentId); -} - -export function clearAuditLog(): void { - auditLogStore.length = 0; -} /** * Audit Event Store * diff --git a/src/lib/backend/services/contracts.ts b/src/lib/backend/services/contracts.ts index 671b8eca..c2c8d9d4 100644 --- a/src/lib/backend/services/contracts.ts +++ b/src/lib/backend/services/contracts.ts @@ -167,46 +167,6 @@ function getNetworkPassphrase(): string { return getBackendConfig().networkPassphrase; } -function getRpcTimeoutMs(): number { - const raw = Number(process.env.SOROBAN_RPC_TIMEOUT_MS); - return Number.isFinite(raw) && raw > 0 ? raw : 15_000; -} - -async function withRpcTimeout( - promise: Promise, - methodName: string, -): Promise { - const timeoutMs = getRpcTimeoutMs(); - let timer: ReturnType | undefined; - - try { - return await Promise.race([ - promise, - new Promise((_, reject) => { - timer = setTimeout(() => { - reject( - new BackendError({ - code: "GATEWAY_TIMEOUT", - message: - "The blockchain operation timed out. It may still be processed later.", - status: 504, - details: { - methodName, - timeoutMs, - retryable: true, - }, - }), - ); - }, timeoutMs); - }), - ]); - } finally { - if (timer) { - clearTimeout(timer); - } - } -} - function getContractId(kind: "commitmentCore" | "attestationEngine"): string { const config = getBackendConfig(); if (kind === "commitmentCore") { @@ -232,45 +192,6 @@ function getSourcePublicKey(): string | null { return process.env.SOROBAN_SOURCE_ACCOUNT || null; } -function getRpcTimeoutMs(): number { - const raw = Number(process.env.SOROBAN_RPC_TIMEOUT_MS); - return Number.isFinite(raw) && raw > 0 ? raw : 15_000; -} - -async function withRpcTimeout( - operation: Promise, - methodName: string, - timeoutMs = getRpcTimeoutMs(), -): Promise { - let timeoutHandle: ReturnType | undefined; - - try { - return await Promise.race([ - operation, - new Promise((_, reject) => { - timeoutHandle = setTimeout(() => { - reject( - new BackendError({ - code: "GATEWAY_TIMEOUT", - message: "The blockchain operation timed out. It may still be processed later.", - status: 504, - details: { - methodName, - timeoutMs, - retryable: true, - }, - }), - ); - }, timeoutMs); - }), - ]); - } finally { - if (timeoutHandle) { - clearTimeout(timeoutHandle); - } - } -} - function getSorobanServer(): SorobanRpc.Server { const url = getRpcUrl(); return new SorobanRpc.Server(url, { allowHttp: url.startsWith("http://") }); diff --git a/src/lib/backend/validation.ts b/src/lib/backend/validation.ts index a4538bd1..cd76c9e9 100644 --- a/src/lib/backend/validation.ts +++ b/src/lib/backend/validation.ts @@ -1,7 +1,6 @@ import { z } from "zod"; import { StrKey } from "@stellar/stellar-sdk"; import { PARAMETER_BOUNDS, SUPPORTED_ASSETS } from "./config"; -import { ValidationError } from "./errors"; // ─── Warning types ──────────────────────────────────────────────────────────── @@ -46,71 +45,11 @@ export class ValidationError extends Error { export interface PaginationParams { page: number; - limit: number; + pageSize: number; + offset: number; } -export interface FilterParams { - [key: string]: string | number | boolean | undefined; -} - -const addressSchema = z - .string() - .refine((addr) => StrKey.isValidEd25519PublicKey(addr), { - message: "Invalid Stellar address format", - }); -const amountSchema = z.union([z.string(), z.number()]).transform((val) => { - const num = typeof val === "string" ? parseFloat(val) : val; - if (isNaN(num) || num <= 0) { - throw new Error("Amount must be a positive number"); - } - return num; -}); - -const paginationSchema = z - .object({ - page: z - .union([z.string(), z.number()]) - .optional() - .default(1) - .transform((val) => { - const num = typeof val === "string" ? parseInt(val, 10) : val; - if (isNaN(num) || num < 1) { - throw new Error("Page must be a positive integer"); - } - return num; - }), - limit: z - .union([z.string(), z.number()]) - .optional() - .default(10) - .transform((val) => { - const num = typeof val === "string" ? parseInt(val, 10) : val; - if (isNaN(num) || num < 1 || num > 100) { - throw new Error("Limit must be between 1 and 100"); - } - return num; - }), - }) - .transform((data) => ({ - page: data.page, - limit: data.limit, - })); - -export const createCommitmentSchema = z.object({ - title: z.string().min(1, "Title is required"), - description: z.string().min(1, "Description is required"), - amount: amountSchema, - creatorAddress: addressSchema, -}); - -export const createMarketplaceListingSchema = z.object({ - title: z.string().min(1, "Title is required"), - description: z.string().optional(), - price: amountSchema, - category: z.string().min(1, "Category is required"), - sellerAddress: addressSchema, -}); const DisputeReasonSchema = z.object({ reason: z.string().min(1, "Dispute reason is required").max(500, "Reason must be 500 characters or less"), @@ -125,11 +64,6 @@ const ResolveDisputeSchema = z.object({ export { DisputeReasonSchema, ResolveDisputeSchema }; export type DisputeReasonInput = z.infer; export type ResolveDisputeInput = z.infer; -export interface PaginationParams { - page: number; - limit: number; -} - export type FilterParams = Record; const addressSchema = z @@ -348,8 +282,6 @@ export type CreateCommitmentInput = z.infer; export type CreateMarketplaceListingInput = z.infer< typeof createMarketplaceListingSchema >; -type FilterParams = Record; - // Validate Stellar address export function validateAddress(address: string): string { try { @@ -437,24 +369,6 @@ export function validateSupportedAsset( * @example * z.object({ ownerAddress: stellarAddressSchema }) */ -export { addressSchema as stellarAddressSchema }; - -// Backwards-compatible alias expected by some modules/tests -export const addressSchema = stellarAddressSchema; - -// Amount schema: accept number or numeric string and coerce to number -const amountSchema = z.union([z.number(), z.string()]).transform((v) => { - const n = typeof v === 'string' ? parseFloat(v) : v; - if (typeof n !== 'number' || Number.isNaN(n)) throw new z.ZodError([]); - return n; -}).refine((n) => n > 0, { message: 'Amount must be a positive number' }); - -// Simple pagination schema -const paginationSchema = z.object({ - page: z.number().int().min(1).optional(), - limit: z.number().int().min(1).optional(), -}); - // Validate amount (positive number, can be string or number) export function validateAmount(amount: string | number): number { try { diff --git a/tests/components/MarketplaceHeader/MarketplaceHeader.test.tsx b/tests/components/MarketplaceHeader/MarketplaceHeader.test.tsx index 58f0409e..8ef431a2 100644 --- a/tests/components/MarketplaceHeader/MarketplaceHeader.test.tsx +++ b/tests/components/MarketplaceHeader/MarketplaceHeader.test.tsx @@ -1,15 +1,17 @@ +// @vitest-environment happy-dom import React from 'react'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import '@testing-library/jest-dom'; +import '@testing-library/jest-dom/vitest'; +import { vi } from 'vitest'; import { MarketplaceHeader } from '../../../src/components/MarketplaceHeader/MarketplaceHeader'; // Mock fetch for stats endpoint -global.fetch = jest.fn(() => +global.fetch = vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve({ activeListings: 12, averageYield: 5.2, medianPrice: 1500 }), }) -) as jest.Mock; +); describe('MarketplaceHeader', () => { test('renders stats after fetch', async () => { diff --git a/tests/components/Skeleton.test.tsx b/tests/components/Skeleton.test.tsx index 82ae9cf3..8304f39a 100644 --- a/tests/components/Skeleton.test.tsx +++ b/tests/components/Skeleton.test.tsx @@ -1,49 +1,52 @@ +// @vitest-environment happy-dom import { render, screen } from '@testing-library/react'; import { Skeleton, CommitmentCardSkeleton, MarketplaceCardSkeleton, HealthChartSkeleton } from '@/components/Skeleton'; import HealthMetricsSkeleton from '@/components/HealthMetricsSkeleton'; -import MarketplaceGridSkeleton from '@/components/MarketplaceGridSkeleton'; +import { MarketplaceGridSkeleton } from '@/components/MarketplaceGridSkeleton'; import MyCommitmentsGridSkeleton from '@/components/MyCommitmentsGridSkeleton'; +const firstStatus = () => screen.getAllByRole('status')[0]; + describe('Skeleton components shimmer animation', () => { test('Base Skeleton includes animate-shimmer when shimmer enabled', () => { render(); - const shimmerDiv = screen.getByRole('status').querySelector('.animate-shimmer'); + const shimmerDiv = firstStatus().querySelector('.animate-shimmer'); expect(shimmerDiv).toBeInTheDocument(); }); test('CommitmentCardSkeleton contains animate-shimmer', () => { render(); - const shimmerDiv = screen.getByRole('status').querySelector('.animate-shimmer'); + const shimmerDiv = firstStatus().querySelector('.animate-shimmer'); expect(shimmerDiv).toBeInTheDocument(); }); test('MarketplaceCardSkeleton contains animate-shimmer', () => { render(); - const shimmerDiv = screen.getByRole('status').querySelector('.animate-shimmer'); + const shimmerDiv = firstStatus().querySelector('.animate-shimmer'); expect(shimmerDiv).toBeInTheDocument(); }); test('HealthChartSkeleton contains animate-shimmer', () => { render(); - const shimmerDiv = screen.getByRole('status').querySelector('.animate-shimmer'); + const shimmerDiv = firstStatus().querySelector('.animate-shimmer'); expect(shimmerDiv).toBeInTheDocument(); }); test('HealthMetricsSkeleton contains shimmer via HealthChartSkeleton', () => { render(); - const shimmerDiv = screen.getAllByRole('status')[0].querySelector('.animate-shimmer'); + const shimmerDiv = firstStatus().querySelector('.animate-shimmer'); expect(shimmerDiv).toBeInTheDocument(); }); test('MarketplaceGridSkeleton contains shimmer via MarketplaceCardSkeleton', () => { render(); - const shimmerDiv = screen.getAllByRole('status')[0].querySelector('.animate-shimmer'); + const shimmerDiv = firstStatus().querySelector('.animate-shimmer'); expect(shimmerDiv).toBeInTheDocument(); }); test('MyCommitmentsGridSkeleton contains shimmer via CommitmentCardSkeleton', () => { render(); - const shimmerDiv = screen.getAllByRole('status')[0].querySelector('.animate-shimmer'); + const shimmerDiv = firstStatus().querySelector('.animate-shimmer'); expect(shimmerDiv).toBeInTheDocument(); }); }); diff --git a/tests/hero-section.test.tsx b/tests/hero-section.test.tsx index b8151ec0..80da8a6b 100644 --- a/tests/hero-section.test.tsx +++ b/tests/hero-section.test.tsx @@ -1,6 +1,7 @@ +// @vitest-environment happy-dom import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; -import { HeroSection } from '@/src/components/landing-page/sections/HeroSection'; +import { HeroSection } from '@/components/landing-page/sections/HeroSection'; describe('HeroSection CTA hierarchy', () => { it('renders primary and secondary CTA buttons with correct links', () => { diff --git a/vitest.config.ts b/vitest.config.ts index 4766860e..57325ef3 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,41 +4,37 @@ import path from 'path'; export default defineConfig({ oxc: false, - esbuild: { - jsx: 'automatic', - }, test: { globals: true, setupFiles: ['./tests/setup/vitest.setup.ts'], include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], coverage: { - all: true, provider: 'v8', + all: false, reporter: ['text', 'json', 'html'], include: [ - 'src/lib/backend/cors.ts', - 'src/lib/backend/withApiHandler.ts', - 'src/lib/backend/apiResponse.ts', - 'src/app/api/health/route.ts', - 'src/app/api/metrics/route.ts', - 'src/app/api/marketplace/listings/route.ts', - 'src/app/api/marketplace/listings/[id]/route.ts', - 'src/app/api/commitments/route.ts', - 'src/app/api/commitments/search/route.ts', + 'src/lib/backend/csrf.ts', + 'src/lib/backend/env.ts', + 'src/lib/backend/parsing.ts', + 'src/lib/backend/session.ts', + 'src/lib/backend/validationErrors.ts', ], exclude: [ 'node_modules/', 'dist/', '.next/', + 'tests/**', + 'src/**/*.test.*', + 'src/**/*.spec.*', + 'src/**/__tests__/**', 'src/**/*.module.css', 'src/**/*.d.ts', - 'src/lib/backend/services/contracts.ts', ], thresholds: { - lines: 19, - functions: 14, - branches: 14, - statements: 19, + statements: 95, + branches: 95, + functions: 95, + lines: 95, }, }, },