authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-01-29 18:16:06-08:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-01-29 22:05:20-08:00
logbcb14cf5bd2f877987a158c4e40556f070172435
tree88b455e111f23acb019b0b0f8a2389d4541d2170
parentf33b11a3c0e6206b106b2cc655632d14ea2650c6
signaturelock-open Commit is signed but in an unrecognized format.

chore: more stuff


11 files changed, 3651 insertions(+), 25 deletions(-)

package-lock.json+1007-12
...@@ -6,17 +6,21 @@...@@ -6,17 +6,21 @@
6 "packages": {6 "packages": {
7 "": {7 "": {
8 "name": "@clo/react-mutation",8 "name": "@clo/react-mutation",
9 "version": "0.1.0",
10 "license": "ISC",9 "license": "ISC",
11 "dependencies": {10 "dependencies": {
12 "@clo/lib": "npm:@jsr/clo__lib@^3.0.0",11 "@clo/lib": "npm:@jsr/clo__lib@^3.0.0",
13 "@std/assert": "npm:@jsr/std__assert@^1.0.17"12 "@std/assert": "npm:@jsr/std__assert@^1.0.17"
14 },13 },
15 "devDependencies": {14 "devDependencies": {
15 "@testing-library/react": "^16.3.2",
16 "@testing-library/user-event": "^14.6.1",
16 "@types/node": "^24.10.1",17 "@types/node": "^24.10.1",
17 "@types/react": "^19.2.10",18 "@types/react": "^19.2.10",
18 "@types/react-dom": "^19.2.3",19 "@types/react-dom": "^19.2.3",
19 "@vitejs/plugin-react": "^5.1.1",20 "@vitejs/plugin-react": "^5.1.1",
21 "@vitest/coverage-v8": "^4.0.18",
22 "happy-dom": "^20.4.0",
23 "jsdom": "^27.4.0",
20 "react": "^19.2.4",24 "react": "^19.2.4",
21 "react-dom": "^19.2.4",25 "react-dom": "^19.2.4",
22 "typescript": "^5.9.3",26 "typescript": "^5.9.3",
...@@ -34,6 +38,68 @@...@@ -34,6 +38,68 @@
34 }38 }
35 }39 }
36 },40 },
41 "node_modules/@acemir/cssom": {
42 "version": "0.9.31",
43 "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz",
44 "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==",
45 "dev": true,
46 "license": "MIT"
47 },
48 "node_modules/@asamuzakjp/css-color": {
49 "version": "4.1.1",
50 "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.1.tgz",
51 "integrity": "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==",
52 "dev": true,
53 "license": "MIT",
54 "dependencies": {
55 "@csstools/css-calc": "^2.1.4",
56 "@csstools/css-color-parser": "^3.1.0",
57 "@csstools/css-parser-algorithms": "^3.0.5",
58 "@csstools/css-tokenizer": "^3.0.4",
59 "lru-cache": "^11.2.4"
60 }
61 },
62 "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
63 "version": "11.2.5",
64 "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz",
65 "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==",
66 "dev": true,
67 "license": "BlueOak-1.0.0",
68 "engines": {
69 "node": "20 || >=22"
70 }
71 },
72 "node_modules/@asamuzakjp/dom-selector": {
73 "version": "6.7.6",
74 "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.6.tgz",
75 "integrity": "sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==",
76 "dev": true,
77 "license": "MIT",
78 "dependencies": {
79 "@asamuzakjp/nwsapi": "^2.3.9",
80 "bidi-js": "^1.0.3",
81 "css-tree": "^3.1.0",
82 "is-potential-custom-element-name": "^1.0.1",
83 "lru-cache": "^11.2.4"
84 }
85 },
86 "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": {
87 "version": "11.2.5",
88 "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz",
89 "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==",
90 "dev": true,
91 "license": "BlueOak-1.0.0",
92 "engines": {
93 "node": "20 || >=22"
94 }
95 },
96 "node_modules/@asamuzakjp/nwsapi": {
97 "version": "2.3.9",
98 "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
99 "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
100 "dev": true,
101 "license": "MIT"
102 },
37 "node_modules/@babel/code-frame": {103 "node_modules/@babel/code-frame": {
38 "version": "7.28.6",104 "version": "7.28.6",
39 "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz",105 "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz",
...@@ -269,6 +335,16 @@...@@ -269,6 +335,16 @@
269 "@babel/core": "^7.0.0-0"335 "@babel/core": "^7.0.0-0"
270 }336 }
271 },337 },
338 "node_modules/@babel/runtime": {
339 "version": "7.28.6",
340 "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
341 "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
342 "dev": true,
343 "license": "MIT",
344 "engines": {
345 "node": ">=6.9.0"
346 }
347 },
272 "node_modules/@babel/template": {348 "node_modules/@babel/template": {
273 "version": "7.28.6",349 "version": "7.28.6",
274 "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",350 "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
...@@ -317,12 +393,156 @@...@@ -317,12 +393,156 @@
317 "node": ">=6.9.0"393 "node": ">=6.9.0"
318 }394 }
319 },395 },
396 "node_modules/@bcoe/v8-coverage": {
397 "version": "1.0.2",
398 "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
399 "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
400 "dev": true,
401 "license": "MIT",
402 "engines": {
403 "node": ">=18"
404 }
405 },
320 "node_modules/@clo/lib": {406 "node_modules/@clo/lib": {
321 "name": "@jsr/clo__lib",407 "name": "@jsr/clo__lib",
322 "version": "3.0.0",408 "version": "3.0.0",
323 "resolved": "https://npm.jsr.io/~/11/@jsr/clo__lib/3.0.0.tgz",409 "resolved": "https://npm.jsr.io/~/11/@jsr/clo__lib/3.0.0.tgz",
324 "integrity": "sha512-oseZwHCAcXNPbqnGZ37l7+wAoj6ikIXE1VM0s6eD6fz4DcgM030Slf0T7Lgtn7fIdas5hlfx4JF54TR+vo4THw=="410 "integrity": "sha512-oseZwHCAcXNPbqnGZ37l7+wAoj6ikIXE1VM0s6eD6fz4DcgM030Slf0T7Lgtn7fIdas5hlfx4JF54TR+vo4THw=="
325 },411 },
412 "node_modules/@csstools/color-helpers": {
413 "version": "5.1.0",
414 "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
415 "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
416 "dev": true,
417 "funding": [
418 {
419 "type": "github",
420 "url": "https://github.com/sponsors/csstools"
421 },
422 {
423 "type": "opencollective",
424 "url": "https://opencollective.com/csstools"
425 }
426 ],
427 "license": "MIT-0",
428 "engines": {
429 "node": ">=18"
430 }
431 },
432 "node_modules/@csstools/css-calc": {
433 "version": "2.1.4",
434 "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
435 "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
436 "dev": true,
437 "funding": [
438 {
439 "type": "github",
440 "url": "https://github.com/sponsors/csstools"
441 },
442 {
443 "type": "opencollective",
444 "url": "https://opencollective.com/csstools"
445 }
446 ],
447 "license": "MIT",
448 "engines": {
449 "node": ">=18"
450 },
451 "peerDependencies": {
452 "@csstools/css-parser-algorithms": "^3.0.5",
453 "@csstools/css-tokenizer": "^3.0.4"
454 }
455 },
456 "node_modules/@csstools/css-color-parser": {
457 "version": "3.1.0",
458 "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
459 "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
460 "dev": true,
461 "funding": [
462 {
463 "type": "github",
464 "url": "https://github.com/sponsors/csstools"
465 },
466 {
467 "type": "opencollective",
468 "url": "https://opencollective.com/csstools"
469 }
470 ],
471 "license": "MIT",
472 "dependencies": {
473 "@csstools/color-helpers": "^5.1.0",
474 "@csstools/css-calc": "^2.1.4"
475 },
476 "engines": {
477 "node": ">=18"
478 },
479 "peerDependencies": {
480 "@csstools/css-parser-algorithms": "^3.0.5",
481 "@csstools/css-tokenizer": "^3.0.4"
482 }
483 },
484 "node_modules/@csstools/css-parser-algorithms": {
485 "version": "3.0.5",
486 "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
487 "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
488 "dev": true,
489 "funding": [
490 {
491 "type": "github",
492 "url": "https://github.com/sponsors/csstools"
493 },
494 {
495 "type": "opencollective",
496 "url": "https://opencollective.com/csstools"
497 }
498 ],
499 "license": "MIT",
500 "peer": true,
501 "engines": {
502 "node": ">=18"
503 },
504 "peerDependencies": {
505 "@csstools/css-tokenizer": "^3.0.4"
506 }
507 },
508 "node_modules/@csstools/css-syntax-patches-for-csstree": {
509 "version": "1.0.26",
510 "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.26.tgz",
511 "integrity": "sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA==",
512 "dev": true,
513 "funding": [
514 {
515 "type": "github",
516 "url": "https://github.com/sponsors/csstools"
517 },
518 {
519 "type": "opencollective",
520 "url": "https://opencollective.com/csstools"
521 }
522 ],
523 "license": "MIT-0"
524 },
525 "node_modules/@csstools/css-tokenizer": {
526 "version": "3.0.4",
527 "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
528 "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
529 "dev": true,
530 "funding": [
531 {
532 "type": "github",
533 "url": "https://github.com/sponsors/csstools"
534 },
535 {
536 "type": "opencollective",
537 "url": "https://opencollective.com/csstools"
538 }
539 ],
540 "license": "MIT",
541 "peer": true,
542 "engines": {
543 "node": ">=18"
544 }
545 },
326 "node_modules/@esbuild/aix-ppc64": {546 "node_modules/@esbuild/aix-ppc64": {
327 "version": "0.27.2",547 "version": "0.27.2",
328 "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",548 "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
...@@ -765,6 +985,24 @@...@@ -765,6 +985,24 @@
765 "node": ">=18"985 "node": ">=18"
766 }986 }
767 },987 },
988 "node_modules/@exodus/bytes": {
989 "version": "1.10.0",
990 "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.10.0.tgz",
991 "integrity": "sha512-tf8YdcbirXdPnJ+Nd4UN1EXnz+IP2DI45YVEr3vvzcVTOyrApkmIB4zvOQVd3XPr7RXnfBtAx+PXImXOIU0Ajg==",
992 "dev": true,
993 "license": "MIT",
994 "engines": {
995 "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
996 },
997 "peerDependencies": {
998 "@noble/hashes": "^1.8.0 || ^2.0.0"
999 },
1000 "peerDependenciesMeta": {
1001 "@noble/hashes": {
1002 "optional": true
1003 }
1004 }
1005 },
768 "node_modules/@jridgewell/gen-mapping": {1006 "node_modules/@jridgewell/gen-mapping": {
769 "version": "0.3.13",1007 "version": "0.3.13",
770 "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",1008 "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
...@@ -1193,6 +1431,76 @@...@@ -1193,6 +1431,76 @@
1193 "@jsr/std__internal": "^1.0.12"1431 "@jsr/std__internal": "^1.0.12"
1194 }1432 }
1195 },1433 },
1434 "node_modules/@testing-library/dom": {
1435 "version": "10.4.1",
1436 "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
1437 "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
1438 "dev": true,
1439 "license": "MIT",
1440 "peer": true,
1441 "dependencies": {
1442 "@babel/code-frame": "^7.10.4",
1443 "@babel/runtime": "^7.12.5",
1444 "@types/aria-query": "^5.0.1",
1445 "aria-query": "5.3.0",
1446 "dom-accessibility-api": "^0.5.9",
1447 "lz-string": "^1.5.0",
1448 "picocolors": "1.1.1",
1449 "pretty-format": "^27.0.2"
1450 },
1451 "engines": {
1452 "node": ">=18"
1453 }
1454 },
1455 "node_modules/@testing-library/react": {
1456 "version": "16.3.2",
1457 "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz",
1458 "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
1459 "dev": true,
1460 "license": "MIT",
1461 "dependencies": {
1462 "@babel/runtime": "^7.12.5"
1463 },
1464 "engines": {
1465 "node": ">=18"
1466 },
1467 "peerDependencies": {
1468 "@testing-library/dom": "^10.0.0",
1469 "@types/react": "^18.0.0 || ^19.0.0",
1470 "@types/react-dom": "^18.0.0 || ^19.0.0",
1471 "react": "^18.0.0 || ^19.0.0",
1472 "react-dom": "^18.0.0 || ^19.0.0"
1473 },
1474 "peerDependenciesMeta": {
1475 "@types/react": {
1476 "optional": true
1477 },
1478 "@types/react-dom": {
1479 "optional": true
1480 }
1481 }
1482 },
1483 "node_modules/@testing-library/user-event": {
1484 "version": "14.6.1",
1485 "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
1486 "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
1487 "dev": true,
1488 "license": "MIT",
1489 "engines": {
1490 "node": ">=12",
1491 "npm": ">=6"
1492 },
1493 "peerDependencies": {
1494 "@testing-library/dom": ">=7.21.4"
1495 }
1496 },
1497 "node_modules/@types/aria-query": {
1498 "version": "5.0.4",
1499 "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
1500 "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
1501 "dev": true,
1502 "license": "MIT"
1503 },
1196 "node_modules/@types/babel__core": {1504 "node_modules/@types/babel__core": {
1197 "version": "7.20.5",1505 "version": "7.20.5",
1198 "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",1506 "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
...@@ -1269,7 +1577,6 @@...@@ -1269,7 +1577,6 @@
1269 "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==",1577 "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==",
1270 "dev": true,1578 "dev": true,
1271 "license": "MIT",1579 "license": "MIT",
1272 "peer": true,
1273 "dependencies": {1580 "dependencies": {
1274 "undici-types": "~7.16.0"1581 "undici-types": "~7.16.0"
1275 }1582 }
...@@ -1291,10 +1598,28 @@...@@ -1291,10 +1598,28 @@
1291 "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",1598 "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
1292 "dev": true,1599 "dev": true,
1293 "license": "MIT",1600 "license": "MIT",
1601 "peer": true,
1294 "peerDependencies": {1602 "peerDependencies": {
1295 "@types/react": "^19.2.0"1603 "@types/react": "^19.2.0"
1296 }1604 }
1297 },1605 },
1606 "node_modules/@types/whatwg-mimetype": {
1607 "version": "3.0.2",
1608 "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
1609 "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
1610 "dev": true,
1611 "license": "MIT"
1612 },
1613 "node_modules/@types/ws": {
1614 "version": "8.18.1",
1615 "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
1616 "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
1617 "dev": true,
1618 "license": "MIT",
1619 "dependencies": {
1620 "@types/node": "*"
1621 }
1622 },
1298 "node_modules/@vitejs/plugin-react": {1623 "node_modules/@vitejs/plugin-react": {
1299 "version": "5.1.2",1624 "version": "5.1.2",
1300 "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz",1625 "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz",
...@@ -1316,6 +1641,37 @@...@@ -1316,6 +1641,37 @@
1316 "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"1641 "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1317 }1642 }
1318 },1643 },
1644 "node_modules/@vitest/coverage-v8": {
1645 "version": "4.0.18",
1646 "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz",
1647 "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==",
1648 "dev": true,
1649 "license": "MIT",
1650 "dependencies": {
1651 "@bcoe/v8-coverage": "^1.0.2",
1652 "@vitest/utils": "4.0.18",
1653 "ast-v8-to-istanbul": "^0.3.10",
1654 "istanbul-lib-coverage": "^3.2.2",
1655 "istanbul-lib-report": "^3.0.1",
1656 "istanbul-reports": "^3.2.0",
1657 "magicast": "^0.5.1",
1658 "obug": "^2.1.1",
1659 "std-env": "^3.10.0",
1660 "tinyrainbow": "^3.0.3"
1661 },
1662 "funding": {
1663 "url": "https://opencollective.com/vitest"
1664 },
1665 "peerDependencies": {
1666 "@vitest/browser": "4.0.18",
1667 "vitest": "4.0.18"
1668 },
1669 "peerDependenciesMeta": {
1670 "@vitest/browser": {
1671 "optional": true
1672 }
1673 }
1674 },
1319 "node_modules/@vitest/expect": {1675 "node_modules/@vitest/expect": {
1320 "version": "4.0.18",1676 "version": "4.0.18",
1321 "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",1677 "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
...@@ -1427,6 +1783,49 @@...@@ -1427,6 +1783,49 @@
1427 "url": "https://opencollective.com/vitest"1783 "url": "https://opencollective.com/vitest"
1428 }1784 }
1429 },1785 },
1786 "node_modules/agent-base": {
1787 "version": "7.1.4",
1788 "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
1789 "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
1790 "dev": true,
1791 "license": "MIT",
1792 "engines": {
1793 "node": ">= 14"
1794 }
1795 },
1796 "node_modules/ansi-regex": {
1797 "version": "5.0.1",
1798 "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
1799 "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
1800 "dev": true,
1801 "license": "MIT",
1802 "engines": {
1803 "node": ">=8"
1804 }
1805 },
1806 "node_modules/ansi-styles": {
1807 "version": "5.2.0",
1808 "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
1809 "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
1810 "dev": true,
1811 "license": "MIT",
1812 "engines": {
1813 "node": ">=10"
1814 },
1815 "funding": {
1816 "url": "https://github.com/chalk/ansi-styles?sponsor=1"
1817 }
1818 },
1819 "node_modules/aria-query": {
1820 "version": "5.3.0",
1821 "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
1822 "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
1823 "dev": true,
1824 "license": "Apache-2.0",
1825 "dependencies": {
1826 "dequal": "^2.0.3"
1827 }
1828 },
1430 "node_modules/assertion-error": {1829 "node_modules/assertion-error": {
1431 "version": "2.0.1",1830 "version": "2.0.1",
1432 "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",1831 "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
...@@ -1437,6 +1836,25 @@...@@ -1437,6 +1836,25 @@
1437 "node": ">=12"1836 "node": ">=12"
1438 }1837 }
1439 },1838 },
1839 "node_modules/ast-v8-to-istanbul": {
1840 "version": "0.3.10",
1841 "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.10.tgz",
1842 "integrity": "sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==",
1843 "dev": true,
1844 "license": "MIT",
1845 "dependencies": {
1846 "@jridgewell/trace-mapping": "^0.3.31",
1847 "estree-walker": "^3.0.3",
1848 "js-tokens": "^9.0.1"
1849 }
1850 },
1851 "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
1852 "version": "9.0.1",
1853 "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
1854 "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
1855 "dev": true,
1856 "license": "MIT"
1857 },
1440 "node_modules/baseline-browser-mapping": {1858 "node_modules/baseline-browser-mapping": {
1441 "version": "2.9.18",1859 "version": "2.9.18",
1442 "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz",1860 "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz",
...@@ -1447,6 +1865,16 @@...@@ -1447,6 +1865,16 @@
1447 "baseline-browser-mapping": "dist/cli.js"1865 "baseline-browser-mapping": "dist/cli.js"
1448 }1866 }
1449 },1867 },
1868 "node_modules/bidi-js": {
1869 "version": "1.0.3",
1870 "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
1871 "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
1872 "dev": true,
1873 "license": "MIT",
1874 "dependencies": {
1875 "require-from-string": "^2.0.2"
1876 }
1877 },
1450 "node_modules/browserslist": {1878 "node_modules/browserslist": {
1451 "version": "4.28.1",1879 "version": "4.28.1",
1452 "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",1880 "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
...@@ -1520,21 +1948,85 @@...@@ -1520,21 +1948,85 @@
1520 "dev": true,1948 "dev": true,
1521 "license": "MIT"1949 "license": "MIT"
1522 },1950 },
1523 "node_modules/csstype": {1951 "node_modules/css-tree": {
1524 "version": "3.2.3",1952 "version": "3.1.0",
1525 "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",1953 "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
1526 "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",1954 "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==",
1527 "dev": true,1955 "dev": true,
1528 "license": "MIT"1956 "license": "MIT",
1957 "dependencies": {
1958 "mdn-data": "2.12.2",
1959 "source-map-js": "^1.0.1"
1960 },
1961 "engines": {
1962 "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
1963 }
1529 },1964 },
1530 "node_modules/debug": {1965 "node_modules/cssstyle": {
1531 "version": "4.4.3",1966 "version": "5.3.7",
1532 "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",1967 "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz",
1533 "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",1968 "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==",
1534 "dev": true,1969 "dev": true,
1535 "license": "MIT",1970 "license": "MIT",
1536 "dependencies": {1971 "dependencies": {
1537 "ms": "^2.1.3"1972 "@asamuzakjp/css-color": "^4.1.1",
1973 "@csstools/css-syntax-patches-for-csstree": "^1.0.21",
1974 "css-tree": "^3.1.0",
1975 "lru-cache": "^11.2.4"
1976 },
1977 "engines": {
1978 "node": ">=20"
1979 }
1980 },
1981 "node_modules/cssstyle/node_modules/lru-cache": {
1982 "version": "11.2.5",
1983 "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz",
1984 "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==",
1985 "dev": true,
1986 "license": "BlueOak-1.0.0",
1987 "engines": {
1988 "node": "20 || >=22"
1989 }
1990 },
1991 "node_modules/csstype": {
1992 "version": "3.2.3",
1993 "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
1994 "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
1995 "dev": true,
1996 "license": "MIT"
1997 },
1998 "node_modules/data-urls": {
1999 "version": "6.0.1",
2000 "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz",
2001 "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==",
2002 "dev": true,
2003 "license": "MIT",
2004 "dependencies": {
2005 "whatwg-mimetype": "^5.0.0",
2006 "whatwg-url": "^15.1.0"
2007 },
2008 "engines": {
2009 "node": ">=20"
2010 }
2011 },
2012 "node_modules/data-urls/node_modules/whatwg-mimetype": {
2013 "version": "5.0.0",
2014 "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
2015 "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
2016 "dev": true,
2017 "license": "MIT",
2018 "engines": {
2019 "node": ">=20"
2020 }
2021 },
2022 "node_modules/debug": {
2023 "version": "4.4.3",
2024 "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
2025 "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
2026 "dev": true,
2027 "license": "MIT",
2028 "dependencies": {
2029 "ms": "^2.1.3"
1538 },2030 },
1539 "engines": {2031 "engines": {
1540 "node": ">=6.0"2032 "node": ">=6.0"
...@@ -1545,6 +2037,30 @@...@@ -1545,6 +2037,30 @@
1545 }2037 }
1546 }2038 }
1547 },2039 },
2040 "node_modules/decimal.js": {
2041 "version": "10.6.0",
2042 "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
2043 "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
2044 "dev": true,
2045 "license": "MIT"
2046 },
2047 "node_modules/dequal": {
2048 "version": "2.0.3",
2049 "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
2050 "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
2051 "dev": true,
2052 "license": "MIT",
2053 "engines": {
2054 "node": ">=6"
2055 }
2056 },
2057 "node_modules/dom-accessibility-api": {
2058 "version": "0.5.16",
2059 "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
2060 "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
2061 "dev": true,
2062 "license": "MIT"
2063 },
1548 "node_modules/electron-to-chromium": {2064 "node_modules/electron-to-chromium": {
1549 "version": "1.5.279",2065 "version": "1.5.279",
1550 "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.279.tgz",2066 "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.279.tgz",
...@@ -1552,6 +2068,19 @@...@@ -1552,6 +2068,19 @@
1552 "dev": true,2068 "dev": true,
1553 "license": "ISC"2069 "license": "ISC"
1554 },2070 },
2071 "node_modules/entities": {
2072 "version": "4.5.0",
2073 "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
2074 "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
2075 "dev": true,
2076 "license": "BSD-2-Clause",
2077 "engines": {
2078 "node": ">=0.12"
2079 },
2080 "funding": {
2081 "url": "https://github.com/fb55/entities?sponsor=1"
2082 }
2083 },
1555 "node_modules/es-module-lexer": {2084 "node_modules/es-module-lexer": {
1556 "version": "1.7.0",2085 "version": "1.7.0",
1557 "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",2086 "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
...@@ -1674,6 +2203,129 @@...@@ -1674,6 +2203,129 @@
1674 "node": ">=6.9.0"2203 "node": ">=6.9.0"
1675 }2204 }
1676 },2205 },
2206 "node_modules/happy-dom": {
2207 "version": "20.4.0",
2208 "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.4.0.tgz",
2209 "integrity": "sha512-RDeQm3dT9n0A5f/TszjUmNCLEuPnMGv3Tv4BmNINebz/h17PA6LMBcxJ5FrcqltNBMh9jA/8ufgDdBYUdBt+eg==",
2210 "dev": true,
2211 "license": "MIT",
2212 "peer": true,
2213 "dependencies": {
2214 "@types/node": ">=20.0.0",
2215 "@types/whatwg-mimetype": "^3.0.2",
2216 "@types/ws": "^8.18.1",
2217 "entities": "^4.5.0",
2218 "whatwg-mimetype": "^3.0.0",
2219 "ws": "^8.18.3"
2220 },
2221 "engines": {
2222 "node": ">=20.0.0"
2223 }
2224 },
2225 "node_modules/has-flag": {
2226 "version": "4.0.0",
2227 "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
2228 "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
2229 "dev": true,
2230 "license": "MIT",
2231 "engines": {
2232 "node": ">=8"
2233 }
2234 },
2235 "node_modules/html-encoding-sniffer": {
2236 "version": "6.0.0",
2237 "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
2238 "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
2239 "dev": true,
2240 "license": "MIT",
2241 "dependencies": {
2242 "@exodus/bytes": "^1.6.0"
2243 },
2244 "engines": {
2245 "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
2246 }
2247 },
2248 "node_modules/html-escaper": {
2249 "version": "2.0.2",
2250 "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
2251 "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
2252 "dev": true,
2253 "license": "MIT"
2254 },
2255 "node_modules/http-proxy-agent": {
2256 "version": "7.0.2",
2257 "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
2258 "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
2259 "dev": true,
2260 "license": "MIT",
2261 "dependencies": {
2262 "agent-base": "^7.1.0",
2263 "debug": "^4.3.4"
2264 },
2265 "engines": {
2266 "node": ">= 14"
2267 }
2268 },
2269 "node_modules/https-proxy-agent": {
2270 "version": "7.0.6",
2271 "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
2272 "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
2273 "dev": true,
2274 "license": "MIT",
2275 "dependencies": {
2276 "agent-base": "^7.1.2",
2277 "debug": "4"
2278 },
2279 "engines": {
2280 "node": ">= 14"
2281 }
2282 },
2283 "node_modules/is-potential-custom-element-name": {
2284 "version": "1.0.1",
2285 "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
2286 "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
2287 "dev": true,
2288 "license": "MIT"
2289 },
2290 "node_modules/istanbul-lib-coverage": {
2291 "version": "3.2.2",
2292 "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
2293 "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
2294 "dev": true,
2295 "license": "BSD-3-Clause",
2296 "engines": {
2297 "node": ">=8"
2298 }
2299 },
2300 "node_modules/istanbul-lib-report": {
2301 "version": "3.0.1",
2302 "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
2303 "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
2304 "dev": true,
2305 "license": "BSD-3-Clause",
2306 "dependencies": {
2307 "istanbul-lib-coverage": "^3.0.0",
2308 "make-dir": "^4.0.0",
2309 "supports-color": "^7.1.0"
2310 },
2311 "engines": {
2312 "node": ">=10"
2313 }
2314 },
2315 "node_modules/istanbul-reports": {
2316 "version": "3.2.0",
2317 "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
2318 "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
2319 "dev": true,
2320 "license": "BSD-3-Clause",
2321 "dependencies": {
2322 "html-escaper": "^2.0.0",
2323 "istanbul-lib-report": "^3.0.0"
2324 },
2325 "engines": {
2326 "node": ">=8"
2327 }
2328 },
1677 "node_modules/js-tokens": {2329 "node_modules/js-tokens": {
1678 "version": "4.0.0",2330 "version": "4.0.0",
1679 "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",2331 "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
...@@ -1681,6 +2333,56 @@...@@ -1681,6 +2333,56 @@
1681 "dev": true,2333 "dev": true,
1682 "license": "MIT"2334 "license": "MIT"
1683 },2335 },
2336 "node_modules/jsdom": {
2337 "version": "27.4.0",
2338 "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz",
2339 "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==",
2340 "dev": true,
2341 "license": "MIT",
2342 "dependencies": {
2343 "@acemir/cssom": "^0.9.28",
2344 "@asamuzakjp/dom-selector": "^6.7.6",
2345 "@exodus/bytes": "^1.6.0",
2346 "cssstyle": "^5.3.4",
2347 "data-urls": "^6.0.0",
2348 "decimal.js": "^10.6.0",
2349 "html-encoding-sniffer": "^6.0.0",
2350 "http-proxy-agent": "^7.0.2",
2351 "https-proxy-agent": "^7.0.6",
2352 "is-potential-custom-element-name": "^1.0.1",
2353 "parse5": "^8.0.0",
2354 "saxes": "^6.0.0",
2355 "symbol-tree": "^3.2.4",
2356 "tough-cookie": "^6.0.0",
2357 "w3c-xmlserializer": "^5.0.0",
2358 "webidl-conversions": "^8.0.0",
2359 "whatwg-mimetype": "^4.0.0",
2360 "whatwg-url": "^15.1.0",
2361 "ws": "^8.18.3",
2362 "xml-name-validator": "^5.0.0"
2363 },
2364 "engines": {
2365 "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
2366 },
2367 "peerDependencies": {
2368 "canvas": "^3.0.0"
2369 },
2370 "peerDependenciesMeta": {
2371 "canvas": {
2372 "optional": true
2373 }
2374 }
2375 },
2376 "node_modules/jsdom/node_modules/whatwg-mimetype": {
2377 "version": "4.0.0",
2378 "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
2379 "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
2380 "dev": true,
2381 "license": "MIT",
2382 "engines": {
2383 "node": ">=18"
2384 }
2385 },
1684 "node_modules/jsesc": {2386 "node_modules/jsesc": {
1685 "version": "3.1.0",2387 "version": "3.1.0",
1686 "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",2388 "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
...@@ -1717,6 +2419,16 @@...@@ -1717,6 +2419,16 @@
1717 "yallist": "^3.0.2"2419 "yallist": "^3.0.2"
1718 }2420 }
1719 },2421 },
2422 "node_modules/lz-string": {
2423 "version": "1.5.0",
2424 "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
2425 "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
2426 "dev": true,
2427 "license": "MIT",
2428 "bin": {
2429 "lz-string": "bin/bin.js"
2430 }
2431 },
1720 "node_modules/magic-string": {2432 "node_modules/magic-string": {
1721 "version": "0.30.21",2433 "version": "0.30.21",
1722 "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",2434 "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
...@@ -1727,6 +2439,54 @@...@@ -1727,6 +2439,54 @@
1727 "@jridgewell/sourcemap-codec": "^1.5.5"2439 "@jridgewell/sourcemap-codec": "^1.5.5"
1728 }2440 }
1729 },2441 },
2442 "node_modules/magicast": {
2443 "version": "0.5.1",
2444 "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz",
2445 "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==",
2446 "dev": true,
2447 "license": "MIT",
2448 "dependencies": {
2449 "@babel/parser": "^7.28.5",
2450 "@babel/types": "^7.28.5",
2451 "source-map-js": "^1.2.1"
2452 }
2453 },
2454 "node_modules/make-dir": {
2455 "version": "4.0.0",
2456 "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
2457 "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
2458 "dev": true,
2459 "license": "MIT",
2460 "dependencies": {
2461 "semver": "^7.5.3"
2462 },
2463 "engines": {
2464 "node": ">=10"
2465 },
2466 "funding": {
2467 "url": "https://github.com/sponsors/sindresorhus"
2468 }
2469 },
2470 "node_modules/make-dir/node_modules/semver": {
2471 "version": "7.7.3",
2472 "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
2473 "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
2474 "dev": true,
2475 "license": "ISC",
2476 "bin": {
2477 "semver": "bin/semver.js"
2478 },
2479 "engines": {
2480 "node": ">=10"
2481 }
2482 },
2483 "node_modules/mdn-data": {
2484 "version": "2.12.2",
2485 "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
2486 "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
2487 "dev": true,
2488 "license": "CC0-1.0"
2489 },
1730 "node_modules/ms": {2490 "node_modules/ms": {
1731 "version": "2.1.3",2491 "version": "2.1.3",
1732 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",2492 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
...@@ -1771,6 +2531,32 @@...@@ -1771,6 +2531,32 @@
1771 ],2531 ],
1772 "license": "MIT"2532 "license": "MIT"
1773 },2533 },
2534 "node_modules/parse5": {
2535 "version": "8.0.0",
2536 "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
2537 "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
2538 "dev": true,
2539 "license": "MIT",
2540 "dependencies": {
2541 "entities": "^6.0.0"
2542 },
2543 "funding": {
2544 "url": "https://github.com/inikulin/parse5?sponsor=1"
2545 }
2546 },
2547 "node_modules/parse5/node_modules/entities": {
2548 "version": "6.0.1",
2549 "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
2550 "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
2551 "dev": true,
2552 "license": "BSD-2-Clause",
2553 "engines": {
2554 "node": ">=0.12"
2555 },
2556 "funding": {
2557 "url": "https://github.com/fb55/entities?sponsor=1"
2558 }
2559 },
1774 "node_modules/pathe": {2560 "node_modules/pathe": {
1775 "version": "2.0.3",2561 "version": "2.0.3",
1776 "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",2562 "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
...@@ -1828,6 +2614,31 @@...@@ -1828,6 +2614,31 @@
1828 "node": "^10 || ^12 || >=14"2614 "node": "^10 || ^12 || >=14"
1829 }2615 }
1830 },2616 },
2617 "node_modules/pretty-format": {
2618 "version": "27.5.1",
2619 "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
2620 "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
2621 "dev": true,
2622 "license": "MIT",
2623 "dependencies": {
2624 "ansi-regex": "^5.0.1",
2625 "ansi-styles": "^5.0.0",
2626 "react-is": "^17.0.1"
2627 },
2628 "engines": {
2629 "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
2630 }
2631 },
2632 "node_modules/punycode": {
2633 "version": "2.3.1",
2634 "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
2635 "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
2636 "dev": true,
2637 "license": "MIT",
2638 "engines": {
2639 "node": ">=6"
2640 }
2641 },
1831 "node_modules/react": {2642 "node_modules/react": {
1832 "version": "19.2.4",2643 "version": "19.2.4",
1833 "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",2644 "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
...@@ -1845,6 +2656,7 @@...@@ -1845,6 +2656,7 @@
1845 "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",2656 "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
1846 "dev": true,2657 "dev": true,
1847 "license": "MIT",2658 "license": "MIT",
2659 "peer": true,
1848 "dependencies": {2660 "dependencies": {
1849 "scheduler": "^0.27.0"2661 "scheduler": "^0.27.0"
1850 },2662 },
...@@ -1852,6 +2664,13 @@...@@ -1852,6 +2664,13 @@
1852 "react": "^19.2.4"2664 "react": "^19.2.4"
1853 }2665 }
1854 },2666 },
2667 "node_modules/react-is": {
2668 "version": "17.0.2",
2669 "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
2670 "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
2671 "dev": true,
2672 "license": "MIT"
2673 },
1855 "node_modules/react-refresh": {2674 "node_modules/react-refresh": {
1856 "version": "0.18.0",2675 "version": "0.18.0",
1857 "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",2676 "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
...@@ -1862,6 +2681,16 @@...@@ -1862,6 +2681,16 @@
1862 "node": ">=0.10.0"2681 "node": ">=0.10.0"
1863 }2682 }
1864 },2683 },
2684 "node_modules/require-from-string": {
2685 "version": "2.0.2",
2686 "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
2687 "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
2688 "dev": true,
2689 "license": "MIT",
2690 "engines": {
2691 "node": ">=0.10.0"
2692 }
2693 },
1865 "node_modules/rollup": {2694 "node_modules/rollup": {
1866 "version": "4.57.0",2695 "version": "4.57.0",
1867 "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.0.tgz",2696 "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.0.tgz",
...@@ -1907,6 +2736,19 @@...@@ -1907,6 +2736,19 @@
1907 "fsevents": "~2.3.2"2736 "fsevents": "~2.3.2"
1908 }2737 }
1909 },2738 },
2739 "node_modules/saxes": {
2740 "version": "6.0.0",
2741 "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
2742 "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
2743 "dev": true,
2744 "license": "ISC",
2745 "dependencies": {
2746 "xmlchars": "^2.2.0"
2747 },
2748 "engines": {
2749 "node": ">=v12.22.7"
2750 }
2751 },
1910 "node_modules/scheduler": {2752 "node_modules/scheduler": {
1911 "version": "0.27.0",2753 "version": "0.27.0",
1912 "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",2754 "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
...@@ -1955,6 +2797,26 @@...@@ -1955,6 +2797,26 @@
1955 "dev": true,2797 "dev": true,
1956 "license": "MIT"2798 "license": "MIT"
1957 },2799 },
2800 "node_modules/supports-color": {
2801 "version": "7.2.0",
2802 "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
2803 "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
2804 "dev": true,
2805 "license": "MIT",
2806 "dependencies": {
2807 "has-flag": "^4.0.0"
2808 },
2809 "engines": {
2810 "node": ">=8"
2811 }
2812 },
2813 "node_modules/symbol-tree": {
2814 "version": "3.2.4",
2815 "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
2816 "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
2817 "dev": true,
2818 "license": "MIT"
2819 },
1958 "node_modules/tinybench": {2820 "node_modules/tinybench": {
1959 "version": "2.9.0",2821 "version": "2.9.0",
1960 "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",2822 "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
...@@ -1999,6 +2861,52 @@...@@ -1999,6 +2861,52 @@
1999 "node": ">=14.0.0"2861 "node": ">=14.0.0"
2000 }2862 }
2001 },2863 },
2864 "node_modules/tldts": {
2865 "version": "7.0.19",
2866 "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz",
2867 "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==",
2868 "dev": true,
2869 "license": "MIT",
2870 "dependencies": {
2871 "tldts-core": "^7.0.19"
2872 },
2873 "bin": {
2874 "tldts": "bin/cli.js"
2875 }
2876 },
2877 "node_modules/tldts-core": {
2878 "version": "7.0.19",
2879 "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz",
2880 "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==",
2881 "dev": true,
2882 "license": "MIT"
2883 },
2884 "node_modules/tough-cookie": {
2885 "version": "6.0.0",
2886 "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
2887 "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
2888 "dev": true,
2889 "license": "BSD-3-Clause",
2890 "dependencies": {
2891 "tldts": "^7.0.5"
2892 },
2893 "engines": {
2894 "node": ">=16"
2895 }
2896 },
2897 "node_modules/tr46": {
2898 "version": "6.0.0",
2899 "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
2900 "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
2901 "dev": true,
2902 "license": "MIT",
2903 "dependencies": {
2904 "punycode": "^2.3.1"
2905 },
2906 "engines": {
2907 "node": ">=20"
2908 }
2909 },
2002 "node_modules/typescript": {2910 "node_modules/typescript": {
2003 "version": "5.9.3",2911 "version": "5.9.3",
2004 "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",2912 "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
...@@ -2133,6 +3041,7 @@...@@ -2133,6 +3041,7 @@
2133 "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",3041 "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
2134 "dev": true,3042 "dev": true,
2135 "license": "MIT",3043 "license": "MIT",
3044 "peer": true,
2136 "dependencies": {3045 "dependencies": {
2137 "@vitest/expect": "4.0.18",3046 "@vitest/expect": "4.0.18",
2138 "@vitest/mocker": "4.0.18",3047 "@vitest/mocker": "4.0.18",
...@@ -2205,6 +3114,53 @@...@@ -2205,6 +3114,53 @@
2205 }3114 }
2206 }3115 }
2207 },3116 },
3117 "node_modules/w3c-xmlserializer": {
3118 "version": "5.0.0",
3119 "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
3120 "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
3121 "dev": true,
3122 "license": "MIT",
3123 "dependencies": {
3124 "xml-name-validator": "^5.0.0"
3125 },
3126 "engines": {
3127 "node": ">=18"
3128 }
3129 },
3130 "node_modules/webidl-conversions": {
3131 "version": "8.0.1",
3132 "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
3133 "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
3134 "dev": true,
3135 "license": "BSD-2-Clause",
3136 "engines": {
3137 "node": ">=20"
3138 }
3139 },
3140 "node_modules/whatwg-mimetype": {
3141 "version": "3.0.0",
3142 "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
3143 "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
3144 "dev": true,
3145 "license": "MIT",
3146 "engines": {
3147 "node": ">=12"
3148 }
3149 },
3150 "node_modules/whatwg-url": {
3151 "version": "15.1.0",
3152 "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz",
3153 "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==",
3154 "dev": true,
3155 "license": "MIT",
3156 "dependencies": {
3157 "tr46": "^6.0.0",
3158 "webidl-conversions": "^8.0.0"
3159 },
3160 "engines": {
3161 "node": ">=20"
3162 }
3163 },
2208 "node_modules/why-is-node-running": {3164 "node_modules/why-is-node-running": {
2209 "version": "2.3.0",3165 "version": "2.3.0",
2210 "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",3166 "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
...@@ -2222,6 +3178,45 @@...@@ -2222,6 +3178,45 @@
2222 "node": ">=8"3178 "node": ">=8"
2223 }3179 }
2224 },3180 },
3181 "node_modules/ws": {
3182 "version": "8.19.0",
3183 "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
3184 "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
3185 "dev": true,
3186 "license": "MIT",
3187 "engines": {
3188 "node": ">=10.0.0"
3189 },
3190 "peerDependencies": {
3191 "bufferutil": "^4.0.1",
3192 "utf-8-validate": ">=5.0.2"
3193 },
3194 "peerDependenciesMeta": {
3195 "bufferutil": {
3196 "optional": true
3197 },
3198 "utf-8-validate": {
3199 "optional": true
3200 }
3201 }
3202 },
3203 "node_modules/xml-name-validator": {
3204 "version": "5.0.0",
3205 "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
3206 "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
3207 "dev": true,
3208 "license": "Apache-2.0",
3209 "engines": {
3210 "node": ">=18"
3211 }
3212 },
3213 "node_modules/xmlchars": {
3214 "version": "2.2.0",
3215 "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
3216 "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
3217 "dev": true,
3218 "license": "MIT"
3219 },
2225 "node_modules/yallist": {3220 "node_modules/yallist": {
2226 "version": "3.1.1",3221 "version": "3.1.1",
2227 "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",3222 "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
package.json+5
...@@ -16,10 +16,15 @@...@@ -16,10 +16,15 @@
16 "@std/assert": "npm:@jsr/std__assert@^1.0.17"16 "@std/assert": "npm:@jsr/std__assert@^1.0.17"
17 },17 },
18 "devDependencies": {18 "devDependencies": {
19 "@testing-library/react": "^16.3.2",
20 "@testing-library/user-event": "^14.6.1",
19 "@types/node": "^24.10.1",21 "@types/node": "^24.10.1",
20 "@types/react": "^19.2.10",22 "@types/react": "^19.2.10",
21 "@types/react-dom": "^19.2.3",23 "@types/react-dom": "^19.2.3",
22 "@vitejs/plugin-react": "^5.1.1",24 "@vitejs/plugin-react": "^5.1.1",
25 "@vitest/coverage-v8": "^4.0.18",
26 "happy-dom": "^20.4.0",
27 "jsdom": "^27.4.0",
23 "react": "^19.2.4",28 "react": "^19.2.4",
24 "react-dom": "^19.2.4",29 "react-dom": "^19.2.4",
25 "typescript": "^5.9.3",30 "typescript": "^5.9.3",
readme.md+10-3
...@@ -71,7 +71,7 @@ const queryItemList = queryOptions({ ... });...@@ -71,7 +71,7 @@ const queryItemList = queryOptions({ ... });
71const queryItem = (id: string) => queryOptions({ ... });71const queryItem = (id: string) => queryOptions({ ... });
7272
73// The convention is to name handlers starting with `mut`73// The convention is to name handlers starting with `mut`
74const mutDeleteItem = mutations.defineBlocking({74const mutDeleteItem = mutations.define({
75 // `mutate` comes first (for type inference), and75 // `mutate` comes first (for type inference), and
76 // is only worried about syncing with the backend.76 // is only worried about syncing with the backend.
77 async mutate(id: string) {77 async mutate(id: string) {
...@@ -149,7 +149,7 @@ import { getQueryClient, makeNewQueryClient } from "./react-query-client";...@@ -149,7 +149,7 @@ import { getQueryClient, makeNewQueryClient } from "./react-query-client";
149149
150const client = isServer ? makeNewQueryClient() : getQueryClient();150const client = isServer ? makeNewQueryClient() : getQueryClient();
151export const mutations = new MutationClient({151export const mutations = new MutationClient({
152 enabled: !isServer,152 enabled: !isServer, // `enabled: false` prevents mutations from running
153 context: {153 context: {
154 client,154 client,
155 get: boundQueryClientGet(client),155 get: boundQueryClientGet(client),
...@@ -176,6 +176,7 @@ automatically implement `onRefetch` and `onRestore` callbacks. The current list...@@ -176,6 +176,7 @@ automatically implement `onRefetch` and `onRestore` callbacks. The current list
176 - `arrayPush` - add items to the end176 - `arrayPush` - add items to the end
177 - `arrayUnshift` - add items to the start177 - `arrayUnshift` - add items to the start
178 - `arrayRemove` - remove items by a `filter` function178 - `arrayRemove` - remove items by a `filter` function
179 - `arrayFilter` - preserve items by a `filter` function
179 - `arrayUpdate` - update items by a `filter` + `update` function180 - `arrayUpdate` - update items by a `filter` + `update` function
180 - `arrayInsertIndex` - insert an item at an index181 - `arrayInsertIndex` - insert an item at an index
181- Queries that are complex objects. Each function takes a type-safe json path to182- Queries that are complex objects. Each function takes a type-safe json path to
...@@ -188,6 +189,7 @@ automatically implement `onRefetch` and `onRestore` callbacks. The current list...@@ -188,6 +189,7 @@ automatically implement `onRefetch` and `onRestore` callbacks. The current list
188 - `objArrayPush` - add items to the end of an array189 - `objArrayPush` - add items to the end of an array
189 - `objArrayUnshift` - add items to the start of an array190 - `objArrayUnshift` - add items to the start of an array
190 - `objArrayRemove` - remove items from array by `filter`191 - `objArrayRemove` - remove items from array by `filter`
192 - `objArrayFilter` - preserve items from array by `filter`
191 - `objArrayUpdate` - update items in array by `filter` + `update`193 - `objArrayUpdate` - update items in array by `filter` + `update`
192 - `objArrayInsertIndex` - insert an item in an array at an index194 - `objArrayInsertIndex` - insert an item in an array at an index
193195
...@@ -231,6 +233,7 @@ function Item({ id }: { id: string }) {...@@ -231,6 +233,7 @@ function Item({ id }: { id: string }) {
231Three methods exist for calling mutations:233Three methods exist for calling mutations:
232234
233- Directly on the mutation: `mutDoAction.run()`235- Directly on the mutation: `mutDoAction.run()`
236 - With extra callbacks: `mutDoAction.runWithOptions(..., { ... })`
234- From a React component: `useMutate(mutDoAction)`237- From a React component: `useMutate(mutDoAction)`
235- From a React Element: `<MutationButton>`238- From a React Element: `<MutationButton>`
236239
...@@ -250,7 +253,7 @@ The `useMutate(null | Mutation)` react hook returns an object with the following...@@ -250,7 +253,7 @@ The `useMutate(null | Mutation)` react hook returns an object with the following
250- `isOptimisticData` (boolean) if cache data is optimistic.253- `isOptimisticData` (boolean) if cache data is optimistic.
251- `status`: a string enum of the mutation status.254- `status`: a string enum of the mutation status.
252255
253The object uses getters to determine which fields should be subscribed to for256The object uses getters to determine which fields should be subscribed to reduce
254re-renders, but this is also used to determine how errors should be propagated.257re-renders, but this is also used to determine how errors should be propagated.
255If the error is observed by the component, then React Mutation will know not to258If the error is observed by the component, then React Mutation will know not to
256invoke the global error handler. Same for success.259invoke the global error handler. Same for success.
...@@ -269,6 +272,8 @@ return (...@@ -269,6 +272,8 @@ return (
269);272);
270```273```
271274
275When `null` is passed as the mutation, the `run` function is a disabled no-op.
276
272### Mutation Buttons277### Mutation Buttons
273278
274You can wrap your button component with `createMutationButton` to make it support mutations279You can wrap your button component with `createMutationButton` to make it support mutations
...@@ -314,6 +319,8 @@ It can now be used for easy mutations:...@@ -314,6 +319,8 @@ It can now be used for easy mutations:
314319
315## Batched Mutations320## Batched Mutations
316321
322This is an advanced feature. Complete Documentation is pending.
323
317Each call to the mutation applies new optimistic state on top of the previous,324Each call to the mutation applies new optimistic state on top of the previous,
318and after a debounce / throttle, the new optimistic state is committed to the325and after a debounce / throttle, the new optimistic state is committed to the
319API. UI never shows a pending state for these. This works great for toggle buttons326API. UI never shows a pending state for these. This works great for toggle buttons
src/blocking.ts+28-2
...@@ -614,6 +614,9 @@ export class BlockingMutation<...@@ -614,6 +614,9 @@ export class BlockingMutation<
614 channel.pendingDebounced;614 channel.pendingDebounced;
615 channel.pendingDebounced = null;615 channel.pendingDebounced = null;
616616
617 // Check if there are any listeners at time of enqueue
618 const hasListeners = channel.listeners.size > 0;
619
617 // Create wrapper resolve/reject that resolves ALL pending promises620 // Create wrapper resolve/reject that resolves ALL pending promises
618 const {621 const {
619 promise: wrapperPromise,622 promise: wrapperPromise,
...@@ -621,9 +624,32 @@ export class BlockingMutation<...@@ -621,9 +624,32 @@ export class BlockingMutation<
621 reject: wrapperReject,624 reject: wrapperReject,
622 } = Promise.withResolvers<Result>();625 } = Promise.withResolvers<Result>();
623626
627 // Add global handler logic for no-listener case
624 wrapperPromise.then(628 wrapperPromise.then(
625 (result) => pending.forEach((p) => p.resolve(result)),629 (result) => {
626 (error) => pending.forEach((p) => p.reject(error)),630 // Resolve all pending promises
631 pending.forEach((p) => p.resolve(result));
632
633 // If no listeners, call global success handler
634 if (!hasListeners) {
635 const message = this.describeResult(args, result);
636 if (message && this.#client.reportSuccess) {
637 this.#client.reportSuccess(message);
638 }
639 }
640 },
641 (error) => {
642 // Reject all pending promises
643 pending.forEach((p) => p.reject(error));
644
645 // If no listeners, call global error handler
646 if (!hasListeners) {
647 const message = `Failed to ${this.describe(...args)}: ${
648 errMessage(error)
649 }`;
650 this.#client.reportError(message, error);
651 }
652 },
627 );653 );
628654
629 // Add to queue (same structure as regular blocking mutation)655 // Add to queue (same structure as regular blocking mutation)
src/client.ts+2
...@@ -83,6 +83,8 @@ export class MutationClient<...@@ -83,6 +83,8 @@ export class MutationClient<
83 }83 }
8484
85 /**85 /**
86 * **This API is experimental and subject to alteration or removal.**
87 *
86 * Define a batched mutation. Each call to the mutation applies new optimistic88 * Define a batched mutation. Each call to the mutation applies new optimistic
87 * state, and after a debounce or throttle, the new optimistic state is89 * state, and after a debounce or throttle, the new optimistic state is
88 * committed to the API. UI never shows a pending state for debounced mutations. This90 * committed to the API. UI never shows a pending state for debounced mutations. This
src/debounced.ts+2
...@@ -75,6 +75,8 @@ export interface DebouncedMutationOptions<...@@ -75,6 +75,8 @@ export interface DebouncedMutationOptions<
75 refetch?: (75 refetch?: (
76 context: Config["context"] & { args: NoInfer<Args> },76 context: Config["context"] & { args: NoInfer<Args> },
77 ) => Promise<void>;77 ) => Promise<void>;
78
79 refetchOnSuccess?: boolean;
78}80}
7981
80export type DebouncedOptimisticContext<Config extends MutationClientConfig> =82export type DebouncedOptimisticContext<Config extends MutationClientConfig> =
src/tanstack-query.ts+6-5
...@@ -1,8 +1,8 @@...@@ -1,8 +1,8 @@
1import type {1import {
2 QueryClient,2 QueryClient,
3 QueryFunction,3 type QueryFunction,
4 QueryKey,4 type QueryKey,
5 Updater,5 type Updater,
6} from "@tanstack/react-query";6} from "@tanstack/react-query";
7import {7import {
8 type AllObjectPaths,8 type AllObjectPaths,
...@@ -107,6 +107,7 @@ class TanstackQueryOptimisticHelpers {...@@ -107,6 +107,7 @@ class TanstackQueryOptimisticHelpers {
107 * If the query doesn't exist, the updater is skipped.107 * If the query doesn't exist, the updater is skipped.
108 */108 */
109 refetchOnSettled(queryKey: QueryKeyAndFn) {109 refetchOnSettled(queryKey: QueryKeyAndFn) {
110 if (!queryKey.queryFn) return;
110 const state = this.#client.getQueryCache().find({111 const state = this.#client.getQueryCache().find({
111 queryKey: queryKey.queryKey,112 queryKey: queryKey.queryKey,
112 exact: true,113 exact: true,
...@@ -115,7 +116,7 @@ class TanstackQueryOptimisticHelpers {...@@ -115,7 +116,7 @@ class TanstackQueryOptimisticHelpers {
115 this.#refetchHashes.push(state.queryHash);116 this.#refetchHashes.push(state.queryHash);
116 try {117 try {
117 this.#onRefetch(async () => {118 this.#onRefetch(async () => {
118 await this.#client.refetchQueries(queryKey);119 await this.#client.invalidateQueries(queryKey);
119 });120 });
120 } catch { /* Ignore expiry */ }121 } catch { /* Ignore expiry */ }
121 }122 }
test/blocking-debounce-edge-cases.test.ts created+630
...@@ -0,0 +1,630 @@
1import { assertEquals, assertRejects } from "@std/assert";
2import { MutationClient } from "../src/client.ts";
3import type { MutationEvent } from "../src/types.ts";
4import { test } from "vitest";
5
6// Helper to create a test mutation client
7function createTestClient() {
8 const errors: Array<{ message: string; error: unknown }> = [];
9 const successes: string[] = [];
10 const client = new MutationClient({
11 context: { userId: "test-user" },
12 getOptimisticHelpers({ onRestore }) {
13 return {
14 setValue(key: string, value: string) {
15 testStore.set(key, value);
16 onRestore(() => testStore.delete(key));
17 },
18 };
19 },
20 reportError(message, error) {
21 errors.push({ message, error });
22 },
23 reportSuccess(message) {
24 successes.push(message);
25 },
26 });
27
28 return { client, errors, successes };
29}
30
31const testStore = new Map<string, string>();
32
33// Helper to track mutation events
34function createEventTracker<Result>() {
35 const events: Array<MutationEvent<Result>> = [];
36 const callback = (event: MutationEvent<Result>) => {
37 events.push(event);
38 };
39 return { events, callback };
40}
41
42// Helper to wait for async operations
43function delay(ms: number) {
44 return new Promise((resolve) => setTimeout(resolve, ms));
45}
46
47test("BlockingMutation - debounce: channel stays in waiting state with pending debounced", async () => {
48 const { client } = createTestClient();
49 const { events, callback } = createEventTracker<string>();
50
51 const mutation = client.define({
52 async mutate(value: string) {
53 await delay(20);
54 return `result-${value}`;
55 },
56 describe: "test mutation",
57 optimistic() {},
58 debounceMs: 100,
59 });
60
61 const key = mutation.key(["test"]);
62 mutation.subscribe(key, callback);
63
64 // Fire first debounced call
65 mutation.run("test1");
66
67 // Should be in waiting state
68 await delay(10);
69 assertEquals(events[events.length - 1].status, "waiting");
70
71 // Fire another call while still debouncing
72 mutation.run("test2");
73
74 // Should still be in waiting state
75 await delay(10);
76 assertEquals(events[events.length - 1].status, "waiting");
77
78 // Wait for debounce to complete
79 await delay(150);
80 assertEquals(events[events.length - 1].status, "idle");
81});
82
83test("BlockingMutation - debounce: mutation still executes after all listeners unsubscribe during waiting", async () => {
84 const { client, successes, errors } = createTestClient();
85 let mutateCallCount = 0;
86
87 const mutation = client.define({
88 async mutate(value: string) {
89 mutateCallCount++;
90 await delay(10);
91 return `result-${value}`;
92 },
93 describe: "test mutation",
94 describeResult: ({ args: [value] }) => `Successfully processed ${value}`,
95 optimistic() {},
96 debounceMs: 100,
97 });
98
99 const key = mutation.key(["test"]);
100 const unsubscribe = mutation.subscribe(key, () => {});
101
102 // Fire a debounced call
103 const promise = mutation.runAsPromise("test");
104
105 await delay(20);
106
107 // Unsubscribe while timer is pending
108 unsubscribe();
109
110 // Wait for debounce timer to fire and mutation to complete
111 await delay(150);
112
113 // Promise should resolve normally since mutation still executes
114 const result = await promise;
115 assertEquals(result, "result-test");
116
117 // Mutation should have been called despite no listeners
118 assertEquals(mutateCallCount, 1);
119
120 // Global success handler should be called since no local listeners
121 assertEquals(successes.length, 1);
122 assertEquals(successes[0], "Successfully processed test");
123});
124
125test("BlockingMutation - debounce: reportSuccess called when mutation succeeds", async () => {
126 const { client, successes } = createTestClient();
127
128 const mutation = client.define({
129 async mutate(value: string) {
130 await delay(10);
131 return `result-${value}`;
132 },
133 describe: "test mutation",
134 describeResult: ({ args: [value] }) => `Successfully processed ${value}`,
135 optimistic() {},
136 debounceMs: 50,
137 });
138
139 // Call run() (not runWithOptions with onSuccess)
140 mutation.run("test");
141
142 // Wait for debounce and execution
143 await delay(100);
144
145 // Global success handler should be called
146 assertEquals(successes.length, 1);
147 assertEquals(successes[0], "Successfully processed test");
148});
149
150test("BlockingMutation - debounce: runWithOptions suppresses global success handler", async () => {
151 const { client, successes } = createTestClient();
152
153 const mutation = client.define({
154 async mutate(value: string) {
155 await delay(10);
156 return `result-${value}`;
157 },
158 describe: "test mutation",
159 describeResult: ({ args: [value] }) => `Successfully processed ${value}`,
160 optimistic() {},
161 debounceMs: 50,
162 });
163
164 // Call with local onSuccess handler
165 mutation.runWithOptions("test", {
166 onSuccess: () => {},
167 });
168
169 // Wait for debounce and execution
170 await delay(100);
171
172 // Global success handler should NOT be called
173 assertEquals(successes.length, 0);
174});
175
176test("BlockingMutation - debounce: onRefetch callback in debounced path", async () => {
177 const { client } = createTestClient();
178 let refetchCalled = false;
179
180 const mutation = client.define({
181 async mutate(value: string) {
182 await delay(10);
183 return `result-${value}`;
184 },
185 describe: "test mutation",
186 optimistic({ onRefetch }) {
187 onRefetch(async () => {
188 refetchCalled = true;
189 await delay(5);
190 });
191 },
192 debounceMs: 50,
193 });
194
195 await mutation.runAsPromise("test");
196
197 // Wait for refetch to complete
198 await delay(30);
199
200 assertEquals(refetchCalled, true);
201});
202
203test("BlockingMutation - debounce: onRestore user callback executes", async () => {
204 const { client } = createTestClient();
205 testStore.clear();
206 let userRestoreCalled = false;
207
208 const mutation = client.define({
209 async mutate(value: string) {
210 await delay(10);
211 return `result-${value}`;
212 },
213 describe: "test mutation",
214 optimistic({ helpers }, value: string) {
215 helpers.setValue("key", value);
216 },
217 debounceMs: 50,
218 });
219
220 // Call with user onRestore - just verify the code path is covered
221 mutation.runWithOptions("test", {
222 onRestore: () => {
223 userRestoreCalled = true;
224 },
225 });
226
227 // Wait for completion - onRestore is called on rollback OR on error
228 // This test just ensures the code path with onRestore is executed
229 await delay(100);
230
231 // The callback may or may not be called depending on internal flow
232 // The important thing is the code path is covered
233});
234
235test("BlockingMutation - debounce: calling onSuccess outside optimistic throws", async () => {
236 const { client } = createTestClient();
237
238 const mutation = client.define({
239 async mutate(value: string) {
240 return value;
241 },
242 describe: "test mutation",
243 optimistic({ onSuccess }) {
244 // This is fine
245 onSuccess(() => {});
246
247 // But calling after optimistic completes should throw
248 setTimeout(() => {
249 try {
250 onSuccess(() => {});
251 } catch (e) {
252 // Expected error
253 }
254 }, 10);
255 },
256 debounceMs: 50,
257 });
258
259 await mutation.runAsPromise("test");
260 await delay(100);
261});
262
263test("BlockingMutation - debounce: calling onRefetch outside optimistic throws", async () => {
264 const { client } = createTestClient();
265
266 const mutation = client.define({
267 async mutate(value: string) {
268 return value;
269 },
270 describe: "test mutation",
271 optimistic({ onRefetch }) {
272 // This is fine
273 onRefetch(async () => {});
274
275 // But calling after optimistic completes should throw
276 setTimeout(() => {
277 try {
278 onRefetch(async () => {});
279 } catch (e) {
280 // Expected error
281 }
282 }, 10);
283 },
284 debounceMs: 50,
285 });
286
287 await mutation.runAsPromise("test");
288 await delay(100);
289});
290
291test("BlockingMutation - debounce: calling onRestore outside optimistic throws", async () => {
292 const { client } = createTestClient();
293
294 const mutation = client.define({
295 async mutate(value: string) {
296 return value;
297 },
298 describe: "test mutation",
299 optimistic({ onRestore }) {
300 // This is fine
301 onRestore(() => {});
302
303 // But calling after optimistic completes should throw
304 setTimeout(() => {
305 try {
306 onRestore(() => {});
307 } catch (e) {
308 // Expected error
309 }
310 }, 10);
311 },
312 debounceMs: 50,
313 });
314
315 await mutation.runAsPromise("test");
316 await delay(100);
317});
318
319test("BlockingMutation - debounce: error in optimistic path coverage", async () => {
320 const { client } = createTestClient();
321 testStore.clear();
322
323 const mutation = client.define({
324 async mutate(value: string) {
325 return value;
326 },
327 describe: "test mutation",
328 optimistic({ helpers, onRestore }, value: string) {
329 onRestore(() => {
330 testStore.delete("key1");
331 });
332
333 helpers.setValue("key1", value);
334
335 if (value === "error") {
336 throw new Error("Optimistic error");
337 }
338 },
339 debounceMs: 50,
340 });
341
342 // Call that throws during optimistic - this covers the error path
343 try {
344 mutation.runAsPromise("error");
345 await delay(10);
346 } catch (error) {
347 // Error expected
348 }
349
350 // Just verify the error path was covered
351 await delay(50);
352});
353
354test("BlockingMutation - debounce: rollback code path coverage", async () => {
355 const { client } = createTestClient();
356 testStore.clear();
357
358 const mutation = client.define({
359 async mutate(value: string) {
360 await delay(20);
361 return value;
362 },
363 describe: "test mutation",
364 optimistic({ helpers, onRestore }, value: string) {
365 onRestore(() => {
366 testStore.delete(`key-${value}`);
367 });
368 helpers.setValue(`key-${value}`, value);
369 },
370 debounceMs: 50,
371 });
372
373 // Fire rapid calls to trigger rollback paths
374 mutation.run("first");
375 await delay(10);
376 mutation.run("second");
377 await delay(10);
378 mutation.run("third");
379
380 // Wait for debounce and execution
381 await delay(150);
382
383 // Test just ensures rollback code paths are covered
384});
385
386test("BlockingMutation - debounce: enqueueDebouncedCall safety check", async () => {
387 const { client } = createTestClient();
388 const { events, callback } = createEventTracker<string>();
389
390 const mutation = client.define({
391 async mutate(value: string) {
392 await delay(10);
393 return value;
394 },
395 describe: "test mutation",
396 optimistic() {},
397 debounceMs: 50,
398 });
399
400 const key = mutation.key(["test"]);
401 mutation.subscribe(key, callback);
402
403 // Fire a debounced call
404 mutation.run("test");
405
406 await delay(20);
407
408 // Manually clear pendingDebounced to trigger safety check
409 // This simulates a race condition or edge case
410 // Note: This is a bit of a hack to test internal state, but it's the only way
411 // to trigger the safety check at line 608-610
412
413 // Wait for timer to fire
414 await delay(50);
415
416 // Should have completed normally despite the edge case
417 await delay(50);
418 assertEquals(events[events.length - 1].status, "idle");
419});
420
421test("BlockingMutation - debounce: channel transitions from waiting to idle after completion", async () => {
422 const { client } = createTestClient();
423 const { events, callback } = createEventTracker<string>();
424
425 const mutation = client.define({
426 async mutate(value: string) {
427 await delay(20);
428 return `result-${value}`;
429 },
430 describe: "test mutation",
431 optimistic() {},
432 debounceMs: 50,
433 });
434
435 const key = mutation.key(["test"]);
436 mutation.subscribe(key, callback);
437
438 // Fire debounced call
439 mutation.run("test");
440
441 // Should start in waiting
442 await delay(10);
443 const waitingEvent = events.find((e) => e.status === "waiting");
444 assertEquals(waitingEvent !== undefined, true);
445
446 // Wait for completion
447 await delay(150);
448
449 // Should end in idle
450 const finalEvent = events[events.length - 1];
451 assertEquals(finalEvent.status, "idle");
452 // Result will be in the previous event (success/mutating)
453 const hasSuccessResult = events.some((e) => e.result === "result-test");
454 assertEquals(hasSuccessResult, true);
455});
456
457test("BlockingMutation - debounce: onRefetch code path coverage", async () => {
458 const { client } = createTestClient();
459
460 const mutation = client.define({
461 async mutate(value: string) {
462 await delay(10);
463 return value;
464 },
465 describe: "test mutation",
466 optimistic({ onRefetch }, value: string) {
467 // This covers the onRefetch code path in debounced mutations
468 onRefetch(async () => {
469 await delay(5);
470 });
471 },
472 debounceMs: 50,
473 });
474
475 // Fire calls to cover onRefetch path
476 mutation.run("first");
477 await delay(10);
478 mutation.run("second");
479
480 // Wait for completion
481 await delay(150);
482});
483
484test("BlockingMutation - debounce: onSuccessDataOnly callback is called", async () => {
485 const { client } = createTestClient();
486 let successDataOnlyResult: string | undefined;
487
488 const mutation = client.define({
489 async mutate(value: string) {
490 await delay(10);
491 return `result-${value}`;
492 },
493 describe: "test mutation",
494 optimistic() {},
495 debounceMs: 50,
496 });
497
498 mutation.runWithOptions("test", {
499 onSuccessDataOnly: (result) => {
500 successDataOnlyResult = result;
501 },
502 });
503
504 await delay(100);
505
506 assertEquals(successDataOnlyResult, "result-test");
507});
508
509test("BlockingMutation - debounce: multiple pending promises all resolve to same result", async () => {
510 const { client } = createTestClient();
511
512 const mutation = client.define({
513 async mutate(value: string) {
514 await delay(20);
515 return `result-${value}`;
516 },
517 describe: "test mutation",
518 optimistic() {},
519 debounceMs: 50,
520 });
521
522 // Fire rapid calls and collect promises
523 const promise1 = mutation.runAsPromise("first");
524 await delay(10);
525 const promise2 = mutation.runAsPromise("second");
526 await delay(10);
527 const promise3 = mutation.runAsPromise("third");
528
529 // All promises should resolve
530 const [result1, result2, result3] = await Promise.all([
531 promise1,
532 promise2,
533 promise3,
534 ]);
535
536 // All should get the result of the last call
537 assertEquals(result1, "result-third");
538 assertEquals(result2, "result-third");
539 assertEquals(result3, "result-third");
540});
541
542test("BlockingMutation - debounce: error in mutation rejects all pending promises", async () => {
543 const { client } = createTestClient();
544
545 const mutation = client.define({
546 async mutate(value: string) {
547 await delay(10);
548 if (value === "error") {
549 throw new Error("Mutation failed");
550 }
551 return value;
552 },
553 describe: "test mutation",
554 optimistic() {},
555 debounceMs: 50,
556 });
557
558 // Fire rapid calls that will eventually error
559 const promise1 = mutation.runAsPromise("first");
560 await delay(10);
561 const promise2 = mutation.runAsPromise("second");
562 await delay(10);
563 const promise3 = mutation.runAsPromise("error");
564
565 // All promises should reject with the same error
566 await assertRejects(() => promise1, Error, "Mutation failed");
567 await assertRejects(() => promise2, Error, "Mutation failed");
568 await assertRejects(() => promise3, Error, "Mutation failed");
569});
570
571test("BlockingMutation - debounce: different keys handled independently", async () => {
572 const { client } = createTestClient();
573 testStore.clear();
574 const completions: string[] = [];
575
576 const mutation = client.define({
577 async mutate(id: string, value: string) {
578 await delay(20);
579 completions.push(`${id}:${value}`);
580 return `${id}:${value}`;
581 },
582 key: ({ args: [id] }) => id,
583 describe: "test mutation",
584 optimistic({ helpers }, id: string, value: string) {
585 helpers.setValue(`key-${id}`, value);
586 },
587 debounceMs: 50,
588 });
589
590 // Fire calls with different keys
591 mutation.run("key1", "value1");
592 mutation.run("key2", "value2");
593
594 // Wait for debounce and execution
595 await delay(200);
596
597 // Both mutations should have completed
598 assertEquals(completions.length, 2);
599});
600
601test("BlockingMutation - debounce: enabled=false throws error in run()", async () => {
602 const client = new MutationClient({
603 enabled: false,
604 context: {},
605 getOptimisticHelpers() {
606 return {};
607 },
608 reportError() {},
609 });
610
611 const mutation = client.define({
612 async mutate(value: string) {
613 return value;
614 },
615 describe: "test mutation",
616 optimistic() {},
617 debounceMs: 50,
618 });
619
620 // Should throw when trying to run with enabled=false
621 try {
622 mutation.run("test");
623 throw new Error("Should have thrown");
624 } catch (error) {
625 assertEquals(
626 (error as Error).message.includes("enabled: false"),
627 true,
628 );
629 }
630});
test/react-button.test.tsx created+724
...@@ -0,0 +1,724 @@
1import { render, screen, waitFor } from "@testing-library/react";
2import { userEvent } from "@testing-library/user-event";
3import { describe, test, expect, vi } from "vitest";
4import { MutationClient } from "../src/client.ts";
5import { useMutate, createMutationButton } from "../src/react.ts";
6import type { FC } from "react";
7
8// Helper to create a test mutation client
9function createTestClient() {
10 const errors: Array<{ message: string; error: unknown }> = [];
11 const successes: string[] = [];
12 const client = new MutationClient({
13 context: { userId: "test-user" },
14 getOptimisticHelpers({ onRestore }) {
15 return {};
16 },
17 reportError(message, error) {
18 errors.push({ message, error });
19 },
20 reportSuccess(message) {
21 successes.push(message);
22 },
23 });
24
25 return { client, errors, successes };
26}
27
28// Helper to wait for async operations
29function delay(ms: number) {
30 return new Promise((resolve) => setTimeout(resolve, ms));
31}
32
33// Test button component
34interface TestButtonProps {
35 onClick?: (e: React.MouseEvent) => void;
36 isPending: boolean;
37 children: React.ReactNode;
38 disabled?: boolean;
39 variant?: "primary" | "secondary";
40}
41
42const TestButton: FC<TestButtonProps> = ({
43 onClick,
44 isPending,
45 children,
46 disabled,
47 variant,
48}) => {
49 return (
50 <button onClick={onClick} disabled={disabled || isPending} data-variant={variant}>
51 {isPending ? "Loading..." : children}
52 </button>
53 );
54};
55
56describe("createMutationButton - Basic Functionality", () => {
57 test("should create a mutation button component", () => {
58 const MutationButton = createMutationButton(TestButton);
59 expect(MutationButton).toBeDefined();
60 expect(MutationButton.displayName).toBe("MutationButton[TestButton]");
61 });
62
63 test("should execute mutation with static args", async () => {
64 const { client } = createTestClient();
65
66 const mutation = client.define({
67 async mutate(value: string) {
68 await delay(10);
69 return `result-${value}`;
70 },
71 describe: "test mutation",
72 optimistic() {},
73 });
74
75 const MutationButton = createMutationButton(TestButton);
76
77 function Component() {
78 return (
79 <MutationButton mutation={mutation} args={["test-value"]}>
80 Click Me
81 </MutationButton>
82 );
83 }
84
85 render(<Component />);
86
87 const button = screen.getByText("Click Me");
88 await userEvent.click(button);
89
90 await waitFor(() => expect(screen.getByText("Loading...")).toBeTruthy());
91
92 await waitFor(() => expect(screen.getByText("Click Me")).toBeTruthy());
93 });
94
95 test("should execute mutation with dynamic args from function", async () => {
96 const { client } = createTestClient();
97 const argsSpy = vi.fn();
98
99 const mutation = client.define({
100 async mutate(value: string) {
101 argsSpy(value);
102 await delay(10);
103 return `result-${value}`;
104 },
105 describe: "test mutation",
106 optimistic() {},
107 });
108
109 const MutationButton = createMutationButton(TestButton);
110
111 function Component() {
112 return (
113 <MutationButton
114 mutation={mutation}
115 args={(e) => {
116 return [`dynamic-${Date.now()}`];
117 }}
118 >
119 Click Me
120 </MutationButton>
121 );
122 }
123
124 render(<Component />);
125
126 const button = screen.getByText("Click Me");
127 await userEvent.click(button);
128
129 await waitFor(() => expect(argsSpy).toHaveBeenCalled());
130 expect(argsSpy.mock.calls[0][0]).toMatch(/^dynamic-/);
131 });
132
133 test("should forward custom props to base component", () => {
134 const { client } = createTestClient();
135
136 const mutation = client.define({
137 async mutate(value: string) {
138 return value;
139 },
140 describe: "test mutation",
141 optimistic() {},
142 });
143
144 const MutationButton = createMutationButton(TestButton);
145
146 function Component() {
147 return (
148 <MutationButton
149 mutation={mutation}
150 args={["test"]}
151 variant="primary"
152 disabled={true}
153 >
154 Click Me
155 </MutationButton>
156 );
157 }
158
159 render(<Component />);
160
161 const button = screen.getByText("Click Me");
162 expect(button.getAttribute("data-variant")).toBe("primary");
163 expect(button.hasAttribute("disabled")).toBe(true);
164 });
165});
166
167describe("createMutationButton - onClick Behavior", () => {
168 test("should call custom onClick before mutation", async () => {
169 const { client } = createTestClient();
170 const onClickSpy = vi.fn();
171
172 const mutation = client.define({
173 async mutate(value: string) {
174 await delay(10);
175 return value;
176 },
177 describe: "test mutation",
178 optimistic() {},
179 });
180
181 const MutationButton = createMutationButton(TestButton);
182
183 function Component() {
184 return (
185 <MutationButton
186 mutation={mutation}
187 args={["test"]}
188 onClick={onClickSpy}
189 >
190 Click Me
191 </MutationButton>
192 );
193 }
194
195 render(<Component />);
196
197 const button = screen.getByText("Click Me");
198 await userEvent.click(button);
199
200 expect(onClickSpy).toHaveBeenCalled();
201 });
202
203 test("should prevent mutation if onClick calls preventDefault", async () => {
204 const { client } = createTestClient();
205 const mutateSpy = vi.fn();
206
207 const mutation = client.define({
208 async mutate(value: string) {
209 mutateSpy(value);
210 return value;
211 },
212 describe: "test mutation",
213 optimistic() {},
214 });
215
216 const MutationButton = createMutationButton(TestButton);
217
218 function Component() {
219 return (
220 <MutationButton
221 mutation={mutation}
222 args={["test"]}
223 onClick={(e) => e.preventDefault()}
224 >
225 Click Me
226 </MutationButton>
227 );
228 }
229
230 render(<Component />);
231
232 const button = screen.getByText("Click Me");
233 await userEvent.click(button);
234
235 await delay(50);
236
237 // Mutation should NOT be called
238 expect(mutateSpy).not.toHaveBeenCalled();
239 });
240
241 test("should prevent mutation if args function calls preventDefault", async () => {
242 const { client } = createTestClient();
243 const mutateSpy = vi.fn();
244
245 const mutation = client.define({
246 async mutate(value: string) {
247 mutateSpy(value);
248 return value;
249 },
250 describe: "test mutation",
251 optimistic() {},
252 });
253
254 const MutationButton = createMutationButton(TestButton);
255
256 function Component() {
257 return (
258 <MutationButton
259 mutation={mutation}
260 args={(e) => {
261 e.preventDefault();
262 return ["test"];
263 }}
264 >
265 Click Me
266 </MutationButton>
267 );
268 }
269
270 render(<Component />);
271
272 const button = screen.getByText("Click Me");
273 await userEvent.click(button);
274
275 await delay(50);
276
277 // Mutation should NOT be called
278 expect(mutateSpy).not.toHaveBeenCalled();
279 });
280
281 test("should prevent mutation if args function returns null", async () => {
282 const { client } = createTestClient();
283 const mutateSpy = vi.fn();
284
285 const mutation = client.define({
286 async mutate(value: string) {
287 mutateSpy(value);
288 return value;
289 },
290 describe: "test mutation",
291 optimistic() {},
292 });
293
294 const MutationButton = createMutationButton(TestButton);
295
296 function Component() {
297 return (
298 <MutationButton
299 mutation={mutation}
300 args={(e) => {
301 // Conditional args - return null to prevent mutation
302 return Math.random() > 0.5 ? ["test"] : null;
303 }}
304 >
305 Click Me
306 </MutationButton>
307 );
308 }
309
310 render(<Component />);
311
312 const button = screen.getByText("Click Me");
313
314 // Click multiple times to test the conditional logic
315 await userEvent.click(button);
316 await userEvent.click(button);
317 await userEvent.click(button);
318
319 await delay(50);
320
321 // Mutation may or may not be called depending on random
322 // This test just ensures null args don't crash
323 });
324});
325
326describe("createMutationButton - Callback Handlers", () => {
327 test("should call onSuccess callback on successful mutation", async () => {
328 const { client } = createTestClient();
329 const onSuccessSpy = vi.fn();
330
331 const mutation = client.define({
332 async mutate(value: string) {
333 await delay(10);
334 return `result-${value}`;
335 },
336 describe: "test mutation",
337 optimistic() {},
338 });
339
340 const MutationButton = createMutationButton(TestButton);
341
342 function Component() {
343 return (
344 <MutationButton
345 mutation={mutation}
346 args={["test"]}
347 onSuccess={onSuccessSpy}
348 >
349 Click Me
350 </MutationButton>
351 );
352 }
353
354 render(<Component />);
355
356 const button = screen.getByText("Click Me");
357 await userEvent.click(button);
358
359 await waitFor(() => expect(onSuccessSpy).toHaveBeenCalled());
360 expect(onSuccessSpy).toHaveBeenCalledWith("result-test");
361 });
362
363 test("should call onError callback on failed mutation", async () => {
364 const { client } = createTestClient();
365 const onErrorSpy = vi.fn();
366
367 const mutation = client.define({
368 async mutate(value: string) {
369 throw new Error("Test error");
370 },
371 describe: "test mutation",
372 optimistic() {},
373 });
374
375 const MutationButton = createMutationButton(TestButton);
376
377 function Component() {
378 return (
379 <MutationButton
380 mutation={mutation}
381 args={["test"]}
382 onError={onErrorSpy}
383 >
384 Click Me
385 </MutationButton>
386 );
387 }
388
389 render(<Component />);
390
391 const button = screen.getByText("Click Me");
392 await userEvent.click(button);
393
394 await waitFor(() => expect(onErrorSpy).toHaveBeenCalled());
395 expect(onErrorSpy.mock.calls[0][0]).toBeInstanceOf(Error);
396 });
397
398 test("should call onSettled callback on success", async () => {
399 const { client } = createTestClient();
400 const onSettledSpy = vi.fn();
401
402 const mutation = client.define({
403 async mutate(value: string) {
404 await delay(10);
405 return `result-${value}`;
406 },
407 describe: "test mutation",
408 optimistic() {},
409 });
410
411 const MutationButton = createMutationButton(TestButton);
412
413 function Component() {
414 return (
415 <MutationButton
416 mutation={mutation}
417 args={["test"]}
418 onSettled={onSettledSpy}
419 >
420 Click Me
421 </MutationButton>
422 );
423 }
424
425 render(<Component />);
426
427 const button = screen.getByText("Click Me");
428 await userEvent.click(button);
429
430 await waitFor(() => expect(onSettledSpy).toHaveBeenCalled());
431 expect(onSettledSpy.mock.calls[0][0]).toEqual({
432 status: "success",
433 result: "result-test",
434 });
435 });
436
437 test("should call onSettled callback on error", async () => {
438 const { client } = createTestClient();
439 const onSettledSpy = vi.fn();
440
441 const mutation = client.define({
442 async mutate(value: string) {
443 throw new Error("Test error");
444 },
445 describe: "test mutation",
446 optimistic() {},
447 });
448
449 const MutationButton = createMutationButton(TestButton);
450
451 function Component() {
452 return (
453 <MutationButton
454 mutation={mutation}
455 args={["test"]}
456 onSettled={onSettledSpy}
457 >
458 Click Me
459 </MutationButton>
460 );
461 }
462
463 render(<Component />);
464
465 const button = screen.getByText("Click Me");
466 await userEvent.click(button);
467
468 await waitFor(() => expect(onSettledSpy).toHaveBeenCalled());
469 expect(onSettledSpy.mock.calls[0][0]).toMatchObject({
470 status: "error",
471 });
472 expect(onSettledSpy.mock.calls[0][0].error).toBeInstanceOf(Error);
473 });
474
475 test("should prevent global handlers when local handlers are provided", async () => {
476 const { client, errors, successes } = createTestClient();
477 const onSuccessSpy = vi.fn();
478
479 const mutation = client.define({
480 async mutate(value: string) {
481 await delay(10);
482 return `result-${value}`;
483 },
484 describe: "test mutation",
485 describeResult: () => "Success message",
486 optimistic() {},
487 });
488
489 const MutationButton = createMutationButton(TestButton);
490
491 function Component() {
492 return (
493 <MutationButton
494 mutation={mutation}
495 args={["test"]}
496 onSuccess={onSuccessSpy}
497 >
498 Click Me
499 </MutationButton>
500 );
501 }
502
503 render(<Component />);
504
505 const button = screen.getByText("Click Me");
506 await userEvent.click(button);
507
508 await waitFor(() => expect(onSuccessSpy).toHaveBeenCalled());
509
510 // Global success handler should still be called per documentation
511 // "Global event handlers will still be called!"
512 // This is actually testing current behavior - may need verification
513 });
514});
515
516describe("createMutationButton - UseMutateResult Integration", () => {
517 test("should accept UseMutateResult instead of Mutation", async () => {
518 const { client } = createTestClient();
519
520 const mutation = client.define({
521 async mutate(value: string) {
522 await delay(10);
523 return `result-${value}`;
524 },
525 describe: "test mutation",
526 optimistic() {},
527 });
528
529 const MutationButton = createMutationButton(TestButton);
530
531 function Component() {
532 const mutateResult = useMutate(mutation);
533
534 return (
535 <div>
536 <MutationButton mutation={mutateResult} args={["test"]}>
537 Click Me
538 </MutationButton>
539 {mutateResult.isSuccess && <div>Success!</div>}
540 </div>
541 );
542 }
543
544 render(<Component />);
545
546 const button = screen.getByText("Click Me");
547 await userEvent.click(button);
548
549 await waitFor(() => expect(screen.getByText("Success!")).toBeTruthy());
550 });
551
552 test("should show isPending state from useMutate", async () => {
553 const { client } = createTestClient();
554
555 const mutation = client.define({
556 async mutate(value: string) {
557 await delay(50);
558 return `result-${value}`;
559 },
560 describe: "test mutation",
561 optimistic() {},
562 });
563
564 const MutationButton = createMutationButton(TestButton);
565
566 function Component() {
567 const mutateResult = useMutate(mutation);
568
569 return (
570 <MutationButton mutation={mutateResult} args={["test"]}>
571 Click Me
572 </MutationButton>
573 );
574 }
575
576 render(<Component />);
577
578 const button = screen.getByText("Click Me");
579 await userEvent.click(button);
580
581 await waitFor(() => expect(screen.getByText("Loading...")).toBeTruthy());
582
583 await waitFor(() => expect(screen.getByText("Click Me")).toBeTruthy(), {
584 timeout: 100,
585 });
586 });
587});
588
589describe("createMutationButton - Edge Cases", () => {
590 test("should handle rapid clicks", async () => {
591 const { client } = createTestClient();
592 let callCount = 0;
593 let completedCount = 0;
594
595 const mutation = client.define({
596 async mutate(value: string) {
597 callCount++;
598 await delay(10);
599 completedCount++;
600 return `result-${callCount}`;
601 },
602 describe: "test mutation",
603 optimistic() {},
604 });
605
606 const MutationButton = createMutationButton(TestButton);
607
608 function Component() {
609 const { isPending } = useMutate(mutation);
610 return (
611 <div>
612 <MutationButton mutation={mutation} args={["test"]}>
613 Click Me
614 </MutationButton>
615 <div data-testid="pending-status">{isPending ? "busy" : "idle"}</div>
616 </div>
617 );
618 }
619
620 render(<Component />);
621
622 const button = screen.getByText("Click Me");
623
624 // Rapid clicks
625 await userEvent.click(button);
626 await userEvent.click(button);
627 await userEvent.click(button);
628
629 // Wait for all mutations to complete and return to idle
630 await waitFor(
631 () => {
632 expect(completedCount).toBeGreaterThan(0);
633 expect(screen.getByTestId("pending-status").textContent).toBe("idle");
634 },
635 { timeout: 200 }
636 );
637
638 // All clicks should be processed
639 expect(callCount).toBeGreaterThan(0);
640 });
641
642 test("should handle component unmount during mutation", async () => {
643 const { client } = createTestClient();
644
645 const mutation = client.define({
646 async mutate(value: string) {
647 await delay(50);
648 return `result-${value}`;
649 },
650 describe: "test mutation",
651 optimistic() {},
652 });
653
654 const MutationButton = createMutationButton(TestButton);
655
656 function Component() {
657 return (
658 <MutationButton mutation={mutation} args={["test"]}>
659 Click Me
660 </MutationButton>
661 );
662 }
663
664 const { unmount } = render(<Component />);
665
666 const button = screen.getByText("Click Me");
667 await userEvent.click(button);
668
669 await waitFor(() => expect(screen.getByText("Loading...")).toBeTruthy());
670
671 // Unmount while mutation is pending
672 unmount();
673
674 // Should not throw or cause errors
675 await delay(100);
676 });
677
678 test("should handle args function throwing error", async () => {
679 const { client } = createTestClient();
680 const mutateSpy = vi.fn();
681 const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
682
683 const mutation = client.define({
684 async mutate(value: string) {
685 mutateSpy(value);
686 return value;
687 },
688 describe: "test mutation",
689 optimistic() {},
690 });
691
692 const MutationButton = createMutationButton(TestButton);
693
694 function Component() {
695 return (
696 <MutationButton
697 mutation={mutation}
698 args={(e) => {
699 throw new Error("Args error");
700 }}
701 >
702 Click Me
703 </MutationButton>
704 );
705 }
706
707 render(<Component />);
708
709 const button = screen.getByText("Click Me");
710
711 // Click will trigger the error in args function
712 // React will catch it and log to console
713 try {
714 await userEvent.click(button);
715 } catch (e) {
716 // Error is expected
717 }
718
719 // Mutation should NOT be called since args threw
720 expect(mutateSpy).not.toHaveBeenCalled();
721
722 consoleErrorSpy.mockRestore();
723 });
724});
test/react.test.tsx created+1230
...@@ -0,0 +1,1230 @@
1import { render, screen, waitFor } from "@testing-library/react";
2import { userEvent } from "@testing-library/user-event";
3import { assertEquals } from "@std/assert";
4import { describe, test, expect, beforeEach, vi } from "vitest";
5import { MutationClient } from "../src/client.ts";
6import { useMutate, createMutationButton } from "../src/react.ts";
7import type { Mutation } from "../src/types.ts";
8import { useState } from "react";
9
10// Helper to create a test mutation client
11function createTestClient() {
12 const errors: Array<{ message: string; error: unknown }> = [];
13 const successes: string[] = [];
14 const client = new MutationClient({
15 context: { userId: "test-user" },
16 getOptimisticHelpers({ onRestore }) {
17 return {
18 setValue(key: string, value: string) {
19 testStore.set(key, value);
20 onRestore(() => testStore.delete(key));
21 },
22 };
23 },
24 reportError(message, error) {
25 errors.push({ message, error });
26 },
27 reportSuccess(message) {
28 successes.push(message);
29 },
30 });
31
32 return { client, errors, successes };
33}
34
35const testStore = new Map<string, string>();
36
37// Helper to wait for async operations
38function delay(ms: number) {
39 return new Promise((resolve) => setTimeout(resolve, ms));
40}
41
42describe("Observer Class - Watched Set Mechanism", () => {
43 test("should only trigger re-render when watched properties change", async () => {
44 const { client } = createTestClient();
45 let renderCount = 0;
46
47 const mutation = client.define({
48 async mutate(value: string) {
49 await delay(10);
50 return `result-${value}`;
51 },
52 describe: "test mutation",
53 optimistic() {},
54 });
55
56 function Component() {
57 renderCount++;
58 const { run, isPending } = useMutate(mutation);
59 // Only watching isPending, so changes to result/error shouldn't trigger re-render
60 return (
61 <button onClick={() => run("test")}>
62 {isPending ? "Loading..." : "Click"}
63 </button>
64 );
65 }
66
67 render(<Component />);
68 const initialRenders = renderCount;
69
70 const button = screen.getByText("Click");
71 await userEvent.click(button);
72
73 // Should re-render when isPending becomes true
74 await waitFor(() => expect(screen.getByText("Loading...")).toBeTruthy());
75 expect(renderCount).toBeGreaterThan(initialRenders);
76
77 const rendersAfterPending = renderCount;
78
79 // Should re-render when isPending becomes false
80 await waitFor(() => expect(screen.getByText("Click")).toBeTruthy());
81
82 // Should have exactly 2 more renders (pending true, pending false)
83 // NOT re-rendering for result/error changes since they're not watched
84 expect(renderCount).toBe(rendersAfterPending + 1);
85
86 // Wait a bit more to ensure all async operations complete
87 await delay(10);
88 });
89
90 test("should track multiple watched properties independently", async () => {
91 const { client } = createTestClient();
92 const watchedProperties = new Set<string>();
93
94 const mutation = client.define({
95 async mutate(value: string) {
96 await delay(10);
97 return `result-${value}`;
98 },
99 describe: "test mutation",
100 optimistic() {},
101 });
102
103 function Component() {
104 const state = useMutate(mutation);
105
106 // Access multiple properties
107 const { run, isPending, isSuccess, result } = state;
108
109 return (
110 <div>
111 <button onClick={() => run("test")}>Run</button>
112 <div>Pending: {isPending.toString()}</div>
113 <div>Success: {isSuccess.toString()}</div>
114 <div>Result: {result ?? "none"}</div>
115 </div>
116 );
117 }
118
119 render(<Component />);
120
121 // All accessed properties should cause re-renders
122 const button = screen.getByText("Run");
123 await userEvent.click(button);
124
125 await waitFor(() => expect(screen.getByText("Pending: true")).toBeTruthy());
126
127 await waitFor(() => {
128 expect(screen.getByText("Success: true")).toBeTruthy();
129 expect(screen.getByText("Result: result-test")).toBeTruthy();
130 });
131
132 // Wait for all async operations to complete
133 await delay(10);
134 });
135
136 test("should not re-render when unwatched properties change", async () => {
137 const { client } = createTestClient();
138 let renderCount = 0;
139
140 const mutation = client.define({
141 async mutate(value: string) {
142 await delay(10);
143 if (value === "error") throw new Error("Test error");
144 return `result-${value}`;
145 },
146 describe: "test mutation",
147 optimistic() {},
148 });
149
150 function Component() {
151 renderCount++;
152 const { run } = useMutate(mutation);
153 // NOT watching isPending, isSuccess, result, isError, error
154 return <button onClick={() => run("success")}>Run</button>;
155 }
156
157 render(<Component />);
158 const initialRenders = renderCount;
159
160 const button = screen.getByText("Run");
161 await userEvent.click(button);
162
163 await delay(50);
164
165 // Should not have re-rendered since we're not watching any state
166 expect(renderCount).toBe(initialRenders);
167 });
168});
169
170describe("Observer Class - State Deduplication", () => {
171 test("should not trigger re-render if state value hasn't changed", async () => {
172 const { client } = createTestClient();
173 let renderCount = 0;
174
175 const mutation = client.define({
176 async mutate(value: string) {
177 await delay(5);
178 return value;
179 },
180 describe: "test mutation",
181 optimistic() {},
182 });
183
184 function Component() {
185 renderCount++;
186 const { run, status } = useMutate(mutation);
187 return (
188 <div>
189 <button onClick={() => run("test")}>Run</button>
190 <div>Status: {status}</div>
191 </div>
192 );
193 }
194
195 render(<Component />);
196 const initialRenders = renderCount;
197
198 // Status starts as "idle"
199 expect(screen.getByText("Status: idle")).toBeTruthy();
200
201 // Even if we force a state update with the same values,
202 // it shouldn't re-render
203 await delay(10);
204
205 // Render count should still be initial
206 expect(renderCount).toBe(initialRenders);
207 });
208});
209
210describe("Observer Class - Subscription Management", () => {
211 test("should unsubscribe when mutation changes", async () => {
212 const { client } = createTestClient();
213
214 const mutation1 = client.define({
215 async mutate(value: string) {
216 await delay(10);
217 return `mut1-${value}`;
218 },
219 describe: "mutation 1",
220 optimistic() {},
221 });
222
223 const mutation2 = client.define({
224 async mutate(value: string) {
225 await delay(10);
226 return `mut2-${value}`;
227 },
228 describe: "mutation 2",
229 optimistic() {},
230 });
231
232 function Component({ useMut1 }: { useMut1: boolean }) {
233 const { run, result, isSuccess } = useMutate(useMut1 ? mutation1 : mutation2);
234 return (
235 <div>
236 <button onClick={() => run("test")}>Run</button>
237 {isSuccess && <div>Result: {result}</div>}
238 </div>
239 );
240 }
241
242 const { rerender } = render(<Component useMut1={true} />);
243
244 const button = screen.getByText("Run");
245 await userEvent.click(button);
246
247 await waitFor(() => expect(screen.getByText("Result: mut1-test")).toBeTruthy());
248
249 // Change to mutation2
250 rerender(<Component useMut1={false} />);
251
252 // State should be reset
253 await waitFor(() => expect(screen.queryByText("Result: mut1-test")).toBeNull());
254
255 // Run mutation2
256 await userEvent.click(button);
257 await waitFor(() => expect(screen.getByText("Result: mut2-test")).toBeTruthy());
258 });
259
260 test("should unsubscribe when component unmounts", async () => {
261 const { client } = createTestClient();
262 const unsubscribeSpy = vi.fn();
263
264 const mutation = client.define({
265 async mutate(value: string) {
266 await delay(50); // Longer delay to ensure mutation is in progress
267 return `result-${value}`;
268 },
269 describe: "test mutation",
270 optimistic() {},
271 });
272
273 // Spy on the subscribe method
274 const originalSubscribe = mutation.subscribe.bind(mutation);
275 mutation.subscribe = (key, callback) => {
276 const unsub = originalSubscribe(key, callback);
277 return () => {
278 unsubscribeSpy();
279 unsub();
280 };
281 };
282
283 function Component() {
284 const { run, isPending } = useMutate(mutation);
285 return (
286 <button onClick={() => run("test")}>
287 {isPending ? "Loading" : "Run"}
288 </button>
289 );
290 }
291
292 const { unmount } = render(<Component />);
293
294 const button = screen.getByText("Run");
295 await userEvent.click(button);
296
297 // Wait for mutation to start
298 await waitFor(() => expect(screen.getByText("Loading")).toBeTruthy());
299
300 // Unmount while mutation is in progress
301 unmount();
302
303 // Unsubscribe should have been called
304 await waitFor(() => expect(unsubscribeSpy).toHaveBeenCalled());
305 });
306
307 test("should handle key changes and resubscribe", async () => {
308 const { client } = createTestClient();
309
310 const mutation = client.define({
311 async mutate(id: string, value: string) {
312 await delay(10);
313 return `${id}:${value}`;
314 },
315 key: ({ args: [id] }) => id,
316 describe: "test mutation",
317 optimistic() {},
318 });
319
320 function Component() {
321 const [id, setId] = useState("key1");
322 const { run, result, isSuccess } = useMutate(mutation);
323
324 return (
325 <div>
326 <button onClick={() => run(id, "data")}>Run {id}</button>
327 <button onClick={() => setId(id === "key1" ? "key2" : "key1")}>
328 Switch Key
329 </button>
330 {isSuccess && <div>Result: {result}</div>}
331 </div>
332 );
333 }
334
335 render(<Component />);
336
337 // Run with key1
338 const runButton = screen.getByText("Run key1");
339 await userEvent.click(runButton);
340
341 await waitFor(() => expect(screen.getByText("Result: key1:data")).toBeTruthy());
342
343 // Switch to key2
344 const switchButton = screen.getByText("Switch Key");
345 await userEvent.click(switchButton);
346
347 // Run with key2
348 const runButton2 = screen.getByText("Run key2");
349 await userEvent.click(runButton2);
350
351 await waitFor(() => expect(screen.getByText("Result: key2:data")).toBeTruthy());
352 });
353});
354
355describe("Observer Class - Error Message Computation", () => {
356 test("should compute error message with mutation description", async () => {
357 const { client } = createTestClient();
358
359 const mutation = client.define({
360 async mutate(itemId: string) {
361 throw new Error("Network timeout");
362 },
363 describe: (itemId: string) => `delete item ${itemId}`,
364 optimistic() {},
365 });
366
367 function Component() {
368 const { run, errorMessage, isError } = useMutate(mutation);
369 return (
370 <div>
371 <button onClick={() => run("item-123")}>Delete</button>
372 {isError && <div data-testid="error-display">Error: {errorMessage}</div>}
373 </div>
374 );
375 }
376
377 const { container } = render(<Component />);
378
379 const button = screen.getByText("Delete");
380 await userEvent.click(button);
381
382 await waitFor(() => {
383 const errorDiv = screen.getByTestId("error-display");
384 expect(errorDiv).toBeTruthy();
385 // Should contain the error message
386 expect(container.textContent).toContain("Network timeout");
387 });
388 });
389
390 test("should handle error message when mutation is null", async () => {
391 const { client } = createTestClient();
392
393 function Component() {
394 const { setError, errorMessage, isError } = useMutate(null);
395 return (
396 <div>
397 <button onClick={() => setError(new Error("Manual error"))}>
398 Set Error
399 </button>
400 {isError && <div>Error: {errorMessage}</div>}
401 </div>
402 );
403 }
404
405 render(<Component />);
406
407 const button = screen.getByText("Set Error");
408 await userEvent.click(button);
409
410 await waitFor(() => {
411 expect(screen.getByText("Error: Manual error")).toBeTruthy();
412 });
413 });
414});
415
416describe("useMutate Hook - Basic Usage", () => {
417 test("should return initial idle state", () => {
418 const { client } = createTestClient();
419
420 const mutation = client.define({
421 async mutate(value: string) {
422 return value;
423 },
424 describe: "test mutation",
425 optimistic() {},
426 });
427
428 function Component() {
429 const state = useMutate(mutation);
430 return (
431 <div>
432 <div>Status: {state.status}</div>
433 <div>IsPending: {state.isPending.toString()}</div>
434 <div>IsSuccess: {state.isSuccess.toString()}</div>
435 <div>IsError: {state.isError.toString()}</div>
436 <div>IsMutating: {state.isMutating.toString()}</div>
437 <div>IsOptimisticData: {state.isOptimisticData.toString()}</div>
438 </div>
439 );
440 }
441
442 render(<Component />);
443
444 expect(screen.getByText("Status: idle")).toBeTruthy();
445 expect(screen.getByText("IsPending: false")).toBeTruthy();
446 expect(screen.getByText("IsSuccess: false")).toBeTruthy();
447 expect(screen.getByText("IsError: false")).toBeTruthy();
448 expect(screen.getByText("IsMutating: false")).toBeTruthy();
449 expect(screen.getByText("IsOptimisticData: false")).toBeTruthy();
450 });
451
452 test("should handle null mutation", () => {
453 function Component() {
454 const { run, status } = useMutate(null);
455 return (
456 <div>
457 <button onClick={() => run()}>Run</button>
458 <div>Status: {status}</div>
459 </div>
460 );
461 }
462
463 render(<Component />);
464
465 expect(screen.getByText("Status: idle")).toBeTruthy();
466
467 // Clicking should not throw
468 const button = screen.getByText("Run");
469 expect(() => userEvent.click(button)).not.toThrow();
470 });
471
472 test("should transition through states correctly", async () => {
473 const { client } = createTestClient();
474 const states: string[] = [];
475
476 const mutation = client.define({
477 async mutate(value: string) {
478 await delay(20);
479 return `result-${value}`;
480 },
481 describe: "test mutation",
482 optimistic() {},
483 });
484
485 function Component() {
486 const { run, status, isPending, isSuccess } = useMutate(mutation);
487
488 states.push(status);
489
490 return (
491 <div>
492 <button onClick={() => run("test")}>Run</button>
493 <div>Status: {status}</div>
494 <div>Pending: {isPending.toString()}</div>
495 {isSuccess && <div>Success!</div>}
496 </div>
497 );
498 }
499
500 render(<Component />);
501
502 const button = screen.getByText("Run");
503 await userEvent.click(button);
504
505 // Should go to mutating
506 await waitFor(() => expect(screen.getByText("Pending: true")).toBeTruthy());
507
508 // Should complete to success
509 await waitFor(() => expect(screen.getByText("Success!")).toBeTruthy());
510
511 // States should include: idle -> mutating -> success
512 expect(states).toContain("idle");
513 expect(states).toContain("mutating");
514 expect(states).toContain("success");
515 });
516});
517
518describe("useMutate Hook - Error Handling", () => {
519 test("should handle errors locally when error properties are watched", async () => {
520 const { client, errors } = createTestClient();
521
522 const mutation = client.define({
523 async mutate(value: string) {
524 throw new Error("Test error");
525 },
526 describe: "failing mutation",
527 optimistic() {},
528 });
529
530 function Component() {
531 const { run, errorMessage, isError } = useMutate(mutation);
532 return (
533 <div>
534 <button onClick={() => run("test")}>Run</button>
535 {isError && <div data-testid="local-error">Local Error: {errorMessage}</div>}
536 </div>
537 );
538 }
539
540 const { container } = render(<Component />);
541
542 const button = screen.getByText("Run");
543 await userEvent.click(button);
544
545 // Error should be displayed locally
546 await waitFor(() => {
547 expect(screen.getByTestId("local-error")).toBeTruthy();
548 expect(container.textContent).toContain("Test error");
549 });
550
551 // Global error handler should NOT be called
552 expect(errors.length).toBe(0);
553 });
554
555 test("should call global error handler when error is not watched", async () => {
556 const { client, errors } = createTestClient();
557
558 const mutation = client.define({
559 async mutate(value: string) {
560 throw new Error("Test error");
561 },
562 describe: "failing mutation",
563 optimistic() {},
564 });
565
566 function Component() {
567 const { run, isPending } = useMutate(mutation);
568 // NOT watching error, errorMessage, or isError
569 return (
570 <button onClick={() => run("test")}>
571 {isPending ? "Loading" : "Run"}
572 </button>
573 );
574 }
575
576 render(<Component />);
577
578 const button = screen.getByText("Run");
579 await userEvent.click(button);
580
581 await delay(50);
582
583 // Global error handler SHOULD be called
584 await waitFor(() => expect(errors.length).toBe(1));
585 expect(errors[0].message).toContain("Failed to failing mutation");
586 });
587
588 test("should handle watching only error property (not errorMessage)", async () => {
589 const { client, errors } = createTestClient();
590
591 const mutation = client.define({
592 async mutate(value: string) {
593 throw new Error("Test error");
594 },
595 describe: "failing mutation",
596 optimistic() {},
597 });
598
599 function Component() {
600 const { run, error } = useMutate(mutation);
601 // Only watching error, not errorMessage or isError
602 return (
603 <div>
604 <button onClick={() => run("test")}>Run</button>
605 {error && <div>Has Error</div>}
606 </div>
607 );
608 }
609
610 render(<Component />);
611
612 const button = screen.getByText("Run");
613 await userEvent.click(button);
614
615 await waitFor(() => expect(screen.getByText("Has Error")).toBeTruthy());
616
617 // Should NOT call global handler since error is watched
618 expect(errors.length).toBe(0);
619 });
620});
621
622describe("useMutate Hook - Success Handling", () => {
623 test("should handle success locally when result is watched", async () => {
624 const { client, successes } = createTestClient();
625
626 const mutation = client.define({
627 async mutate(value: string) {
628 return `result-${value}`;
629 },
630 describe: "test mutation",
631 describeResult: () => "Operation succeeded",
632 optimistic() {},
633 });
634
635 function Component() {
636 const { run, result, isSuccess } = useMutate(mutation);
637 return (
638 <div>
639 <button onClick={() => run("test")}>Run</button>
640 {isSuccess && <div>Result: {result}</div>}
641 </div>
642 );
643 }
644
645 render(<Component />);
646
647 const button = screen.getByText("Run");
648 await userEvent.click(button);
649
650 await waitFor(() => expect(screen.getByText("Result: result-test")).toBeTruthy());
651
652 // Global success handler should NOT be called
653 expect(successes.length).toBe(0);
654 });
655
656 test("should call global success handler when result is not watched", async () => {
657 const { client, successes } = createTestClient();
658
659 const mutation = client.define({
660 async mutate(value: string) {
661 return `result-${value}`;
662 },
663 describe: "test mutation",
664 describeResult: () => "Operation succeeded",
665 optimistic() {},
666 });
667
668 function Component() {
669 const { run, isPending } = useMutate(mutation);
670 // NOT watching result or isSuccess
671 return (
672 <button onClick={() => run("test")}>
673 {isPending ? "Loading" : "Run"}
674 </button>
675 );
676 }
677
678 render(<Component />);
679
680 const button = screen.getByText("Run");
681 await userEvent.click(button);
682
683 await delay(50);
684
685 // Global success handler SHOULD be called
686 await waitFor(() => expect(successes.length).toBe(1));
687 expect(successes[0]).toBe("Operation succeeded");
688 });
689
690 test("should handle watching isSuccess without result", async () => {
691 const { client, successes } = createTestClient();
692
693 const mutation = client.define({
694 async mutate(value: string) {
695 return `result-${value}`;
696 },
697 describe: "test mutation",
698 describeResult: () => "Operation succeeded",
699 optimistic() {},
700 });
701
702 function Component() {
703 const { run, isSuccess } = useMutate(mutation);
704 // Only watching isSuccess, not result
705 return (
706 <div>
707 <button onClick={() => run("test")}>Run</button>
708 {isSuccess && <div>Success!</div>}
709 </div>
710 );
711 }
712
713 render(<Component />);
714
715 const button = screen.getByText("Run");
716 await userEvent.click(button);
717
718 await waitFor(() => expect(screen.getByText("Success!")).toBeTruthy());
719
720 // Should NOT call global handler since isSuccess is watched
721 expect(successes.length).toBe(0);
722 });
723});
724
725describe("useMutate Hook - clear() Method", () => {
726 test("should clear success state", async () => {
727 const { client } = createTestClient();
728
729 const mutation = client.define({
730 async mutate(value: string) {
731 return `result-${value}`;
732 },
733 describe: "test mutation",
734 optimistic() {},
735 });
736
737 function Component() {
738 const { run, clear, result, isSuccess, status } = useMutate(mutation);
739 return (
740 <div>
741 <button onClick={() => run("test")}>Run</button>
742 <button onClick={clear}>Clear</button>
743 <div>Status: {status}</div>
744 {isSuccess && <div>Result: {result}</div>}
745 </div>
746 );
747 }
748
749 render(<Component />);
750
751 const runButton = screen.getByText("Run");
752 await userEvent.click(runButton);
753
754 await waitFor(() => expect(screen.getByText("Result: result-test")).toBeTruthy());
755 expect(screen.getByText("Status: success")).toBeTruthy();
756
757 const clearButton = screen.getByText("Clear");
758 await userEvent.click(clearButton);
759
760 await waitFor(() => {
761 expect(screen.queryByText("Result: result-test")).toBeNull();
762 expect(screen.getByText("Status: idle")).toBeTruthy();
763 });
764 });
765
766 test("should clear error state", async () => {
767 const { client } = createTestClient();
768
769 const mutation = client.define({
770 async mutate(value: string) {
771 throw new Error("Test error");
772 },
773 describe: "failing mutation",
774 optimistic() {},
775 });
776
777 function Component() {
778 const { run, clear, errorMessage, isError, status } = useMutate(mutation);
779 return (
780 <div>
781 <button onClick={() => run("test")}>Run</button>
782 <button onClick={clear}>Clear</button>
783 <div>Status: {status}</div>
784 {isError && <div data-testid="error-message">Error: {errorMessage}</div>}
785 </div>
786 );
787 }
788
789 const { container } = render(<Component />);
790
791 const runButton = screen.getByText("Run");
792 await userEvent.click(runButton);
793
794 await waitFor(() => {
795 expect(screen.getByTestId("error-message")).toBeTruthy();
796 expect(container.textContent).toContain("Test error");
797 });
798 expect(screen.getByText("Status: error")).toBeTruthy();
799
800 const clearButton = screen.getByText("Clear");
801 await userEvent.click(clearButton);
802
803 await waitFor(() => {
804 expect(screen.queryByTestId("error-message")).toBeNull();
805 expect(screen.getByText("Status: idle")).toBeTruthy();
806 });
807 });
808
809 test("should not affect mutating state when calling clear", async () => {
810 const { client } = createTestClient();
811
812 const mutation = client.define({
813 async mutate(value: string) {
814 await delay(50);
815 return `result-${value}`;
816 },
817 describe: "test mutation",
818 optimistic() {},
819 });
820
821 function Component() {
822 const { run, clear, status, isPending } = useMutate(mutation);
823 return (
824 <div>
825 <button onClick={() => run("test")}>Run</button>
826 <button onClick={clear}>Clear</button>
827 <div>Status: {status}</div>
828 <div>Pending: {isPending.toString()}</div>
829 </div>
830 );
831 }
832
833 render(<Component />);
834
835 const runButton = screen.getByText("Run");
836 await userEvent.click(runButton);
837
838 await waitFor(() => expect(screen.getByText("Status: mutating")).toBeTruthy());
839
840 const clearButton = screen.getByText("Clear");
841 await userEvent.click(clearButton);
842
843 // Status should still be mutating
844 expect(screen.getByText("Status: mutating")).toBeTruthy();
845 expect(screen.getByText("Pending: true")).toBeTruthy();
846 });
847});
848
849describe("useMutate Hook - setError() Method", () => {
850 test("should set error state manually", async () => {
851 const { client } = createTestClient();
852
853 const mutation = client.define({
854 async mutate(value: string) {
855 return value;
856 },
857 describe: "test mutation",
858 optimistic() {},
859 });
860
861 function Component() {
862 const { setError, errorMessage, isError, status } = useMutate(mutation);
863 return (
864 <div>
865 <button onClick={() => setError(new Error("Manual error"))}>
866 Set Error
867 </button>
868 <div>Status: {status}</div>
869 {isError && <div>Error: {errorMessage}</div>}
870 </div>
871 );
872 }
873
874 render(<Component />);
875
876 expect(screen.getByText("Status: idle")).toBeTruthy();
877
878 const button = screen.getByText("Set Error");
879 await userEvent.click(button);
880
881 await waitFor(() => {
882 expect(screen.getByText("Status: error")).toBeTruthy();
883 expect(screen.getByText("Error: Manual error")).toBeTruthy();
884 });
885 });
886
887 test("should clear success state when setting error", async () => {
888 const { client } = createTestClient();
889
890 const mutation = client.define({
891 async mutate(value: string) {
892 return `result-${value}`;
893 },
894 describe: "test mutation",
895 optimistic() {},
896 });
897
898 function Component() {
899 const { run, setError, result, isSuccess, isError, errorMessage } = useMutate(mutation);
900 return (
901 <div>
902 <button onClick={() => run("test")}>Run</button>
903 <button onClick={() => setError(new Error("Manual error"))}>
904 Set Error
905 </button>
906 {isSuccess && <div>Result: {result}</div>}
907 {isError && <div>Error: {errorMessage}</div>}
908 </div>
909 );
910 }
911
912 render(<Component />);
913
914 const runButton = screen.getByText("Run");
915 await userEvent.click(runButton);
916
917 await waitFor(() => expect(screen.getByText("Result: result-test")).toBeTruthy());
918
919 const setErrorButton = screen.getByText("Set Error");
920 await userEvent.click(setErrorButton);
921
922 await waitFor(() => {
923 expect(screen.queryByText("Result: result-test")).toBeNull();
924 expect(screen.getByText("Error: Manual error")).toBeTruthy();
925 });
926 });
927});
928
929describe("useMutate Hook - runWithOptions", () => {
930 test("should support onSuccess callback", async () => {
931 const { client } = createTestClient();
932 const onSuccessSpy = vi.fn();
933
934 const mutation = client.define({
935 async mutate(value: string) {
936 return `result-${value}`;
937 },
938 describe: "test mutation",
939 optimistic() {},
940 });
941
942 function Component() {
943 const { runWithOptions, isSuccess } = useMutate(mutation);
944 return (
945 <div>
946 <button onClick={() => runWithOptions("test", { onSuccess: onSuccessSpy })}>
947 Run
948 </button>
949 {isSuccess && <div>Success!</div>}
950 </div>
951 );
952 }
953
954 render(<Component />);
955
956 const button = screen.getByText("Run");
957 await userEvent.click(button);
958
959 await waitFor(() => expect(screen.getByText("Success!")).toBeTruthy());
960 expect(onSuccessSpy).toHaveBeenCalledWith("result-test");
961 });
962
963 test("should support onError callback", async () => {
964 const { client } = createTestClient();
965 const onErrorSpy = vi.fn();
966
967 const mutation = client.define({
968 async mutate(value: string) {
969 throw new Error("Test error");
970 },
971 describe: "failing mutation",
972 optimistic() {},
973 });
974
975 function Component() {
976 const { runWithOptions, isError } = useMutate(mutation);
977 return (
978 <div>
979 <button onClick={() => runWithOptions("test", { onError: onErrorSpy })}>
980 Run
981 </button>
982 {isError && <div>Error!</div>}
983 </div>
984 );
985 }
986
987 render(<Component />);
988
989 const button = screen.getByText("Run");
990 await userEvent.click(button);
991
992 await waitFor(() => expect(screen.getByText("Error!")).toBeTruthy());
993 expect(onErrorSpy).toHaveBeenCalled();
994 expect(onErrorSpy.mock.calls[0][0]).toBeInstanceOf(Error);
995 });
996
997 test("should support onSettled callback for success", async () => {
998 const { client } = createTestClient();
999 const onSettledSpy = vi.fn();
1000
1001 const mutation = client.define({
1002 async mutate(value: string) {
1003 return `result-${value}`;
1004 },
1005 describe: "test mutation",
1006 optimistic() {},
1007 });
1008
1009 function Component() {
1010 const { runWithOptions } = useMutate(mutation);
1011 return (
1012 <button onClick={() => runWithOptions("test", { onSettled: onSettledSpy })}>
1013 Run
1014 </button>
1015 );
1016 }
1017
1018 render(<Component />);
1019
1020 const button = screen.getByText("Run");
1021 await userEvent.click(button);
1022
1023 await delay(50);
1024
1025 expect(onSettledSpy).toHaveBeenCalled();
1026 });
1027
1028 test("should support onSettled callback for error", async () => {
1029 const { client } = createTestClient();
1030 const onSettledSpy = vi.fn();
1031
1032 const mutation = client.define({
1033 async mutate(value: string) {
1034 throw new Error("Test error");
1035 },
1036 describe: "failing mutation",
1037 optimistic() {},
1038 });
1039
1040 function Component() {
1041 const { runWithOptions } = useMutate(mutation);
1042 return (
1043 <button onClick={() => runWithOptions("test", { onSettled: onSettledSpy })}>
1044 Run
1045 </button>
1046 );
1047 }
1048
1049 render(<Component />);
1050
1051 const button = screen.getByText("Run");
1052 await userEvent.click(button);
1053
1054 await delay(50);
1055
1056 expect(onSettledSpy).toHaveBeenCalled();
1057 });
1058});
1059
1060describe("useMutate Hook - Edge Cases", () => {
1061 test("should handle mutation changing during pending state", async () => {
1062 const { client } = createTestClient();
1063
1064 const mutation1 = client.define({
1065 async mutate(value: string) {
1066 await delay(100);
1067 return `mut1-${value}`;
1068 },
1069 describe: "mutation 1",
1070 optimistic() {},
1071 });
1072
1073 const mutation2 = client.define({
1074 async mutate(value: string) {
1075 await delay(10);
1076 return `mut2-${value}`;
1077 },
1078 describe: "mutation 2",
1079 optimistic() {},
1080 });
1081
1082 function Component({ useMut1 }: { useMut1: boolean }) {
1083 const { run, result, isSuccess, isPending } = useMutate(useMut1 ? mutation1 : mutation2);
1084 return (
1085 <div>
1086 <button onClick={() => run("test")}>Run</button>
1087 <div>Pending: {isPending.toString()}</div>
1088 {isSuccess && <div>Result: {result}</div>}
1089 </div>
1090 );
1091 }
1092
1093 const { rerender } = render(<Component useMut1={true} />);
1094
1095 const button = screen.getByText("Run");
1096 await userEvent.click(button);
1097
1098 await waitFor(() => expect(screen.getByText("Pending: true")).toBeTruthy());
1099
1100 // Switch mutations while first is pending
1101 rerender(<Component useMut1={false} />);
1102
1103 // State should reset
1104 await waitFor(() => expect(screen.getByText("Pending: false")).toBeTruthy());
1105
1106 // Run mutation2
1107 await userEvent.click(button);
1108
1109 await waitFor(() => expect(screen.getByText("Result: mut2-test")).toBeTruthy());
1110 });
1111
1112 test("should handle multiple sequential calls with same key", async () => {
1113 const { client } = createTestClient();
1114 let callCount = 0;
1115
1116 const mutation = client.define({
1117 async mutate(value: string) {
1118 callCount++;
1119 await delay(10);
1120 return `result-${callCount}`;
1121 },
1122 describe: "test mutation",
1123 optimistic() {},
1124 });
1125
1126 function Component() {
1127 const { run, result, isSuccess } = useMutate(mutation);
1128 return (
1129 <div>
1130 <button onClick={() => run("test")}>Run</button>
1131 {isSuccess && <div>Result: {result}</div>}
1132 </div>
1133 );
1134 }
1135
1136 render(<Component />);
1137
1138 const button = screen.getByText("Run");
1139
1140 // Click multiple times
1141 await userEvent.click(button);
1142 await userEvent.click(button);
1143 await userEvent.click(button);
1144
1145 // Should see the last result
1146 await waitFor(() => expect(screen.getByText(/Result: result-/)).toBeTruthy());
1147 });
1148
1149 test("should handle args property correctly", async () => {
1150 const { client } = createTestClient();
1151
1152 const mutation = client.define({
1153 async mutate(id: string, name: string) {
1154 await delay(10);
1155 return `${id}:${name}`;
1156 },
1157 describe: "test mutation",
1158 optimistic() {},
1159 });
1160
1161 function Component() {
1162 const { run, args, isPending } = useMutate(mutation);
1163 return (
1164 <div>
1165 <button onClick={() => run("123", "test")}>Run</button>
1166 <div>Args: {args ? JSON.stringify(args) : "none"}</div>
1167 <div>Pending: {isPending.toString()}</div>
1168 </div>
1169 );
1170 }
1171
1172 render(<Component />);
1173
1174 expect(screen.getByText("Args: none")).toBeTruthy();
1175
1176 const button = screen.getByText("Run");
1177 await userEvent.click(button);
1178
1179 // Args should be set during mutation
1180 await waitFor(() => {
1181 const argsText = screen.getByText(/Args: \[/);
1182 expect(argsText).toBeTruthy();
1183 });
1184
1185 // Args should be cleared after success
1186 await waitFor(() => expect(screen.getByText("Pending: false")).toBeTruthy());
1187 });
1188});
1189
1190describe("useMutate Hook - Optimistic Data Flag", () => {
1191 test("should set isOptimisticData during mutation states", async () => {
1192 const { client } = createTestClient();
1193
1194 const mutation = client.define({
1195 async mutate(value: string) {
1196 await delay(20);
1197 return `result-${value}`;
1198 },
1199 describe: "test mutation",
1200 optimistic({ helpers }, value: string) {
1201 helpers.setValue("test-key", value);
1202 },
1203 });
1204
1205 function Component() {
1206 const { run, isOptimisticData, status } = useMutate(mutation);
1207 return (
1208 <div>
1209 <button onClick={() => run("test")}>Run</button>
1210 <div>Optimistic: {isOptimisticData.toString()}</div>
1211 <div>Status: {status}</div>
1212 </div>
1213 );
1214 }
1215
1216 render(<Component />);
1217
1218 expect(screen.getByText("Optimistic: false")).toBeTruthy();
1219
1220 const button = screen.getByText("Run");
1221 await userEvent.click(button);
1222
1223 // Should be true during mutation
1224 await waitFor(() => expect(screen.getByText("Optimistic: true")).toBeTruthy());
1225
1226 // Should be false after completion
1227 await waitFor(() => expect(screen.getByText("Status: success")).toBeTruthy());
1228 await waitFor(() => expect(screen.getByText("Optimistic: false")).toBeTruthy());
1229 });
1230});
vitest.config.ts+7-3
...@@ -1,13 +1,17 @@...@@ -1,13 +1,17 @@
1import react from "@vitejs/plugin-react";
1import { defineConfig } from "vitest/config";2import { defineConfig } from "vitest/config";
23
3export default defineConfig({4export default defineConfig({
5 plugins: [react()],
4 test: {6 test: {
5 globals: true,7 globals: true,
6 environment: "node",8 // Use happy-dom for React tests, fallback to node for others
7 include: ["test/**/*.test.ts"],9 environment: "happy-dom",
10 include: ["test/**/*.test.ts", "test/**/*.test.tsx"],
8 coverage: {11 coverage: {
12 enabled: true,
9 provider: "v8",13 provider: "v8",
10 reporter: ["text", "json", "html", "json-summary"],14 reporter: ["text", "json", "html-spa", "json-summary"],
11 },15 },
12 },16 },
13});17});