diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json
index 9eb34b3ccc..127824f788 100644
--- a/invokeai/frontend/web/package.json
+++ b/invokeai/frontend/web/package.json
@@ -141,6 +141,7 @@
"eslint-plugin-storybook": "^9.0.17",
"eslint-plugin-unused-imports": "^4.1.4",
"globals": "^16.3.0",
+ "jsdom": "^29.0.1",
"knip": "^5.61.3",
"magic-string": "^0.30.17",
"openapi-types": "^12.1.3",
diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml
index 70806814dc..efaee2098d 100644
--- a/invokeai/frontend/web/pnpm-lock.yaml
+++ b/invokeai/frontend/web/pnpm-lock.yaml
@@ -297,6 +297,9 @@ importers:
globals:
specifier: ^16.3.0
version: 16.3.0
+ jsdom:
+ specifier: ^29.0.1
+ version: 29.0.1
knip:
specifier: ^5.61.3
version: 5.61.3(@types/node@22.16.0)(typescript@5.8.3)
@@ -338,7 +341,7 @@ importers:
version: 5.1.4(typescript@5.8.3)(vite@7.0.5(@types/node@22.16.0)(jiti@2.4.2))
vitest:
specifier: ^3.1.2
- version: 3.2.4(@types/node@22.16.0)(@vitest/ui@3.2.4)(jiti@2.4.2)
+ version: 3.2.4(@types/node@22.16.0)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@29.0.1)
packages:
@@ -349,6 +352,17 @@ packages:
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
+ '@asamuzakjp/css-color@5.1.5':
+ resolution: {integrity: sha512-8cMAA1bE66Mb/tfmkhcfJLjEPgyT7SSy6lW6id5XL113ai1ky76d/1L27sGnXCMsLfq66DInAU3OzuahB4lu9Q==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
+ '@asamuzakjp/dom-selector@7.0.6':
+ resolution: {integrity: sha512-Tgmk6EQM0nc9xvp7sEHRVavbknhb/vGKht+04yAT3t5KQwZ02CSobCtcFgaHH04ZrjD1BhEKNA8tRhzFV20gkA==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
+ '@asamuzakjp/nwsapi@2.3.9':
+ resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
+
'@atlaskit/pragmatic-drag-and-drop-auto-scroll@2.1.1':
resolution: {integrity: sha512-VAQEb3NVLY9Q5ZgC5Eiws9Uf6xOINY9/pAZMdbOVlF90uRXEkmpYqdTL+zeyZ8U8deuqYCmXr7oWIEnxpNQVzA==}
@@ -441,6 +455,10 @@ packages:
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
+ '@bramus/specificity@2.4.2':
+ resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==}
+ hasBin: true
+
'@chakra-ui/anatomy@2.2.2':
resolution: {integrity: sha512-MV6D4VLRIHr4PkW4zMyqfrNS1mPlCTiCXwvYGtDFQYr+xHFfonhAuf9WjsSc0nyp2m0OdkSLnzmVKkZFLo25Tg==}
@@ -579,6 +597,42 @@ packages:
peerDependencies:
react: '>=16.8.0'
+ '@csstools/color-helpers@6.0.2':
+ resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==}
+ engines: {node: '>=20.19.0'}
+
+ '@csstools/css-calc@3.1.1':
+ resolution: {integrity: sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^4.0.0
+ '@csstools/css-tokenizer': ^4.0.0
+
+ '@csstools/css-color-parser@4.0.2':
+ resolution: {integrity: sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^4.0.0
+ '@csstools/css-tokenizer': ^4.0.0
+
+ '@csstools/css-parser-algorithms@4.0.0':
+ resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ '@csstools/css-tokenizer': ^4.0.0
+
+ '@csstools/css-syntax-patches-for-csstree@1.1.2':
+ resolution: {integrity: sha512-5GkLzz4prTIpoyeUiIu3iV6CSG3Plo7xRVOFPKI7FVEJ3mZ0A8SwK0XU3Gl7xAkiQ+mDyam+NNp875/C5y+jSA==}
+ peerDependencies:
+ css-tree: ^3.2.1
+ peerDependenciesMeta:
+ css-tree:
+ optional: true
+
+ '@csstools/css-tokenizer@4.0.0':
+ resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
+ engines: {node: '>=20.19.0'}
+
'@dagrejs/dagre@1.1.5':
resolution: {integrity: sha512-Ghgrh08s12DCL5SeiR6AoyE80mQELTWhJBRmXfFoqDiFkR458vPEdgTbbjA0T+9ETNxUblnD0QW55tfdvi5pjQ==}
@@ -852,6 +906,15 @@ packages:
resolution: {integrity: sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@exodus/bytes@1.15.0':
+ resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+ peerDependencies:
+ '@noble/hashes': ^1.8.0 || ^2.0.0
+ peerDependenciesMeta:
+ '@noble/hashes':
+ optional: true
+
'@floating-ui/core@1.7.2':
resolution: {integrity: sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==}
@@ -1880,6 +1943,9 @@ packages:
resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==}
engines: {node: '>=12.0.0'}
+ bidi-js@1.0.3:
+ resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
+
bind-event-listener@3.0.0:
resolution: {integrity: sha512-PJvH288AWQhKs2v9zyfYdPzlPqf5bXbGMmhmUIY9x4dAUGIWgomO771oBQNwJnMQSnUIXhKu6sgzpBRXTlvb8Q==}
@@ -2044,6 +2110,10 @@ packages:
resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==}
engines: {node: '>=8.0.0'}
+ css-tree@3.2.1:
+ resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+
css.escape@1.5.1:
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
@@ -2088,6 +2158,10 @@ packages:
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
engines: {node: '>=12'}
+ data-urls@7.0.0:
+ resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
data-view-buffer@1.0.2:
resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
engines: {node: '>= 0.4'}
@@ -2126,6 +2200,9 @@ packages:
supports-color:
optional: true
+ decimal.js@10.6.0:
+ resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
+
decode-uri-component@0.4.1:
resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==}
engines: {node: '>=14.16'}
@@ -2214,6 +2291,10 @@ packages:
resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==}
engines: {node: '>=10.0.0'}
+ entities@6.0.1:
+ resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
+ engines: {node: '>=0.12'}
+
error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
@@ -2654,6 +2735,10 @@ packages:
hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
+ html-encoding-sniffer@6.0.0:
+ resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
@@ -2801,6 +2886,9 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
+ is-potential-custom-element-name@1.0.1:
+ resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
+
is-regex@1.2.1:
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
engines: {node: '>= 0.4'}
@@ -2895,6 +2983,15 @@ packages:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true
+ jsdom@29.0.1:
+ resolution: {integrity: sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0}
+ peerDependencies:
+ canvas: ^3.0.0
+ peerDependenciesMeta:
+ canvas:
+ optional: true
+
jsesc@3.1.0:
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
engines: {node: '>=6'}
@@ -3009,6 +3106,10 @@ packages:
resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==}
engines: {node: 20 || >=22}
+ lru-cache@11.2.7:
+ resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==}
+ engines: {node: 20 || >=22}
+
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
@@ -3036,6 +3137,9 @@ packages:
mdn-data@2.0.14:
resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
+ mdn-data@2.27.1:
+ resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
+
memoize-one@6.0.0:
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
@@ -3245,6 +3349,9 @@ packages:
resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==}
engines: {node: '>=18'}
+ parse5@8.0.0:
+ resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==}
+
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@@ -3696,6 +3803,10 @@ packages:
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
engines: {node: '>=10'}
+ saxes@6.0.0:
+ resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
+ engines: {node: '>=v12.22.7'}
+
scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
@@ -3933,6 +4044,9 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
+ symbol-tree@3.2.4:
+ resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
+
test-exclude@7.0.1:
resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
engines: {node: '>=18'}
@@ -3966,6 +4080,13 @@ packages:
resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==}
engines: {node: '>=14.0.0'}
+ tldts-core@7.0.28:
+ resolution: {integrity: sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==}
+
+ tldts@7.0.28:
+ resolution: {integrity: sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==}
+ hasBin: true
+
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -3977,9 +4098,17 @@ packages:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'}
+ tough-cookie@6.0.1:
+ resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==}
+ engines: {node: '>=16'}
+
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+ tr46@6.0.0:
+ resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
+ engines: {node: '>=20'}
+
tree-kill@1.2.2:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
@@ -4062,6 +4191,10 @@ packages:
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
+ undici@7.24.7:
+ resolution: {integrity: sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==}
+ engines: {node: '>=20.18.1'}
+
unicorn-magic@0.1.0:
resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==}
engines: {node: '>=18'}
@@ -4247,6 +4380,10 @@ packages:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
engines: {node: '>=0.10.0'}
+ w3c-xmlserializer@5.0.0:
+ resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
+ engines: {node: '>=18'}
+
walk-up-path@4.0.0:
resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==}
engines: {node: 20 || >=22}
@@ -4257,9 +4394,21 @@ packages:
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+ webidl-conversions@8.0.1:
+ resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
+ engines: {node: '>=20'}
+
webpack-virtual-modules@0.6.2:
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
+ whatwg-mimetype@5.0.0:
+ resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==}
+ engines: {node: '>=20'}
+
+ whatwg-url@16.0.1:
+ resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
@@ -4325,6 +4474,13 @@ packages:
utf-8-validate:
optional: true
+ xml-name-validator@5.0.0:
+ resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
+ engines: {node: '>=18'}
+
+ xmlchars@2.2.0:
+ resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
+
xmlhttprequest-ssl@2.1.2:
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
engines: {node: '>=0.4.0'}
@@ -4395,6 +4551,24 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.12
'@jridgewell/trace-mapping': 0.3.29
+ '@asamuzakjp/css-color@5.1.5':
+ dependencies:
+ '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-color-parser': 4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-tokenizer': 4.0.0
+ lru-cache: 11.2.7
+
+ '@asamuzakjp/dom-selector@7.0.6':
+ dependencies:
+ '@asamuzakjp/nwsapi': 2.3.9
+ bidi-js: 1.0.3
+ css-tree: 3.2.1
+ is-potential-custom-element-name: 1.0.1
+ lru-cache: 11.2.7
+
+ '@asamuzakjp/nwsapi@2.3.9': {}
+
'@atlaskit/pragmatic-drag-and-drop-auto-scroll@2.1.1':
dependencies:
'@atlaskit/pragmatic-drag-and-drop': 1.7.4
@@ -4522,6 +4696,10 @@ snapshots:
'@bcoe/v8-coverage@1.0.2': {}
+ '@bramus/specificity@2.4.2':
+ dependencies:
+ css-tree: 3.2.1
+
'@chakra-ui/anatomy@2.2.2': {}
'@chakra-ui/anatomy@2.3.4': {}
@@ -4715,6 +4893,30 @@ snapshots:
lodash.mergewith: 4.6.2
react: 18.3.1
+ '@csstools/color-helpers@6.0.2': {}
+
+ '@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
+ dependencies:
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-tokenizer': 4.0.0
+
+ '@csstools/css-color-parser@4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
+ dependencies:
+ '@csstools/color-helpers': 6.0.2
+ '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-tokenizer': 4.0.0
+
+ '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)':
+ dependencies:
+ '@csstools/css-tokenizer': 4.0.0
+
+ '@csstools/css-syntax-patches-for-csstree@1.1.2(css-tree@3.2.1)':
+ optionalDependencies:
+ css-tree: 3.2.1
+
+ '@csstools/css-tokenizer@4.0.0': {}
+
'@dagrejs/dagre@1.1.5':
dependencies:
'@dagrejs/graphlib': 2.2.4
@@ -4952,6 +5154,8 @@ snapshots:
'@eslint/core': 0.15.1
levn: 0.4.1
+ '@exodus/bytes@1.15.0': {}
+
'@floating-ui/core@1.7.2':
dependencies:
'@floating-ui/utils': 0.2.10
@@ -5778,7 +5982,7 @@ snapshots:
std-env: 3.9.0
test-exclude: 7.0.1
tinyrainbow: 2.0.0
- vitest: 3.2.4(@types/node@22.16.0)(@vitest/ui@3.2.4)(jiti@2.4.2)
+ vitest: 3.2.4(@types/node@22.16.0)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@29.0.1)
transitivePeerDependencies:
- supports-color
@@ -5827,7 +6031,7 @@ snapshots:
sirv: 3.0.1
tinyglobby: 0.2.14
tinyrainbow: 2.0.0
- vitest: 3.2.4(@types/node@22.16.0)(@vitest/ui@3.2.4)(jiti@2.4.2)
+ vitest: 3.2.4(@types/node@22.16.0)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@29.0.1)
'@vitest/utils@3.2.4':
dependencies:
@@ -6019,6 +6223,10 @@ snapshots:
dependencies:
open: 8.4.2
+ bidi-js@1.0.3:
+ dependencies:
+ require-from-string: 2.0.2
+
bind-event-listener@3.0.0: {}
bl@4.1.0:
@@ -6210,6 +6418,11 @@ snapshots:
mdn-data: 2.0.14
source-map: 0.6.1
+ css-tree@3.2.1:
+ dependencies:
+ mdn-data: 2.27.1
+ source-map-js: 1.2.1
+
css.escape@1.5.1: {}
csstype@3.1.3: {}
@@ -6250,6 +6463,13 @@ snapshots:
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
+ data-urls@7.0.0:
+ dependencies:
+ whatwg-mimetype: 5.0.0
+ whatwg-url: 16.0.1
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
data-view-buffer@1.0.2:
dependencies:
call-bound: 1.0.4
@@ -6282,6 +6502,8 @@ snapshots:
optionalDependencies:
supports-color: 10.0.0
+ decimal.js@10.6.0: {}
+
decode-uri-component@0.4.1: {}
deep-eql@5.0.2: {}
@@ -6374,6 +6596,8 @@ snapshots:
engine.io-parser@5.2.3: {}
+ entities@6.0.1: {}
+
error-ex@1.3.2:
dependencies:
is-arrayish: 0.2.1
@@ -6946,6 +7170,12 @@ snapshots:
dependencies:
react-is: 16.13.1
+ html-encoding-sniffer@6.0.0:
+ dependencies:
+ '@exodus/bytes': 1.15.0
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
html-escaper@2.0.2: {}
html-parse-stringify@3.0.1:
@@ -7082,6 +7312,8 @@ snapshots:
is-number@7.0.0: {}
+ is-potential-custom-element-name@1.0.1: {}
+
is-regex@1.2.1:
dependencies:
call-bound: 1.0.4
@@ -7181,6 +7413,32 @@ snapshots:
dependencies:
argparse: 2.0.1
+ jsdom@29.0.1:
+ dependencies:
+ '@asamuzakjp/css-color': 5.1.5
+ '@asamuzakjp/dom-selector': 7.0.6
+ '@bramus/specificity': 2.4.2
+ '@csstools/css-syntax-patches-for-csstree': 1.1.2(css-tree@3.2.1)
+ '@exodus/bytes': 1.15.0
+ css-tree: 3.2.1
+ data-urls: 7.0.0
+ decimal.js: 10.6.0
+ html-encoding-sniffer: 6.0.0
+ is-potential-custom-element-name: 1.0.1
+ lru-cache: 11.2.7
+ parse5: 8.0.0
+ saxes: 6.0.0
+ symbol-tree: 3.2.4
+ tough-cookie: 6.0.1
+ undici: 7.24.7
+ w3c-xmlserializer: 5.0.0
+ webidl-conversions: 8.0.1
+ whatwg-mimetype: 5.0.0
+ whatwg-url: 16.0.1
+ xml-name-validator: 5.0.0
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
jsesc@3.1.0: {}
json-buffer@3.0.1: {}
@@ -7290,6 +7548,8 @@ snapshots:
lru-cache@11.1.0: {}
+ lru-cache@11.2.7: {}
+
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
@@ -7316,6 +7576,8 @@ snapshots:
mdn-data@2.0.14: {}
+ mdn-data@2.27.1: {}
+
memoize-one@6.0.0: {}
merge2@1.4.1: {}
@@ -7553,6 +7815,10 @@ snapshots:
index-to-position: 1.1.0
type-fest: 4.41.0
+ parse5@8.0.0:
+ dependencies:
+ entities: 6.0.1
+
path-exists@4.0.0: {}
path-exists@5.0.0: {}
@@ -8033,6 +8299,10 @@ snapshots:
safe-stable-stringify@2.5.0: {}
+ saxes@6.0.0:
+ dependencies:
+ xmlchars: 2.2.0
+
scheduler@0.23.2:
dependencies:
loose-envify: 1.4.0
@@ -8306,6 +8576,8 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
+ symbol-tree@3.2.4: {}
+
test-exclude@7.0.1:
dependencies:
'@istanbuljs/schema': 0.1.3
@@ -8331,6 +8603,12 @@ snapshots:
tinyspy@4.0.3: {}
+ tldts-core@7.0.28: {}
+
+ tldts@7.0.28:
+ dependencies:
+ tldts-core: 7.0.28
+
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@@ -8339,8 +8617,16 @@ snapshots:
totalist@3.0.1: {}
+ tough-cookie@6.0.1:
+ dependencies:
+ tldts: 7.0.28
+
tr46@0.0.3: {}
+ tr46@6.0.0:
+ dependencies:
+ punycode: 2.3.1
+
tree-kill@1.2.2: {}
ts-api-utils@2.1.0(typescript@5.8.3):
@@ -8426,6 +8712,8 @@ snapshots:
undici-types@6.21.0: {}
+ undici@7.24.7: {}
+
unicorn-magic@0.1.0: {}
universalify@2.0.1: {}
@@ -8550,7 +8838,7 @@ snapshots:
fsevents: 2.3.3
jiti: 2.4.2
- vitest@3.2.4(@types/node@22.16.0)(@vitest/ui@3.2.4)(jiti@2.4.2):
+ vitest@3.2.4(@types/node@22.16.0)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@29.0.1):
dependencies:
'@types/chai': 5.2.2
'@vitest/expect': 3.2.4
@@ -8578,6 +8866,7 @@ snapshots:
optionalDependencies:
'@types/node': 22.16.0
'@vitest/ui': 3.2.4(vitest@3.2.4)
+ jsdom: 29.0.1
transitivePeerDependencies:
- jiti
- less
@@ -8594,6 +8883,10 @@ snapshots:
void-elements@3.1.0: {}
+ w3c-xmlserializer@5.0.0:
+ dependencies:
+ xml-name-validator: 5.0.0
+
walk-up-path@4.0.0: {}
wcwidth@1.0.1:
@@ -8602,8 +8895,20 @@ snapshots:
webidl-conversions@3.0.1: {}
+ webidl-conversions@8.0.1: {}
+
webpack-virtual-modules@0.6.2: {}
+ whatwg-mimetype@5.0.0: {}
+
+ whatwg-url@16.0.1:
+ dependencies:
+ '@exodus/bytes': 1.15.0
+ tr46: 6.0.0
+ webidl-conversions: 8.0.1
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
@@ -8677,6 +8982,10 @@ snapshots:
ws@8.18.3: {}
+ xml-name-validator@5.0.0: {}
+
+ xmlchars@2.2.0: {}
+
xmlhttprequest-ssl@2.1.2: {}
y18n@5.0.8: {}
diff --git a/invokeai/frontend/web/src/common/components/IAIImageFallback.test.tsx b/invokeai/frontend/web/src/common/components/IAIImageFallback.test.tsx
new file mode 100644
index 0000000000..4957c3dc13
--- /dev/null
+++ b/invokeai/frontend/web/src/common/components/IAIImageFallback.test.tsx
@@ -0,0 +1,49 @@
+import { screen } from '@testing-library/react';
+import { describe, expect, it } from 'vitest';
+
+import { renderWithProviders } from '../../tests/test-utils';
+import { IAINoContentFallback, IAINoContentFallbackWithSpinner } from './IAIImageFallback';
+
+describe('IAINoContentFallback', () => {
+ it('renders without a label', () => {
+ const { container } = renderWithProviders();
+ expect(container.firstChild).toBeInTheDocument();
+ });
+
+ it('renders the label text when provided', () => {
+ renderWithProviders();
+ expect(screen.getByText('No images found')).toBeInTheDocument();
+ });
+
+ it('does not render a label element when label is not provided', () => {
+ renderWithProviders();
+ expect(screen.queryByRole('paragraph')).not.toBeInTheDocument();
+ });
+
+ it('does not render an icon when icon is null', () => {
+ const { container } = renderWithProviders();
+ expect(container.querySelector('svg')).not.toBeInTheDocument();
+ });
+
+ it('renders an icon by default', () => {
+ const { container } = renderWithProviders();
+ expect(container.querySelector('svg')).toBeInTheDocument();
+ });
+});
+
+describe('IAINoContentFallbackWithSpinner', () => {
+ it('renders without a label', () => {
+ const { container } = renderWithProviders();
+ expect(container.firstChild).toBeInTheDocument();
+ });
+
+ it('renders the label text when provided', () => {
+ renderWithProviders();
+ expect(screen.getByText('Loading images...')).toBeInTheDocument();
+ });
+
+ it('does not render a label when label is not provided', () => {
+ renderWithProviders();
+ expect(screen.queryByText('Loading images...')).not.toBeInTheDocument();
+ });
+});
diff --git a/invokeai/frontend/web/src/common/components/WavyLine.test.tsx b/invokeai/frontend/web/src/common/components/WavyLine.test.tsx
new file mode 100644
index 0000000000..46b30b9c85
--- /dev/null
+++ b/invokeai/frontend/web/src/common/components/WavyLine.test.tsx
@@ -0,0 +1,44 @@
+import { render, screen } from '@testing-library/react';
+import { describe, expect, it } from 'vitest';
+
+import WavyLine from './WavyLine';
+
+describe('WavyLine', () => {
+ it('renders an SVG element', () => {
+ const { container } = render();
+ const svg = container.querySelector('svg');
+ expect(svg).toBeInTheDocument();
+ });
+
+ it('sets the correct SVG dimensions', () => {
+ const { container } = render();
+ const svg = container.querySelector('svg');
+ expect(svg).toHaveAttribute('width', '200');
+ expect(svg).toHaveAttribute('height', '50');
+ });
+
+ it('renders a straight line when amplitude is 0', () => {
+ const { container } = render();
+ const path = container.querySelector('path');
+ expect(path).toHaveAttribute('d', 'M0,20 L100,20');
+ });
+
+ it('renders a wavy path when amplitude is non-zero', () => {
+ const { container } = render();
+ const path = container.querySelector('path');
+ const d = path?.getAttribute('d') ?? '';
+ expect(d).toContain('Q');
+ });
+
+ it('applies the correct stroke color to the path', () => {
+ const { container } = render();
+ const path = container.querySelector('path');
+ expect(path).toHaveAttribute('stroke', '#ff0000');
+ });
+
+ it('applies the correct stroke width to the path', () => {
+ const { container } = render();
+ const path = container.querySelector('path');
+ expect(path).toHaveAttribute('stroke-width', '3');
+ });
+});
diff --git a/invokeai/frontend/web/src/tests/setup.ts b/invokeai/frontend/web/src/tests/setup.ts
new file mode 100644
index 0000000000..53ead6c593
--- /dev/null
+++ b/invokeai/frontend/web/src/tests/setup.ts
@@ -0,0 +1,8 @@
+import '@testing-library/jest-dom/vitest';
+import { cleanup } from '@testing-library/react';
+import { afterEach } from 'vitest';
+
+// Automatically cleanup rendered components after each test
+afterEach(() => {
+ cleanup();
+});
diff --git a/invokeai/frontend/web/src/tests/test-utils.tsx b/invokeai/frontend/web/src/tests/test-utils.tsx
new file mode 100644
index 0000000000..863ffb55e9
--- /dev/null
+++ b/invokeai/frontend/web/src/tests/test-utils.tsx
@@ -0,0 +1,35 @@
+import { ChakraProvider, extendTheme, theme as baseTheme, TOAST_OPTIONS } from '@invoke-ai/ui-library';
+import { render } from '@testing-library/react';
+import type { RenderOptions, RenderResult } from '@testing-library/react';
+import { createStore } from 'app/store/store';
+import type { AppStore } from 'app/store/store';
+import type { ReactNode } from 'react';
+import { Provider } from 'react-redux';
+
+const defaultTheme = extendTheme({ ...baseTheme });
+
+interface RenderWithProvidersOptions extends Omit {
+ store?: AppStore;
+}
+
+interface RenderWithProvidersResult extends RenderResult {
+ store: AppStore;
+}
+
+export function renderWithProviders(
+ ui: ReactNode,
+ { store = createStore(), ...renderOptions }: RenderWithProvidersOptions = {}
+): RenderWithProvidersResult {
+ function Wrapper({ children }: { children: ReactNode }) {
+ return (
+
+
+ {children}
+
+
+ );
+ }
+
+ const result = render(ui, { wrapper: Wrapper, ...renderOptions });
+ return { ...result, store };
+}
diff --git a/invokeai/frontend/web/vite.config.mts b/invokeai/frontend/web/vite.config.mts
index d15c35d6bc..206566a961 100644
--- a/invokeai/frontend/web/vite.config.mts
+++ b/invokeai/frontend/web/vite.config.mts
@@ -39,6 +39,8 @@ export default defineConfig(({ mode }) => {
host: '0.0.0.0',
},
test: {
+ environment: 'jsdom',
+ setupFiles: ['src/tests/setup.ts'],
typecheck: {
enabled: true,
ignoreSourceErrors: true,