mirror of
https://github.com/tiennm99/loldle.git
synced 2026-09-03 08:19:03 +00:00
test: add characterization tests for lib layer
Cover classic-mode comparison rules, game-engine state machine and localStorage persistence, and champion-data search/seeding/loading with Vitest. Storage keys are asserted literally so returning players' saved games and stats cannot silently break. Node environment with a hand-rolled localStorage stub; fixtures are four hand-written champions so the weekly data sync cannot destabilise the suite.
This commit is contained in:
+5
-2
@@ -7,7 +7,9 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
"lint": "eslint",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "16.2.10",
|
||||
@@ -18,6 +20,7 @@
|
||||
"@tailwindcss/postcss": "^4.3.3",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.10",
|
||||
"tailwindcss": "^4.3.3"
|
||||
"tailwindcss": "^4.3.3",
|
||||
"vitest": "^4.1.10"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+543
@@ -35,6 +35,9 @@ importers:
|
||||
tailwindcss:
|
||||
specifier: ^4.3.3
|
||||
version: 4.3.3
|
||||
vitest:
|
||||
specifier: ^4.1.10
|
||||
version: 4.1.10(vite@8.1.5(jiti@2.7.0))
|
||||
|
||||
packages:
|
||||
|
||||
@@ -129,12 +132,21 @@ packages:
|
||||
'@emnapi/core@1.10.0':
|
||||
resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
|
||||
|
||||
'@emnapi/core@1.11.1':
|
||||
resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==}
|
||||
|
||||
'@emnapi/runtime@1.10.0':
|
||||
resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
|
||||
|
||||
'@emnapi/runtime@1.11.1':
|
||||
resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==}
|
||||
|
||||
'@emnapi/wasi-threads@1.2.1':
|
||||
resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
|
||||
|
||||
'@emnapi/wasi-threads@1.2.2':
|
||||
resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
|
||||
|
||||
'@eslint-community/eslint-utils@4.9.1':
|
||||
resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@@ -368,6 +380,12 @@ packages:
|
||||
'@emnapi/core': ^1.7.1
|
||||
'@emnapi/runtime': ^1.7.1
|
||||
|
||||
'@napi-rs/wasm-runtime@1.1.6':
|
||||
resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==}
|
||||
peerDependencies:
|
||||
'@emnapi/core': ^1.7.1
|
||||
'@emnapi/runtime': ^1.7.1
|
||||
|
||||
'@next/env@16.2.10':
|
||||
resolution: {integrity: sha512-zLPxg9M0MEHmygpj5OuxjQ+vHMiy/K7cSp74G8ecYolmgUWw0RwN02tF56npup/+qaI8JB97hQgS/r2Hb6QwVA==}
|
||||
|
||||
@@ -442,9 +460,113 @@ packages:
|
||||
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
|
||||
engines: {node: '>=12.4.0'}
|
||||
|
||||
'@oxc-project/types@0.139.0':
|
||||
resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==}
|
||||
|
||||
'@rolldown/binding-android-arm64@1.1.5':
|
||||
resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rolldown/binding-darwin-arm64@1.1.5':
|
||||
resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/binding-darwin-x64@1.1.5':
|
||||
resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/binding-freebsd-x64@1.1.5':
|
||||
resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.1.5':
|
||||
resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-arm64-gnu@1.1.5':
|
||||
resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.1.5':
|
||||
resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-linux-ppc64-gnu@1.1.5':
|
||||
resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-s390x-gnu@1.1.5':
|
||||
resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.1.5':
|
||||
resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.1.5':
|
||||
resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.1.5':
|
||||
resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@rolldown/binding-wasm32-wasi@1.1.5':
|
||||
resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@rolldown/binding-win32-arm64-msvc@1.1.5':
|
||||
resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rolldown/binding-win32-x64-msvc@1.1.5':
|
||||
resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@rolldown/pluginutils@1.0.1':
|
||||
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
|
||||
|
||||
'@rtsao/scc@1.1.0':
|
||||
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
||||
|
||||
'@standard-schema/spec@1.1.0':
|
||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||
|
||||
@@ -543,6 +665,15 @@ packages:
|
||||
'@tybys/wasm-util@0.10.2':
|
||||
resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
|
||||
|
||||
'@tybys/wasm-util@0.10.3':
|
||||
resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
|
||||
|
||||
'@types/chai@5.2.3':
|
||||
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
||||
|
||||
'@types/deep-eql@4.0.2':
|
||||
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
||||
|
||||
'@types/estree@1.0.9':
|
||||
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
||||
|
||||
@@ -731,6 +862,35 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@vitest/expect@4.1.10':
|
||||
resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==}
|
||||
|
||||
'@vitest/mocker@4.1.10':
|
||||
resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==}
|
||||
peerDependencies:
|
||||
msw: ^2.4.9
|
||||
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
msw:
|
||||
optional: true
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
'@vitest/pretty-format@4.1.10':
|
||||
resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==}
|
||||
|
||||
'@vitest/runner@4.1.10':
|
||||
resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==}
|
||||
|
||||
'@vitest/snapshot@4.1.10':
|
||||
resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==}
|
||||
|
||||
'@vitest/spy@4.1.10':
|
||||
resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==}
|
||||
|
||||
'@vitest/utils@4.1.10':
|
||||
resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==}
|
||||
|
||||
acorn-jsx@5.3.2:
|
||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||
peerDependencies:
|
||||
@@ -787,6 +947,10 @@ packages:
|
||||
resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
assertion-error@2.0.1:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ast-types-flow@0.0.8:
|
||||
resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
|
||||
|
||||
@@ -853,6 +1017,10 @@ packages:
|
||||
caniuse-lite@1.0.30001793:
|
||||
resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==}
|
||||
|
||||
chai@6.2.2:
|
||||
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
chalk@4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -958,6 +1126,9 @@ packages:
|
||||
resolution: {integrity: sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-module-lexer@2.3.1:
|
||||
resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1098,10 +1269,17 @@ packages:
|
||||
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
|
||||
engines: {node: '>=4.0'}
|
||||
|
||||
estree-walker@3.0.3:
|
||||
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||
|
||||
esutils@2.0.3:
|
||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
expect-type@1.4.0:
|
||||
resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
@@ -1150,6 +1328,11 @@ packages:
|
||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
function-bind@1.1.2:
|
||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||
|
||||
@@ -1618,6 +1801,10 @@ packages:
|
||||
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
obug@2.1.4:
|
||||
resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
|
||||
optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -1649,6 +1836,9 @@ packages:
|
||||
path-parse@1.0.7:
|
||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
@@ -1660,6 +1850,10 @@ packages:
|
||||
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
picomatch@4.0.5:
|
||||
resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
possible-typed-array-names@1.1.0:
|
||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1718,6 +1912,11 @@ packages:
|
||||
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||
|
||||
rolldown@1.1.5:
|
||||
resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
|
||||
run-parallel@1.2.0:
|
||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||
|
||||
@@ -1785,6 +1984,9 @@ packages:
|
||||
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
siginfo@2.0.0:
|
||||
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1792,6 +1994,12 @@ packages:
|
||||
stable-hash@0.0.5:
|
||||
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
|
||||
|
||||
stackback@0.0.2:
|
||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||
|
||||
std-env@4.2.0:
|
||||
resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==}
|
||||
|
||||
stop-iteration-iterator@1.1.0:
|
||||
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1855,10 +2063,25 @@ packages:
|
||||
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tinybench@2.9.0:
|
||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||
|
||||
tinyexec@1.2.4:
|
||||
resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tinyglobby@0.2.16:
|
||||
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tinyglobby@0.2.17:
|
||||
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tinyrainbow@3.1.0:
|
||||
resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
@@ -1923,6 +2146,90 @@ packages:
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
vite@8.1.5:
|
||||
resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@types/node': ^20.19.0 || >=22.12.0
|
||||
'@vitejs/devtools': ^0.3.0
|
||||
esbuild: ^0.27.0 || ^0.28.0
|
||||
jiti: '>=1.21.0'
|
||||
less: ^4.0.0
|
||||
sass: ^1.70.0
|
||||
sass-embedded: ^1.70.0
|
||||
stylus: '>=0.54.8'
|
||||
sugarss: ^5.0.0
|
||||
terser: ^5.16.0
|
||||
tsx: ^4.8.1
|
||||
yaml: ^2.4.2
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
'@vitejs/devtools':
|
||||
optional: true
|
||||
esbuild:
|
||||
optional: true
|
||||
jiti:
|
||||
optional: true
|
||||
less:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
sass-embedded:
|
||||
optional: true
|
||||
stylus:
|
||||
optional: true
|
||||
sugarss:
|
||||
optional: true
|
||||
terser:
|
||||
optional: true
|
||||
tsx:
|
||||
optional: true
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
vitest@4.1.10:
|
||||
resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==}
|
||||
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@edge-runtime/vm': '*'
|
||||
'@opentelemetry/api': ^1.9.0
|
||||
'@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
|
||||
'@vitest/browser-playwright': 4.1.10
|
||||
'@vitest/browser-preview': 4.1.10
|
||||
'@vitest/browser-webdriverio': 4.1.10
|
||||
'@vitest/coverage-istanbul': 4.1.10
|
||||
'@vitest/coverage-v8': 4.1.10
|
||||
'@vitest/ui': 4.1.10
|
||||
happy-dom: '*'
|
||||
jsdom: '*'
|
||||
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
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/coverage-istanbul':
|
||||
optional: true
|
||||
'@vitest/coverage-v8':
|
||||
optional: true
|
||||
'@vitest/ui':
|
||||
optional: true
|
||||
happy-dom:
|
||||
optional: true
|
||||
jsdom:
|
||||
optional: true
|
||||
|
||||
which-boxed-primitive@1.1.1:
|
||||
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1944,6 +2251,11 @@ packages:
|
||||
engines: {node: '>= 8'}
|
||||
hasBin: true
|
||||
|
||||
why-is-node-running@2.3.0:
|
||||
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
|
||||
word-wrap@1.2.5:
|
||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2087,16 +2399,32 @@ snapshots:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/core@1.11.1':
|
||||
dependencies:
|
||||
'@emnapi/wasi-threads': 1.2.2
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/runtime@1.10.0':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/runtime@1.11.1':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/wasi-threads@1.2.1':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/wasi-threads@1.2.2':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.5(jiti@2.7.0))':
|
||||
dependencies:
|
||||
eslint: 9.39.5(jiti@2.7.0)
|
||||
@@ -2282,6 +2610,13 @@ snapshots:
|
||||
'@tybys/wasm-util': 0.10.2
|
||||
optional: true
|
||||
|
||||
'@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.11.1
|
||||
'@emnapi/runtime': 1.11.1
|
||||
'@tybys/wasm-util': 0.10.3
|
||||
optional: true
|
||||
|
||||
'@next/env@16.2.10': {}
|
||||
|
||||
'@next/eslint-plugin-next@16.2.10':
|
||||
@@ -2326,8 +2661,63 @@ snapshots:
|
||||
|
||||
'@nolyfill/is-core-module@1.0.39': {}
|
||||
|
||||
'@oxc-project/types@0.139.0': {}
|
||||
|
||||
'@rolldown/binding-android-arm64@1.1.5':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-darwin-arm64@1.1.5':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-darwin-x64@1.1.5':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-freebsd-x64@1.1.5':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.1.5':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm64-gnu@1.1.5':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.1.5':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-ppc64-gnu@1.1.5':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-s390x-gnu@1.1.5':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.1.5':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.1.5':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.1.5':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-wasm32-wasi@1.1.5':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.11.1
|
||||
'@emnapi/runtime': 1.11.1
|
||||
'@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-arm64-msvc@1.1.5':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-x64-msvc@1.1.5':
|
||||
optional: true
|
||||
|
||||
'@rolldown/pluginutils@1.0.1': {}
|
||||
|
||||
'@rtsao/scc@1.1.0': {}
|
||||
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@@ -2406,6 +2796,18 @@ snapshots:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@tybys/wasm-util@0.10.3':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@types/chai@5.2.3':
|
||||
dependencies:
|
||||
'@types/deep-eql': 4.0.2
|
||||
assertion-error: 2.0.1
|
||||
|
||||
'@types/deep-eql@4.0.2': {}
|
||||
|
||||
'@types/estree@1.0.9': {}
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
@@ -2573,6 +2975,47 @@ snapshots:
|
||||
'@unrs/resolver-binding-win32-x64-msvc@1.12.2':
|
||||
optional: true
|
||||
|
||||
'@vitest/expect@4.1.10':
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.1.0
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/spy': 4.1.10
|
||||
'@vitest/utils': 4.1.10
|
||||
chai: 6.2.2
|
||||
tinyrainbow: 3.1.0
|
||||
|
||||
'@vitest/mocker@4.1.10(vite@8.1.5(jiti@2.7.0))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.1.10
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 8.1.5(jiti@2.7.0)
|
||||
|
||||
'@vitest/pretty-format@4.1.10':
|
||||
dependencies:
|
||||
tinyrainbow: 3.1.0
|
||||
|
||||
'@vitest/runner@4.1.10':
|
||||
dependencies:
|
||||
'@vitest/utils': 4.1.10
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/snapshot@4.1.10':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 4.1.10
|
||||
'@vitest/utils': 4.1.10
|
||||
magic-string: 0.30.21
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/spy@4.1.10': {}
|
||||
|
||||
'@vitest/utils@4.1.10':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 4.1.10
|
||||
convert-source-map: 2.0.0
|
||||
tinyrainbow: 3.1.0
|
||||
|
||||
acorn-jsx@5.3.2(acorn@8.16.0):
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
@@ -2661,6 +3104,8 @@ snapshots:
|
||||
get-intrinsic: 1.3.0
|
||||
is-array-buffer: 3.0.5
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
ast-types-flow@0.0.8: {}
|
||||
|
||||
async-function@1.0.0: {}
|
||||
@@ -2721,6 +3166,8 @@ snapshots:
|
||||
|
||||
caniuse-lite@1.0.30001793: {}
|
||||
|
||||
chai@6.2.2: {}
|
||||
|
||||
chalk@4.1.2:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
@@ -2887,6 +3334,8 @@ snapshots:
|
||||
iterator.prototype: 1.1.5
|
||||
math-intrinsics: 1.1.0
|
||||
|
||||
es-module-lexer@2.3.1: {}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
@@ -3115,8 +3564,14 @@ snapshots:
|
||||
|
||||
estraverse@5.3.0: {}
|
||||
|
||||
estree-walker@3.0.3:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.9
|
||||
|
||||
esutils@2.0.3: {}
|
||||
|
||||
expect-type@1.4.0: {}
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-glob@3.3.1:
|
||||
@@ -3163,6 +3618,9 @@ snapshots:
|
||||
dependencies:
|
||||
is-callable: 1.2.7
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
function-bind@1.1.2: {}
|
||||
|
||||
function.prototype.name@1.1.8:
|
||||
@@ -3612,6 +4070,8 @@ snapshots:
|
||||
define-properties: 1.2.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
obug@2.1.4: {}
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
@@ -3645,12 +4105,16 @@ snapshots:
|
||||
|
||||
path-parse@1.0.7: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@2.3.2: {}
|
||||
|
||||
picomatch@4.0.4: {}
|
||||
|
||||
picomatch@4.0.5: {}
|
||||
|
||||
possible-typed-array-names@1.1.0: {}
|
||||
|
||||
postcss@8.5.19:
|
||||
@@ -3715,6 +4179,27 @@ snapshots:
|
||||
|
||||
reusify@1.1.0: {}
|
||||
|
||||
rolldown@1.1.5:
|
||||
dependencies:
|
||||
'@oxc-project/types': 0.139.0
|
||||
'@rolldown/pluginutils': 1.0.1
|
||||
optionalDependencies:
|
||||
'@rolldown/binding-android-arm64': 1.1.5
|
||||
'@rolldown/binding-darwin-arm64': 1.1.5
|
||||
'@rolldown/binding-darwin-x64': 1.1.5
|
||||
'@rolldown/binding-freebsd-x64': 1.1.5
|
||||
'@rolldown/binding-linux-arm-gnueabihf': 1.1.5
|
||||
'@rolldown/binding-linux-arm64-gnu': 1.1.5
|
||||
'@rolldown/binding-linux-arm64-musl': 1.1.5
|
||||
'@rolldown/binding-linux-ppc64-gnu': 1.1.5
|
||||
'@rolldown/binding-linux-s390x-gnu': 1.1.5
|
||||
'@rolldown/binding-linux-x64-gnu': 1.1.5
|
||||
'@rolldown/binding-linux-x64-musl': 1.1.5
|
||||
'@rolldown/binding-openharmony-arm64': 1.1.5
|
||||
'@rolldown/binding-wasm32-wasi': 1.1.5
|
||||
'@rolldown/binding-win32-arm64-msvc': 1.1.5
|
||||
'@rolldown/binding-win32-x64-msvc': 1.1.5
|
||||
|
||||
run-parallel@1.2.0:
|
||||
dependencies:
|
||||
queue-microtask: 1.2.3
|
||||
@@ -3832,10 +4317,16 @@ snapshots:
|
||||
side-channel-map: 1.0.1
|
||||
side-channel-weakmap: 1.0.2
|
||||
|
||||
siginfo@2.0.0: {}
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
stable-hash@0.0.5: {}
|
||||
|
||||
stackback@0.0.2: {}
|
||||
|
||||
std-env@4.2.0: {}
|
||||
|
||||
stop-iteration-iterator@1.1.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
@@ -3912,11 +4403,22 @@ snapshots:
|
||||
|
||||
tapable@2.3.3: {}
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
||||
tinyexec@1.2.4: {}
|
||||
|
||||
tinyglobby@0.2.16:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.4)
|
||||
picomatch: 4.0.4
|
||||
|
||||
tinyglobby@0.2.17:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.4)
|
||||
picomatch: 4.0.4
|
||||
|
||||
tinyrainbow@3.1.0: {}
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
dependencies:
|
||||
is-number: 7.0.0
|
||||
@@ -4028,6 +4530,42 @@ snapshots:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
vite@8.1.5(jiti@2.7.0):
|
||||
dependencies:
|
||||
lightningcss: 1.32.0
|
||||
picomatch: 4.0.5
|
||||
postcss: 8.5.19
|
||||
rolldown: 1.1.5
|
||||
tinyglobby: 0.2.17
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.7.0
|
||||
|
||||
vitest@4.1.10(vite@8.1.5(jiti@2.7.0)):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.1.10
|
||||
'@vitest/mocker': 4.1.10(vite@8.1.5(jiti@2.7.0))
|
||||
'@vitest/pretty-format': 4.1.10
|
||||
'@vitest/runner': 4.1.10
|
||||
'@vitest/snapshot': 4.1.10
|
||||
'@vitest/spy': 4.1.10
|
||||
'@vitest/utils': 4.1.10
|
||||
es-module-lexer: 2.3.1
|
||||
expect-type: 1.4.0
|
||||
magic-string: 0.30.21
|
||||
obug: 2.1.4
|
||||
pathe: 2.0.3
|
||||
picomatch: 4.0.4
|
||||
std-env: 4.2.0
|
||||
tinybench: 2.9.0
|
||||
tinyexec: 1.2.4
|
||||
tinyglobby: 0.2.16
|
||||
tinyrainbow: 3.1.0
|
||||
vite: 8.1.5(jiti@2.7.0)
|
||||
why-is-node-running: 2.3.0
|
||||
transitivePeerDependencies:
|
||||
- msw
|
||||
|
||||
which-boxed-primitive@1.1.1:
|
||||
dependencies:
|
||||
is-bigint: 1.1.0
|
||||
@@ -4073,6 +4611,11 @@ snapshots:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
why-is-node-running@2.3.0:
|
||||
dependencies:
|
||||
siginfo: 2.0.0
|
||||
stackback: 0.0.2
|
||||
|
||||
word-wrap@1.2.5: {}
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { getChampionImageUrl, getTodaySeed } from "../lib/champion-data";
|
||||
import { AATROX, CHAMPIONS } from "./fixtures/champions";
|
||||
|
||||
/**
|
||||
* `champions` is module-private with no reset export, so each group gets a fresh
|
||||
* module instance instead of adding production code for testability.
|
||||
*/
|
||||
async function freshModule({ champions = CHAMPIONS, ok = true, status = 200 } = {}) {
|
||||
vi.resetModules();
|
||||
const fetchMock = vi.fn(async () => ({
|
||||
ok,
|
||||
status,
|
||||
json: async () => champions,
|
||||
}));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
const mod = await import("../lib/champion-data");
|
||||
return { mod, fetchMock };
|
||||
}
|
||||
|
||||
/** Load a module instance with its champion list already populated. */
|
||||
async function loadedModule(champions = CHAMPIONS) {
|
||||
const { mod, fetchMock } = await freshModule({ champions });
|
||||
await mod.loadChampions();
|
||||
return { mod, fetchMock };
|
||||
}
|
||||
|
||||
function named(names) {
|
||||
return names.map((name) => ({ ...AATROX, id: name, name }));
|
||||
}
|
||||
|
||||
describe("loadChampions", () => {
|
||||
it("fetches the static champions file and returns the parsed list", async () => {
|
||||
const { mod, fetchMock } = await freshModule();
|
||||
await expect(mod.loadChampions()).resolves.toEqual(CHAMPIONS);
|
||||
expect(fetchMock).toHaveBeenCalledWith("/champions.json");
|
||||
});
|
||||
|
||||
it("serves later calls from the cache without refetching", async () => {
|
||||
const { mod, fetchMock } = await loadedModule();
|
||||
await mod.loadChampions();
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("is single-flight — concurrent callers share one fetch", async () => {
|
||||
const { mod, fetchMock } = await freshModule();
|
||||
const [first, second] = await Promise.all([
|
||||
mod.loadChampions(),
|
||||
mod.loadChampions(),
|
||||
]);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(first).toBe(second);
|
||||
});
|
||||
|
||||
it("rejects with the status code when the response is not ok", async () => {
|
||||
const { mod } = await freshModule({ ok: false, status: 404 });
|
||||
await expect(mod.loadChampions()).rejects.toThrow(
|
||||
"Failed to load champions: 404",
|
||||
);
|
||||
});
|
||||
|
||||
it("clears the in-flight promise on failure so a retry can succeed", async () => {
|
||||
const { mod, fetchMock } = await freshModule({ ok: false, status: 500 });
|
||||
await expect(mod.loadChampions()).rejects.toThrow();
|
||||
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => CHAMPIONS,
|
||||
});
|
||||
|
||||
await expect(mod.loadChampions()).resolves.toEqual(CHAMPIONS);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("searchChampions", () => {
|
||||
it("returns nothing for an empty query", async () => {
|
||||
const { mod } = await loadedModule();
|
||||
expect(mod.searchChampions("")).toEqual([]);
|
||||
});
|
||||
|
||||
it("matches case-insensitively on a substring", async () => {
|
||||
const { mod } = await loadedModule();
|
||||
expect(mod.searchChampions("AATR").map((c) => c.name)).toEqual(["Aatrox"]);
|
||||
});
|
||||
|
||||
it("sorts prefix matches ahead of substring matches", async () => {
|
||||
const { mod } = await loadedModule(named(["Akali", "Kayle", "Kayn"]));
|
||||
// "Kayle"/"Kayn" start with "ka"; "Akali" only contains it
|
||||
expect(mod.searchChampions("ka").map((c) => c.name)).toEqual([
|
||||
"Kayle",
|
||||
"Kayn",
|
||||
"Akali",
|
||||
]);
|
||||
});
|
||||
|
||||
it("sorts alphabetically within each tier", async () => {
|
||||
const { mod } = await loadedModule(named(["Azir", "Ahri", "Aatrox"]));
|
||||
expect(mod.searchChampions("a").map((c) => c.name)).toEqual([
|
||||
"Aatrox",
|
||||
"Ahri",
|
||||
"Azir",
|
||||
]);
|
||||
});
|
||||
|
||||
it("caps the result at 8", async () => {
|
||||
const many = named([
|
||||
"Aatrox", "Ahri", "Akali", "Alistar", "Amumu",
|
||||
"Anivia", "Annie", "Aphelios", "Ashe", "Azir",
|
||||
]);
|
||||
const { mod } = await loadedModule(many);
|
||||
expect(mod.searchChampions("a")).toHaveLength(8);
|
||||
});
|
||||
|
||||
it("filters excluded names case-insensitively", async () => {
|
||||
const { mod } = await loadedModule(named(["Kayle", "Kayn"]));
|
||||
expect(mod.searchChampions("ka", ["kAyN"]).map((c) => c.name)).toEqual([
|
||||
"Kayle",
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns an empty list when nothing matches", async () => {
|
||||
const { mod } = await loadedModule();
|
||||
expect(mod.searchChampions("zzzz")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRandomChampion", () => {
|
||||
it("is deterministic for a given seed", async () => {
|
||||
const { mod } = await loadedModule();
|
||||
expect(mod.getRandomChampion("2026-07-25")).toBe(
|
||||
mod.getRandomChampion("2026-07-25"),
|
||||
);
|
||||
});
|
||||
|
||||
it("always returns a champion from the loaded list", async () => {
|
||||
const { mod } = await loadedModule();
|
||||
for (const seed of ["a", "b", "2026-07-25", "unlimited_1_0.5", ""]) {
|
||||
expect(CHAMPIONS).toContain(mod.getRandomChampion(seed));
|
||||
}
|
||||
});
|
||||
|
||||
it("returns null before champions are loaded", async () => {
|
||||
const { mod } = await freshModule();
|
||||
expect(mod.getRandomChampion("2026-07-25")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTodaySeed", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("formats as YYYY-MM-DD", () => {
|
||||
vi.setSystemTime(new Date("2026-07-25T09:00:00Z"));
|
||||
expect(getTodaySeed()).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
||||
});
|
||||
|
||||
it("reflects the local date, not the UTC date", () => {
|
||||
// 18:00 UTC is already the next day in UTC+7 and still the same day in UTC,
|
||||
// so this instant fails any implementation that reaches for UTC getters.
|
||||
vi.setSystemTime(new Date("2026-07-25T18:00:00Z"));
|
||||
// en-CA renders as YYYY-MM-DD — an independent derivation of the same date.
|
||||
expect(getTodaySeed()).toBe(new Date().toLocaleDateString("en-CA"));
|
||||
});
|
||||
|
||||
it("zero-pads single-digit months and days", () => {
|
||||
vi.setSystemTime(new Date(2026, 0, 5, 12, 0, 0));
|
||||
expect(getTodaySeed()).toBe("2026-01-05");
|
||||
});
|
||||
|
||||
it("advances by one day across a local midnight", () => {
|
||||
vi.setSystemTime(new Date(2026, 6, 25, 23, 59, 0));
|
||||
const before = getTodaySeed();
|
||||
vi.setSystemTime(new Date(2026, 6, 26, 0, 1, 0));
|
||||
expect(getTodaySeed()).not.toBe(before);
|
||||
expect(getTodaySeed()).toBe("2026-07-26");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getChampionImageUrl", () => {
|
||||
it("returns the absolute Data Dragon tile URL", () => {
|
||||
expect(getChampionImageUrl("Aatrox")).toBe(
|
||||
"https://ddragon.leagueoflegends.com/cdn/img/champion/tiles/Aatrox_0.jpg",
|
||||
);
|
||||
});
|
||||
|
||||
it("stays absolute — it must never be rewritten through a base path", () => {
|
||||
expect(getChampionImageUrl("Belveth")).toMatch(/^https:\/\//);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,163 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { CLASSIC_ATTRIBUTES, compareChampions } from "../lib/classic-mode";
|
||||
import { AATROX, AHRI, AKALI, BELVETH } from "./fixtures/champions";
|
||||
|
||||
/** Pull one attribute's comparison out of the 7-entry result array. */
|
||||
function cell(guess, target, key) {
|
||||
return compareChampions(guess, target).find((r) => r.key === key);
|
||||
}
|
||||
|
||||
describe("CLASSIC_ATTRIBUTES", () => {
|
||||
it("exposes 7 attributes with gender first and releaseDate last", () => {
|
||||
expect(CLASSIC_ATTRIBUTES).toHaveLength(7);
|
||||
expect(CLASSIC_ATTRIBUTES[0].key).toBe("gender");
|
||||
expect(CLASSIC_ATTRIBUTES[6].key).toBe("releaseDate");
|
||||
});
|
||||
|
||||
it("keeps the display order the grid header depends on", () => {
|
||||
expect(CLASSIC_ATTRIBUTES.map((a) => a.key)).toEqual([
|
||||
"gender",
|
||||
"genre",
|
||||
"attackType",
|
||||
"resource",
|
||||
"region",
|
||||
"lane",
|
||||
"releaseDate",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("compareChampions — result shape", () => {
|
||||
it("returns one entry per attribute, carrying label and type through", () => {
|
||||
const results = compareChampions(AHRI, AATROX);
|
||||
expect(results).toHaveLength(7);
|
||||
expect(results[0]).toMatchObject({
|
||||
key: "gender",
|
||||
label: "Gender",
|
||||
type: "exact",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("compareChampions — exact", () => {
|
||||
it("marks an identical value correct", () => {
|
||||
expect(cell(AKALI, AHRI, "gender").result).toBe("correct");
|
||||
});
|
||||
|
||||
it("marks a differing value wrong", () => {
|
||||
expect(cell(AHRI, AATROX, "gender").result).toBe("wrong");
|
||||
});
|
||||
|
||||
it("compares case-insensitively", () => {
|
||||
const shouty = { ...AATROX, gender: "MALE" };
|
||||
expect(cell(shouty, AATROX, "gender").result).toBe("correct");
|
||||
});
|
||||
|
||||
it("treats a missing value as not matching a present one", () => {
|
||||
expect(cell(BELVETH, AATROX, "resource").result).toBe("wrong");
|
||||
});
|
||||
});
|
||||
|
||||
describe("compareChampions — multi", () => {
|
||||
it("marks identical sets correct", () => {
|
||||
expect(cell(BELVETH, AATROX, "genre").result).toBe("correct");
|
||||
});
|
||||
|
||||
it("marks intersecting sets partial", () => {
|
||||
// AKALI lane "top,mid" overlaps AATROX lane "top"
|
||||
expect(cell(AKALI, AATROX, "lane").result).toBe("partial");
|
||||
});
|
||||
|
||||
it("marks disjoint sets wrong", () => {
|
||||
expect(cell(AHRI, AATROX, "lane").result).toBe("wrong");
|
||||
});
|
||||
|
||||
it("marks two empty sets correct", () => {
|
||||
const a = { ...AATROX, lane: "" };
|
||||
const b = { ...AATROX, lane: "" };
|
||||
expect(cell(a, b, "lane").result).toBe("correct");
|
||||
});
|
||||
|
||||
it("marks one empty set against a populated one wrong", () => {
|
||||
const empty = { ...AATROX, lane: "" };
|
||||
expect(cell(empty, AATROX, "lane").result).toBe("wrong");
|
||||
});
|
||||
|
||||
it("ignores order and whitespace when comparing sets", () => {
|
||||
const spaced = { ...AKALI, lane: " mid , top " };
|
||||
expect(cell(spaced, AKALI, "lane").result).toBe("correct");
|
||||
});
|
||||
});
|
||||
|
||||
describe("compareChampions — year", () => {
|
||||
it("marks the same year correct with no direction hint", () => {
|
||||
const twin = { ...AHRI, releaseDate: 2013 };
|
||||
const result = cell(twin, AATROX, "releaseDate");
|
||||
expect(result.result).toBe("correct");
|
||||
expect(result.direction).toBeUndefined();
|
||||
});
|
||||
|
||||
it("points up when the guess is older than the target", () => {
|
||||
expect(cell(AHRI, AATROX, "releaseDate")).toMatchObject({
|
||||
result: "wrong",
|
||||
direction: "up",
|
||||
});
|
||||
});
|
||||
|
||||
it("points down when the guess is newer than the target", () => {
|
||||
expect(cell(BELVETH, AATROX, "releaseDate")).toMatchObject({
|
||||
result: "wrong",
|
||||
direction: "down",
|
||||
});
|
||||
});
|
||||
|
||||
it("gives no direction when either year is unparseable", () => {
|
||||
const undated = { ...AHRI, releaseDate: "" };
|
||||
const result = cell(undated, AATROX, "releaseDate");
|
||||
expect(result.result).toBe("wrong");
|
||||
expect(result.direction).toBeUndefined();
|
||||
});
|
||||
|
||||
it("renders a missing year as ?", () => {
|
||||
const undated = { ...AHRI, releaseDate: "" };
|
||||
expect(cell(undated, AATROX, "releaseDate").guessValue).toBe("?");
|
||||
});
|
||||
|
||||
it("passes the year through unformatted", () => {
|
||||
expect(cell(AHRI, AATROX, "releaseDate")).toMatchObject({
|
||||
guessValue: 2011,
|
||||
targetValue: 2013,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("compareChampions — value formatting", () => {
|
||||
it("capitalizes gender", () => {
|
||||
expect(cell(AATROX, AATROX, "gender").guessValue).toBe("Male");
|
||||
});
|
||||
|
||||
it("renders attackType close as Melee and range as Ranged", () => {
|
||||
expect(cell(AATROX, AATROX, "attackType").guessValue).toBe("Melee");
|
||||
expect(cell(AHRI, AHRI, "attackType").guessValue).toBe("Ranged");
|
||||
});
|
||||
|
||||
it("splits hyphenated regions into capitalized words", () => {
|
||||
expect(cell(AKALI, AKALI, "region").guessValue).toBe("Shadow Isles");
|
||||
});
|
||||
|
||||
it("title-cases comma lists and rejoins them with a space", () => {
|
||||
expect(cell(AHRI, AHRI, "genre").guessValue).toBe("Mage, Assassin");
|
||||
expect(cell(AKALI, AKALI, "lane").guessValue).toBe("Top, Mid");
|
||||
});
|
||||
|
||||
it("renders an empty value as an em dash", () => {
|
||||
expect(cell(BELVETH, BELVETH, "resource").guessValue).toBe("—");
|
||||
});
|
||||
|
||||
it("formats the target value the same way as the guess value", () => {
|
||||
expect(cell(AHRI, AKALI, "region")).toMatchObject({
|
||||
guessValue: "Ionia",
|
||||
targetValue: "Shadow Isles",
|
||||
});
|
||||
});
|
||||
});
|
||||
Vendored
+66
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Hand-written champion fixtures, deliberately small so the weekly champions.json
|
||||
* sync can never break the suite. Shapes mirror the real data exactly:
|
||||
* `releaseDate` is a number, `attackType` is "close" | "range".
|
||||
*
|
||||
* Chosen to exercise every compareChampions branch against AATROX as target:
|
||||
* AHRI — disjoint genre, disjoint lane, older year (direction "up")
|
||||
* AKALI — disjoint genre, overlapping lane (partial), hyphenated region
|
||||
* BELVETH — identical genre (correct), disjoint lane, empty resource ("—"),
|
||||
* newer year (direction "down")
|
||||
*/
|
||||
|
||||
export const AATROX = {
|
||||
id: "Aatrox",
|
||||
name: "Aatrox",
|
||||
title: "the Darkin Blade",
|
||||
resource: "Blood Well",
|
||||
genre: "Fighter",
|
||||
gender: "male",
|
||||
attackType: "close",
|
||||
releaseDate: 2013,
|
||||
region: "runeterra",
|
||||
lane: "top",
|
||||
};
|
||||
|
||||
export const AHRI = {
|
||||
id: "Ahri",
|
||||
name: "Ahri",
|
||||
title: "the Nine-Tailed Fox",
|
||||
resource: "Mana",
|
||||
genre: "Mage,Assassin",
|
||||
gender: "female",
|
||||
attackType: "range",
|
||||
releaseDate: 2011,
|
||||
region: "ionia",
|
||||
lane: "mid",
|
||||
};
|
||||
|
||||
export const AKALI = {
|
||||
id: "Akali",
|
||||
name: "Akali",
|
||||
title: "the Rogue Assassin",
|
||||
resource: "Energy",
|
||||
genre: "Assassin",
|
||||
gender: "female",
|
||||
attackType: "close",
|
||||
releaseDate: 2010,
|
||||
region: "shadow-isles",
|
||||
lane: "top,mid",
|
||||
};
|
||||
|
||||
export const BELVETH = {
|
||||
id: "Belveth",
|
||||
name: "Bel'Veth",
|
||||
title: "the Empress of the Void",
|
||||
resource: "",
|
||||
genre: "Fighter",
|
||||
gender: "female",
|
||||
attackType: "close",
|
||||
releaseDate: 2022,
|
||||
region: "void",
|
||||
lane: "jungle",
|
||||
};
|
||||
|
||||
/** Load order matters for getRandomChampion index assertions. */
|
||||
export const CHAMPIONS = [AATROX, AHRI, AKALI, BELVETH];
|
||||
@@ -0,0 +1,357 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
clearExpiredCache,
|
||||
clearUnlimitedState,
|
||||
createGame,
|
||||
getOrCreateUnlimitedSeed,
|
||||
loadUnlimitedStats,
|
||||
saveUnlimitedStats,
|
||||
submitGuess,
|
||||
} from "../lib/game-engine";
|
||||
import { AATROX, AHRI } from "./fixtures/champions";
|
||||
|
||||
/** Minimal stand-in for compareChampions — the engine only stores what this returns. */
|
||||
const compareFn = (guess, target) => [
|
||||
{ key: "name", result: guess.name === target.name ? "correct" : "wrong" },
|
||||
];
|
||||
|
||||
/** Synthesize a distinct non-target champion. */
|
||||
function other(name) {
|
||||
return { ...AHRI, id: name, name };
|
||||
}
|
||||
|
||||
function daily(overrides = {}) {
|
||||
return createGame({
|
||||
target: AATROX,
|
||||
compareFn,
|
||||
maxGuesses: 6,
|
||||
mode: "daily",
|
||||
seed: "2026-07-25",
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function unlimited(overrides = {}) {
|
||||
return createGame({
|
||||
target: AATROX,
|
||||
compareFn,
|
||||
maxGuesses: 0,
|
||||
mode: "unlimited",
|
||||
seed: "unlimited_123",
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
describe("createGame", () => {
|
||||
it("starts empty and open", () => {
|
||||
const game = daily();
|
||||
expect(game.guesses).toEqual([]);
|
||||
expect(game.results).toEqual([]);
|
||||
expect(game.isOver).toBe(false);
|
||||
expect(game.isWon).toBe(false);
|
||||
});
|
||||
|
||||
it("carries config through onto the game object", () => {
|
||||
expect(daily()).toMatchObject({
|
||||
target: AATROX,
|
||||
maxGuesses: 6,
|
||||
mode: "daily",
|
||||
seed: "2026-07-25",
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults to 6 guesses in daily mode when maxGuesses is omitted", () => {
|
||||
const game = createGame({ target: AATROX, compareFn });
|
||||
expect(game.maxGuesses).toBe(6);
|
||||
expect(game.mode).toBe("daily");
|
||||
});
|
||||
});
|
||||
|
||||
describe("submitGuess", () => {
|
||||
it("appends the guess and its comparison result", () => {
|
||||
const game = submitGuess(daily(), AHRI);
|
||||
expect(game.guesses).toEqual([AHRI]);
|
||||
expect(game.results).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("does not mutate the game it was given", () => {
|
||||
const before = daily();
|
||||
submitGuess(before, AHRI);
|
||||
expect(before.guesses).toHaveLength(0);
|
||||
expect(before.results).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("returns fresh array identities so reassignment is observable", () => {
|
||||
const before = daily();
|
||||
const after = submitGuess(before, AHRI);
|
||||
expect(after).not.toBe(before);
|
||||
expect(after.guesses).not.toBe(before.guesses);
|
||||
expect(after.results).not.toBe(before.results);
|
||||
});
|
||||
|
||||
it("rejects a duplicate guess with null", () => {
|
||||
const game = submitGuess(daily(), AHRI);
|
||||
expect(submitGuess(game, AHRI)).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects any guess once the game is over", () => {
|
||||
const game = submitGuess(daily(), AATROX);
|
||||
expect(game.isOver).toBe(true);
|
||||
expect(submitGuess(game, AHRI)).toBeNull();
|
||||
});
|
||||
|
||||
it("wins on the correct champion", () => {
|
||||
expect(submitGuess(daily(), AATROX)).toMatchObject({
|
||||
isWon: true,
|
||||
isOver: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("still counts a win made with the final allowed guess", () => {
|
||||
let game = daily();
|
||||
for (const name of ["a", "b", "c", "d", "e"]) {
|
||||
game = submitGuess(game, other(name));
|
||||
}
|
||||
expect(game.isOver).toBe(false);
|
||||
|
||||
game = submitGuess(game, AATROX);
|
||||
expect(game).toMatchObject({ isWon: true, isOver: true });
|
||||
expect(game.guesses).toHaveLength(6);
|
||||
});
|
||||
|
||||
it("ends the game as a loss at maxGuesses wrong guesses", () => {
|
||||
let game = daily();
|
||||
for (const name of ["a", "b", "c", "d", "e", "f"]) {
|
||||
game = submitGuess(game, other(name));
|
||||
}
|
||||
expect(game).toMatchObject({ isOver: true, isWon: false });
|
||||
expect(game.guesses).toHaveLength(6);
|
||||
});
|
||||
|
||||
it("never ends on guess count when maxGuesses is 0", () => {
|
||||
let game = unlimited();
|
||||
for (let i = 0; i < 10; i++) {
|
||||
game = submitGuess(game, other(`c${i}`));
|
||||
}
|
||||
expect(game.isOver).toBe(false);
|
||||
expect(game.guesses).toHaveLength(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe("persistence — storage key contract", () => {
|
||||
it("writes daily state to exactly loldle_daily_<seed>", () => {
|
||||
submitGuess(daily(), AHRI);
|
||||
expect(localStorage.getItem("loldle_daily_2026-07-25")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("writes unlimited state to exactly loldle_unlimited_current", () => {
|
||||
submitGuess(unlimited(), AHRI);
|
||||
expect(localStorage.getItem("loldle_unlimited_current")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("ignores the seed when keying unlimited state", () => {
|
||||
submitGuess(unlimited({ seed: "unlimited_999" }), AHRI);
|
||||
expect(localStorage.getItem("loldle_unlimited_current")).not.toBeNull();
|
||||
expect(localStorage.getItem("loldle_unlimited_unlimited_999")).toBeNull();
|
||||
});
|
||||
|
||||
it("stores the target name, guesses, results and flags", () => {
|
||||
submitGuess(daily(), AHRI);
|
||||
const saved = JSON.parse(localStorage.getItem("loldle_daily_2026-07-25"));
|
||||
expect(saved).toMatchObject({
|
||||
targetName: "Aatrox",
|
||||
guesses: [AHRI],
|
||||
isOver: false,
|
||||
isWon: false,
|
||||
});
|
||||
expect(saved.results).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("persistence — restore on createGame", () => {
|
||||
it("restores an in-progress game for the same target", () => {
|
||||
submitGuess(daily(), AHRI);
|
||||
const restored = daily();
|
||||
expect(restored.guesses).toEqual([AHRI]);
|
||||
expect(restored.results).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("restores the over and won flags", () => {
|
||||
submitGuess(daily(), AATROX);
|
||||
expect(daily()).toMatchObject({ isOver: true, isWon: true });
|
||||
});
|
||||
|
||||
it("discards saved state when the target changed — the date-rollover case", () => {
|
||||
submitGuess(daily(), AHRI);
|
||||
const fresh = createGame({
|
||||
target: { ...AHRI, name: "Ahri" },
|
||||
compareFn,
|
||||
maxGuesses: 6,
|
||||
mode: "daily",
|
||||
seed: "2026-07-25",
|
||||
});
|
||||
expect(fresh.guesses).toEqual([]);
|
||||
expect(fresh.isOver).toBe(false);
|
||||
});
|
||||
|
||||
it("starts fresh when storage holds malformed JSON", () => {
|
||||
localStorage.setItem("loldle_daily_2026-07-25", "{not json");
|
||||
expect(daily().guesses).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearExpiredCache", () => {
|
||||
it("removes yesterday's daily entry but keeps today's", () => {
|
||||
localStorage.setItem("loldle_daily_2026-07-24", "{}");
|
||||
localStorage.setItem("loldle_daily_2026-07-25", "{}");
|
||||
|
||||
clearExpiredCache("2026-07-25");
|
||||
|
||||
expect(localStorage.getItem("loldle_daily_2026-07-24")).toBeNull();
|
||||
expect(localStorage.getItem("loldle_daily_2026-07-25")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("leaves unlimited keys untouched", () => {
|
||||
localStorage.setItem("loldle_unlimited_current", "{}");
|
||||
localStorage.setItem("loldle_unlimited_stats", "{}");
|
||||
localStorage.setItem("loldle_unlimited_seed", "s");
|
||||
|
||||
clearExpiredCache("2026-07-25");
|
||||
|
||||
expect(localStorage.getItem("loldle_unlimited_current")).not.toBeNull();
|
||||
expect(localStorage.getItem("loldle_unlimited_stats")).not.toBeNull();
|
||||
expect(localStorage.getItem("loldle_unlimited_seed")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("removes every stale daily entry, not just the first", () => {
|
||||
localStorage.setItem("loldle_daily_2026-07-20", "{}");
|
||||
localStorage.setItem("loldle_daily_2026-07-21", "{}");
|
||||
localStorage.setItem("loldle_daily_2026-07-22", "{}");
|
||||
|
||||
clearExpiredCache("2026-07-25");
|
||||
|
||||
expect(localStorage.length).toBe(0);
|
||||
});
|
||||
|
||||
it("ignores unrelated keys", () => {
|
||||
localStorage.setItem("unrelated", "keep");
|
||||
clearExpiredCache("2026-07-25");
|
||||
expect(localStorage.getItem("unrelated")).toBe("keep");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unlimited seed", () => {
|
||||
it("creates and persists a seed on first call", () => {
|
||||
const seed = getOrCreateUnlimitedSeed();
|
||||
expect(seed).toMatch(/^unlimited_/);
|
||||
expect(localStorage.getItem("loldle_unlimited_seed")).toBe(seed);
|
||||
});
|
||||
|
||||
it("returns the same seed on subsequent calls", () => {
|
||||
expect(getOrCreateUnlimitedSeed()).toBe(getOrCreateUnlimitedSeed());
|
||||
});
|
||||
|
||||
it("clearUnlimitedState drops both the seed and the current game", () => {
|
||||
submitGuess(unlimited(), AHRI);
|
||||
getOrCreateUnlimitedSeed();
|
||||
|
||||
clearUnlimitedState();
|
||||
|
||||
expect(localStorage.getItem("loldle_unlimited_current")).toBeNull();
|
||||
expect(localStorage.getItem("loldle_unlimited_seed")).toBeNull();
|
||||
});
|
||||
|
||||
it("clearUnlimitedState preserves accumulated stats", () => {
|
||||
localStorage.setItem("loldle_unlimited_stats", '{"gamesPlayed":3}');
|
||||
clearUnlimitedState();
|
||||
expect(loadUnlimitedStats().gamesPlayed).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("saveUnlimitedStats", () => {
|
||||
/**
|
||||
* Play an unlimited game to completion; `won` picks the ending.
|
||||
* Clears saved state first, mirroring handleNewGame — otherwise createGame
|
||||
* restores the previous finished game and submitGuess refuses to run.
|
||||
*/
|
||||
function finish(won, wrongGuesses = 0) {
|
||||
clearUnlimitedState();
|
||||
let game = unlimited();
|
||||
for (let i = 0; i < wrongGuesses; i++) {
|
||||
game = submitGuess(game, other(`c${i}`));
|
||||
}
|
||||
if (won) game = submitGuess(game, AATROX);
|
||||
else game = { ...game, isOver: true };
|
||||
return game;
|
||||
}
|
||||
|
||||
it("counts a played game", () => {
|
||||
saveUnlimitedStats(finish(false));
|
||||
expect(loadUnlimitedStats().gamesPlayed).toBe(1);
|
||||
});
|
||||
|
||||
it("counts a win and records its guess count", () => {
|
||||
saveUnlimitedStats(finish(true, 2));
|
||||
expect(loadUnlimitedStats()).toMatchObject({
|
||||
gamesPlayed: 1,
|
||||
gamesWon: 1,
|
||||
guessDistribution: { 3: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
it("does not count a win when the game was lost", () => {
|
||||
saveUnlimitedStats(finish(false));
|
||||
expect(loadUnlimitedStats()).toMatchObject({
|
||||
gamesPlayed: 1,
|
||||
gamesWon: 0,
|
||||
guessDistribution: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("accumulates across games", () => {
|
||||
saveUnlimitedStats(finish(true, 1));
|
||||
saveUnlimitedStats(finish(true, 1));
|
||||
saveUnlimitedStats(finish(false));
|
||||
|
||||
expect(loadUnlimitedStats()).toMatchObject({
|
||||
gamesPlayed: 3,
|
||||
gamesWon: 2,
|
||||
guessDistribution: { 2: 2 },
|
||||
});
|
||||
});
|
||||
|
||||
it("stamps lastPlayed", () => {
|
||||
saveUnlimitedStats(finish(true, 0));
|
||||
expect(typeof loadUnlimitedStats().lastPlayed).toBe("number");
|
||||
});
|
||||
|
||||
it("is a no-op for daily mode", () => {
|
||||
const game = submitGuess(daily(), AATROX);
|
||||
saveUnlimitedStats(game);
|
||||
expect(localStorage.getItem("loldle_unlimited_stats")).toBeNull();
|
||||
});
|
||||
|
||||
it("is a no-op for an unfinished game", () => {
|
||||
saveUnlimitedStats(submitGuess(unlimited(), AHRI));
|
||||
expect(localStorage.getItem("loldle_unlimited_stats")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadUnlimitedStats", () => {
|
||||
it("returns zeroed stats when nothing is stored", () => {
|
||||
expect(loadUnlimitedStats()).toEqual({
|
||||
gamesPlayed: 0,
|
||||
gamesWon: 0,
|
||||
guessDistribution: {},
|
||||
lastPlayed: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns zeroed stats when storage holds malformed JSON", () => {
|
||||
localStorage.setItem("loldle_unlimited_stats", "not json at all");
|
||||
expect(loadUnlimitedStats()).toMatchObject({
|
||||
gamesPlayed: 0,
|
||||
gamesWon: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { beforeEach, vi } from "vitest";
|
||||
|
||||
/**
|
||||
* Minimal spec-shaped localStorage stub.
|
||||
* `key(i)` and `length` are required: clearExpiredCache iterates by index.
|
||||
*/
|
||||
class LocalStorageStub {
|
||||
#store = new Map();
|
||||
|
||||
get length() {
|
||||
return this.#store.size;
|
||||
}
|
||||
|
||||
key(i) {
|
||||
return [...this.#store.keys()][i] ?? null;
|
||||
}
|
||||
|
||||
getItem(k) {
|
||||
return this.#store.has(k) ? this.#store.get(k) : null;
|
||||
}
|
||||
|
||||
setItem(k, v) {
|
||||
this.#store.set(k, String(v));
|
||||
}
|
||||
|
||||
removeItem(k) {
|
||||
this.#store.delete(k);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.#store.clear();
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("localStorage", new LocalStorageStub());
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: "node",
|
||||
setupFiles: ["./test/setup-local-storage.js"],
|
||||
include: ["test/**/*.test.js"],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user