From 36bb09dc37738609d902afea1f21087d66ef07b6 Mon Sep 17 00:00:00 2001 From: trinnode Date: Fri, 20 Feb 2026 11:41:03 +0100 Subject: [PATCH 1/3] feat: implement drag-and-drop employee reordering with optimistic updates --- package-lock.json | 1514 ++++++++++++++++- package.json | 14 +- src/App.tsx | 16 +- src/__tests__/EmployeeTable.test.tsx | 180 ++ src/__tests__/SortableRow.test.tsx | 125 ++ src/__tests__/setup.ts | 1 + src/__tests__/useUpdateEmployeeOrder.test.tsx | 201 +++ src/components/SortableRow.module.css | 40 + src/components/SortableRow.tsx | 80 + src/hooks/useUpdateEmployeeOrder.ts | 72 + src/pages/EmployeeTable.module.css | 70 + src/pages/EmployeeTable.tsx | 113 ++ src/services/employeeApi.ts | 31 + src/types/employee.ts | 15 + tsconfig.app.json | 3 +- vite.config.ts | 7 + 16 files changed, 2429 insertions(+), 53 deletions(-) create mode 100644 src/__tests__/EmployeeTable.test.tsx create mode 100644 src/__tests__/SortableRow.test.tsx create mode 100644 src/__tests__/setup.ts create mode 100644 src/__tests__/useUpdateEmployeeOrder.test.tsx create mode 100644 src/components/SortableRow.module.css create mode 100644 src/components/SortableRow.tsx create mode 100644 src/hooks/useUpdateEmployeeOrder.ts create mode 100644 src/pages/EmployeeTable.module.css create mode 100644 src/pages/EmployeeTable.tsx create mode 100644 src/services/employeeApi.ts create mode 100644 src/types/employee.ts diff --git a/package-lock.json b/package-lock.json index 4f2420d6..53e40547 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,9 @@ ], "dependencies": { "@creit.tech/stellar-wallets-kit": "^1.9.5", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@stellar/design-system": "^3.2.5", "@stellar/stellar-sdk": "^14.3.3", "@stellar/stellar-xdr-json": "^23.0.0", @@ -24,11 +27,15 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/lodash": "^4.17.21", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@types/react-router-dom": "^5.3.3", "@vitejs/plugin-react": "^5.1.1", + "@vitest/coverage-v8": "^4.0.18", "concurrently": "^9.2.1", "dotenv": "^17.2.3", "eslint": "^9.39.1", @@ -40,21 +47,85 @@ "glob": "^13.0.0", "globals": "^16.5.0", "husky": "^9.1.7", + "jsdom": "^28.1.0", "lint-staged": "^16.2.7", "prettier": "3.6.2", "typescript": "~5.9.3", "typescript-eslint": "^8.48.1", "vite": "^7.2.6", "vite-plugin-node-polyfills": "^0.24.0", - "vite-plugin-wasm": "^3.5.0" + "vite-plugin-wasm": "^3.5.0", + "vitest": "^4.0.18" } }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true + }, "node_modules/@albedo-link/intent": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@albedo-link/intent/-/intent-0.12.0.tgz", "integrity": "sha512-UlGBhi0qASDYOjLrOL4484vQ26Ee3zTK2oAgvPMClOs+1XNk3zbs3dECKZv+wqeSI8SkHow8mXLTa16eVh+dQA==", "license": "MIT" }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", + "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==", + "dev": true, + "dependencies": { + "@csstools/css-calc": "^3.0.0", + "@csstools/css-color-parser": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.5" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -262,13 +333,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -353,11 +423,10 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" @@ -366,6 +435,27 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, "node_modules/@creit.tech/stellar-wallets-kit": { "version": "1.9.5", "resolved": "https://registry.npmjs.org/@creit.tech/stellar-wallets-kit/-/stellar-wallets-kit-1.9.5.tgz", @@ -479,6 +569,181 @@ "node": ">=16" } }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.1.tgz", + "integrity": "sha512-NmXRccUJMk2AWA5A7e5a//3bCIMyOu2hAtdRYrhPPHjDxINuCwX1w6rnIZ4xjLcp0ayv6h8Pc3X0eJUGiAAXHQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.1.tgz", + "integrity": "sha512-vYwO15eRBEkeF6xjAno/KQ61HacNhfQuuU/eGwH67DplL0zD5ZixUa563phQvUelA07yDczIXdtmYojCphKJcw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/color-helpers": "^6.0.1", + "@csstools/css-calc": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.27", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.27.tgz", + "integrity": "sha512-sxP33Jwg1bviSUXAV43cVYdmjt2TLnLXNqCWl9xmxHawWVjGz/kEbdkr7F9pxJNBN2Mh+dq0crgItbW6tQvyow==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ] + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@emurgo/cardano-serialization-lib-browser": { "version": "13.2.1", "resolved": "https://registry.npmjs.org/@emurgo/cardano-serialization-lib-browser/-/cardano-serialization-lib-browser-13.2.1.tgz", @@ -1693,6 +1958,27 @@ "license": "Apache-2.0", "peer": true }, + "node_modules/@near-js/providers/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "optional": true, + "peer": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/@near-js/signers": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@near-js/signers/-/signers-0.2.2.tgz", @@ -3330,6 +3616,12 @@ "@stablelib/wipe": "^1.0.1" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true + }, "node_modules/@stellar/design-system": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/@stellar/design-system/-/design-system-3.2.5.tgz", @@ -3482,6 +3774,91 @@ "react": "^18 || ^19" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@trezor/analytics": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@trezor/analytics/-/analytics-1.4.3.tgz", @@ -4266,6 +4643,13 @@ "tslib": "^2.6.2" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4311,6 +4695,16 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -4320,6 +4714,12 @@ "@types/node": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -4710,32 +5110,175 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/@wallet-standard/base": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@wallet-standard/base/-/base-1.1.0.tgz", - "integrity": "sha512-DJDQhjKmSNVLKWItoKThJS+CsJQjR9AOBOirBVT1F9YpRyC9oYHE+ZnSf8y8bxUphtKqdQMPVQ2mHohYdRvDVQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=16" + "node_modules/@vitest/coverage-v8": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", + "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.0.18", + "ast-v8-to-istanbul": "^0.3.10", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.1", + "obug": "^2.1.1", + "std-env": "^3.10.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.0.18", + "vitest": "4.0.18" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } } }, - "node_modules/@wallet-standard/features": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@wallet-standard/features/-/features-1.1.0.tgz", - "integrity": "sha512-hiEivWNztx73s+7iLxsuD1sOJ28xtRix58W7Xnz4XzzA/pF0+aicnWgjOdA10doVDEDZdUuZCIIqG96SFNlDUg==", - "license": "Apache-2.0", + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, "dependencies": { - "@wallet-standard/base": "^1.1.0" + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" }, - "engines": { - "node": ">=16" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@walletconnect/core": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.11.2.tgz", - "integrity": "sha512-bB4SiXX8hX3/hyBfVPC5gwZCXCl+OPj+/EDVM71iAO3TDsh78KPbrVAbDnnsbHzZVHlsMohtXX3j5XVsheN3+g==", - "license": "Apache-2.0", + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@wallet-standard/base": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@wallet-standard/base/-/base-1.1.0.tgz", + "integrity": "sha512-DJDQhjKmSNVLKWItoKThJS+CsJQjR9AOBOirBVT1F9YpRyC9oYHE+ZnSf8y8bxUphtKqdQMPVQ2mHohYdRvDVQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16" + } + }, + "node_modules/@wallet-standard/features": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@wallet-standard/features/-/features-1.1.0.tgz", + "integrity": "sha512-hiEivWNztx73s+7iLxsuD1sOJ28xtRix58W7Xnz4XzzA/pF0+aicnWgjOdA10doVDEDZdUuZCIIqG96SFNlDUg==", + "license": "Apache-2.0", + "dependencies": { + "@wallet-standard/base": "^1.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@walletconnect/core": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.11.2.tgz", + "integrity": "sha512-bB4SiXX8hX3/hyBfVPC5gwZCXCl+OPj+/EDVM71iAO3TDsh78KPbrVAbDnnsbHzZVHlsMohtXX3j5XVsheN3+g==", + "license": "Apache-2.0", "dependencies": { "@walletconnect/heartbeat": "1.2.1", "@walletconnect/jsonrpc-provider": "1.0.13", @@ -5299,6 +5842,15 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/asn1.js": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", @@ -5330,6 +5882,41 @@ "util": "^0.12.5" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.11.tgz", + "integrity": "sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -5502,6 +6089,15 @@ "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", "license": "MIT" }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/big-integer": { "version": "1.6.36", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.36.tgz", @@ -5984,6 +6580,15 @@ "node": ">=20" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -6498,6 +7103,49 @@ "node": "*" } }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "node_modules/cssstyle": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.0.1.tgz", + "integrity": "sha512-IoJs7La+oFp/AB033wBStxNOJt4+9hHMxsXUPANcoXL2b3W4DZKghlJ2cI/eyeRZIQ9ysvYEorVhjrcYctWbog==", + "dev": true, + "dependencies": { + "@asamuzakjp/css-color": "^4.1.2", + "@csstools/css-syntax-patches-for-csstree": "^1.0.26", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -6505,6 +7153,85 @@ "devOptional": true, "license": "MIT" }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/@exodus/bytes": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.14.1.tgz", + "integrity": "sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==", + "dev": true, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/data-urls/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -6531,6 +7258,12 @@ "node": ">=0.10.0" } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true + }, "node_modules/decode-uri-component": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", @@ -6618,6 +7351,15 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/des.js": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", @@ -6683,6 +7425,13 @@ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", "license": "MIT" }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "peer": true + }, "node_modules/domain-browser": { "version": "4.22.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz", @@ -6787,6 +7536,18 @@ "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -6818,6 +7579,12 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -7306,6 +8073,15 @@ "safe-buffer": "^5.1.1" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -7866,6 +8642,55 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/html-encoding-sniffer/node_modules/@exodus/bytes": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.14.1.tgz", + "integrity": "sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==", + "dev": true, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/html-encoding-sniffer/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "node_modules/http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", @@ -7900,6 +8725,19 @@ "license": "ISC", "peer": true }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", @@ -7907,6 +8745,19 @@ "dev": true, "license": "MIT" }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -7995,6 +8846,15 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -8193,6 +9053,12 @@ "node": ">=0.12.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, "node_modules/is-property": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", @@ -8308,6 +9174,54 @@ "ws": "*" } }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jayson": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.2.0.tgz", @@ -8367,30 +9281,136 @@ } } }, - "node_modules/js-sha256": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.1.tgz", - "integrity": "sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg==", - "license": "MIT" + "node_modules/js-sha256": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.1.tgz", + "integrity": "sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "28.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", + "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", + "dev": true, + "dependencies": { + "@acemir/cssom": "^0.9.31", + "@asamuzakjp/dom-selector": "^6.8.1", + "@bramus/specificity": "^2.4.2", + "@exodus/bytes": "^1.11.0", + "cssstyle": "^6.0.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "undici": "^7.21.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/@exodus/bytes": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.14.1.tgz", + "integrity": "sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==", + "dev": true, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", "dev": true, - "license": "MIT" + "engines": { + "node": ">=20" + } }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", "dev": true, - "license": "MIT", "dependencies": { - "argparse": "^2.0.1" + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/jsesc": { @@ -8752,6 +9772,16 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -8762,6 +9792,32 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -8782,6 +9838,12 @@ "safe-buffer": "^5.1.2" } }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -8849,6 +9911,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -9279,6 +10350,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ] + }, "node_modules/ofetch": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", @@ -9423,6 +10504,18 @@ "node": ">= 0.10" } }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -9483,6 +10576,12 @@ "node": "20 || >=22" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, "node_modules/pbkdf2": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", @@ -9656,6 +10755,44 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -10061,6 +11198,13 @@ "react": "^19.2.0" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "peer": true + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -10145,6 +11289,19 @@ "node": ">= 12.13.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/require-addon": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz", @@ -10167,6 +11324,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -10412,6 +11578,18 @@ "node": ">=10" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -10639,6 +11817,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -10754,6 +11938,12 @@ "node": ">= 10.x" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, "node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -10764,6 +11954,12 @@ "node": ">= 0.6" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true + }, "node_modules/stream-browserify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", @@ -10859,6 +12055,18 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -10910,6 +12118,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, "node_modules/text-encoding-utf-8": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", @@ -10960,6 +12174,21 @@ "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -11008,6 +12237,33 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz", + "integrity": "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==", + "dev": true, + "dependencies": { + "tldts-core": "^7.0.23" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.23.tgz", + "integrity": "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==", + "dev": true + }, "node_modules/to-buffer": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", @@ -11057,6 +12313,18 @@ "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", "license": "MIT" }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -11311,6 +12579,15 @@ "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", "license": "MIT" }, + "node_modules/undici": { + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", + "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "dev": true, + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -11742,6 +13019,95 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", @@ -11749,12 +13115,33 @@ "dev": true, "license": "MIT" }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "engines": { + "node": ">=20" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -11808,6 +13195,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wif": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/wif/-/wif-5.0.0.tgz", @@ -11854,6 +13257,21 @@ } } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, "node_modules/xrpl": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/xrpl/-/xrpl-4.4.3.tgz", diff --git a/package.json b/package.json index ee25cc2b..549ea4d1 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,9 @@ "build": "tsc -b && vite build", "install:contracts": "npm install --workspace=packages && npm run build --workspace=packages", "preview": "vite preview", + "test": "vitest", + "test:run": "vitest run", + "test:coverage": "vitest run --coverage", "lint": "eslint .", "format": "prettier . --write", "prepare": "husky" @@ -18,6 +21,9 @@ ], "dependencies": { "@creit.tech/stellar-wallets-kit": "^1.9.5", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@stellar/design-system": "^3.2.5", "@stellar/stellar-sdk": "^14.3.3", "@stellar/stellar-xdr-json": "^23.0.0", @@ -30,11 +36,15 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/lodash": "^4.17.21", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@types/react-router-dom": "^5.3.3", "@vitejs/plugin-react": "^5.1.1", + "@vitest/coverage-v8": "^4.0.18", "concurrently": "^9.2.1", "dotenv": "^17.2.3", "eslint": "^9.39.1", @@ -46,13 +56,15 @@ "glob": "^13.0.0", "globals": "^16.5.0", "husky": "^9.1.7", + "jsdom": "^28.1.0", "lint-staged": "^16.2.7", "prettier": "3.6.2", "typescript": "~5.9.3", "typescript-eslint": "^8.48.1", "vite": "^7.2.6", "vite-plugin-node-polyfills": "^0.24.0", - "vite-plugin-wasm": "^3.5.0" + "vite-plugin-wasm": "^3.5.0", + "vitest": "^4.0.18" }, "lint-staged": { "**/*": [ diff --git a/src/App.tsx b/src/App.tsx index a74f33b0..0eaf482f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import Debugger from "./pages/Debugger.tsx"; import PayrollScheduler from "./pages/PayrollScheduler"; import EmployeeEntry from "./pages/EmployeeEntry"; import FeeEstimation from "./pages/FeeEstimation"; +import EmployeeTable from "./pages/EmployeeTable"; const AppLayout: React.FC = () => (
@@ -19,7 +20,7 @@ const AppLayout: React.FC = () => ( - `text-sm font-medium ${isActive ? 'text-blue-600' : 'text-gray-500 hover:text-gray-700'}` + `text-sm font-medium ${isActive ? "text-blue-600" : "text-gray-500 hover:text-gray-700"}` } > Payroll @@ -27,15 +28,23 @@ const AppLayout: React.FC = () => ( - `text-sm font-medium ${isActive ? 'text-blue-600' : 'text-gray-500 hover:text-gray-700'}` + `text-sm font-medium ${isActive ? "text-blue-600" : "text-gray-500 hover:text-gray-700"}` } > Employees + + `text-sm font-medium ${isActive ? "text-blue-600" : "text-gray-500 hover:text-gray-700"}` + } + > + Employee Table + - `text-sm font-medium ${isActive ? 'text-blue-600' : 'text-gray-500 hover:text-gray-700'}` + `text-sm font-medium ${isActive ? "text-blue-600" : "text-gray-500 hover:text-gray-700"}` } > Fee Estimator @@ -87,6 +96,7 @@ function App() { {/* } /> */} } /> } /> + } /> } /> } /> } /> diff --git a/src/__tests__/EmployeeTable.test.tsx b/src/__tests__/EmployeeTable.test.tsx new file mode 100644 index 00000000..ff65b744 --- /dev/null +++ b/src/__tests__/EmployeeTable.test.tsx @@ -0,0 +1,180 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import type { ReactNode } from "react"; + +import EmployeeTable from "../pages/EmployeeTable"; +import * as employeeApi from "../services/employeeApi"; +import type { Employee } from "../types/employee"; + +// ── Mocks ───────────────────────────────────────────────────────────────────── + +// Isolate from the real dnd-kit browser APIs +vi.mock("@dnd-kit/core", () => ({ + DndContext: ({ children }: { children: ReactNode }) => <>{children}, + closestCenter: vi.fn(), + KeyboardSensor: class {}, + PointerSensor: class {}, + useSensor: vi.fn(), + useSensors: vi.fn(() => []), +})); + +vi.mock("@dnd-kit/sortable", () => ({ + SortableContext: ({ children }: { children: ReactNode }) => <>{children}, + sortableKeyboardCoordinates: vi.fn(), + verticalListSortingStrategy: vi.fn(), + arrayMove: vi.fn((arr: unknown[]) => arr), +})); + +// Thin stand-in so we can assert row rendering without mounting useSortable +vi.mock("../components/SortableRow", () => ({ + SortableRow: ({ employee }: { employee: Employee }) => ( + + {employee.name} + {employee.role} + + ), +})); + +vi.mock("../services/employeeApi", () => ({ + fetchEmployees: vi.fn(), + updateEmployeeOrder: vi.fn(), +})); + +// Allow per-test control of mutationError +const mockReorder = vi.fn(); +let mockMutationError: Error | null = null; + +vi.mock("../hooks/useUpdateEmployeeOrder", () => ({ + useUpdateEmployeeOrder: () => ({ + mutate: mockReorder, + error: mockMutationError, + }), + EMPLOYEES_QUERY_KEY: ["employees"], +})); + +// ── Fixtures ─────────────────────────────────────────────────────────────────── + +const EMPLOYEES: Employee[] = [ + { + id: "1", + name: "Alice", + role: "Engineer", + walletAddress: "ADDR1", + currency: "USDC", + salary: 5000, + orderIndex: 0, + }, + { + id: "2", + name: "Bob", + role: "Designer", + walletAddress: "ADDR2", + currency: "USDC", + salary: 4500, + orderIndex: 1, + }, +]; + +function createClient() { + return new QueryClient({ + defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, + }); +} + +function renderTable(queryClient = createClient()) { + return render( + + + , + ); +} + +// ── Tests ────────────────────────────────────────────────────────────────────── + +describe("EmployeeTable", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockMutationError = null; + }); + + it("shows a loading indicator while data is in-flight", () => { + vi.mocked(employeeApi.fetchEmployees).mockReturnValue( + new Promise(() => {}), + ); + + renderTable(); + + expect(screen.getByText(/loading employees/i)).toBeInTheDocument(); + }); + + it("shows an error message when fetchEmployees rejects", async () => { + vi.mocked(employeeApi.fetchEmployees).mockRejectedValue(new Error("500")); + + renderTable(); + + await waitFor(() => { + expect(screen.getByText(/failed to load employees/i)).toBeInTheDocument(); + }); + }); + + it("renders the table headings and a row for each employee", async () => { + vi.mocked(employeeApi.fetchEmployees).mockResolvedValue(EMPLOYEES); + + renderTable(); + + await waitFor(() => { + expect(screen.getByText("Name")).toBeInTheDocument(); + }); + + expect(screen.getByText("Role")).toBeInTheDocument(); + expect(screen.getByText("Wallet")).toBeInTheDocument(); + expect(screen.getByText("Currency")).toBeInTheDocument(); + expect(screen.getByText("Salary")).toBeInTheDocument(); + + const rows = screen.getAllByTestId("employee-row"); + expect(rows).toHaveLength(EMPLOYEES.length); + expect(screen.getByText("Alice")).toBeInTheDocument(); + expect(screen.getByText("Bob")).toBeInTheDocument(); + }); + + it("shows a mutation error alert when the reorder API call fails", async () => { + vi.mocked(employeeApi.fetchEmployees).mockResolvedValue(EMPLOYEES); + mockMutationError = new Error("Reorder failed"); + + renderTable(); + + await waitFor(() => { + expect(screen.getByRole("alert")).toBeInTheDocument(); + }); + + expect(screen.getByRole("alert")).toHaveTextContent(/reorder failed/i); + expect(screen.getByRole("alert")).toHaveTextContent( + /previous order has been restored/i, + ); + }); + + it("does not show the alert banner when there is no mutation error", async () => { + vi.mocked(employeeApi.fetchEmployees).mockResolvedValue(EMPLOYEES); + mockMutationError = null; + + renderTable(); + + await waitFor(() => { + expect(screen.queryByRole("alert")).not.toBeInTheDocument(); + }); + }); + + it("renders a table element with thead and tbody", async () => { + vi.mocked(employeeApi.fetchEmployees).mockResolvedValue(EMPLOYEES); + + const { container } = renderTable(); + + await waitFor(() => { + expect(container.querySelector("table")).toBeInTheDocument(); + }); + + expect(container.querySelector("thead")).toBeInTheDocument(); + expect(container.querySelector("tbody")).toBeInTheDocument(); + }); +}); diff --git a/src/__tests__/SortableRow.test.tsx b/src/__tests__/SortableRow.test.tsx new file mode 100644 index 00000000..720c8498 --- /dev/null +++ b/src/__tests__/SortableRow.test.tsx @@ -0,0 +1,125 @@ +import { render, screen } from "@testing-library/react"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { SortableRow } from "../components/SortableRow"; +import type { Employee } from "../types/employee"; + +// ── Mocks ───────────────────────────────────────────────────────────────────── + +vi.mock("@dnd-kit/sortable", () => ({ + useSortable: vi.fn(() => ({ + attributes: { role: "button" }, + listeners: { onKeyDown: vi.fn() }, + setNodeRef: vi.fn(), + setActivatorNodeRef: vi.fn(), + transform: null, + transition: undefined, + isDragging: false, + })), +})); + +vi.mock("@dnd-kit/utilities", () => ({ + CSS: { Transform: { toString: () => "" } }, +})); + +// CSS modules resolve to plain objects in jsdom — no extra mock needed. + +// Fixtures + +const EMPLOYEE: Employee = { + id: "emp-1", + name: "Alice Nguyen", + role: "Engineer", + walletAddress: "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGPWQTESTADDR", + currency: "USDC", + salary: 5000, + orderIndex: 0, +}; + +function renderRow(employee: Employee = EMPLOYEE) { + // SortableRow renders a which requires a table context to be valid HTML. + return render( + + + + +
, + ); +} + +// ── Tests ────────────────────────────────────────────────────────────────────── + +describe("SortableRow", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders the employee name, role, currency and salary", () => { + renderRow(); + + expect(screen.getByText("Alice Nguyen")).toBeInTheDocument(); + expect(screen.getByText("Engineer")).toBeInTheDocument(); + expect(screen.getByText("USDC")).toBeInTheDocument(); + expect(screen.getByText("5,000")).toBeInTheDocument(); + }); + + it("truncates the wallet address and preserves full address in title", () => { + renderRow(); + + const walletSpan = screen.getByTitle(EMPLOYEE.walletAddress); + expect(walletSpan).toBeInTheDocument(); + + // First 6 chars + ellipsis + last 4 chars + const expected = `${EMPLOYEE.walletAddress.slice(0, 6)}…${EMPLOYEE.walletAddress.slice(-4)}`; + expect(walletSpan.textContent).toBe(expected); + }); + + it("renders a drag handle button with the correct aria-label", () => { + renderRow(); + + const handle = screen.getByRole("button", { name: "Reorder employee" }); + expect(handle).toBeInTheDocument(); + }); + + it("drag handle has tabIndex={0} for keyboard accessibility", () => { + renderRow(); + + const handle = screen.getByRole("button", { name: "Reorder employee" }); + expect(handle).toHaveAttribute("tabindex", "0"); + }); + + it("grip SVG icon has aria-hidden so screen readers skip it", () => { + renderRow(); + + const svg = screen + .getByRole("button", { name: "Reorder employee" }) + .querySelector("svg"); + + expect(svg).toHaveAttribute("aria-hidden", "true"); + }); + + it("applies the dragging class when isDragging is true", async () => { + const { useSortable } = await import("@dnd-kit/sortable"); + vi.mocked(useSortable).mockReturnValueOnce({ + attributes: {}, + listeners: {}, + setNodeRef: vi.fn(), + setActivatorNodeRef: vi.fn(), + transform: null, + transition: undefined, + isDragging: true, + // cast because real return type has many more fields we don't need + } as unknown as ReturnType); + + const { container } = renderRow(); + const row = container.querySelector("tr"); + + // The CSS module class name in jsdom tests resolves to the key string. + expect(row?.className).toContain("dragging"); + }); + + it("does not apply a className when isDragging is false", () => { + const { container } = renderRow(); + const row = container.querySelector("tr"); + expect(row?.className).toBeFalsy(); + }); +}); diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts new file mode 100644 index 00000000..d0de870d --- /dev/null +++ b/src/__tests__/setup.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom"; diff --git a/src/__tests__/useUpdateEmployeeOrder.test.tsx b/src/__tests__/useUpdateEmployeeOrder.test.tsx new file mode 100644 index 00000000..28edf05d --- /dev/null +++ b/src/__tests__/useUpdateEmployeeOrder.test.tsx @@ -0,0 +1,201 @@ +import { renderHook, waitFor, act } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import type { ReactNode } from "react"; + +import { + useUpdateEmployeeOrder, + EMPLOYEES_QUERY_KEY, +} from "../hooks/useUpdateEmployeeOrder"; +import * as employeeApi from "../services/employeeApi"; +import type { Employee } from "../types/employee"; + +// ── Mocks ───────────────────────────────────────────────────────────────────── + +vi.mock("../services/employeeApi", () => ({ + updateEmployeeOrder: vi.fn(), + fetchEmployees: vi.fn(), +})); + +// ── Fixtures ─────────────────────────────────────────────────────────────────── + +const EMPLOYEES: Employee[] = [ + { + id: "1", + name: "Alice", + role: "Eng", + walletAddress: "ADDR1111", + currency: "USDC", + salary: 5000, + orderIndex: 0, + }, + { + id: "2", + name: "Bob", + role: "Des", + walletAddress: "ADDR2222", + currency: "USDC", + salary: 4500, + orderIndex: 1, + }, + { + id: "3", + name: "Carol", + role: "PM", + walletAddress: "ADDR3333", + currency: "XLM", + salary: 6000, + orderIndex: 2, + }, +]; + +function createWrapper() { + const queryClient = new QueryClient({ + defaultOptions: { mutations: { retry: false }, queries: { retry: false } }, + }); + queryClient.setQueryData(EMPLOYEES_QUERY_KEY, [...EMPLOYEES]); + + function Wrapper({ children }: { children: ReactNode }) { + return ( + {children} + ); + } + + return { queryClient, Wrapper }; +} + +// ── Tests ────────────────────────────────────────────────────────────────────── + +describe("useUpdateEmployeeOrder", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("optimistically reorders the cache in onMutate before the API resolves", async () => { + // Never resolves — lets us inspect cache while mutationFn is in-flight + vi.mocked(employeeApi.updateEmployeeOrder).mockReturnValue( + new Promise(() => {}), + ); + + const { queryClient, Wrapper } = createWrapper(); + const { result } = renderHook(() => useUpdateEmployeeOrder(), { + wrapper: Wrapper, + }); + + act(() => { + result.current.mutate({ activeId: "1", overId: "3" }); + }); + + // onMutate is async but settles on the microtask queue + await waitFor(() => { + const cached = queryClient.getQueryData(EMPLOYEES_QUERY_KEY)!; + // Alice (id=1) should now be at index 2, Carol (id=3) at index 0 + expect(cached[0].id).toBe("2"); + expect(cached[1].id).toBe("3"); + expect(cached[2].id).toBe("1"); + }); + }); + + it("sends only minimal { id, newIndex } payload — not the full Employee object", async () => { + vi.mocked(employeeApi.updateEmployeeOrder).mockResolvedValue(undefined); + + const { Wrapper } = createWrapper(); + const { result } = renderHook(() => useUpdateEmployeeOrder(), { + wrapper: Wrapper, + }); + + act(() => { + result.current.mutate({ activeId: "1", overId: "2" }); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + const callArg = vi.mocked(employeeApi.updateEmployeeOrder).mock.calls[0][0]; + + // Every item must only carry id and newIndex + callArg.forEach((item, index) => { + expect(Object.keys(item)).toEqual(["id", "newIndex"]); + expect(item.newIndex).toBe(index); + }); + }); + + it("rollbacks to the previous cache state when the API returns an error", async () => { + vi.mocked(employeeApi.updateEmployeeOrder).mockRejectedValue( + new Error("Network failure"), + ); + + const { queryClient, Wrapper } = createWrapper(); + const { result } = renderHook(() => useUpdateEmployeeOrder(), { + wrapper: Wrapper, + }); + + act(() => { + result.current.mutate({ activeId: "1", overId: "3" }); + }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + + const cached = queryClient.getQueryData(EMPLOYEES_QUERY_KEY)!; + expect(cached.map((e) => e.id)).toEqual(["1", "2", "3"]); + }); + + it("exposes the API error message after a failed mutation", async () => { + vi.mocked(employeeApi.updateEmployeeOrder).mockRejectedValue( + new Error("Server unavailable"), + ); + + const { Wrapper } = createWrapper(); + const { result } = renderHook(() => useUpdateEmployeeOrder(), { + wrapper: Wrapper, + }); + + act(() => { + result.current.mutate({ activeId: "1", overId: "2" }); + }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + expect(result.current.error?.message).toBe("Server unavailable"); + }); + + it("invalidates the employees query on settled (success path)", async () => { + vi.mocked(employeeApi.updateEmployeeOrder).mockResolvedValue(undefined); + + const { queryClient, Wrapper } = createWrapper(); + const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries"); + + const { result } = renderHook(() => useUpdateEmployeeOrder(), { + wrapper: Wrapper, + }); + + act(() => { + result.current.mutate({ activeId: "1", overId: "2" }); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(invalidateSpy).toHaveBeenCalledWith({ + queryKey: EMPLOYEES_QUERY_KEY, + }); + }); + + it("invalidates the employees query on settled (error path)", async () => { + vi.mocked(employeeApi.updateEmployeeOrder).mockRejectedValue( + new Error("fail"), + ); + + const { queryClient, Wrapper } = createWrapper(); + const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries"); + + const { result } = renderHook(() => useUpdateEmployeeOrder(), { + wrapper: Wrapper, + }); + + act(() => { + result.current.mutate({ activeId: "1", overId: "2" }); + }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + expect(invalidateSpy).toHaveBeenCalledWith({ + queryKey: EMPLOYEES_QUERY_KEY, + }); + }); +}); diff --git a/src/components/SortableRow.module.css b/src/components/SortableRow.module.css new file mode 100644 index 00000000..1190c7a6 --- /dev/null +++ b/src/components/SortableRow.module.css @@ -0,0 +1,40 @@ +.dragHandle { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 4px; + border: none; + background: transparent; + border-radius: 4px; + color: var(--color-gray-500, #6b7280); + cursor: grab; + line-height: 0; + transition: + color 0.15s ease, + background-color 0.15s ease; +} + +.dragHandle:hover { + color: var(--color-gray-900, #111827); + background-color: var(--color-gray-100, #f3f4f6); +} + +.dragHandle:focus-visible { + outline: 2px solid var(--color-indigo-500, #6366f1); + outline-offset: 2px; +} + +.dragHandle:active { + cursor: grabbing; +} + +.dragging { + opacity: 0.4; + background-color: var(--color-indigo-50, #eef2ff); +} + +.handleCell { + width: 40px; + padding: 0 8px; + vertical-align: middle; +} diff --git a/src/components/SortableRow.tsx b/src/components/SortableRow.tsx new file mode 100644 index 00000000..75efb4b2 --- /dev/null +++ b/src/components/SortableRow.tsx @@ -0,0 +1,80 @@ +import type { CSSProperties } from "react"; +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; + +import type { Employee } from "../types/employee"; +import styles from "./SortableRow.module.css"; + +interface Props { + employee: Employee; +} + +function GripVertical() { + return ( + + ); +} + +export function SortableRow({ employee }: Props) { + const { + attributes, + listeners, + setNodeRef, + setActivatorNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: employee.id }); + + const rowStyle: CSSProperties = { + transform: CSS.Transform.toString(transform), + transition, + }; + + return ( + + {/* Drag handle — only this element activates the drag sensor */} + + + + + {employee.name} + {employee.role} + + + {employee.walletAddress.slice(0, 6)}… + {employee.walletAddress.slice(-4)} + + + {employee.currency} + {employee.salary.toLocaleString()} + + ); +} diff --git a/src/hooks/useUpdateEmployeeOrder.ts b/src/hooks/useUpdateEmployeeOrder.ts new file mode 100644 index 00000000..1f53b2d5 --- /dev/null +++ b/src/hooks/useUpdateEmployeeOrder.ts @@ -0,0 +1,72 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { arrayMove } from "@dnd-kit/sortable"; + +import { updateEmployeeOrder } from "../services/employeeApi"; +import type { Employee, EmployeeOrderPayload } from "../types/employee"; + +export const EMPLOYEES_QUERY_KEY = ["employees"] as const; + +export interface ReorderArgs { + activeId: string; + overId: string; +} + +interface MutationContext { + previous: Employee[]; +} + +export function useUpdateEmployeeOrder() { + const queryClient = useQueryClient(); + + return useMutation({ + /** + * By the time mutationFn fires, onMutate has already applied the + * optimistic reorder to the cache — so we read the new order from + * the cache and build the minimal payload the API actually needs. + */ + mutationFn: async () => { + const reordered = + queryClient.getQueryData(EMPLOYEES_QUERY_KEY) ?? []; + + const payload: EmployeeOrderPayload[] = reordered.map((emp, i) => ({ + id: emp.id, + newIndex: i, + })); + + return updateEmployeeOrder(payload); + }, + + onMutate: async ({ activeId, overId }) => { + // Stop any in-flight refetch from overwriting our optimistic update. + await queryClient.cancelQueries({ queryKey: EMPLOYEES_QUERY_KEY }); + + const previous = + queryClient.getQueryData(EMPLOYEES_QUERY_KEY) ?? []; + + const activeIndex = previous.findIndex((e) => e.id === activeId); + const overIndex = previous.findIndex((e) => e.id === overId); + + if (activeIndex !== -1 && overIndex !== -1) { + queryClient.setQueryData( + EMPLOYEES_QUERY_KEY, + arrayMove(previous, activeIndex, overIndex), + ); + } + + return { previous }; + }, + + onError: (_err, _vars, context) => { + if (context?.previous) { + queryClient.setQueryData( + EMPLOYEES_QUERY_KEY, + context.previous, + ); + } + }, + + onSettled: () => { + void queryClient.invalidateQueries({ queryKey: EMPLOYEES_QUERY_KEY }); + }, + }); +} diff --git a/src/pages/EmployeeTable.module.css b/src/pages/EmployeeTable.module.css new file mode 100644 index 00000000..9551a814 --- /dev/null +++ b/src/pages/EmployeeTable.module.css @@ -0,0 +1,70 @@ +.wrapper { + padding: 24px; +} + +.title { + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 16px; + color: var(--color-gray-900, #111827); +} + +.tableContainer { + overflow-x: auto; + border: 1px solid var(--color-gray-200, #e5e7eb); + border-radius: 8px; +} + +.table { + width: 100%; + border-collapse: collapse; + font-size: 0.875rem; +} + +.table thead th { + background-color: var(--color-gray-50, #f9fafb); + color: var(--color-gray-500, #6b7280); + font-weight: 500; + text-transform: uppercase; + font-size: 0.75rem; + letter-spacing: 0.05em; + padding: 10px 12px; + text-align: left; + border-bottom: 1px solid var(--color-gray-200, #e5e7eb); +} + +.table tbody tr { + border-bottom: 1px solid var(--color-gray-100, #f3f4f6); + background-color: var(--color-white, #ffffff); + transition: background-color 0.1s ease; +} + +.table tbody tr:last-child { + border-bottom: none; +} + +.table tbody tr:hover { + background-color: var(--color-gray-50, #f9fafb); +} + +.table tbody td { + padding: 12px 12px; + color: var(--color-gray-700, #374151); + vertical-align: middle; +} + +.error { + color: var(--color-red-600, #dc2626); + padding: 16px; +} + +.skeleton { + padding: 24px; + color: var(--color-gray-400, #9ca3af); +} + +.mutationError { + margin-top: 8px; + font-size: 0.8rem; + color: var(--color-red-600, #dc2626); +} diff --git a/src/pages/EmployeeTable.tsx b/src/pages/EmployeeTable.tsx new file mode 100644 index 00000000..1e697be5 --- /dev/null +++ b/src/pages/EmployeeTable.tsx @@ -0,0 +1,113 @@ +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import type { DragEndEvent } from "@dnd-kit/core"; +import { + SortableContext, + sortableKeyboardCoordinates, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { useQuery } from "@tanstack/react-query"; + +import { SortableRow } from "../components/SortableRow"; +import { fetchEmployees } from "../services/employeeApi"; +import { + EMPLOYEES_QUERY_KEY, + useUpdateEmployeeOrder, +} from "../hooks/useUpdateEmployeeOrder"; +import styles from "./EmployeeTable.module.css"; + +export default function EmployeeTable() { + const { + data: employees, + isLoading, + isError, + } = useQuery({ + queryKey: EMPLOYEES_QUERY_KEY, + queryFn: fetchEmployees, + // Keep the stale snapshot in place while an optimistic update is live + staleTime: 30_000, + }); + + const { mutate: reorder, error: mutationError } = useUpdateEmployeeOrder(); + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); + + function onDragEnd(event: DragEndEvent) { + const { active, over } = event; + if (!over || active.id === over.id) return; + + reorder({ + activeId: String(active.id), + overId: String(over.id), + }); + } + + if (isLoading) { + return
Loading employees…
; + } + + if (isError || !employees) { + return ( +
+ Failed to load employees. Please try again. +
+ ); + } + + const employeeIds = employees.map((e) => e.id); + + return ( +
+

Employees

+ + {mutationError && ( +

+ Failed to save order: {mutationError.message}. The previous order has + been restored. +

+ )} + +
+ + + + + + + + + + + + + + {employees.map((employee) => ( + + ))} + + +
+ NameRoleWalletCurrencySalary
+
+
+
+ ); +} diff --git a/src/services/employeeApi.ts b/src/services/employeeApi.ts new file mode 100644 index 00000000..9db29be9 --- /dev/null +++ b/src/services/employeeApi.ts @@ -0,0 +1,31 @@ +import type { Employee, EmployeeOrderPayload } from "../types/employee"; + +const BASE_URL = + (import.meta.env.VITE_API_BASE_URL as string | undefined) ?? "/api"; + +async function handleResponse(res: Response): Promise { + if (!res.ok) { + const message = await res.text().catch(() => res.statusText); + throw new Error(message || `HTTP ${res.status}`); + } +} + +export async function fetchEmployees(): Promise { + const res = await fetch(`${BASE_URL}/employees`); + if (!res.ok) { + const message = await res.text().catch(() => res.statusText); + throw new Error(message || `HTTP ${res.status}`); + } + return res.json() as Promise; +} + +export async function updateEmployeeOrder( + updates: EmployeeOrderPayload[], +): Promise { + const res = await fetch(`${BASE_URL}/employees/order`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(updates), + }); + await handleResponse(res); +} diff --git a/src/types/employee.ts b/src/types/employee.ts new file mode 100644 index 00000000..66475504 --- /dev/null +++ b/src/types/employee.ts @@ -0,0 +1,15 @@ +export interface Employee { + id: string; + name: string; + role: string; + walletAddress: string; + currency: string; + salary: number; + orderIndex: number; +} + +// Payload that the backend needs to reorder rows. +export interface EmployeeOrderPayload { + id: string; + newIndex: number; +} diff --git a/tsconfig.app.json b/tsconfig.app.json index 6f1747ba..0a5292c4 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -18,7 +18,8 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true + "noUncheckedSideEffectImports": true, + "types": ["vitest/globals"] }, "include": ["src"] } diff --git a/vite.config.ts b/vite.config.ts index 07633c4b..627cd671 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,3 +1,4 @@ +/// import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import wasm from "vite-plugin-wasm"; @@ -26,6 +27,12 @@ export default defineConfig(() => { global: "window", }, envPrefix: "PUBLIC_", + test: { + environment: "jsdom", + globals: true, + setupFiles: ["./src/__tests__/setup.ts"], + css: true, + }, server: { proxy: { "/friendbot": { From 7d2ebcac3d1cd2d3348211f9c984fc94ebac86b5 Mon Sep 17 00:00:00 2001 From: trinnode Date: Mon, 23 Feb 2026 15:29:41 +0100 Subject: [PATCH 2/3] Revert "feat: implement drag-and-drop employee reordering with optimistic updates" This reverts commit 36bb09dc37738609d902afea1f21087d66ef07b6. --- package-lock.json | 1468 +---------------- package.json | 14 +- src/App.tsx | 16 +- src/__tests__/EmployeeTable.test.tsx | 180 -- src/__tests__/SortableRow.test.tsx | 125 -- src/__tests__/setup.ts | 1 - src/__tests__/useUpdateEmployeeOrder.test.tsx | 201 --- src/components/SortableRow.module.css | 40 - src/components/SortableRow.tsx | 80 - src/hooks/useUpdateEmployeeOrder.ts | 72 - src/pages/EmployeeTable.module.css | 70 - src/pages/EmployeeTable.tsx | 113 -- src/services/employeeApi.ts | 31 - src/types/employee.ts | 15 - tsconfig.app.json | 3 +- vite.config.ts | 7 - 16 files changed, 30 insertions(+), 2406 deletions(-) delete mode 100644 src/__tests__/EmployeeTable.test.tsx delete mode 100644 src/__tests__/SortableRow.test.tsx delete mode 100644 src/__tests__/setup.ts delete mode 100644 src/__tests__/useUpdateEmployeeOrder.test.tsx delete mode 100644 src/components/SortableRow.module.css delete mode 100644 src/components/SortableRow.tsx delete mode 100644 src/hooks/useUpdateEmployeeOrder.ts delete mode 100644 src/pages/EmployeeTable.module.css delete mode 100644 src/pages/EmployeeTable.tsx delete mode 100644 src/services/employeeApi.ts delete mode 100644 src/types/employee.ts diff --git a/package-lock.json b/package-lock.json index 53e40547..4f2420d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,9 +12,6 @@ ], "dependencies": { "@creit.tech/stellar-wallets-kit": "^1.9.5", - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", "@stellar/design-system": "^3.2.5", "@stellar/stellar-sdk": "^14.3.3", "@stellar/stellar-xdr-json": "^23.0.0", @@ -27,15 +24,11 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", - "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^16.3.2", - "@testing-library/user-event": "^14.6.1", "@types/lodash": "^4.17.21", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@types/react-router-dom": "^5.3.3", "@vitejs/plugin-react": "^5.1.1", - "@vitest/coverage-v8": "^4.0.18", "concurrently": "^9.2.1", "dotenv": "^17.2.3", "eslint": "^9.39.1", @@ -47,85 +40,21 @@ "glob": "^13.0.0", "globals": "^16.5.0", "husky": "^9.1.7", - "jsdom": "^28.1.0", "lint-staged": "^16.2.7", "prettier": "3.6.2", "typescript": "~5.9.3", "typescript-eslint": "^8.48.1", "vite": "^7.2.6", "vite-plugin-node-polyfills": "^0.24.0", - "vite-plugin-wasm": "^3.5.0", - "vitest": "^4.0.18" + "vite-plugin-wasm": "^3.5.0" } }, - "node_modules/@acemir/cssom": { - "version": "0.9.31", - "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", - "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", - "dev": true - }, - "node_modules/@adobe/css-tools": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", - "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", - "dev": true - }, "node_modules/@albedo-link/intent": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@albedo-link/intent/-/intent-0.12.0.tgz", "integrity": "sha512-UlGBhi0qASDYOjLrOL4484vQ26Ee3zTK2oAgvPMClOs+1XNk3zbs3dECKZv+wqeSI8SkHow8mXLTa16eVh+dQA==", "license": "MIT" }, - "node_modules/@asamuzakjp/css-color": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", - "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==", - "dev": true, - "dependencies": { - "@csstools/css-calc": "^3.0.0", - "@csstools/css-color-parser": "^4.0.1", - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0", - "lru-cache": "^11.2.5" - } - }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "dev": true, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@asamuzakjp/dom-selector": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", - "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", - "dev": true, - "dependencies": { - "@asamuzakjp/nwsapi": "^2.3.9", - "bidi-js": "^1.0.3", - "css-tree": "^3.1.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.6" - } - }, - "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "dev": true, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "dev": true - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -333,12 +262,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -423,10 +353,11 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" @@ -435,27 +366,6 @@ "node": ">=6.9.0" } }, - "node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "dev": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@bramus/specificity": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", - "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", - "dev": true, - "dependencies": { - "css-tree": "^3.0.0" - }, - "bin": { - "specificity": "bin/cli.js" - } - }, "node_modules/@creit.tech/stellar-wallets-kit": { "version": "1.9.5", "resolved": "https://registry.npmjs.org/@creit.tech/stellar-wallets-kit/-/stellar-wallets-kit-1.9.5.tgz", @@ -569,181 +479,6 @@ "node": ">=16" } }, - "node_modules/@csstools/color-helpers": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.1.tgz", - "integrity": "sha512-NmXRccUJMk2AWA5A7e5a//3bCIMyOu2hAtdRYrhPPHjDxINuCwX1w6rnIZ4xjLcp0ayv6h8Pc3X0eJUGiAAXHQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@csstools/css-calc": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", - "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.1.tgz", - "integrity": "sha512-vYwO15eRBEkeF6xjAno/KQ61HacNhfQuuU/eGwH67DplL0zD5ZixUa563phQvUelA07yDczIXdtmYojCphKJcw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "dependencies": { - "@csstools/color-helpers": "^6.0.1", - "@csstools/css-calc": "^3.0.0" - }, - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", - "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.27", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.27.tgz", - "integrity": "sha512-sxP33Jwg1bviSUXAV43cVYdmjt2TLnLXNqCWl9xmxHawWVjGz/kEbdkr7F9pxJNBN2Mh+dq0crgItbW6tQvyow==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ] - }, - "node_modules/@csstools/css-tokenizer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", - "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@dnd-kit/accessibility": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", - "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/core": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", - "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", - "dependencies": { - "@dnd-kit/accessibility": "^3.1.1", - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/sortable": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", - "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", - "dependencies": { - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "@dnd-kit/core": "^6.3.0", - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/utilities": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", - "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, "node_modules/@emurgo/cardano-serialization-lib-browser": { "version": "13.2.1", "resolved": "https://registry.npmjs.org/@emurgo/cardano-serialization-lib-browser/-/cardano-serialization-lib-browser-13.2.1.tgz", @@ -1958,27 +1693,6 @@ "license": "Apache-2.0", "peer": true }, - "node_modules/@near-js/providers/node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "optional": true, - "peer": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/@near-js/signers": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@near-js/signers/-/signers-0.2.2.tgz", @@ -3616,12 +3330,6 @@ "@stablelib/wipe": "^1.0.1" } }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true - }, "node_modules/@stellar/design-system": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/@stellar/design-system/-/design-system-3.2.5.tgz", @@ -3774,91 +3482,6 @@ "react": "^18 || ^19" } }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/jest-dom": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", - "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", - "dev": true, - "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "picocolors": "^1.1.1", - "redent": "^3.0.0" - }, - "engines": { - "node": ">=14", - "npm": ">=6", - "yarn": ">=1" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "dev": true - }, - "node_modules/@testing-library/react": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", - "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@testing-library/user-event": { - "version": "14.6.1", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", - "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", - "dev": true, - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, "node_modules/@trezor/analytics": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@trezor/analytics/-/analytics-1.4.3.tgz", @@ -4643,13 +4266,6 @@ "tslib": "^2.6.2" } }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "peer": true - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4695,16 +4311,6 @@ "@babel/types": "^7.28.2" } }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -4714,12 +4320,6 @@ "@types/node": "*" } }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -5110,168 +4710,25 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/@vitest/coverage-v8": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", - "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", - "dev": true, - "dependencies": { - "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.0.18", - "ast-v8-to-istanbul": "^0.3.10", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.2.0", - "magicast": "^0.5.1", - "obug": "^2.1.1", - "std-env": "^3.10.0", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@vitest/browser": "4.0.18", - "vitest": "4.0.18" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } + "node_modules/@wallet-standard/base": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@wallet-standard/base/-/base-1.1.0.tgz", + "integrity": "sha512-DJDQhjKmSNVLKWItoKThJS+CsJQjR9AOBOirBVT1F9YpRyC9oYHE+ZnSf8y8bxUphtKqdQMPVQ2mHohYdRvDVQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16" } }, - "node_modules/@vitest/expect": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", - "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", - "dev": true, + "node_modules/@wallet-standard/features": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@wallet-standard/features/-/features-1.1.0.tgz", + "integrity": "sha512-hiEivWNztx73s+7iLxsuD1sOJ28xtRix58W7Xnz4XzzA/pF0+aicnWgjOdA10doVDEDZdUuZCIIqG96SFNlDUg==", + "license": "Apache-2.0", "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "chai": "^6.2.1", - "tinyrainbow": "^3.0.3" + "@wallet-standard/base": "^1.1.0" }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", - "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", - "dev": true, - "dependencies": { - "@vitest/spy": "4.0.18", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/mocker/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", - "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", - "dev": true, - "dependencies": { - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", - "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", - "dev": true, - "dependencies": { - "@vitest/utils": "4.0.18", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", - "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", - "dev": true, - "dependencies": { - "@vitest/pretty-format": "4.0.18", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", - "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", - "dev": true, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", - "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", - "dev": true, - "dependencies": { - "@vitest/pretty-format": "4.0.18", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@wallet-standard/base": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@wallet-standard/base/-/base-1.1.0.tgz", - "integrity": "sha512-DJDQhjKmSNVLKWItoKThJS+CsJQjR9AOBOirBVT1F9YpRyC9oYHE+ZnSf8y8bxUphtKqdQMPVQ2mHohYdRvDVQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=16" - } - }, - "node_modules/@wallet-standard/features": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@wallet-standard/features/-/features-1.1.0.tgz", - "integrity": "sha512-hiEivWNztx73s+7iLxsuD1sOJ28xtRix58W7Xnz4XzzA/pF0+aicnWgjOdA10doVDEDZdUuZCIIqG96SFNlDUg==", - "license": "Apache-2.0", - "dependencies": { - "@wallet-standard/base": "^1.1.0" - }, - "engines": { - "node": ">=16" + "engines": { + "node": ">=16" } }, "node_modules/@walletconnect/core": { @@ -5842,15 +5299,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "dependencies": { - "dequal": "^2.0.3" - } - }, "node_modules/asn1.js": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", @@ -5882,41 +5330,6 @@ "util": "^0.12.5" } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/ast-v8-to-istanbul": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.11.tgz", - "integrity": "sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "estree-walker": "^3.0.3", - "js-tokens": "^10.0.0" - } - }, - "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "dev": true - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -6089,15 +5502,6 @@ "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", "license": "MIT" }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "dev": true, - "dependencies": { - "require-from-string": "^2.0.2" - } - }, "node_modules/big-integer": { "version": "1.6.36", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.36.tgz", @@ -6580,15 +5984,6 @@ "node": ">=20" } }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, - "engines": { - "node": ">=18" - } - }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -7103,49 +6498,6 @@ "node": "*" } }, - "node_modules/css-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", - "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", - "dev": true, - "dependencies": { - "mdn-data": "2.12.2", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", - "dev": true - }, - "node_modules/cssstyle": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.0.1.tgz", - "integrity": "sha512-IoJs7La+oFp/AB033wBStxNOJt4+9hHMxsXUPANcoXL2b3W4DZKghlJ2cI/eyeRZIQ9ysvYEorVhjrcYctWbog==", - "dev": true, - "dependencies": { - "@asamuzakjp/css-color": "^4.1.2", - "@csstools/css-syntax-patches-for-csstree": "^1.0.26", - "css-tree": "^3.1.0", - "lru-cache": "^11.2.5" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/cssstyle/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "dev": true, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -7153,85 +6505,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/data-urls": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", - "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", - "dev": true, - "dependencies": { - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/data-urls/node_modules/@exodus/bytes": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.14.1.tgz", - "integrity": "sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==", - "dev": true, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } - } - }, - "node_modules/data-urls/node_modules/@noble/hashes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", - "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/data-urls/node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "dev": true, - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/data-urls/node_modules/webidl-conversions": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", - "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", - "dev": true, - "engines": { - "node": ">=20" - } - }, - "node_modules/data-urls/node_modules/whatwg-url": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", - "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", - "dev": true, - "dependencies": { - "@exodus/bytes": "^1.11.0", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -7258,12 +6531,6 @@ "node": ">=0.10.0" } }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true - }, "node_modules/decode-uri-component": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", @@ -7351,15 +6618,6 @@ "node": ">= 0.8" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/des.js": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", @@ -7425,13 +6683,6 @@ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", "license": "MIT" }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "peer": true - }, "node_modules/domain-browser": { "version": "4.22.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz", @@ -7536,18 +6787,6 @@ "once": "^1.4.0" } }, - "node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -7579,12 +6818,6 @@ "node": ">= 0.4" } }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true - }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -8073,15 +7306,6 @@ "safe-buffer": "^5.1.1" } }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -8642,55 +7866,6 @@ "minimalistic-crypto-utils": "^1.0.1" } }, - "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", - "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", - "dev": true, - "dependencies": { - "@exodus/bytes": "^1.6.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/html-encoding-sniffer/node_modules/@exodus/bytes": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.14.1.tgz", - "integrity": "sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==", - "dev": true, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } - } - }, - "node_modules/html-encoding-sniffer/node_modules/@noble/hashes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", - "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, "node_modules/http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", @@ -8725,19 +7900,6 @@ "license": "ISC", "peer": true }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", @@ -8745,19 +7907,6 @@ "dev": true, "license": "MIT" }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -8846,15 +7995,6 @@ "node": ">=0.8.19" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -9053,12 +8193,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true - }, "node_modules/is-property": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", @@ -9174,54 +8308,6 @@ "ws": "*" } }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jayson": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.2.0.tgz", @@ -9307,112 +8393,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsdom": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", - "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", - "dev": true, - "dependencies": { - "@acemir/cssom": "^0.9.31", - "@asamuzakjp/dom-selector": "^6.8.1", - "@bramus/specificity": "^2.4.2", - "@exodus/bytes": "^1.11.0", - "cssstyle": "^6.0.1", - "data-urls": "^7.0.0", - "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^6.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", - "is-potential-custom-element-name": "^1.0.1", - "parse5": "^8.0.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.0", - "undici": "^7.21.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.1", - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/@exodus/bytes": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.14.1.tgz", - "integrity": "sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==", - "dev": true, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/@noble/hashes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", - "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/jsdom/node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "dev": true, - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/jsdom/node_modules/webidl-conversions": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", - "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", - "dev": true, - "engines": { - "node": ">=20" - } - }, - "node_modules/jsdom/node_modules/whatwg-url": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", - "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", - "dev": true, - "dependencies": { - "@exodus/bytes": "^1.11.0", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -9772,16 +8752,6 @@ "yallist": "^3.0.2" } }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "peer": true, - "bin": { - "lz-string": "bin/bin.js" - } - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -9792,32 +8762,6 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/magicast": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", - "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "source-map-js": "^1.2.1" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -9838,12 +8782,6 @@ "safe-buffer": "^5.1.2" } }, - "node_modules/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", - "dev": true - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -9911,15 +8849,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -10350,16 +9279,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ] - }, "node_modules/ofetch": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", @@ -10504,18 +9423,6 @@ "node": ">= 0.10" } }, - "node_modules/parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", - "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", - "dev": true, - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -10576,12 +9483,6 @@ "node": "20 || >=22" } }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true - }, "node_modules/pbkdf2": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", @@ -10755,44 +9656,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -11198,13 +10061,6 @@ "react": "^19.2.0" } }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "peer": true - }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -11289,19 +10145,6 @@ "node": ">= 12.13.0" } }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/require-addon": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz", @@ -11324,15 +10167,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -11578,18 +10412,6 @@ "node": ">=10" } }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -11817,12 +10639,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true - }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -11938,12 +10754,6 @@ "node": ">= 10.x" } }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true - }, "node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -11954,12 +10764,6 @@ "node": ">= 0.6" } }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true - }, "node_modules/stream-browserify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", @@ -12055,18 +10859,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -12118,12 +10910,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true - }, "node_modules/text-encoding-utf-8": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", @@ -12174,21 +10960,6 @@ "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", "license": "MIT" }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true - }, - "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", - "dev": true, - "engines": { - "node": ">=18" - } - }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -12237,33 +11008,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tinyrainbow": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tldts": { - "version": "7.0.23", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz", - "integrity": "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==", - "dev": true, - "dependencies": { - "tldts-core": "^7.0.23" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "7.0.23", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.23.tgz", - "integrity": "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==", - "dev": true - }, "node_modules/to-buffer": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", @@ -12313,18 +11057,6 @@ "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", "license": "MIT" }, - "node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", - "dev": true, - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -12579,15 +11311,6 @@ "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", "license": "MIT" }, - "node_modules/undici": { - "version": "7.22.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", - "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", - "dev": true, - "engines": { - "node": ">=20.18.1" - } - }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -13019,95 +11742,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/vitest": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", - "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", - "dev": true, - "dependencies": { - "@vitest/expect": "4.0.18", - "@vitest/mocker": "4.0.18", - "@vitest/pretty-format": "4.0.18", - "@vitest/runner": "4.0.18", - "@vitest/snapshot": "4.0.18", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "es-module-lexer": "^1.7.0", - "expect-type": "^1.2.2", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^3.10.0", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.18", - "@vitest/browser-preview": "4.0.18", - "@vitest/browser-webdriverio": "4.0.18", - "@vitest/ui": "4.0.18", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", @@ -13115,33 +11749,12 @@ "dev": true, "license": "MIT" }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, - "node_modules/whatwg-mimetype": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", - "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", - "dev": true, - "engines": { - "node": ">=20" - } - }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -13195,22 +11808,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wif": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/wif/-/wif-5.0.0.tgz", @@ -13257,21 +11854,6 @@ } } }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true - }, "node_modules/xrpl": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/xrpl/-/xrpl-4.4.3.tgz", diff --git a/package.json b/package.json index 549ea4d1..ee25cc2b 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,6 @@ "build": "tsc -b && vite build", "install:contracts": "npm install --workspace=packages && npm run build --workspace=packages", "preview": "vite preview", - "test": "vitest", - "test:run": "vitest run", - "test:coverage": "vitest run --coverage", "lint": "eslint .", "format": "prettier . --write", "prepare": "husky" @@ -21,9 +18,6 @@ ], "dependencies": { "@creit.tech/stellar-wallets-kit": "^1.9.5", - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", "@stellar/design-system": "^3.2.5", "@stellar/stellar-sdk": "^14.3.3", "@stellar/stellar-xdr-json": "^23.0.0", @@ -36,15 +30,11 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", - "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^16.3.2", - "@testing-library/user-event": "^14.6.1", "@types/lodash": "^4.17.21", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@types/react-router-dom": "^5.3.3", "@vitejs/plugin-react": "^5.1.1", - "@vitest/coverage-v8": "^4.0.18", "concurrently": "^9.2.1", "dotenv": "^17.2.3", "eslint": "^9.39.1", @@ -56,15 +46,13 @@ "glob": "^13.0.0", "globals": "^16.5.0", "husky": "^9.1.7", - "jsdom": "^28.1.0", "lint-staged": "^16.2.7", "prettier": "3.6.2", "typescript": "~5.9.3", "typescript-eslint": "^8.48.1", "vite": "^7.2.6", "vite-plugin-node-polyfills": "^0.24.0", - "vite-plugin-wasm": "^3.5.0", - "vitest": "^4.0.18" + "vite-plugin-wasm": "^3.5.0" }, "lint-staged": { "**/*": [ diff --git a/src/App.tsx b/src/App.tsx index 0eaf482f..a74f33b0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,6 @@ import Debugger from "./pages/Debugger.tsx"; import PayrollScheduler from "./pages/PayrollScheduler"; import EmployeeEntry from "./pages/EmployeeEntry"; import FeeEstimation from "./pages/FeeEstimation"; -import EmployeeTable from "./pages/EmployeeTable"; const AppLayout: React.FC = () => (
@@ -20,7 +19,7 @@ const AppLayout: React.FC = () => ( - `text-sm font-medium ${isActive ? "text-blue-600" : "text-gray-500 hover:text-gray-700"}` + `text-sm font-medium ${isActive ? 'text-blue-600' : 'text-gray-500 hover:text-gray-700'}` } > Payroll @@ -28,23 +27,15 @@ const AppLayout: React.FC = () => ( - `text-sm font-medium ${isActive ? "text-blue-600" : "text-gray-500 hover:text-gray-700"}` + `text-sm font-medium ${isActive ? 'text-blue-600' : 'text-gray-500 hover:text-gray-700'}` } > Employees - - `text-sm font-medium ${isActive ? "text-blue-600" : "text-gray-500 hover:text-gray-700"}` - } - > - Employee Table - - `text-sm font-medium ${isActive ? "text-blue-600" : "text-gray-500 hover:text-gray-700"}` + `text-sm font-medium ${isActive ? 'text-blue-600' : 'text-gray-500 hover:text-gray-700'}` } > Fee Estimator @@ -96,7 +87,6 @@ function App() { {/* } /> */} } /> } /> - } /> } /> } /> } /> diff --git a/src/__tests__/EmployeeTable.test.tsx b/src/__tests__/EmployeeTable.test.tsx deleted file mode 100644 index ff65b744..00000000 --- a/src/__tests__/EmployeeTable.test.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import { render, screen, waitFor } from "@testing-library/react"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { describe, it, expect, vi, beforeEach } from "vitest"; -import type { ReactNode } from "react"; - -import EmployeeTable from "../pages/EmployeeTable"; -import * as employeeApi from "../services/employeeApi"; -import type { Employee } from "../types/employee"; - -// ── Mocks ───────────────────────────────────────────────────────────────────── - -// Isolate from the real dnd-kit browser APIs -vi.mock("@dnd-kit/core", () => ({ - DndContext: ({ children }: { children: ReactNode }) => <>{children}, - closestCenter: vi.fn(), - KeyboardSensor: class {}, - PointerSensor: class {}, - useSensor: vi.fn(), - useSensors: vi.fn(() => []), -})); - -vi.mock("@dnd-kit/sortable", () => ({ - SortableContext: ({ children }: { children: ReactNode }) => <>{children}, - sortableKeyboardCoordinates: vi.fn(), - verticalListSortingStrategy: vi.fn(), - arrayMove: vi.fn((arr: unknown[]) => arr), -})); - -// Thin stand-in so we can assert row rendering without mounting useSortable -vi.mock("../components/SortableRow", () => ({ - SortableRow: ({ employee }: { employee: Employee }) => ( - - {employee.name} - {employee.role} - - ), -})); - -vi.mock("../services/employeeApi", () => ({ - fetchEmployees: vi.fn(), - updateEmployeeOrder: vi.fn(), -})); - -// Allow per-test control of mutationError -const mockReorder = vi.fn(); -let mockMutationError: Error | null = null; - -vi.mock("../hooks/useUpdateEmployeeOrder", () => ({ - useUpdateEmployeeOrder: () => ({ - mutate: mockReorder, - error: mockMutationError, - }), - EMPLOYEES_QUERY_KEY: ["employees"], -})); - -// ── Fixtures ─────────────────────────────────────────────────────────────────── - -const EMPLOYEES: Employee[] = [ - { - id: "1", - name: "Alice", - role: "Engineer", - walletAddress: "ADDR1", - currency: "USDC", - salary: 5000, - orderIndex: 0, - }, - { - id: "2", - name: "Bob", - role: "Designer", - walletAddress: "ADDR2", - currency: "USDC", - salary: 4500, - orderIndex: 1, - }, -]; - -function createClient() { - return new QueryClient({ - defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, - }); -} - -function renderTable(queryClient = createClient()) { - return render( - - - , - ); -} - -// ── Tests ────────────────────────────────────────────────────────────────────── - -describe("EmployeeTable", () => { - beforeEach(() => { - vi.clearAllMocks(); - mockMutationError = null; - }); - - it("shows a loading indicator while data is in-flight", () => { - vi.mocked(employeeApi.fetchEmployees).mockReturnValue( - new Promise(() => {}), - ); - - renderTable(); - - expect(screen.getByText(/loading employees/i)).toBeInTheDocument(); - }); - - it("shows an error message when fetchEmployees rejects", async () => { - vi.mocked(employeeApi.fetchEmployees).mockRejectedValue(new Error("500")); - - renderTable(); - - await waitFor(() => { - expect(screen.getByText(/failed to load employees/i)).toBeInTheDocument(); - }); - }); - - it("renders the table headings and a row for each employee", async () => { - vi.mocked(employeeApi.fetchEmployees).mockResolvedValue(EMPLOYEES); - - renderTable(); - - await waitFor(() => { - expect(screen.getByText("Name")).toBeInTheDocument(); - }); - - expect(screen.getByText("Role")).toBeInTheDocument(); - expect(screen.getByText("Wallet")).toBeInTheDocument(); - expect(screen.getByText("Currency")).toBeInTheDocument(); - expect(screen.getByText("Salary")).toBeInTheDocument(); - - const rows = screen.getAllByTestId("employee-row"); - expect(rows).toHaveLength(EMPLOYEES.length); - expect(screen.getByText("Alice")).toBeInTheDocument(); - expect(screen.getByText("Bob")).toBeInTheDocument(); - }); - - it("shows a mutation error alert when the reorder API call fails", async () => { - vi.mocked(employeeApi.fetchEmployees).mockResolvedValue(EMPLOYEES); - mockMutationError = new Error("Reorder failed"); - - renderTable(); - - await waitFor(() => { - expect(screen.getByRole("alert")).toBeInTheDocument(); - }); - - expect(screen.getByRole("alert")).toHaveTextContent(/reorder failed/i); - expect(screen.getByRole("alert")).toHaveTextContent( - /previous order has been restored/i, - ); - }); - - it("does not show the alert banner when there is no mutation error", async () => { - vi.mocked(employeeApi.fetchEmployees).mockResolvedValue(EMPLOYEES); - mockMutationError = null; - - renderTable(); - - await waitFor(() => { - expect(screen.queryByRole("alert")).not.toBeInTheDocument(); - }); - }); - - it("renders a table element with thead and tbody", async () => { - vi.mocked(employeeApi.fetchEmployees).mockResolvedValue(EMPLOYEES); - - const { container } = renderTable(); - - await waitFor(() => { - expect(container.querySelector("table")).toBeInTheDocument(); - }); - - expect(container.querySelector("thead")).toBeInTheDocument(); - expect(container.querySelector("tbody")).toBeInTheDocument(); - }); -}); diff --git a/src/__tests__/SortableRow.test.tsx b/src/__tests__/SortableRow.test.tsx deleted file mode 100644 index 720c8498..00000000 --- a/src/__tests__/SortableRow.test.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { SortableRow } from "../components/SortableRow"; -import type { Employee } from "../types/employee"; - -// ── Mocks ───────────────────────────────────────────────────────────────────── - -vi.mock("@dnd-kit/sortable", () => ({ - useSortable: vi.fn(() => ({ - attributes: { role: "button" }, - listeners: { onKeyDown: vi.fn() }, - setNodeRef: vi.fn(), - setActivatorNodeRef: vi.fn(), - transform: null, - transition: undefined, - isDragging: false, - })), -})); - -vi.mock("@dnd-kit/utilities", () => ({ - CSS: { Transform: { toString: () => "" } }, -})); - -// CSS modules resolve to plain objects in jsdom — no extra mock needed. - -// Fixtures - -const EMPLOYEE: Employee = { - id: "emp-1", - name: "Alice Nguyen", - role: "Engineer", - walletAddress: "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGPWQTESTADDR", - currency: "USDC", - salary: 5000, - orderIndex: 0, -}; - -function renderRow(employee: Employee = EMPLOYEE) { - // SortableRow renders a which requires a table context to be valid HTML. - return render( - - - - -
, - ); -} - -// ── Tests ────────────────────────────────────────────────────────────────────── - -describe("SortableRow", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("renders the employee name, role, currency and salary", () => { - renderRow(); - - expect(screen.getByText("Alice Nguyen")).toBeInTheDocument(); - expect(screen.getByText("Engineer")).toBeInTheDocument(); - expect(screen.getByText("USDC")).toBeInTheDocument(); - expect(screen.getByText("5,000")).toBeInTheDocument(); - }); - - it("truncates the wallet address and preserves full address in title", () => { - renderRow(); - - const walletSpan = screen.getByTitle(EMPLOYEE.walletAddress); - expect(walletSpan).toBeInTheDocument(); - - // First 6 chars + ellipsis + last 4 chars - const expected = `${EMPLOYEE.walletAddress.slice(0, 6)}…${EMPLOYEE.walletAddress.slice(-4)}`; - expect(walletSpan.textContent).toBe(expected); - }); - - it("renders a drag handle button with the correct aria-label", () => { - renderRow(); - - const handle = screen.getByRole("button", { name: "Reorder employee" }); - expect(handle).toBeInTheDocument(); - }); - - it("drag handle has tabIndex={0} for keyboard accessibility", () => { - renderRow(); - - const handle = screen.getByRole("button", { name: "Reorder employee" }); - expect(handle).toHaveAttribute("tabindex", "0"); - }); - - it("grip SVG icon has aria-hidden so screen readers skip it", () => { - renderRow(); - - const svg = screen - .getByRole("button", { name: "Reorder employee" }) - .querySelector("svg"); - - expect(svg).toHaveAttribute("aria-hidden", "true"); - }); - - it("applies the dragging class when isDragging is true", async () => { - const { useSortable } = await import("@dnd-kit/sortable"); - vi.mocked(useSortable).mockReturnValueOnce({ - attributes: {}, - listeners: {}, - setNodeRef: vi.fn(), - setActivatorNodeRef: vi.fn(), - transform: null, - transition: undefined, - isDragging: true, - // cast because real return type has many more fields we don't need - } as unknown as ReturnType); - - const { container } = renderRow(); - const row = container.querySelector("tr"); - - // The CSS module class name in jsdom tests resolves to the key string. - expect(row?.className).toContain("dragging"); - }); - - it("does not apply a className when isDragging is false", () => { - const { container } = renderRow(); - const row = container.querySelector("tr"); - expect(row?.className).toBeFalsy(); - }); -}); diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts deleted file mode 100644 index d0de870d..00000000 --- a/src/__tests__/setup.ts +++ /dev/null @@ -1 +0,0 @@ -import "@testing-library/jest-dom"; diff --git a/src/__tests__/useUpdateEmployeeOrder.test.tsx b/src/__tests__/useUpdateEmployeeOrder.test.tsx deleted file mode 100644 index 28edf05d..00000000 --- a/src/__tests__/useUpdateEmployeeOrder.test.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import { renderHook, waitFor, act } from "@testing-library/react"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { describe, it, expect, vi, beforeEach } from "vitest"; -import type { ReactNode } from "react"; - -import { - useUpdateEmployeeOrder, - EMPLOYEES_QUERY_KEY, -} from "../hooks/useUpdateEmployeeOrder"; -import * as employeeApi from "../services/employeeApi"; -import type { Employee } from "../types/employee"; - -// ── Mocks ───────────────────────────────────────────────────────────────────── - -vi.mock("../services/employeeApi", () => ({ - updateEmployeeOrder: vi.fn(), - fetchEmployees: vi.fn(), -})); - -// ── Fixtures ─────────────────────────────────────────────────────────────────── - -const EMPLOYEES: Employee[] = [ - { - id: "1", - name: "Alice", - role: "Eng", - walletAddress: "ADDR1111", - currency: "USDC", - salary: 5000, - orderIndex: 0, - }, - { - id: "2", - name: "Bob", - role: "Des", - walletAddress: "ADDR2222", - currency: "USDC", - salary: 4500, - orderIndex: 1, - }, - { - id: "3", - name: "Carol", - role: "PM", - walletAddress: "ADDR3333", - currency: "XLM", - salary: 6000, - orderIndex: 2, - }, -]; - -function createWrapper() { - const queryClient = new QueryClient({ - defaultOptions: { mutations: { retry: false }, queries: { retry: false } }, - }); - queryClient.setQueryData(EMPLOYEES_QUERY_KEY, [...EMPLOYEES]); - - function Wrapper({ children }: { children: ReactNode }) { - return ( - {children} - ); - } - - return { queryClient, Wrapper }; -} - -// ── Tests ────────────────────────────────────────────────────────────────────── - -describe("useUpdateEmployeeOrder", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("optimistically reorders the cache in onMutate before the API resolves", async () => { - // Never resolves — lets us inspect cache while mutationFn is in-flight - vi.mocked(employeeApi.updateEmployeeOrder).mockReturnValue( - new Promise(() => {}), - ); - - const { queryClient, Wrapper } = createWrapper(); - const { result } = renderHook(() => useUpdateEmployeeOrder(), { - wrapper: Wrapper, - }); - - act(() => { - result.current.mutate({ activeId: "1", overId: "3" }); - }); - - // onMutate is async but settles on the microtask queue - await waitFor(() => { - const cached = queryClient.getQueryData(EMPLOYEES_QUERY_KEY)!; - // Alice (id=1) should now be at index 2, Carol (id=3) at index 0 - expect(cached[0].id).toBe("2"); - expect(cached[1].id).toBe("3"); - expect(cached[2].id).toBe("1"); - }); - }); - - it("sends only minimal { id, newIndex } payload — not the full Employee object", async () => { - vi.mocked(employeeApi.updateEmployeeOrder).mockResolvedValue(undefined); - - const { Wrapper } = createWrapper(); - const { result } = renderHook(() => useUpdateEmployeeOrder(), { - wrapper: Wrapper, - }); - - act(() => { - result.current.mutate({ activeId: "1", overId: "2" }); - }); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - const callArg = vi.mocked(employeeApi.updateEmployeeOrder).mock.calls[0][0]; - - // Every item must only carry id and newIndex - callArg.forEach((item, index) => { - expect(Object.keys(item)).toEqual(["id", "newIndex"]); - expect(item.newIndex).toBe(index); - }); - }); - - it("rollbacks to the previous cache state when the API returns an error", async () => { - vi.mocked(employeeApi.updateEmployeeOrder).mockRejectedValue( - new Error("Network failure"), - ); - - const { queryClient, Wrapper } = createWrapper(); - const { result } = renderHook(() => useUpdateEmployeeOrder(), { - wrapper: Wrapper, - }); - - act(() => { - result.current.mutate({ activeId: "1", overId: "3" }); - }); - - await waitFor(() => expect(result.current.isError).toBe(true)); - - const cached = queryClient.getQueryData(EMPLOYEES_QUERY_KEY)!; - expect(cached.map((e) => e.id)).toEqual(["1", "2", "3"]); - }); - - it("exposes the API error message after a failed mutation", async () => { - vi.mocked(employeeApi.updateEmployeeOrder).mockRejectedValue( - new Error("Server unavailable"), - ); - - const { Wrapper } = createWrapper(); - const { result } = renderHook(() => useUpdateEmployeeOrder(), { - wrapper: Wrapper, - }); - - act(() => { - result.current.mutate({ activeId: "1", overId: "2" }); - }); - - await waitFor(() => expect(result.current.isError).toBe(true)); - expect(result.current.error?.message).toBe("Server unavailable"); - }); - - it("invalidates the employees query on settled (success path)", async () => { - vi.mocked(employeeApi.updateEmployeeOrder).mockResolvedValue(undefined); - - const { queryClient, Wrapper } = createWrapper(); - const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries"); - - const { result } = renderHook(() => useUpdateEmployeeOrder(), { - wrapper: Wrapper, - }); - - act(() => { - result.current.mutate({ activeId: "1", overId: "2" }); - }); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - expect(invalidateSpy).toHaveBeenCalledWith({ - queryKey: EMPLOYEES_QUERY_KEY, - }); - }); - - it("invalidates the employees query on settled (error path)", async () => { - vi.mocked(employeeApi.updateEmployeeOrder).mockRejectedValue( - new Error("fail"), - ); - - const { queryClient, Wrapper } = createWrapper(); - const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries"); - - const { result } = renderHook(() => useUpdateEmployeeOrder(), { - wrapper: Wrapper, - }); - - act(() => { - result.current.mutate({ activeId: "1", overId: "2" }); - }); - - await waitFor(() => expect(result.current.isError).toBe(true)); - expect(invalidateSpy).toHaveBeenCalledWith({ - queryKey: EMPLOYEES_QUERY_KEY, - }); - }); -}); diff --git a/src/components/SortableRow.module.css b/src/components/SortableRow.module.css deleted file mode 100644 index 1190c7a6..00000000 --- a/src/components/SortableRow.module.css +++ /dev/null @@ -1,40 +0,0 @@ -.dragHandle { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 4px; - border: none; - background: transparent; - border-radius: 4px; - color: var(--color-gray-500, #6b7280); - cursor: grab; - line-height: 0; - transition: - color 0.15s ease, - background-color 0.15s ease; -} - -.dragHandle:hover { - color: var(--color-gray-900, #111827); - background-color: var(--color-gray-100, #f3f4f6); -} - -.dragHandle:focus-visible { - outline: 2px solid var(--color-indigo-500, #6366f1); - outline-offset: 2px; -} - -.dragHandle:active { - cursor: grabbing; -} - -.dragging { - opacity: 0.4; - background-color: var(--color-indigo-50, #eef2ff); -} - -.handleCell { - width: 40px; - padding: 0 8px; - vertical-align: middle; -} diff --git a/src/components/SortableRow.tsx b/src/components/SortableRow.tsx deleted file mode 100644 index 75efb4b2..00000000 --- a/src/components/SortableRow.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import type { CSSProperties } from "react"; -import { useSortable } from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; - -import type { Employee } from "../types/employee"; -import styles from "./SortableRow.module.css"; - -interface Props { - employee: Employee; -} - -function GripVertical() { - return ( - - ); -} - -export function SortableRow({ employee }: Props) { - const { - attributes, - listeners, - setNodeRef, - setActivatorNodeRef, - transform, - transition, - isDragging, - } = useSortable({ id: employee.id }); - - const rowStyle: CSSProperties = { - transform: CSS.Transform.toString(transform), - transition, - }; - - return ( - - {/* Drag handle — only this element activates the drag sensor */} - - - - - {employee.name} - {employee.role} - - - {employee.walletAddress.slice(0, 6)}… - {employee.walletAddress.slice(-4)} - - - {employee.currency} - {employee.salary.toLocaleString()} - - ); -} diff --git a/src/hooks/useUpdateEmployeeOrder.ts b/src/hooks/useUpdateEmployeeOrder.ts deleted file mode 100644 index 1f53b2d5..00000000 --- a/src/hooks/useUpdateEmployeeOrder.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { arrayMove } from "@dnd-kit/sortable"; - -import { updateEmployeeOrder } from "../services/employeeApi"; -import type { Employee, EmployeeOrderPayload } from "../types/employee"; - -export const EMPLOYEES_QUERY_KEY = ["employees"] as const; - -export interface ReorderArgs { - activeId: string; - overId: string; -} - -interface MutationContext { - previous: Employee[]; -} - -export function useUpdateEmployeeOrder() { - const queryClient = useQueryClient(); - - return useMutation({ - /** - * By the time mutationFn fires, onMutate has already applied the - * optimistic reorder to the cache — so we read the new order from - * the cache and build the minimal payload the API actually needs. - */ - mutationFn: async () => { - const reordered = - queryClient.getQueryData(EMPLOYEES_QUERY_KEY) ?? []; - - const payload: EmployeeOrderPayload[] = reordered.map((emp, i) => ({ - id: emp.id, - newIndex: i, - })); - - return updateEmployeeOrder(payload); - }, - - onMutate: async ({ activeId, overId }) => { - // Stop any in-flight refetch from overwriting our optimistic update. - await queryClient.cancelQueries({ queryKey: EMPLOYEES_QUERY_KEY }); - - const previous = - queryClient.getQueryData(EMPLOYEES_QUERY_KEY) ?? []; - - const activeIndex = previous.findIndex((e) => e.id === activeId); - const overIndex = previous.findIndex((e) => e.id === overId); - - if (activeIndex !== -1 && overIndex !== -1) { - queryClient.setQueryData( - EMPLOYEES_QUERY_KEY, - arrayMove(previous, activeIndex, overIndex), - ); - } - - return { previous }; - }, - - onError: (_err, _vars, context) => { - if (context?.previous) { - queryClient.setQueryData( - EMPLOYEES_QUERY_KEY, - context.previous, - ); - } - }, - - onSettled: () => { - void queryClient.invalidateQueries({ queryKey: EMPLOYEES_QUERY_KEY }); - }, - }); -} diff --git a/src/pages/EmployeeTable.module.css b/src/pages/EmployeeTable.module.css deleted file mode 100644 index 9551a814..00000000 --- a/src/pages/EmployeeTable.module.css +++ /dev/null @@ -1,70 +0,0 @@ -.wrapper { - padding: 24px; -} - -.title { - font-size: 1.25rem; - font-weight: 600; - margin-bottom: 16px; - color: var(--color-gray-900, #111827); -} - -.tableContainer { - overflow-x: auto; - border: 1px solid var(--color-gray-200, #e5e7eb); - border-radius: 8px; -} - -.table { - width: 100%; - border-collapse: collapse; - font-size: 0.875rem; -} - -.table thead th { - background-color: var(--color-gray-50, #f9fafb); - color: var(--color-gray-500, #6b7280); - font-weight: 500; - text-transform: uppercase; - font-size: 0.75rem; - letter-spacing: 0.05em; - padding: 10px 12px; - text-align: left; - border-bottom: 1px solid var(--color-gray-200, #e5e7eb); -} - -.table tbody tr { - border-bottom: 1px solid var(--color-gray-100, #f3f4f6); - background-color: var(--color-white, #ffffff); - transition: background-color 0.1s ease; -} - -.table tbody tr:last-child { - border-bottom: none; -} - -.table tbody tr:hover { - background-color: var(--color-gray-50, #f9fafb); -} - -.table tbody td { - padding: 12px 12px; - color: var(--color-gray-700, #374151); - vertical-align: middle; -} - -.error { - color: var(--color-red-600, #dc2626); - padding: 16px; -} - -.skeleton { - padding: 24px; - color: var(--color-gray-400, #9ca3af); -} - -.mutationError { - margin-top: 8px; - font-size: 0.8rem; - color: var(--color-red-600, #dc2626); -} diff --git a/src/pages/EmployeeTable.tsx b/src/pages/EmployeeTable.tsx deleted file mode 100644 index 1e697be5..00000000 --- a/src/pages/EmployeeTable.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { - DndContext, - closestCenter, - KeyboardSensor, - PointerSensor, - useSensor, - useSensors, -} from "@dnd-kit/core"; -import type { DragEndEvent } from "@dnd-kit/core"; -import { - SortableContext, - sortableKeyboardCoordinates, - verticalListSortingStrategy, -} from "@dnd-kit/sortable"; -import { useQuery } from "@tanstack/react-query"; - -import { SortableRow } from "../components/SortableRow"; -import { fetchEmployees } from "../services/employeeApi"; -import { - EMPLOYEES_QUERY_KEY, - useUpdateEmployeeOrder, -} from "../hooks/useUpdateEmployeeOrder"; -import styles from "./EmployeeTable.module.css"; - -export default function EmployeeTable() { - const { - data: employees, - isLoading, - isError, - } = useQuery({ - queryKey: EMPLOYEES_QUERY_KEY, - queryFn: fetchEmployees, - // Keep the stale snapshot in place while an optimistic update is live - staleTime: 30_000, - }); - - const { mutate: reorder, error: mutationError } = useUpdateEmployeeOrder(); - - const sensors = useSensors( - useSensor(PointerSensor), - useSensor(KeyboardSensor, { - coordinateGetter: sortableKeyboardCoordinates, - }), - ); - - function onDragEnd(event: DragEndEvent) { - const { active, over } = event; - if (!over || active.id === over.id) return; - - reorder({ - activeId: String(active.id), - overId: String(over.id), - }); - } - - if (isLoading) { - return
Loading employees…
; - } - - if (isError || !employees) { - return ( -
- Failed to load employees. Please try again. -
- ); - } - - const employeeIds = employees.map((e) => e.id); - - return ( -
-

Employees

- - {mutationError && ( -

- Failed to save order: {mutationError.message}. The previous order has - been restored. -

- )} - -
- - - - - - - - - - - - - - {employees.map((employee) => ( - - ))} - - -
- NameRoleWalletCurrencySalary
-
-
-
- ); -} diff --git a/src/services/employeeApi.ts b/src/services/employeeApi.ts deleted file mode 100644 index 9db29be9..00000000 --- a/src/services/employeeApi.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { Employee, EmployeeOrderPayload } from "../types/employee"; - -const BASE_URL = - (import.meta.env.VITE_API_BASE_URL as string | undefined) ?? "/api"; - -async function handleResponse(res: Response): Promise { - if (!res.ok) { - const message = await res.text().catch(() => res.statusText); - throw new Error(message || `HTTP ${res.status}`); - } -} - -export async function fetchEmployees(): Promise { - const res = await fetch(`${BASE_URL}/employees`); - if (!res.ok) { - const message = await res.text().catch(() => res.statusText); - throw new Error(message || `HTTP ${res.status}`); - } - return res.json() as Promise; -} - -export async function updateEmployeeOrder( - updates: EmployeeOrderPayload[], -): Promise { - const res = await fetch(`${BASE_URL}/employees/order`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(updates), - }); - await handleResponse(res); -} diff --git a/src/types/employee.ts b/src/types/employee.ts deleted file mode 100644 index 66475504..00000000 --- a/src/types/employee.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface Employee { - id: string; - name: string; - role: string; - walletAddress: string; - currency: string; - salary: number; - orderIndex: number; -} - -// Payload that the backend needs to reorder rows. -export interface EmployeeOrderPayload { - id: string; - newIndex: number; -} diff --git a/tsconfig.app.json b/tsconfig.app.json index 0a5292c4..6f1747ba 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -18,8 +18,7 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true, - "types": ["vitest/globals"] + "noUncheckedSideEffectImports": true }, "include": ["src"] } diff --git a/vite.config.ts b/vite.config.ts index 627cd671..07633c4b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,4 +1,3 @@ -/// import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import wasm from "vite-plugin-wasm"; @@ -27,12 +26,6 @@ export default defineConfig(() => { global: "window", }, envPrefix: "PUBLIC_", - test: { - environment: "jsdom", - globals: true, - setupFiles: ["./src/__tests__/setup.ts"], - css: true, - }, server: { proxy: { "/friendbot": { From 411044f7e65e60866dc028518c8dc84fa15b2dea Mon Sep 17 00:00:00 2001 From: trinnode Date: Mon, 23 Feb 2026 16:06:43 +0100 Subject: [PATCH 3/3] feat: restore employee DnD reorder implementation --- src/__tests__/EmployeeTable.test.tsx | 180 ++++++++++++++++ src/__tests__/SortableRow.test.tsx | 125 +++++++++++ src/__tests__/useUpdateEmployeeOrder.test.tsx | 201 ++++++++++++++++++ src/components/SortableRow.module.css | 40 ++++ src/components/SortableRow.tsx | 80 +++++++ src/hooks/useUpdateEmployeeOrder.ts | 72 +++++++ src/pages/EmployeeTable.module.css | 70 ++++++ src/pages/EmployeeTable.tsx | 113 ++++++++++ src/services/employeeApi.ts | 31 +++ src/types/employee.ts | 15 ++ 10 files changed, 927 insertions(+) create mode 100644 src/__tests__/EmployeeTable.test.tsx create mode 100644 src/__tests__/SortableRow.test.tsx create mode 100644 src/__tests__/useUpdateEmployeeOrder.test.tsx create mode 100644 src/components/SortableRow.module.css create mode 100644 src/components/SortableRow.tsx create mode 100644 src/hooks/useUpdateEmployeeOrder.ts create mode 100644 src/pages/EmployeeTable.module.css create mode 100644 src/pages/EmployeeTable.tsx create mode 100644 src/services/employeeApi.ts create mode 100644 src/types/employee.ts diff --git a/src/__tests__/EmployeeTable.test.tsx b/src/__tests__/EmployeeTable.test.tsx new file mode 100644 index 00000000..ff65b744 --- /dev/null +++ b/src/__tests__/EmployeeTable.test.tsx @@ -0,0 +1,180 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import type { ReactNode } from "react"; + +import EmployeeTable from "../pages/EmployeeTable"; +import * as employeeApi from "../services/employeeApi"; +import type { Employee } from "../types/employee"; + +// ── Mocks ───────────────────────────────────────────────────────────────────── + +// Isolate from the real dnd-kit browser APIs +vi.mock("@dnd-kit/core", () => ({ + DndContext: ({ children }: { children: ReactNode }) => <>{children}, + closestCenter: vi.fn(), + KeyboardSensor: class {}, + PointerSensor: class {}, + useSensor: vi.fn(), + useSensors: vi.fn(() => []), +})); + +vi.mock("@dnd-kit/sortable", () => ({ + SortableContext: ({ children }: { children: ReactNode }) => <>{children}, + sortableKeyboardCoordinates: vi.fn(), + verticalListSortingStrategy: vi.fn(), + arrayMove: vi.fn((arr: unknown[]) => arr), +})); + +// Thin stand-in so we can assert row rendering without mounting useSortable +vi.mock("../components/SortableRow", () => ({ + SortableRow: ({ employee }: { employee: Employee }) => ( + + {employee.name} + {employee.role} + + ), +})); + +vi.mock("../services/employeeApi", () => ({ + fetchEmployees: vi.fn(), + updateEmployeeOrder: vi.fn(), +})); + +// Allow per-test control of mutationError +const mockReorder = vi.fn(); +let mockMutationError: Error | null = null; + +vi.mock("../hooks/useUpdateEmployeeOrder", () => ({ + useUpdateEmployeeOrder: () => ({ + mutate: mockReorder, + error: mockMutationError, + }), + EMPLOYEES_QUERY_KEY: ["employees"], +})); + +// ── Fixtures ─────────────────────────────────────────────────────────────────── + +const EMPLOYEES: Employee[] = [ + { + id: "1", + name: "Alice", + role: "Engineer", + walletAddress: "ADDR1", + currency: "USDC", + salary: 5000, + orderIndex: 0, + }, + { + id: "2", + name: "Bob", + role: "Designer", + walletAddress: "ADDR2", + currency: "USDC", + salary: 4500, + orderIndex: 1, + }, +]; + +function createClient() { + return new QueryClient({ + defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, + }); +} + +function renderTable(queryClient = createClient()) { + return render( + + + , + ); +} + +// ── Tests ────────────────────────────────────────────────────────────────────── + +describe("EmployeeTable", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockMutationError = null; + }); + + it("shows a loading indicator while data is in-flight", () => { + vi.mocked(employeeApi.fetchEmployees).mockReturnValue( + new Promise(() => {}), + ); + + renderTable(); + + expect(screen.getByText(/loading employees/i)).toBeInTheDocument(); + }); + + it("shows an error message when fetchEmployees rejects", async () => { + vi.mocked(employeeApi.fetchEmployees).mockRejectedValue(new Error("500")); + + renderTable(); + + await waitFor(() => { + expect(screen.getByText(/failed to load employees/i)).toBeInTheDocument(); + }); + }); + + it("renders the table headings and a row for each employee", async () => { + vi.mocked(employeeApi.fetchEmployees).mockResolvedValue(EMPLOYEES); + + renderTable(); + + await waitFor(() => { + expect(screen.getByText("Name")).toBeInTheDocument(); + }); + + expect(screen.getByText("Role")).toBeInTheDocument(); + expect(screen.getByText("Wallet")).toBeInTheDocument(); + expect(screen.getByText("Currency")).toBeInTheDocument(); + expect(screen.getByText("Salary")).toBeInTheDocument(); + + const rows = screen.getAllByTestId("employee-row"); + expect(rows).toHaveLength(EMPLOYEES.length); + expect(screen.getByText("Alice")).toBeInTheDocument(); + expect(screen.getByText("Bob")).toBeInTheDocument(); + }); + + it("shows a mutation error alert when the reorder API call fails", async () => { + vi.mocked(employeeApi.fetchEmployees).mockResolvedValue(EMPLOYEES); + mockMutationError = new Error("Reorder failed"); + + renderTable(); + + await waitFor(() => { + expect(screen.getByRole("alert")).toBeInTheDocument(); + }); + + expect(screen.getByRole("alert")).toHaveTextContent(/reorder failed/i); + expect(screen.getByRole("alert")).toHaveTextContent( + /previous order has been restored/i, + ); + }); + + it("does not show the alert banner when there is no mutation error", async () => { + vi.mocked(employeeApi.fetchEmployees).mockResolvedValue(EMPLOYEES); + mockMutationError = null; + + renderTable(); + + await waitFor(() => { + expect(screen.queryByRole("alert")).not.toBeInTheDocument(); + }); + }); + + it("renders a table element with thead and tbody", async () => { + vi.mocked(employeeApi.fetchEmployees).mockResolvedValue(EMPLOYEES); + + const { container } = renderTable(); + + await waitFor(() => { + expect(container.querySelector("table")).toBeInTheDocument(); + }); + + expect(container.querySelector("thead")).toBeInTheDocument(); + expect(container.querySelector("tbody")).toBeInTheDocument(); + }); +}); diff --git a/src/__tests__/SortableRow.test.tsx b/src/__tests__/SortableRow.test.tsx new file mode 100644 index 00000000..720c8498 --- /dev/null +++ b/src/__tests__/SortableRow.test.tsx @@ -0,0 +1,125 @@ +import { render, screen } from "@testing-library/react"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { SortableRow } from "../components/SortableRow"; +import type { Employee } from "../types/employee"; + +// ── Mocks ───────────────────────────────────────────────────────────────────── + +vi.mock("@dnd-kit/sortable", () => ({ + useSortable: vi.fn(() => ({ + attributes: { role: "button" }, + listeners: { onKeyDown: vi.fn() }, + setNodeRef: vi.fn(), + setActivatorNodeRef: vi.fn(), + transform: null, + transition: undefined, + isDragging: false, + })), +})); + +vi.mock("@dnd-kit/utilities", () => ({ + CSS: { Transform: { toString: () => "" } }, +})); + +// CSS modules resolve to plain objects in jsdom — no extra mock needed. + +// Fixtures + +const EMPLOYEE: Employee = { + id: "emp-1", + name: "Alice Nguyen", + role: "Engineer", + walletAddress: "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGPWQTESTADDR", + currency: "USDC", + salary: 5000, + orderIndex: 0, +}; + +function renderRow(employee: Employee = EMPLOYEE) { + // SortableRow renders a which requires a table context to be valid HTML. + return render( + + + + +
, + ); +} + +// ── Tests ────────────────────────────────────────────────────────────────────── + +describe("SortableRow", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders the employee name, role, currency and salary", () => { + renderRow(); + + expect(screen.getByText("Alice Nguyen")).toBeInTheDocument(); + expect(screen.getByText("Engineer")).toBeInTheDocument(); + expect(screen.getByText("USDC")).toBeInTheDocument(); + expect(screen.getByText("5,000")).toBeInTheDocument(); + }); + + it("truncates the wallet address and preserves full address in title", () => { + renderRow(); + + const walletSpan = screen.getByTitle(EMPLOYEE.walletAddress); + expect(walletSpan).toBeInTheDocument(); + + // First 6 chars + ellipsis + last 4 chars + const expected = `${EMPLOYEE.walletAddress.slice(0, 6)}…${EMPLOYEE.walletAddress.slice(-4)}`; + expect(walletSpan.textContent).toBe(expected); + }); + + it("renders a drag handle button with the correct aria-label", () => { + renderRow(); + + const handle = screen.getByRole("button", { name: "Reorder employee" }); + expect(handle).toBeInTheDocument(); + }); + + it("drag handle has tabIndex={0} for keyboard accessibility", () => { + renderRow(); + + const handle = screen.getByRole("button", { name: "Reorder employee" }); + expect(handle).toHaveAttribute("tabindex", "0"); + }); + + it("grip SVG icon has aria-hidden so screen readers skip it", () => { + renderRow(); + + const svg = screen + .getByRole("button", { name: "Reorder employee" }) + .querySelector("svg"); + + expect(svg).toHaveAttribute("aria-hidden", "true"); + }); + + it("applies the dragging class when isDragging is true", async () => { + const { useSortable } = await import("@dnd-kit/sortable"); + vi.mocked(useSortable).mockReturnValueOnce({ + attributes: {}, + listeners: {}, + setNodeRef: vi.fn(), + setActivatorNodeRef: vi.fn(), + transform: null, + transition: undefined, + isDragging: true, + // cast because real return type has many more fields we don't need + } as unknown as ReturnType); + + const { container } = renderRow(); + const row = container.querySelector("tr"); + + // The CSS module class name in jsdom tests resolves to the key string. + expect(row?.className).toContain("dragging"); + }); + + it("does not apply a className when isDragging is false", () => { + const { container } = renderRow(); + const row = container.querySelector("tr"); + expect(row?.className).toBeFalsy(); + }); +}); diff --git a/src/__tests__/useUpdateEmployeeOrder.test.tsx b/src/__tests__/useUpdateEmployeeOrder.test.tsx new file mode 100644 index 00000000..28edf05d --- /dev/null +++ b/src/__tests__/useUpdateEmployeeOrder.test.tsx @@ -0,0 +1,201 @@ +import { renderHook, waitFor, act } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import type { ReactNode } from "react"; + +import { + useUpdateEmployeeOrder, + EMPLOYEES_QUERY_KEY, +} from "../hooks/useUpdateEmployeeOrder"; +import * as employeeApi from "../services/employeeApi"; +import type { Employee } from "../types/employee"; + +// ── Mocks ───────────────────────────────────────────────────────────────────── + +vi.mock("../services/employeeApi", () => ({ + updateEmployeeOrder: vi.fn(), + fetchEmployees: vi.fn(), +})); + +// ── Fixtures ─────────────────────────────────────────────────────────────────── + +const EMPLOYEES: Employee[] = [ + { + id: "1", + name: "Alice", + role: "Eng", + walletAddress: "ADDR1111", + currency: "USDC", + salary: 5000, + orderIndex: 0, + }, + { + id: "2", + name: "Bob", + role: "Des", + walletAddress: "ADDR2222", + currency: "USDC", + salary: 4500, + orderIndex: 1, + }, + { + id: "3", + name: "Carol", + role: "PM", + walletAddress: "ADDR3333", + currency: "XLM", + salary: 6000, + orderIndex: 2, + }, +]; + +function createWrapper() { + const queryClient = new QueryClient({ + defaultOptions: { mutations: { retry: false }, queries: { retry: false } }, + }); + queryClient.setQueryData(EMPLOYEES_QUERY_KEY, [...EMPLOYEES]); + + function Wrapper({ children }: { children: ReactNode }) { + return ( + {children} + ); + } + + return { queryClient, Wrapper }; +} + +// ── Tests ────────────────────────────────────────────────────────────────────── + +describe("useUpdateEmployeeOrder", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("optimistically reorders the cache in onMutate before the API resolves", async () => { + // Never resolves — lets us inspect cache while mutationFn is in-flight + vi.mocked(employeeApi.updateEmployeeOrder).mockReturnValue( + new Promise(() => {}), + ); + + const { queryClient, Wrapper } = createWrapper(); + const { result } = renderHook(() => useUpdateEmployeeOrder(), { + wrapper: Wrapper, + }); + + act(() => { + result.current.mutate({ activeId: "1", overId: "3" }); + }); + + // onMutate is async but settles on the microtask queue + await waitFor(() => { + const cached = queryClient.getQueryData(EMPLOYEES_QUERY_KEY)!; + // Alice (id=1) should now be at index 2, Carol (id=3) at index 0 + expect(cached[0].id).toBe("2"); + expect(cached[1].id).toBe("3"); + expect(cached[2].id).toBe("1"); + }); + }); + + it("sends only minimal { id, newIndex } payload — not the full Employee object", async () => { + vi.mocked(employeeApi.updateEmployeeOrder).mockResolvedValue(undefined); + + const { Wrapper } = createWrapper(); + const { result } = renderHook(() => useUpdateEmployeeOrder(), { + wrapper: Wrapper, + }); + + act(() => { + result.current.mutate({ activeId: "1", overId: "2" }); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + const callArg = vi.mocked(employeeApi.updateEmployeeOrder).mock.calls[0][0]; + + // Every item must only carry id and newIndex + callArg.forEach((item, index) => { + expect(Object.keys(item)).toEqual(["id", "newIndex"]); + expect(item.newIndex).toBe(index); + }); + }); + + it("rollbacks to the previous cache state when the API returns an error", async () => { + vi.mocked(employeeApi.updateEmployeeOrder).mockRejectedValue( + new Error("Network failure"), + ); + + const { queryClient, Wrapper } = createWrapper(); + const { result } = renderHook(() => useUpdateEmployeeOrder(), { + wrapper: Wrapper, + }); + + act(() => { + result.current.mutate({ activeId: "1", overId: "3" }); + }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + + const cached = queryClient.getQueryData(EMPLOYEES_QUERY_KEY)!; + expect(cached.map((e) => e.id)).toEqual(["1", "2", "3"]); + }); + + it("exposes the API error message after a failed mutation", async () => { + vi.mocked(employeeApi.updateEmployeeOrder).mockRejectedValue( + new Error("Server unavailable"), + ); + + const { Wrapper } = createWrapper(); + const { result } = renderHook(() => useUpdateEmployeeOrder(), { + wrapper: Wrapper, + }); + + act(() => { + result.current.mutate({ activeId: "1", overId: "2" }); + }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + expect(result.current.error?.message).toBe("Server unavailable"); + }); + + it("invalidates the employees query on settled (success path)", async () => { + vi.mocked(employeeApi.updateEmployeeOrder).mockResolvedValue(undefined); + + const { queryClient, Wrapper } = createWrapper(); + const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries"); + + const { result } = renderHook(() => useUpdateEmployeeOrder(), { + wrapper: Wrapper, + }); + + act(() => { + result.current.mutate({ activeId: "1", overId: "2" }); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(invalidateSpy).toHaveBeenCalledWith({ + queryKey: EMPLOYEES_QUERY_KEY, + }); + }); + + it("invalidates the employees query on settled (error path)", async () => { + vi.mocked(employeeApi.updateEmployeeOrder).mockRejectedValue( + new Error("fail"), + ); + + const { queryClient, Wrapper } = createWrapper(); + const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries"); + + const { result } = renderHook(() => useUpdateEmployeeOrder(), { + wrapper: Wrapper, + }); + + act(() => { + result.current.mutate({ activeId: "1", overId: "2" }); + }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + expect(invalidateSpy).toHaveBeenCalledWith({ + queryKey: EMPLOYEES_QUERY_KEY, + }); + }); +}); diff --git a/src/components/SortableRow.module.css b/src/components/SortableRow.module.css new file mode 100644 index 00000000..1190c7a6 --- /dev/null +++ b/src/components/SortableRow.module.css @@ -0,0 +1,40 @@ +.dragHandle { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 4px; + border: none; + background: transparent; + border-radius: 4px; + color: var(--color-gray-500, #6b7280); + cursor: grab; + line-height: 0; + transition: + color 0.15s ease, + background-color 0.15s ease; +} + +.dragHandle:hover { + color: var(--color-gray-900, #111827); + background-color: var(--color-gray-100, #f3f4f6); +} + +.dragHandle:focus-visible { + outline: 2px solid var(--color-indigo-500, #6366f1); + outline-offset: 2px; +} + +.dragHandle:active { + cursor: grabbing; +} + +.dragging { + opacity: 0.4; + background-color: var(--color-indigo-50, #eef2ff); +} + +.handleCell { + width: 40px; + padding: 0 8px; + vertical-align: middle; +} diff --git a/src/components/SortableRow.tsx b/src/components/SortableRow.tsx new file mode 100644 index 00000000..75efb4b2 --- /dev/null +++ b/src/components/SortableRow.tsx @@ -0,0 +1,80 @@ +import type { CSSProperties } from "react"; +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; + +import type { Employee } from "../types/employee"; +import styles from "./SortableRow.module.css"; + +interface Props { + employee: Employee; +} + +function GripVertical() { + return ( + + ); +} + +export function SortableRow({ employee }: Props) { + const { + attributes, + listeners, + setNodeRef, + setActivatorNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: employee.id }); + + const rowStyle: CSSProperties = { + transform: CSS.Transform.toString(transform), + transition, + }; + + return ( + + {/* Drag handle — only this element activates the drag sensor */} + + + + + {employee.name} + {employee.role} + + + {employee.walletAddress.slice(0, 6)}… + {employee.walletAddress.slice(-4)} + + + {employee.currency} + {employee.salary.toLocaleString()} + + ); +} diff --git a/src/hooks/useUpdateEmployeeOrder.ts b/src/hooks/useUpdateEmployeeOrder.ts new file mode 100644 index 00000000..1f53b2d5 --- /dev/null +++ b/src/hooks/useUpdateEmployeeOrder.ts @@ -0,0 +1,72 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { arrayMove } from "@dnd-kit/sortable"; + +import { updateEmployeeOrder } from "../services/employeeApi"; +import type { Employee, EmployeeOrderPayload } from "../types/employee"; + +export const EMPLOYEES_QUERY_KEY = ["employees"] as const; + +export interface ReorderArgs { + activeId: string; + overId: string; +} + +interface MutationContext { + previous: Employee[]; +} + +export function useUpdateEmployeeOrder() { + const queryClient = useQueryClient(); + + return useMutation({ + /** + * By the time mutationFn fires, onMutate has already applied the + * optimistic reorder to the cache — so we read the new order from + * the cache and build the minimal payload the API actually needs. + */ + mutationFn: async () => { + const reordered = + queryClient.getQueryData(EMPLOYEES_QUERY_KEY) ?? []; + + const payload: EmployeeOrderPayload[] = reordered.map((emp, i) => ({ + id: emp.id, + newIndex: i, + })); + + return updateEmployeeOrder(payload); + }, + + onMutate: async ({ activeId, overId }) => { + // Stop any in-flight refetch from overwriting our optimistic update. + await queryClient.cancelQueries({ queryKey: EMPLOYEES_QUERY_KEY }); + + const previous = + queryClient.getQueryData(EMPLOYEES_QUERY_KEY) ?? []; + + const activeIndex = previous.findIndex((e) => e.id === activeId); + const overIndex = previous.findIndex((e) => e.id === overId); + + if (activeIndex !== -1 && overIndex !== -1) { + queryClient.setQueryData( + EMPLOYEES_QUERY_KEY, + arrayMove(previous, activeIndex, overIndex), + ); + } + + return { previous }; + }, + + onError: (_err, _vars, context) => { + if (context?.previous) { + queryClient.setQueryData( + EMPLOYEES_QUERY_KEY, + context.previous, + ); + } + }, + + onSettled: () => { + void queryClient.invalidateQueries({ queryKey: EMPLOYEES_QUERY_KEY }); + }, + }); +} diff --git a/src/pages/EmployeeTable.module.css b/src/pages/EmployeeTable.module.css new file mode 100644 index 00000000..9551a814 --- /dev/null +++ b/src/pages/EmployeeTable.module.css @@ -0,0 +1,70 @@ +.wrapper { + padding: 24px; +} + +.title { + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 16px; + color: var(--color-gray-900, #111827); +} + +.tableContainer { + overflow-x: auto; + border: 1px solid var(--color-gray-200, #e5e7eb); + border-radius: 8px; +} + +.table { + width: 100%; + border-collapse: collapse; + font-size: 0.875rem; +} + +.table thead th { + background-color: var(--color-gray-50, #f9fafb); + color: var(--color-gray-500, #6b7280); + font-weight: 500; + text-transform: uppercase; + font-size: 0.75rem; + letter-spacing: 0.05em; + padding: 10px 12px; + text-align: left; + border-bottom: 1px solid var(--color-gray-200, #e5e7eb); +} + +.table tbody tr { + border-bottom: 1px solid var(--color-gray-100, #f3f4f6); + background-color: var(--color-white, #ffffff); + transition: background-color 0.1s ease; +} + +.table tbody tr:last-child { + border-bottom: none; +} + +.table tbody tr:hover { + background-color: var(--color-gray-50, #f9fafb); +} + +.table tbody td { + padding: 12px 12px; + color: var(--color-gray-700, #374151); + vertical-align: middle; +} + +.error { + color: var(--color-red-600, #dc2626); + padding: 16px; +} + +.skeleton { + padding: 24px; + color: var(--color-gray-400, #9ca3af); +} + +.mutationError { + margin-top: 8px; + font-size: 0.8rem; + color: var(--color-red-600, #dc2626); +} diff --git a/src/pages/EmployeeTable.tsx b/src/pages/EmployeeTable.tsx new file mode 100644 index 00000000..1e697be5 --- /dev/null +++ b/src/pages/EmployeeTable.tsx @@ -0,0 +1,113 @@ +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import type { DragEndEvent } from "@dnd-kit/core"; +import { + SortableContext, + sortableKeyboardCoordinates, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { useQuery } from "@tanstack/react-query"; + +import { SortableRow } from "../components/SortableRow"; +import { fetchEmployees } from "../services/employeeApi"; +import { + EMPLOYEES_QUERY_KEY, + useUpdateEmployeeOrder, +} from "../hooks/useUpdateEmployeeOrder"; +import styles from "./EmployeeTable.module.css"; + +export default function EmployeeTable() { + const { + data: employees, + isLoading, + isError, + } = useQuery({ + queryKey: EMPLOYEES_QUERY_KEY, + queryFn: fetchEmployees, + // Keep the stale snapshot in place while an optimistic update is live + staleTime: 30_000, + }); + + const { mutate: reorder, error: mutationError } = useUpdateEmployeeOrder(); + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); + + function onDragEnd(event: DragEndEvent) { + const { active, over } = event; + if (!over || active.id === over.id) return; + + reorder({ + activeId: String(active.id), + overId: String(over.id), + }); + } + + if (isLoading) { + return
Loading employees…
; + } + + if (isError || !employees) { + return ( +
+ Failed to load employees. Please try again. +
+ ); + } + + const employeeIds = employees.map((e) => e.id); + + return ( +
+

Employees

+ + {mutationError && ( +

+ Failed to save order: {mutationError.message}. The previous order has + been restored. +

+ )} + +
+ + + + + + + + + + + + + + {employees.map((employee) => ( + + ))} + + +
+ NameRoleWalletCurrencySalary
+
+
+
+ ); +} diff --git a/src/services/employeeApi.ts b/src/services/employeeApi.ts new file mode 100644 index 00000000..d4fb721d --- /dev/null +++ b/src/services/employeeApi.ts @@ -0,0 +1,31 @@ +import type { Employee, EmployeeOrderPayload } from "../types/employee"; + +const BASE_URL = (import.meta.env.VITE_API_BASE_URL as string | undefined) ?? "/api"; + +async function handleResponse(res: Response): Promise { + if (!res.ok) { + const message = await res.text().catch(() => res.statusText); + throw new Error(message || `HTTP ${res.status}`); + } +} + +export async function fetchEmployees(): Promise { + const res = await fetch(`${BASE_URL}/employees`); + if (!res.ok) { + const message = await res.text().catch(() => res.statusText); + throw new Error(message || `HTTP ${res.status}`); + } + return res.json() as Promise; +} + +export async function updateEmployeeOrder( + updates: EmployeeOrderPayload[], +): Promise { + const res = await fetch(`${BASE_URL}/employees/order`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(updates), + }); + + await handleResponse(res); +} diff --git a/src/types/employee.ts b/src/types/employee.ts new file mode 100644 index 00000000..66475504 --- /dev/null +++ b/src/types/employee.ts @@ -0,0 +1,15 @@ +export interface Employee { + id: string; + name: string; + role: string; + walletAddress: string; + currency: string; + salary: number; + orderIndex: number; +} + +// Payload that the backend needs to reorder rows. +export interface EmployeeOrderPayload { + id: string; + newIndex: number; +}