diff --git a/web-app/package-lock.json b/web-app/package-lock.json index a23e7cd..ef6e137 100644 --- a/web-app/package-lock.json +++ b/web-app/package-lock.json @@ -31,6 +31,7 @@ "@vitejs/plugin-vue": "^5.2.1", "@vue/tsconfig": "^0.7.0", "gh-pages": "^6.1.1", + "jsdom": "^27.0.0", "npm-run-all2": "^7.0.2", "typescript": "~5.7.3", "vite": "^6.0.11", @@ -73,6 +74,61 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.5.tgz", + "integrity": "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.1" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.5.6", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.5.6.tgz", + "integrity": "sha512-Mj3Hu9ymlsERd7WOsUKNUZnJYL4IZ/I9wVVYgtvOsWYiEFbkQ4G7VRIh2USxTVW4BBDIsLG+gBUgqOqf2Kvqow==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.1" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "devOptional": true, + "license": "ISC", + "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==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -609,6 +665,144 @@ "zustand": "5.0.3" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.14.tgz", + "integrity": "sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@ctrl/tinycolor": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", @@ -4994,6 +5188,16 @@ } } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/alien-signals": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", @@ -5162,6 +5366,16 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/big.js": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.2.tgz", @@ -5710,12 +5924,92 @@ "uncrypto": "^0.1.3" } }, + "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==", + "devOptional": true, + "license": "MIT", + "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/cssstyle": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.1.tgz", + "integrity": "sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.0.3", + "@csstools/css-syntax-patches-for-csstree": "^1.0.14", + "css-tree": "^3.1.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/data-urls": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", + "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -5771,6 +6065,13 @@ "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==", + "devOptional": true, + "license": "MIT" + }, "node_modules/decode-uri-component": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", @@ -7036,6 +7337,47 @@ "dev": true, "license": "MIT" }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", @@ -7046,6 +7388,19 @@ "node": ">=18.18.0" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/idb-keyval": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz", @@ -7245,6 +7600,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -7410,6 +7772,105 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/jsdom": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.0.tgz", + "integrity": "sha512-lIHeR1qlIRrIN5VMccd8tI2Sgw6ieYXSVktcSHaNe3Z5nE/tcPQYQWOq00wxMvYOsz+73eAkNenVvmPC6bba9A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/dom-selector": "^6.5.4", + "cssstyle": "^5.3.0", + "data-urls": "^6.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^7.3.0", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0", + "ws": "^8.18.2", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -7651,6 +8112,13 @@ "node": ">= 0.4" } }, + "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==", + "devOptional": true, + "license": "CC0-1.0" + }, "node_modules/memoize-one": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", @@ -8239,6 +8707,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -8524,6 +9018,16 @@ "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qrcode": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", @@ -8671,6 +9175,16 @@ "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==", + "devOptional": true, + "license": "MIT", + "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", @@ -8736,6 +9250,13 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "devOptional": true, + "license": "MIT" + }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -8819,6 +9340,13 @@ "node": ">=10" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/sass": { "version": "1.93.1", "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.1.tgz", @@ -8839,6 +9367,19 @@ "@parcel/watcher": "^2.4.1" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -9277,6 +9818,13 @@ "node": ">=14.0.0" } }, + "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==", + "devOptional": true, + "license": "MIT" + }, "node_modules/thread-stream": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-0.15.2.tgz", @@ -9379,6 +9927,26 @@ "@popperjs/core": "^2.9.0" } }, + "node_modules/tldts": { + "version": "7.0.16", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.16.tgz", + "integrity": "sha512-5bdPHSwbKTeHmXrgecID4Ljff8rQjv7g8zKQPkCozRo2HWWni+p310FSn5ImI+9kWw9kK4lzOB5q/a6iv0IJsw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.16" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.16", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.16.tgz", + "integrity": "sha512-XHhPmHxphLi+LGbH0G/O7dmUH9V65OY20R7vH8gETHsp5AZCjBk9l8sqmRKLaGOxnETU7XNSDUPtewAy/K6jbA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/to-buffer": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", @@ -9416,6 +9984,19 @@ "node": ">=6" } }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "devOptional": true, + "license": "BSD-3-Clause", + "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", @@ -10275,6 +10856,19 @@ "typescript": ">=5.0.0" } }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/webextension-polyfill": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/webextension-polyfill/-/webextension-polyfill-0.10.0.tgz", @@ -10287,6 +10881,29 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -10489,6 +11106,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "devOptional": true, + "license": "MIT" + }, "node_modules/xmlhttprequest-ssl": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", diff --git a/web-app/package.json b/web-app/package.json index d5ea688..86860d2 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -9,7 +9,8 @@ "preview": "vite preview", "build-only": "vite build", "type-check": "vue-tsc --build", - "subtree": "git subtree push --prefix dist origin gh-pages" + "subtree": "git subtree push --prefix dist origin gh-pages", + "test": "vitest" }, "dependencies": { "@tanstack/vue-query": "^5.64.2", @@ -35,6 +36,7 @@ "@vitejs/plugin-vue": "^5.2.1", "@vue/tsconfig": "^0.7.0", "gh-pages": "^6.1.1", + "jsdom": "^27.0.0", "npm-run-all2": "^7.0.2", "typescript": "~5.7.3", "vite": "^6.0.11", diff --git a/web-app/src/components/StakeHolder.vue b/web-app/src/components/StakeHolder.vue index 0667eca..8931161 100644 --- a/web-app/src/components/StakeHolder.vue +++ b/web-app/src/components/StakeHolder.vue @@ -1,127 +1,95 @@ + \ No newline at end of file diff --git a/web-app/src/composables/__tests__/useSnatchSelection.spec.ts b/web-app/src/composables/__tests__/useSnatchSelection.spec.ts new file mode 100644 index 0000000..a62aa9e --- /dev/null +++ b/web-app/src/composables/__tests__/useSnatchSelection.spec.ts @@ -0,0 +1,274 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { ref, nextTick } from 'vue' +import { useSnatchSelection } from '../useSnatchSelection' +import { usePositions } from '../usePositions' +import { useStake } from '../useStake' +import { useWallet } from '../useWallet' +import { useStatCollection } from '../useStatCollection' +import { useAdjustTaxRate } from '../useAdjustTaxRates' + +// Mock all composables +vi.mock('../usePositions', () => ({ + usePositions: vi.fn() +})) + +vi.mock('../useStake', () => ({ + useStake: vi.fn() +})) + +vi.mock('../useWallet', () => ({ + useWallet: vi.fn() +})) + +vi.mock('../useStatCollection', () => ({ + useStatCollection: vi.fn() +})) + +vi.mock('../useAdjustTaxRates', () => ({ + useAdjustTaxRate: vi.fn() +})) + +vi.mock('kraiken-lib', () => ({ + calculateSnatchShortfall: vi.fn((outstandingStake, stakingShares, stakeTotalSupply) => { + return stakingShares > outstandingStake ? 0n : outstandingStake - stakingShares + }), + selectSnatchPositions: vi.fn((candidates, options) => { + if (candidates.length === 0) { + return { selected: [], remainingShortfall: options.shortfallShares } + } + return { + selected: candidates, + remainingShortfall: 0n, + maxSelectedTaxRateIndex: candidates[candidates.length - 1].taxRateIndex + } + }), + minimumTaxRate: vi.fn(() => 0.01) +})) + +describe('useSnatchSelection', () => { + beforeEach(() => { + // Reset all mocks + vi.clearAllMocks() + + // Setup default mock values with proper refs + vi.mocked(usePositions).mockReturnValue({ + activePositions: ref([]) + } as any) + + vi.mocked(useStake).mockReturnValue({ + stakingAmountShares: 0n, + taxRate: 1.0 + } as any) + + vi.mocked(useWallet).mockReturnValue({ + account: { address: '0x123' } + } as any) + + // Mock with realistic values for local computation + // stakeTotalSupply is typically 10^(18+7) = 10^25 from contract + // kraikenTotalSupply is dynamic but starts around 10^25 + vi.mocked(useStatCollection).mockReturnValue({ + stakeTotalSupply: 10000000000000000000000000n, // 10^25 + kraikenTotalSupply: 10000000000000000000000000n, // 10^25 + outstandingStake: 500n + } as any) + + vi.mocked(useAdjustTaxRate).mockReturnValue({ + taxRates: [{ year: 1 }] + } as any) + }) + + it('should initialize with empty snatchable positions', () => { + const { snatchablePositions } = useSnatchSelection() + expect(snatchablePositions.value).toEqual([]) + }) + + it('should handle no active positions', () => { + const { snatchablePositions, floorTax } = useSnatchSelection() + expect(snatchablePositions.value).toEqual([]) + expect(floorTax.value).toBe(1) + }) + + it('should handle no shortfall', () => { + vi.mocked(useStatCollection).mockReturnValue({ + stakeTotalSupply: 1000n, + outstandingStake: 100n + }) + + vi.mocked(useStake).mockReturnValue({ + stakingAmountShares: 900n, + taxRate: 1.0 + }) + + const { snatchablePositions, openPositionsAvailable } = useSnatchSelection() + expect(snatchablePositions.value).toEqual([]) + expect(openPositionsAvailable.value).toBe(true) + }) + + it('should filter out positions with higher tax rate', async () => { + vi.mocked(usePositions).mockReturnValue({ + activePositions: ref([ + { + positionId: 1n, + owner: '0x456', + harbDeposit: 100n, + taxRate: 2.0, + taxRateIndex: 1, + iAmOwner: false + } + ]) + } as any) + + vi.mocked(useStake).mockReturnValue({ + stakingAmountShares: 100n, + taxRate: 1.0 + } as any) + + const { snatchablePositions } = useSnatchSelection() + + // Wait for watchEffect to run + await new Promise(resolve => setTimeout(resolve, 0)) + + expect(snatchablePositions.value).toEqual([]) + }) + + it('should filter out owned positions by default', async () => { + vi.mocked(usePositions).mockReturnValue({ + activePositions: ref([ + { + positionId: 1n, + owner: '0x123', + harbDeposit: 100n, + taxRate: 0.5, + taxRateIndex: 1, + iAmOwner: true + } + ]) + } as any) + + const { snatchablePositions } = useSnatchSelection() + await new Promise(resolve => setTimeout(resolve, 0)) + expect(snatchablePositions.value).toEqual([]) + }) + + it('should include owned positions when demo mode is enabled', async () => { + const position = { + positionId: 1n, + owner: '0x123', + harbDeposit: 100n, + taxRate: 0.005, // 0.5% tax rate (less than maxTaxRate) + taxRateIndex: 1, + iAmOwner: true + } + + vi.mocked(usePositions).mockReturnValue({ + activePositions: ref([position]) + } as any) + + vi.mocked(useStake).mockReturnValue({ + stakingAmountShares: 100n, + taxRate: 1.0 // Will be converted to 0.01 (1%) decimal + } as any) + + // Need outstandingStake > stakingAmountShares to create shortfall + vi.mocked(useStatCollection).mockReturnValue({ + stakeTotalSupply: 10000000000000000000000000n, + kraikenTotalSupply: 10000000000000000000000000n, + outstandingStake: 500n + } as any) + + const { snatchablePositions } = useSnatchSelection(true) + + // Wait for watchEffect to run (no longer async) + await nextTick() + + expect(snatchablePositions.value).toContainEqual(position) + }) + + it('should handle partial fills', async () => { + const position1 = { + positionId: 1n, + owner: '0x456', + harbDeposit: 100n, + taxRate: 0.005, // 0.5% tax rate + taxRateIndex: 1, + iAmOwner: false + } + + const position2 = { + positionId: 2n, + owner: '0x789', + harbDeposit: 200n, + taxRate: 0.006, // 0.6% tax rate + taxRateIndex: 2, + iAmOwner: false + } + + vi.mocked(usePositions).mockReturnValue({ + activePositions: ref([position1, position2]) + } as any) + + vi.mocked(useStake).mockReturnValue({ + stakingAmountShares: 150n, + taxRate: 1.0 // Will be converted to 0.01 (1%) decimal + } as any) + + // Need outstandingStake > stakingAmountShares to create shortfall + vi.mocked(useStatCollection).mockReturnValue({ + stakeTotalSupply: 10000000000000000000000000n, + kraikenTotalSupply: 10000000000000000000000000n, + outstandingStake: 500n + } as any) + + const { snatchablePositions } = useSnatchSelection() + + // Wait for watchEffect to run + await nextTick() + + expect(snatchablePositions.value).toEqual([position1, position2]) + }) + + it('should update floor tax based on selected positions', async () => { + const position = { + positionId: 1n, + owner: '0x456', + harbDeposit: 100n, + taxRate: 0.005, // 0.5% tax rate + taxRateIndex: 1, + iAmOwner: false + } + + vi.mocked(usePositions).mockReturnValue({ + activePositions: ref([position]) + } as any) + + vi.mocked(useStake).mockReturnValue({ + stakingAmountShares: 100n, + taxRate: 1.0 // Will be converted to 0.01 (1%) decimal + } as any) + + // Need outstandingStake > stakingAmountShares to create shortfall + vi.mocked(useStatCollection).mockReturnValue({ + stakeTotalSupply: 10000000000000000000000000n, + kraikenTotalSupply: 10000000000000000000000000n, + outstandingStake: 500n + } as any) + + vi.mocked(useAdjustTaxRate).mockReturnValue({ + taxRates: [ + { year: 1 }, + { year: 2 }, + { year: 3 } + ] + } as any) + + const { floorTax } = useSnatchSelection() + + // Wait for watchEffect to run + await nextTick() + + // Floor tax should be taxRates[maxSelectedTaxRateIndex + 1] + // Position has taxRateIndex: 1, so nextIndex = 2, taxRates[2] = { year: 3 } + expect(floorTax.value).toBe(3) + }) +}) \ No newline at end of file diff --git a/web-app/src/composables/useSnatchSelection.ts b/web-app/src/composables/useSnatchSelection.ts new file mode 100644 index 0000000..d1a5f12 --- /dev/null +++ b/web-app/src/composables/useSnatchSelection.ts @@ -0,0 +1,180 @@ +import { ref, watchEffect, computed } from 'vue' +import { usePositions, type Position } from './usePositions' +import { useStake } from './useStake' +import { useWallet } from './useWallet' +import { useStatCollection } from './useStatCollection' +import { useAdjustTaxRate } from './useAdjustTaxRates' +import { + calculateSnatchShortfall, + selectSnatchPositions, + minimumTaxRate, + type SnatchablePosition, +} from 'kraiken-lib' + +/** + * Converts Kraiken token assets to shares using the same formula as Stake.sol: + * shares = (assets * stakeTotalSupply) / kraikenTotalSupply + * + * @param assets - Amount of Kraiken tokens + * @param kraikenTotalSupply - Total supply of Kraiken tokens + * @param stakeTotalSupply - Total supply of stake shares (constant from contract) + * @returns Number of shares corresponding to the assets + */ +function assetsToSharesLocal( + assets: bigint, + kraikenTotalSupply: bigint, + stakeTotalSupply: bigint +): bigint { + if (kraikenTotalSupply === 0n) { + return 0n + } + // Equivalent to: assets.mulDiv(stakeTotalSupply, kraikenTotalSupply, Math.Rounding.Down) + return (assets * stakeTotalSupply) / kraikenTotalSupply +} + + +export function useSnatchSelection(demo = false) { + const { activePositions } = usePositions() + const stake = useStake() + const wallet = useWallet() + const statCollection = useStatCollection() + const adjustTaxRate = useAdjustTaxRate() + + const snatchablePositions = ref([]) + const shortfallShares = ref(0n) + const floorTax = ref(1) + let selectionRun = 0 + + const openPositionsAvailable = computed(() => shortfallShares.value <= 0n) + + function getMinFloorTax() { + const minRate = minimumTaxRate(activePositions.value, 0) + return Math.round(minRate * 100) + } + + watchEffect((onCleanup) => { + const runId = ++selectionRun + let cancelled = false + onCleanup(() => { + cancelled = true + }) + + // No longer async since we compute shares locally + const compute = () => { + if (statCollection.stakeTotalSupply === 0n) { + shortfallShares.value = 0n + if (!cancelled && runId === selectionRun) { + snatchablePositions.value = [] + floorTax.value = getMinFloorTax() + } + return + } + + const stakingShares = stake.stakingAmountShares ?? 0n + const shortfall = calculateSnatchShortfall( + statCollection.outstandingStake, + stakingShares, + statCollection.stakeTotalSupply, + 2n, + 10n + ) + + shortfallShares.value = shortfall + + if (shortfall <= 0n) { + if (!cancelled && runId === selectionRun) { + snatchablePositions.value = [] + floorTax.value = getMinFloorTax() + } + return + } + + const maxTaxRateDecimal = (stake.taxRate ?? 0) / 100 + const includeOwned = demo + const recipient = wallet.account.address ?? null + + const eligiblePositions = activePositions.value.filter((position: Position) => { + if (position.taxRate >= maxTaxRateDecimal) { + return false + } + if (!includeOwned && position.iAmOwner) { + return false + } + return true + }) + + if (eligiblePositions.length === 0) { + if (!cancelled && runId === selectionRun) { + snatchablePositions.value = [] + floorTax.value = getMinFloorTax() + } + return + } + + const candidates: SnatchablePosition[] = [] + + // Compute shares locally using the same formula as Stake.sol + for (const position of eligiblePositions) { + const shares = assetsToSharesLocal( + position.harbDeposit, + statCollection.kraikenTotalSupply, + statCollection.stakeTotalSupply + ) + candidates.push({ + id: position.positionId, + owner: position.owner, + stakeShares: shares, + taxRate: position.taxRate, + taxRateIndex: position.taxRateIndex, + }) + } + + const selection = selectSnatchPositions(candidates, { + shortfallShares: shortfall, + maxTaxRate: maxTaxRateDecimal, + includeOwned, + recipientAddress: recipient, + }) + + if (cancelled || runId !== selectionRun) { + return + } + + if (selection.remainingShortfall > 0n) { + snatchablePositions.value = [] + floorTax.value = getMinFloorTax() + return + } + + const positionById = new Map() + for (const position of activePositions.value) { + positionById.set(position.positionId, position) + } + + const selectedPositions = selection.selected + .map((candidate) => positionById.get(candidate.id)) + .filter((value): value is Position => Boolean(value)) + + snatchablePositions.value = selectedPositions + + if (selection.maxSelectedTaxRateIndex !== undefined) { + const nextIndex = selection.maxSelectedTaxRateIndex + 1 + const option = + adjustTaxRate.taxRates[nextIndex] ?? + adjustTaxRate.taxRates[selection.maxSelectedTaxRateIndex] + floorTax.value = option ? option.year : getMinFloorTax() + } else { + floorTax.value = getMinFloorTax() + } + } + + compute() + }) + + return { + snatchablePositions, + shortfallShares, + floorTax, + openPositionsAvailable, + } +} \ No newline at end of file diff --git a/web-app/vitest.config.ts b/web-app/vitest.config.ts new file mode 100644 index 0000000..27cd8e3 --- /dev/null +++ b/web-app/vitest.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import { fileURLToPath } from 'node:url' + +export default defineConfig({ + plugins: [vue()], + test: { + environment: 'jsdom', + globals: true, + }, + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + } +}) \ No newline at end of file