mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
chore(ui): upgrade the dashboard to React 19 (#37411)
* chore(ui): upgrade the dashboard to React 19 Bumps react and react-dom from 18.3.1 to 19.2.8 with matching @types. Next 16 already required a React 19 peer, so this aligns the dashboard with what the framework expects and unblocks Base UI and shadcn work that assumes the React 19 ref model. React 19 passes ref through as a regular prop, so the setup file's forwardRef tripwire and the ref-forwarding test's forwardRef case no longer describe real behavior; both now assert the React 19 contract instead. useRef<T>(null) now yields RefObject<T | null>, which is the one prop type MessageList had to widen. * test(ui): wait for a Base UI select popup to open before clicking an option The option lands in the DOM one render before the popup finishes entering, while its positioner still carries pointer-events: none, so clicking it throws. Waiting on the option's text alone was a race that React 19's flush timing loses, which is why four ToolPolicies cases went red on the bump. chooseSelectOption in test-utils opens the trigger, finds the option by role, waits for it to stop being pointer-blocked, then clicks. It also replaces the last-match-by-text hack, which only worked because the popup happens to portal after the table.
This commit is contained in:
parent
3d51eb378a
commit
f1e143a87c
11 changed files with 146 additions and 159 deletions
157
ui/litellm-dashboard/package-lock.json
generated
157
ui/litellm-dashboard/package-lock.json
generated
|
|
@ -32,9 +32,9 @@
|
|||
"openapi-fetch": "^0.17.0",
|
||||
"openapi-react-query": "^0.5.4",
|
||||
"papaparse": "5.5.3",
|
||||
"react": "18.3.1",
|
||||
"react": "19.2.8",
|
||||
"react-copy-to-clipboard": "5.1.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-dom": "19.2.8",
|
||||
"react-hook-form": "7.82.0",
|
||||
"react-json-view-lite": "2.5.0",
|
||||
"react-markdown": "9.1.0",
|
||||
|
|
@ -55,9 +55,9 @@
|
|||
"@testing-library/react": "16.3.2",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@types/node": "20.19.37",
|
||||
"@types/react": "18.2.48",
|
||||
"@types/react": "19.2.18",
|
||||
"@types/react-copy-to-clipboard": "5.0.7",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@types/react-dom": "19.2.4",
|
||||
"@types/react-syntax-highlighter": "15.5.13",
|
||||
"@vitest/coverage-v8": "3.2.6",
|
||||
"@vitest/ui": "3.2.6",
|
||||
|
|
@ -200,9 +200,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@ant-design/icons-svg": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz",
|
||||
"integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==",
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.5.0.tgz",
|
||||
"integrity": "sha512-1BTUFyKPTBZ53MuTP8s0k5SFEXL7o3VHEOwLgzaoWKwnBeqIcqUtVshc4SKzhI6uACfqhJqBwBUE9FsWR3uULA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ant-design/react-slick": {
|
||||
|
|
@ -606,19 +606,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@base-ui/react/node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz",
|
||||
"integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.7.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@base-ui/utils": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.3.1.tgz",
|
||||
|
|
@ -1441,28 +1428,41 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
|
||||
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz",
|
||||
"integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.11"
|
||||
"@floating-ui/utils": "^0.2.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.6",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
|
||||
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz",
|
||||
"integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.5",
|
||||
"@floating-ui/utils": "^0.2.11"
|
||||
"@floating-ui/core": "^1.8.0",
|
||||
"@floating-ui/utils": "^0.2.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz",
|
||||
"integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
|
||||
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz",
|
||||
"integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@headlessui/tailwindcss": {
|
||||
|
|
@ -2627,9 +2627,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rc-component/async-validator": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.1.0.tgz",
|
||||
"integrity": "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==",
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.1.2.tgz",
|
||||
"integrity": "sha512-WYbrZSjzznU1ekD0qFq2qRxt309VoS61MTG5npnFQlKYcoy9IzU8T+ZCIhq5bGAXRbXysABFWTspicMfmWFwow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.24.4"
|
||||
|
|
@ -2669,9 +2669,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rc-component/mini-decimal": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.3.tgz",
|
||||
"integrity": "sha512-bk/FJ09fLf+NLODMAFll6CfYrHPBioTedhW6lxDBuuWucJEqFUd4l/D/5JgIi3dina6sYahB8iuPAZTNz2pMxw==",
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.4.tgz",
|
||||
"integrity": "sha512-xiuXcaCwyOWpD8a8scdExFl+bntNphAW8XeenL1ig2en0AAZY0Pcp4pC0dI22qJ+NvxKn9RoNIoRdqYU3BLH4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.0"
|
||||
|
|
@ -2717,9 +2717,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rc-component/qrcode": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.1.tgz",
|
||||
"integrity": "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA==",
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.3.tgz",
|
||||
"integrity": "sha512-aGv6alnn4HbDEsURzKP+jv13rbi1VxmAYfBNZr5GKF1iohMNWy5tAVoJ1E3cOvzMB1kbUPvCXchM6zSFlRGPhA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.24.7"
|
||||
|
|
@ -3661,12 +3661,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-store": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.11.0.tgz",
|
||||
"integrity": "sha512-tX4YXh3PDkmpvGQWkWqKpzs/MSqbtuwY9dWdWhtV9Q50PmO+jOkUKIWIX4G85dwt7lxdHLXsiaEKPdKmC8F41w==",
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.11.1.tgz",
|
||||
"integrity": "sha512-HaIGKI3YLmjBYIvy5DFDY23oNaYZIsTZfngey07Uh5iLVJgM3bIGCnZeOFOqzjFld9JHWcaHJnasD/bKoGKwJQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/store": "0.11.0",
|
||||
"@tanstack/store": "0.11.1",
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"funding": {
|
||||
|
|
@ -3699,9 +3699,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tanstack/store": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.11.0.tgz",
|
||||
"integrity": "sha512-WlzzCt3xi0G6pCAJu1U+2jiECwabETDpQDi3hfkFZvJii9AuZqEKbOiVarX1/bWhTNjU486yQtJCCasi/0q+Cw==",
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.11.1.tgz",
|
||||
"integrity": "sha512-mzTOBhypOuDJAy/D8n2MfUZ1HFkXnmSETviRyhqEC8LUE7/IZQExOTxMANj3KjTofYTkFNpBY67qaVrT41YccA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
|
|
@ -3999,21 +3999,13 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.2.48",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.48.tgz",
|
||||
"integrity": "sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==",
|
||||
"version": "19.2.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
|
||||
"integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
"csstype": "^3.0.2"
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-copy-to-clipboard": {
|
||||
|
|
@ -4027,13 +4019,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||
"version": "19.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0"
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-syntax-highlighter": {
|
||||
|
|
@ -4046,12 +4038,6 @@
|
|||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/scheduler": {
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.26.0.tgz",
|
||||
"integrity": "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||
|
|
@ -11565,13 +11551,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"version": "19.2.8",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
||||
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -11590,16 +11573,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"version": "19.2.8",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
|
||||
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1"
|
||||
"react": "^19.2.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
|
|
@ -12204,13 +12186,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scroll-into-view-if-needed": {
|
||||
"version": "3.1.0",
|
||||
|
|
|
|||
|
|
@ -48,9 +48,9 @@
|
|||
"openapi-fetch": "^0.17.0",
|
||||
"openapi-react-query": "^0.5.4",
|
||||
"papaparse": "5.5.3",
|
||||
"react": "18.3.1",
|
||||
"react": "19.2.8",
|
||||
"react-copy-to-clipboard": "5.1.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-dom": "19.2.8",
|
||||
"react-hook-form": "7.82.0",
|
||||
"react-json-view-lite": "2.5.0",
|
||||
"react-markdown": "9.1.0",
|
||||
|
|
@ -71,9 +71,9 @@
|
|||
"@testing-library/react": "16.3.2",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@types/node": "20.19.37",
|
||||
"@types/react": "18.2.48",
|
||||
"@types/react": "19.2.18",
|
||||
"@types/react-copy-to-clipboard": "5.0.7",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@types/react-dom": "19.2.4",
|
||||
"@types/react-syntax-highlighter": "15.5.13",
|
||||
"@vitest/coverage-v8": "3.2.6",
|
||||
"@vitest/ui": "3.2.6",
|
||||
|
|
|
|||
|
|
@ -1939,6 +1939,24 @@ describe("MCPServerEdit OAuth flow prefill display", () => {
|
|||
expect(screen.queryByText("This server has no OAuth flow set")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("never flashes the warning while mounting a server that already has a flow", async () => {
|
||||
const flashes: Node[] = [];
|
||||
const observer = new MutationObserver((records) => {
|
||||
for (const record of records) {
|
||||
for (const node of record.addedNodes) {
|
||||
if (node.textContent?.includes("This server has no OAuth flow set")) flashes.push(node);
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
renderEdit({ oauth2_flow: "client_credentials" });
|
||||
await screen.findByText("Machine-to-Machine (M2M)");
|
||||
observer.disconnect();
|
||||
|
||||
expect(flashes).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("does not warn for a delegate (PKCE passthrough) server even with no flow set", () => {
|
||||
renderEdit({ oauth2_flow: null, delegate_auth_to_upstream: true });
|
||||
|
||||
|
|
|
|||
|
|
@ -173,7 +173,10 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
const isTokenExchangeAuthType = authType === AUTH_TYPE.OAUTH2_TOKEN_EXCHANGE;
|
||||
const isIdJagAuthType = authType === AUTH_TYPE.OAUTH2_ID_JAG;
|
||||
const isAwsSigV4AuthType = authType === AUTH_TYPE.AWS_SIGV4;
|
||||
const oauthFlowTypeValue = mountedValues.oauth_flow_type as string | undefined;
|
||||
// Same fallback as the delegate switch below: the value is undefined until the field mounts, so
|
||||
// reading it alone flashes the "no OAuth flow set" warning at a server that already has one.
|
||||
const oauthFlowTypeValue =
|
||||
(mountedValues.oauth_flow_type as string | undefined) ?? oauth2FlowToFormValue(mcpServer.oauth2_flow);
|
||||
const isM2MFlow = isOAuthAuthType && oauthFlowTypeValue === OAUTH_FLOW.M2M;
|
||||
// Watch reflects a live toggle when the delegate switch is mounted; fall back to
|
||||
// the stored value otherwise (useWatch returns undefined for an unmounted field,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ interface MessageListProps {
|
|||
messages: Message[];
|
||||
isLoading: boolean;
|
||||
hasVariables: boolean;
|
||||
messagesEndRef: React.RefObject<HTMLDivElement>;
|
||||
messagesEndRef: React.RefObject<HTMLDivElement | null>;
|
||||
}
|
||||
|
||||
const MessageList: React.FC<MessageListProps> = ({ messages, isLoading, hasVariables, messagesEndRef }) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { render, screen, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { chooseSelectOption } from "@/../tests/test-utils";
|
||||
import { UsageViewSelect } from "./UsageViewSelect";
|
||||
|
||||
const openMenu = async (user: ReturnType<typeof userEvent.setup>) => {
|
||||
|
|
@ -35,9 +36,7 @@ describe("UsageViewSelect", () => {
|
|||
const user = userEvent.setup();
|
||||
render(<UsageViewSelect value="global" onChange={mockOnChange} userRole="Admin" />);
|
||||
|
||||
await openMenu(user);
|
||||
const matches = screen.getAllByText("Team Usage");
|
||||
await user.click(matches[matches.length - 1]);
|
||||
await chooseSelectOption(user, screen.getByRole("combobox"), /^Team Usage/);
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalled();
|
||||
expect(mockOnChange.mock.calls[0][0]).toBe("team");
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { act, render, screen, waitFor, within } from "@testing-library/react";
|
|||
import userEvent from "@testing-library/user-event";
|
||||
import { focusManager, QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
import { renderWithProviders, testQueryClient } from "../../../tests/test-utils";
|
||||
import { chooseSelectOption, renderWithProviders, testQueryClient } from "../../../tests/test-utils";
|
||||
import type { ToolRow } from "@/components/networking";
|
||||
import { ToolPoliciesPanel } from "./ToolPoliciesPanel";
|
||||
import { toast } from "@/lib/toast";
|
||||
|
|
@ -84,17 +84,6 @@ const policyValue = (toolId: string, kind: "input" | "output"): string => {
|
|||
const isSaving = (toolId: string, kind: "input" | "output"): boolean =>
|
||||
policySelect(toolId, kind).hasAttribute("disabled");
|
||||
|
||||
const chooseOption = async (user: ReturnType<typeof userEvent.setup>, trigger: HTMLElement, label: string) => {
|
||||
await user.click(trigger);
|
||||
// The label also renders in the trigger once selected, so take the last match:
|
||||
// the popup is portalled after the table in document order.
|
||||
const option = await waitFor(() => {
|
||||
const matches = screen.getAllByText(label);
|
||||
return matches[matches.length - 1];
|
||||
});
|
||||
await user.click(option);
|
||||
};
|
||||
|
||||
const renderPanel = (onSelectTool = vi.fn()) =>
|
||||
renderWithProviders(<ToolPoliciesPanel accessToken="sk-token" onSelectTool={onSelectTool} />);
|
||||
|
||||
|
|
@ -202,7 +191,7 @@ describe("ToolPoliciesPanel inline policy editing", () => {
|
|||
renderPanel();
|
||||
await waitForRows();
|
||||
|
||||
await chooseOption(user, policySelect("tool-1", "input"), "trusted");
|
||||
await chooseSelectOption(user, policySelect("tool-1", "input"), "trusted");
|
||||
|
||||
expect(updateToolPolicy).toHaveBeenCalledWith("sk-token", "get_weather", { input_policy: "trusted" });
|
||||
await waitFor(() => expect(policyValue("tool-1", "input")).toBe("trusted"));
|
||||
|
|
@ -214,7 +203,7 @@ describe("ToolPoliciesPanel inline policy editing", () => {
|
|||
renderPanel();
|
||||
await waitForRows();
|
||||
|
||||
await chooseOption(user, policySelect("tool-1", "output"), "trusted");
|
||||
await chooseSelectOption(user, policySelect("tool-1", "output"), "trusted");
|
||||
|
||||
expect(updateToolPolicy).toHaveBeenCalledWith("sk-token", "get_weather", { output_policy: "trusted" });
|
||||
});
|
||||
|
|
@ -225,8 +214,8 @@ describe("ToolPoliciesPanel inline policy editing", () => {
|
|||
renderPanel();
|
||||
await waitForRows();
|
||||
|
||||
await chooseOption(user, policySelect("tool-1", "input"), "trusted");
|
||||
await chooseOption(user, policySelect("tool-2", "input"), "blocked");
|
||||
await chooseSelectOption(user, policySelect("tool-1", "input"), "trusted");
|
||||
await chooseSelectOption(user, policySelect("tool-2", "input"), "blocked");
|
||||
|
||||
expect(isSaving("tool-2", "input")).toBe(true);
|
||||
expect(isSaving("tool-1", "input")).toBe(true);
|
||||
|
|
@ -241,8 +230,8 @@ describe("ToolPoliciesPanel inline policy editing", () => {
|
|||
renderPanel();
|
||||
await waitForRows();
|
||||
|
||||
await chooseOption(user, policySelect("tool-1", "input"), "trusted");
|
||||
await chooseOption(user, policySelect("tool-2", "input"), "blocked");
|
||||
await chooseSelectOption(user, policySelect("tool-1", "input"), "trusted");
|
||||
await chooseSelectOption(user, policySelect("tool-2", "input"), "blocked");
|
||||
await act(async () => {
|
||||
finishFirst();
|
||||
});
|
||||
|
|
@ -262,7 +251,7 @@ describe("ToolPoliciesPanel inline policy editing", () => {
|
|||
() => new Promise<ToolRow[]>((resolve) => (landStaleRefresh = () => resolve(TOOLS))),
|
||||
);
|
||||
await user.click(screen.getByTestId("datatable-refresh"));
|
||||
await chooseOption(user, policySelect("tool-1", "input"), "trusted");
|
||||
await chooseSelectOption(user, policySelect("tool-1", "input"), "trusted");
|
||||
await waitFor(() => expect(policyValue("tool-1", "input")).toBe("trusted"));
|
||||
|
||||
await act(async () => {
|
||||
|
|
@ -281,7 +270,7 @@ describe("ToolPoliciesPanel inline policy editing", () => {
|
|||
renderPanel();
|
||||
await waitForRows();
|
||||
|
||||
await chooseOption(user, policySelect("tool-1", "input"), "trusted");
|
||||
await chooseSelectOption(user, policySelect("tool-1", "input"), "trusted");
|
||||
|
||||
await waitFor(() => expect(fromBackend).toHaveBeenCalledWith("Failed to update input policy: nope"));
|
||||
expect(policyValue("tool-1", "input")).toBe("untrusted");
|
||||
|
|
@ -293,7 +282,7 @@ describe("ToolPoliciesPanel inline policy editing", () => {
|
|||
renderPanel();
|
||||
await waitForRows();
|
||||
|
||||
await chooseOption(user, policySelect("tool-1", "input"), "trusted");
|
||||
await chooseSelectOption(user, policySelect("tool-1", "input"), "trusted");
|
||||
|
||||
await waitFor(() => expect(isSaving("tool-1", "input")).toBe(true));
|
||||
expect(isSaving("tool-1", "output")).toBe(false);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import * as networking from "@/components/networking";
|
|||
import { fireEvent, screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders, testQueryClient } from "../../../tests/test-utils";
|
||||
import { chooseSelectOption, renderWithProviders, testQueryClient } from "../../../tests/test-utils";
|
||||
import TeamInfoView from "./TeamInfo";
|
||||
|
||||
const authState = vi.hoisted(() => ({ userRole: "Admin" }));
|
||||
|
|
@ -993,8 +993,7 @@ describe("TeamInfoView", () => {
|
|||
const user = userEvent.setup({ delay: null });
|
||||
const resetBudgetSelect = await openSettingsEditorForTeam(user, { budget_duration: "30d" });
|
||||
|
||||
await user.click(resetBudgetSelect);
|
||||
await user.click(await screen.findByText("Never resets"));
|
||||
await chooseSelectOption(user, resetBudgetSelect, "Never resets");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(resetBudgetSelect).toHaveTextContent("Never resets");
|
||||
|
|
@ -1028,8 +1027,7 @@ describe("TeamInfoView", () => {
|
|||
const user = userEvent.setup({ delay: null });
|
||||
const resetBudgetSelect = await openSettingsEditorForTeam(user, { budget_duration: null });
|
||||
|
||||
await user.click(resetBudgetSelect);
|
||||
await user.click(await screen.findByText("weekly"));
|
||||
await chooseSelectOption(user, resetBudgetSelect, "weekly");
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /save changes/i }));
|
||||
|
||||
|
|
|
|||
|
|
@ -138,15 +138,13 @@ describe("ui primitives forward refs to their DOM node", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("setupTests ref tripwire", () => {
|
||||
it("records a violation when a ref is passed to a plain function component", () => {
|
||||
const Plain = (props: React.ComponentPropsWithoutRef<"span">) => <span {...props} />;
|
||||
describe("plain function components", () => {
|
||||
it("receives a ref as a prop instead of dropping it", () => {
|
||||
const Plain = (props: React.ComponentPropsWithoutRef<"span"> & { ref?: React.Ref<HTMLSpanElement> }) => (
|
||||
<span {...props} />
|
||||
);
|
||||
const ref = React.createRef<HTMLSpanElement>();
|
||||
render(React.createElement(Plain as never, { ref }));
|
||||
const consume = (globalThis as { __consumePendingRefWarnings?: () => string[] }).__consumePendingRefWarnings;
|
||||
expect(consume).toBeDefined();
|
||||
const violations = consume!();
|
||||
expect(violations).toHaveLength(1);
|
||||
expect(violations[0]).toContain("Function components cannot be given refs");
|
||||
render(<Plain ref={ref}>ok</Plain>);
|
||||
expect(ref.current).toBeInstanceOf(HTMLSpanElement);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -113,31 +113,7 @@ vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
const pendingRefWarnings: string[] = [];
|
||||
const consumePendingRefWarnings = (): string[] => pendingRefWarnings.splice(0, pendingRefWarnings.length);
|
||||
(globalThis as { __consumePendingRefWarnings?: () => string[] }).__consumePendingRefWarnings =
|
||||
consumePendingRefWarnings;
|
||||
|
||||
const originalConsoleError = console.error.bind(console);
|
||||
vi.spyOn(console, "error").mockImplementation((...args: unknown[]) => {
|
||||
originalConsoleError(...args);
|
||||
if (typeof args[0] === "string" && args[0].includes("Function components cannot be given refs")) {
|
||||
pendingRefWarnings.push(args.map(String).join(" "));
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
const refWarnings = consumePendingRefWarnings();
|
||||
if (refWarnings.length > 0) {
|
||||
throw new Error(
|
||||
"A ref was passed to a plain function component and silently dropped under React 18, which breaks " +
|
||||
"ref-based composition (Base UI render triggers, tooltips, focus). Wrap the component in React.forwardRef. " +
|
||||
"This tripwire lives in tests/setupTests.ts and can be removed after the React 19 upgrade.\n\n" +
|
||||
refWarnings.join("\n\n"),
|
||||
);
|
||||
}
|
||||
});
|
||||
afterEach(cleanup);
|
||||
|
||||
// Make toLocaleString deterministic in tests; individual tests can override
|
||||
// This returns ISO-like strings to keep assertions stable.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import React, { PropsWithChildren } from "react";
|
||||
import { render, RenderOptions } from "@testing-library/react";
|
||||
import { render, RenderOptions, screen, waitFor } from "@testing-library/react";
|
||||
import type userEvent from "@testing-library/user-event";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { NuqsTestingAdapter, OnUrlUpdateFunction } from "nuqs/adapters/testing";
|
||||
import { expect } from "vitest";
|
||||
|
||||
// Create a client for testing
|
||||
export const testQueryClient = new QueryClient({
|
||||
|
|
@ -35,4 +37,29 @@ export const renderWithProviders = (ui: React.ReactElement, options?: RenderOpti
|
|||
return render(ui, { wrapper: Providers, ...renderOptions });
|
||||
};
|
||||
|
||||
const pointerBlocked = (element: HTMLElement): boolean => {
|
||||
for (let node: HTMLElement | null = element; node !== null; node = node.parentElement) {
|
||||
if (node.style.pointerEvents === "none") return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens a Base UI Select and picks an option by its accessible name.
|
||||
*
|
||||
* The option is in the DOM one render before the popup finishes entering, and until then its
|
||||
* positioner still carries `pointer-events: none`, which user-event refuses to click. Waiting on
|
||||
* the option text alone is a race that React 19's flush timing loses.
|
||||
*/
|
||||
export const chooseSelectOption = async (
|
||||
user: ReturnType<typeof userEvent.setup>,
|
||||
trigger: HTMLElement,
|
||||
optionName: string | RegExp,
|
||||
) => {
|
||||
await user.click(trigger);
|
||||
const option = await screen.findByRole("option", { name: optionName });
|
||||
await waitFor(() => expect(pointerBlocked(option)).toBe(false));
|
||||
await user.click(option);
|
||||
};
|
||||
|
||||
export * from "@testing-library/react";
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue