diff --git a/package.json b/package.json index 860cff737..59938d2e3 100644 --- a/package.json +++ b/package.json @@ -87,22 +87,21 @@ "license": "Apache-2.0", "devDependencies": { "@ai-sdk/azure": "^2.0.53", - "@emotion/css": "^11.13.5", "@ai-sdk/google": "^2.0.23", "@ai-sdk/mcp": "^0.0.8", "@ai-sdk/openai": "^2.0.52", + "@emotion/css": "^11.13.5", "@eslint/js": "^9.34.0", "@leafygreen-ui/table": "^15.2.2", "@modelcontextprotocol/inspector": "^0.17.1", "@mongodb-js/oidc-mock-provider": "^0.12.0", "@redocly/cli": "^2.0.8", + "@testing-library/react": "^16.3.1", "@types/express": "^5.0.3", "@types/node": "^24.5.2", "@types/proper-lockfile": "^4.1.4", "@types/react": "^18.3.0", "@types/react-dom": "^19.2.3", - "react": "^18.3.0", - "react-dom": "^18.3.0", "@types/semver": "^7.7.0", "@types/yargs-parser": "^21.0.3", "@typescript-eslint/parser": "^8.44.0", @@ -116,6 +115,7 @@ "eslint-plugin-prettier": "^5.5.4", "globals": "^16.3.0", "husky": "^9.1.7", + "jsdom": "^27.3.0", "knip": "^5.63.1", "mongodb": "^6.21.0", "mongodb-runner": "^6.2.0", @@ -123,6 +123,8 @@ "openapi-typescript": "^7.9.1", "prettier": "^3.6.2", "proper-lockfile": "^4.1.2", + "react": "^18.3.0", + "react-dom": "^18.3.0", "semver": "^7.7.2", "simple-git": "^3.28.0", "testcontainers": "^11.7.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5c59d8f4..f037dbfc7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -106,6 +106,9 @@ importers: '@redocly/cli': specifier: ^2.0.8 version: 2.12.5(@opentelemetry/api@1.9.0)(ajv@8.17.1)(core-js@3.47.0) + '@testing-library/react': + specifier: ^16.3.1 + version: 16.3.1(@testing-library/dom@9.3.1)(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/express': specifier: ^5.0.3 version: 5.0.6 @@ -135,10 +138,10 @@ importers: version: 5.1.1(vite@5.4.21(@types/node@24.10.1)) '@vitest/coverage-v8': specifier: ^3.2.4 - version: 3.2.4(vitest@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.3.0)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/eslint-plugin': specifier: ^1.3.4 - version: 1.4.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 1.4.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.3.0)(tsx@4.21.0)(yaml@2.8.2)) concurrently: specifier: ^9.2.1 version: 9.2.1 @@ -160,6 +163,9 @@ importers: husky: specifier: ^9.1.7 version: 9.1.7 + jsdom: + specifier: ^27.3.0 + version: 27.3.0 knip: specifier: ^5.63.1 version: 5.70.1(@types/node@24.10.1)(typescript@5.9.3) @@ -219,7 +225,7 @@ importers: version: 2.3.0(rollup@4.53.3)(vite@5.4.21(@types/node@24.10.1)) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@24.10.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.3.0)(tsx@4.21.0)(yaml@2.8.2) optionalDependencies: '@mongodb-js/atlas-local': specifier: ^1.1.0 @@ -230,6 +236,9 @@ importers: packages: + '@acemir/cssom@0.9.29': + resolution: {integrity: sha512-G90x0VW+9nW4dFajtjCoT+NM0scAfH9Mb08IcjgFHYbfiL/lU04dTF9JuVOi3/OH+DJCQdcIseSXkdCB9Ky6JA==} + '@ai-sdk/azure@2.0.71': resolution: {integrity: sha512-AMwgXMHcs9uJoM+TaR6mPlmyUlP4JRcPV27Evou57StYWO9kUu/ygU2yjPMFwcaouu/Nl9mQki59mFNzF+03qQ==} engines: {node: '>=18'} @@ -280,6 +289,15 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@asamuzakjp/css-color@4.1.1': + resolution: {integrity: sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==} + + '@asamuzakjp/dom-selector@6.7.6': + resolution: {integrity: sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -378,6 +396,38 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.22': + resolution: {integrity: sha512-qBcx6zYlhleiFfdtzkRgwNC7VVoAwfK76Vmsw5t+PbvtdknO9StgRk7ROvq9so1iqbdW4uLIDAsXRsTfUrIoOw==} + engines: {node: '>=18'} + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + '@emnapi/core@1.7.1': resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} @@ -2037,6 +2087,21 @@ packages: resolution: {integrity: sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==} engines: {node: '>=14'} + '@testing-library/react@16.3.1': + resolution: {integrity: sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@tootallnate/quickjs-emscripten@0.23.0': resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} @@ -2541,6 +2606,9 @@ packages: peerDependencies: ajv: 4.11.8 - 8 + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -2872,6 +2940,14 @@ packages: css-to-react-native@3.2.0: resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + cssstyle@5.3.5: + resolution: {integrity: sha512-GlsEptulso7Jg0VaOZ8BXQi3AkYM5BOJKEO/rjMidSCq70FkIC5y0eawrCXeYzxgt3OCf4Ls+eoxN+/05vN0Ag==} + engines: {node: '>=20'} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -2886,6 +2962,10 @@ packages: resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} engines: {node: '>= 14'} + data-urls@6.0.0: + resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==} + engines: {node: '>=20'} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -2907,6 +2987,9 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decko@1.2.0: resolution: {integrity: sha512-m8FnyHXV1QX+S1cl+KPFDIl6NMkxtKsy6+U/aYyjrOqWMuwAwYWu7ePqrsUHtDR5Y8Yk2pi/KIDSgF+vT4cPOQ==} @@ -3062,6 +3145,10 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} @@ -3530,6 +3617,10 @@ packages: hmac-drbg@1.0.1: resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -3564,6 +3655,10 @@ packages: engines: {node: '>=18'} hasBin: true + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + iconv-lite@0.7.0: resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} engines: {node: '>=0.10.0'} @@ -3712,6 +3807,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-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -3832,6 +3930,15 @@ packages: jsbn@1.1.0: resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + jsdom@27.3.0: + resolution: {integrity: sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg==} + engines: {node: ^20.19.0 || ^22.12.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'} @@ -3986,6 +4093,9 @@ packages: md5.js@1.3.5: resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -4392,6 +4502,9 @@ packages: resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} engines: {node: '>=18'} + parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -4814,6 +4927,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -5114,6 +5231,9 @@ packages: resolution: {integrity: sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==} hasBin: true + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + synckit@0.11.11: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -5187,6 +5307,13 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} + tldts-core@7.0.19: + resolution: {integrity: sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==} + + tldts@7.0.19: + resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==} + hasBin: true + tmp@0.2.5: resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} engines: {node: '>=14.14'} @@ -5203,6 +5330,10 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -5210,6 +5341,10 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} + 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 @@ -5523,6 +5658,10 @@ packages: zod: optional: true + 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} @@ -5538,10 +5677,26 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + webidl-conversions@8.0.0: + resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==} + engines: {node: '>=20'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + whatwg-url@14.2.0: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} + whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -5621,6 +5776,13 @@ packages: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} + 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==} + xtend@2.1.2: resolution: {integrity: sha512-vMNKzr2rHP9Dp/e1NQFnLQlwlhp9L/LfvnsVdHxN1f+uggyVI3i08uD14GPvCToPkdsRfyPqIyYGmIk58V98ZQ==} engines: {node: '>=0.4'} @@ -5695,6 +5857,8 @@ packages: snapshots: + '@acemir/cssom@0.9.29': {} + '@ai-sdk/azure@2.0.71(zod@3.25.76)': dependencies: '@ai-sdk/openai': 2.0.69(zod@3.25.76) @@ -5751,6 +5915,24 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 + '@asamuzakjp/css-color@4.1.1': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 11.2.4 + + '@asamuzakjp/dom-selector@6.7.6': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.1.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.2.4 + + '@asamuzakjp/nwsapi@2.3.9': {} + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -5873,6 +6055,28 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.22': {} + + '@csstools/css-tokenizer@3.0.4': {} + '@emnapi/core@1.7.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -7585,6 +7789,16 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 + '@testing-library/react@16.3.1(@testing-library/dom@9.3.1)(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@testing-library/dom': 9.3.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 19.2.3(@types/react@18.3.27) + '@tootallnate/quickjs-emscripten@0.23.0': {} '@tsconfig/node10@1.0.12': {} @@ -7896,7 +8110,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.3.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -7911,18 +8125,18 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@24.10.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.3.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@vitest/eslint-plugin@1.4.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/eslint-plugin@1.4.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.3.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@typescript-eslint/scope-manager': 8.47.0 '@typescript-eslint/utils': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) optionalDependencies: typescript: 5.9.3 - vitest: 3.2.4(@types/node@24.10.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.3.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -8194,6 +8408,10 @@ snapshots: jsonpointer: 5.0.1 leven: 3.1.0 + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + binary-extensions@2.3.0: {} bindings@1.5.0: @@ -8589,6 +8807,17 @@ snapshots: css-color-keywords: 1.0.0 postcss-value-parser: 4.2.0 + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + + cssstyle@5.3.5: + dependencies: + '@asamuzakjp/css-color': 4.1.1 + '@csstools/css-syntax-patches-for-csstree': 1.0.22 + css-tree: 3.1.0 + csstype@3.1.3: {} csstype@3.2.3: {} @@ -8597,6 +8826,11 @@ snapshots: data-uri-to-buffer@6.0.2: {} + data-urls@6.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -8621,6 +8855,8 @@ snapshots: optionalDependencies: supports-color: 10.2.2 + decimal.js@10.6.0: {} + decko@1.2.0: {} decompress-response@6.0.0: @@ -8823,6 +9059,8 @@ snapshots: dependencies: once: 1.4.0 + entities@6.0.1: {} + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 @@ -9468,6 +9706,10 @@ snapshots: minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + html-escaper@2.0.2: {} html-tokenize@2.0.1: @@ -9514,6 +9756,10 @@ snapshots: husky@9.1.7: {} + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.7.0: dependencies: safer-buffer: 2.1.2 @@ -9651,6 +9897,8 @@ snapshots: is-number@7.0.0: {} + is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: {} is-regex@1.2.1: @@ -9762,6 +10010,33 @@ snapshots: jsbn@1.1.0: {} + jsdom@27.3.0: + dependencies: + '@acemir/cssom': 0.9.29 + '@asamuzakjp/dom-selector': 6.7.6 + cssstyle: 5.3.5 + data-urls: 6.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6(supports-color@10.2.2) + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -9904,6 +10179,8 @@ snapshots: inherits: 2.0.4 safe-buffer: 5.2.1 + mdn-data@2.12.2: {} + media-typer@1.1.0: {} memory-pager@1.5.0: {} @@ -10394,6 +10671,10 @@ snapshots: index-to-position: 1.2.0 type-fest: 4.41.0 + parse5@8.0.0: + dependencies: + entities: 6.0.1 + parseurl@1.3.3: {} path-browserify@1.0.1: {} @@ -10896,6 +11177,10 @@ snapshots: safer-buffer@2.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -11287,6 +11572,8 @@ snapshots: transitivePeerDependencies: - encoding + symbol-tree@3.2.4: {} + synckit@0.11.11: dependencies: '@pkgr/core': 0.2.9 @@ -11414,6 +11701,12 @@ snapshots: tinyspy@4.0.4: {} + tldts-core@7.0.19: {} + + tldts@7.0.19: + dependencies: + tldts-core: 7.0.19 + tmp@0.2.5: {} to-buffer@1.2.2: @@ -11428,12 +11721,20 @@ snapshots: toidentifier@1.0.1: {} + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.19 + tr46@0.0.3: {} tr46@5.1.1: dependencies: punycode: 2.3.1 + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + tree-kill@1.2.2: {} ts-algebra@1.2.2: {} @@ -11680,7 +11981,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vitest@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2): + vitest@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.3.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 @@ -11707,6 +12008,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.10.1 + jsdom: 27.3.0 transitivePeerDependencies: - jiti - less @@ -11730,6 +12032,10 @@ snapshots: optionalDependencies: zod: 3.25.76 + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + walk-up-path@4.0.0: {} web-streams-polyfill@3.3.3: {} @@ -11738,11 +12044,24 @@ snapshots: webidl-conversions@7.0.0: {} + webidl-conversions@8.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + whatwg-url@14.2.0: dependencies: tr46: 5.1.1 webidl-conversions: 7.0.0 + whatwg-url@15.1.0: + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.0 + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -11830,6 +12149,10 @@ snapshots: dependencies: is-wsl: 3.1.0 + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + xtend@2.1.2: dependencies: object-keys: 0.4.0 diff --git a/src/ui/hooks/index.ts b/src/ui/hooks/index.ts index e9d3be4c8..168af5dd2 100644 --- a/src/ui/hooks/index.ts +++ b/src/ui/hooks/index.ts @@ -1 +1,2 @@ export { useRenderData } from "./useRenderData.js"; +export { useHostCommunication } from "./useHostCommunication.js"; diff --git a/src/ui/hooks/useHostCommunication.ts b/src/ui/hooks/useHostCommunication.ts new file mode 100644 index 000000000..ae07a97b6 --- /dev/null +++ b/src/ui/hooks/useHostCommunication.ts @@ -0,0 +1,76 @@ +import { useCallback } from "react"; +import { + postUIActionResult, + uiActionResultIntent, + uiActionResultNotification, + uiActionResultPrompt, + uiActionResultToolCall, + uiActionResultLink, +} from "@mcp-ui/server"; + +/** Return type for the useHostCommunication hook */ +interface UseHostCommunicationResult { + /** Sends an intent message for the host to act on */ + intent: typeof uiActionResultIntent; + /** Notifies the host of something that happened */ + notify: typeof uiActionResultNotification; + /** Asks the host to run a prompt */ + prompt: typeof uiActionResultPrompt; + /** Asks the host to execute a tool */ + tool: typeof uiActionResultToolCall; + /** Asks the host to navigate to a URL */ + link: typeof uiActionResultLink; +} + +/** + * Hook for sending UI actions to the parent window via postMessage + * This is used by iframe-based UI components to communicate back to an MCP client + * + * @example + * ```tsx + * function MyComponent() { + * const { intent, tool, link } = useHostCommunication(); + * + * return ; + * } + * ``` + */ +export function useHostCommunication(): UseHostCommunicationResult { + const intent: typeof uiActionResultIntent = useCallback((...args) => { + const result = uiActionResultIntent(...args); + postUIActionResult(result); + return result; + }, []); + + const notify: typeof uiActionResultNotification = useCallback((...args) => { + const result = uiActionResultNotification(...args); + postUIActionResult(result); + return result; + }, []); + + const prompt: typeof uiActionResultPrompt = useCallback((...args) => { + const result = uiActionResultPrompt(...args); + postUIActionResult(result); + return result; + }, []); + + const tool: typeof uiActionResultToolCall = useCallback((...args) => { + const result = uiActionResultToolCall(...args); + postUIActionResult(result); + return result; + }, []); + + const link: typeof uiActionResultLink = useCallback((...args) => { + const result = uiActionResultLink(...args); + postUIActionResult(result); + return result; + }, []); + + return { + intent, + notify, + prompt, + tool, + link, + }; +} diff --git a/src/ui/hooks/useRenderData.ts b/src/ui/hooks/useRenderData.ts index 461bbc983..6556b7f63 100644 --- a/src/ui/hooks/useRenderData.ts +++ b/src/ui/hooks/useRenderData.ts @@ -33,7 +33,6 @@ interface UseRenderDataResult { * * function MyComponent() { * const { data, isLoading, error } = useRenderData(); - * // ... * } * ``` */ diff --git a/tests/unit/ui/useHostCommunication.test.ts b/tests/unit/ui/useHostCommunication.test.ts new file mode 100644 index 000000000..af9a9bd9f --- /dev/null +++ b/tests/unit/ui/useHostCommunication.test.ts @@ -0,0 +1,155 @@ +/** + * @vitest-environment jsdom + */ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { renderHook } from "@testing-library/react"; +import { useHostCommunication } from "../../../src/ui/hooks/useHostCommunication.js"; + +describe("useHostCommunication", () => { + let postMessageMock: ReturnType; + let originalParent: typeof window.parent; + + beforeEach(() => { + postMessageMock = vi.fn(); + originalParent = window.parent; + + // Mock window.parent.postMessage without replacing the entire window object + Object.defineProperty(window, "parent", { + value: { postMessage: postMessageMock }, + writable: true, + configurable: true, + }); + }); + + afterEach(() => { + Object.defineProperty(window, "parent", { + value: originalParent, + writable: true, + configurable: true, + }); + vi.restoreAllMocks(); + }); + + it("intent() sends a message with name and params", () => { + const { result } = renderHook(() => useHostCommunication()); + + result.current.intent("create-task", { title: "Test Task" }); + + expect(postMessageMock).toHaveBeenCalledWith( + expect.objectContaining({ + type: "intent", + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + payload: expect.objectContaining({ + intent: "create-task", + params: { title: "Test Task" }, + }), + }), + "*" + ); + }); + + it("intent() sends a message with empty params", () => { + const { result } = renderHook(() => useHostCommunication()); + + result.current.intent("cancel", {}); + + expect(postMessageMock).toHaveBeenCalledWith( + expect.objectContaining({ + type: "intent", + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + payload: expect.objectContaining({ + intent: "cancel", + params: {}, + }), + }), + "*" + ); + }); + + it("notify() sends a notification message", () => { + const { result } = renderHook(() => useHostCommunication()); + + result.current.notify("Operation completed successfully"); + + expect(postMessageMock).toHaveBeenCalledWith( + expect.objectContaining({ + type: "notify", + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + payload: expect.objectContaining({ + message: "Operation completed successfully", + }), + }), + "*" + ); + }); + + it("prompt() sends a prompt message", () => { + const { result } = renderHook(() => useHostCommunication()); + + result.current.prompt("What is the status of my database?"); + + expect(postMessageMock).toHaveBeenCalledWith( + expect.objectContaining({ + type: "prompt", + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + payload: expect.objectContaining({ + prompt: "What is the status of my database?", + }), + }), + "*" + ); + }); + + it("tool() sends a tool message with name and params", () => { + const { result } = renderHook(() => useHostCommunication()); + + result.current.tool("listDatabases", { connectionString: "mongodb://localhost" }); + + expect(postMessageMock).toHaveBeenCalledWith( + expect.objectContaining({ + type: "tool", + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + payload: expect.objectContaining({ + toolName: "listDatabases", + params: { connectionString: "mongodb://localhost" }, + }), + }), + "*" + ); + }); + + it("tool() sends a tool message with empty params", () => { + const { result } = renderHook(() => useHostCommunication()); + + result.current.tool("getServerInfo", {}); + + expect(postMessageMock).toHaveBeenCalledWith( + expect.objectContaining({ + type: "tool", + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + payload: expect.objectContaining({ + toolName: "getServerInfo", + params: {}, + }), + }), + "*" + ); + }); + + it("link() sends a link message with a URL", () => { + const { result } = renderHook(() => useHostCommunication()); + + result.current.link("https://mongodb.com/docs"); + + expect(postMessageMock).toHaveBeenCalledWith( + expect.objectContaining({ + type: "link", + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + payload: expect.objectContaining({ + url: "https://mongodb.com/docs", + }), + }), + "*" + ); + }); +}); diff --git a/tests/unit/ui/useRenderData.test.ts b/tests/unit/ui/useRenderData.test.ts new file mode 100644 index 000000000..4a38d6ecb --- /dev/null +++ b/tests/unit/ui/useRenderData.test.ts @@ -0,0 +1,62 @@ +/** + * @vitest-environment jsdom + */ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { renderHook } from "@testing-library/react"; +import { useRenderData } from "../../../src/ui/hooks/useRenderData.js"; + +interface TestData { + items: string[]; +} + +describe("useRenderData", () => { + let postMessageMock: ReturnType; + let originalParent: typeof window.parent; + + beforeEach(() => { + postMessageMock = vi.fn(); + originalParent = window.parent; + + // Mock window.parent.postMessage without replacing the entire window object + Object.defineProperty(window, "parent", { + value: { postMessage: postMessageMock }, + writable: true, + configurable: true, + }); + }); + + afterEach(() => { + Object.defineProperty(window, "parent", { + value: originalParent, + writable: true, + configurable: true, + }); + vi.restoreAllMocks(); + }); + + it("returns initial state with isLoading true", () => { + const { result } = renderHook(() => useRenderData()); + + expect(result.current.data).toBeNull(); + expect(result.current.isLoading).toBe(true); + expect(result.current.error).toBeNull(); + }); + + it("includes expected properties in return type", () => { + const { result } = renderHook(() => useRenderData()); + + expect(result.current).toHaveProperty("data"); + expect(result.current).toHaveProperty("isLoading"); + expect(result.current).toHaveProperty("error"); + }); + + it("returns a stable object shape for destructuring", () => { + const { result } = renderHook(() => useRenderData()); + + const { data, isLoading, error } = result.current; + + expect(data).toBeNull(); + expect(isLoading).toBe(true); + expect(error).toBeNull(); + }); +});