From 691d35b11a1f7a63b5d4e0eda102f9385bd9ae3a Mon Sep 17 00:00:00 2001
From: Koen Vlaswinkel <koesie10@github.com>
Date: Thu, 24 Apr 2025 15:40:42 +0200
Subject: [PATCH] Use vitest browser mode for view tests

---
 extensions/ql-vscode/package-lock.json        | 764 ++++++++++++++++--
 extensions/ql-vscode/package.json             |   7 +-
 .../SuggestBox/__tests__/SuggestBox.test.tsx  |   4 +-
 .../__tests__/useEffectEvent.test.ts          |   4 +-
 .../SuggestBox/__tests__/useOpenKey.test.ts   |   6 +-
 .../src/view/jest-environment-jsdom.ts        |  15 -
 extensions/ql-vscode/src/view/jest.config.ts  | 203 -----
 extensions/ql-vscode/src/view/jest.setup.ts   |  41 -
 .../__tests__/MethodModeling.spec.tsx         |   2 +-
 .../__tests__/MethodModelingInputs.spec.tsx   |   2 +-
 .../MultipleModeledMethodsPanel.spec.tsx      |   2 +-
 .../__tests__/LibraryRow.spec.tsx             |  10 +-
 .../model-editor/__tests__/MethodRow.spec.tsx |   4 +-
 .../__tests__/ModelEvaluation.spec.tsx        |   6 +-
 .../__tests__/ModelKindDropdown.spec.tsx      |   2 +-
 .../__tests__/ModelTypeDropdown.spec.tsx      |   2 +-
 .../__tests__/ModeledMethodDataGrid.spec.tsx  |   4 +-
 .../__tests__/ModeledMethodsList.spec.tsx     |  10 +-
 .../ModelingStatusIndicator.spec.tsx          |   2 +-
 .../__tests__/AlertTablePathRow.spec.tsx      |   4 +-
 .../__tests__/AlertTableResultRow.spec.tsx    |   4 +-
 .../view/results/__tests__/results.spec.tsx   |   7 +-
 .../results/__tests__/validSarif.sarif.json   |  57 ++
 extensions/ql-vscode/src/view/tsconfig.json   |   3 +-
 .../ql-vscode/src/view/tsconfig.spec.json     |   7 -
 .../__tests__/QueryDetails.spec.tsx           |  10 +-
 .../__tests__/VariantAnalysisActions.spec.tsx |   6 +-
 .../VariantAnalysisOutcomePanels.spec.tsx     |   2 +-
 .../__tests__/VariantAnalysisStats.spec.tsx   |   2 +-
 .../ql-vscode/src/view/vitest.config.ts       |  15 +
 extensions/ql-vscode/src/view/vitest.setup.ts |   7 +
 extensions/ql-vscode/vitest.workspace.ts      |   3 +
 32 files changed, 813 insertions(+), 404 deletions(-)
 delete mode 100644 extensions/ql-vscode/src/view/jest-environment-jsdom.ts
 delete mode 100644 extensions/ql-vscode/src/view/jest.config.ts
 delete mode 100644 extensions/ql-vscode/src/view/jest.setup.ts
 create mode 100644 extensions/ql-vscode/src/view/results/__tests__/validSarif.sarif.json
 delete mode 100644 extensions/ql-vscode/src/view/tsconfig.spec.json
 create mode 100644 extensions/ql-vscode/src/view/vitest.config.ts
 create mode 100644 extensions/ql-vscode/src/view/vitest.setup.ts
 create mode 100644 extensions/ql-vscode/vitest.workspace.ts

diff --git a/extensions/ql-vscode/package-lock.json b/extensions/ql-vscode/package-lock.json
index 28561888a8b..834d930999b 100644
--- a/extensions/ql-vscode/package-lock.json
+++ b/extensions/ql-vscode/package-lock.json
@@ -97,6 +97,8 @@
         "@types/yauzl": "^2.10.3",
         "@typescript-eslint/eslint-plugin": "^8.30.1",
         "@typescript-eslint/parser": "^8.30.1",
+        "@vitejs/plugin-react": "^4.4.1",
+        "@vitest/browser": "^3.1.2",
         "@vscode/test-electron": "^2.3.9",
         "@vscode/vsce": "^3.2.1",
         "ansi-colors": "^4.1.1",
@@ -142,7 +144,8 @@
         "typescript": "^5.6.2",
         "typescript-plugin-css-modules": "^5.1.0",
         "vite": "^6.2.6",
-        "vite-node": "^3.0.7"
+        "vite-node": "^3.0.7",
+        "vitest": "^3.1.2"
       },
       "engines": {
         "node": "^20.18.3",
@@ -622,30 +625,32 @@
       }
     },
     "node_modules/@babel/compat-data": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.6.tgz",
-      "integrity": "sha512-aC2DGhBq5eEdyXWqrDInSqQjO0k8xtPRf5YylULqx8MCd6jBtzqfta/3ETMRpuKIc5hyswfO80ObyA1MvkCcUQ==",
+      "version": "7.26.8",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz",
+      "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/core": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.6.tgz",
-      "integrity": "sha512-qAHSfAdVyFmIvl0VHELib8xar7ONuSHrE2hLnsaWkYNTI68dmi1x8GYDhJjMI/e7XWal9QBlZkwbOnkcw7Z8gQ==",
+      "version": "7.26.10",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz",
+      "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@ampproject/remapping": "^2.2.0",
-        "@babel/code-frame": "^7.24.6",
-        "@babel/generator": "^7.24.6",
-        "@babel/helper-compilation-targets": "^7.24.6",
-        "@babel/helper-module-transforms": "^7.24.6",
-        "@babel/helpers": "^7.24.6",
-        "@babel/parser": "^7.24.6",
-        "@babel/template": "^7.24.6",
-        "@babel/traverse": "^7.24.6",
-        "@babel/types": "^7.24.6",
+        "@babel/code-frame": "^7.26.2",
+        "@babel/generator": "^7.26.10",
+        "@babel/helper-compilation-targets": "^7.26.5",
+        "@babel/helper-module-transforms": "^7.26.0",
+        "@babel/helpers": "^7.26.10",
+        "@babel/parser": "^7.26.10",
+        "@babel/template": "^7.26.9",
+        "@babel/traverse": "^7.26.10",
+        "@babel/types": "^7.26.10",
         "convert-source-map": "^2.0.0",
         "debug": "^4.1.0",
         "gensync": "^1.0.0-beta.2",
@@ -670,13 +675,14 @@
       }
     },
     "node_modules/@babel/generator": {
-      "version": "7.26.2",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz",
-      "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==",
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz",
+      "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/parser": "^7.26.2",
-        "@babel/types": "^7.26.0",
+        "@babel/parser": "^7.27.0",
+        "@babel/types": "^7.27.0",
         "@jridgewell/gen-mapping": "^0.3.5",
         "@jridgewell/trace-mapping": "^0.3.25",
         "jsesc": "^3.0.2"
@@ -710,14 +716,15 @@
       }
     },
     "node_modules/@babel/helper-compilation-targets": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.6.tgz",
-      "integrity": "sha512-VZQ57UsDGlX/5fFA7GkVPplZhHsVc+vuErWgdOiysI9Ksnw0Pbbd6pnPiR/mmJyKHgyIW0c7KT32gmhiF+cirg==",
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz",
+      "integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/compat-data": "^7.24.6",
-        "@babel/helper-validator-option": "^7.24.6",
-        "browserslist": "^4.22.2",
+        "@babel/compat-data": "^7.26.8",
+        "@babel/helper-validator-option": "^7.25.9",
+        "browserslist": "^4.24.0",
         "lru-cache": "^5.1.1",
         "semver": "^6.3.1"
       },
@@ -1014,25 +1021,27 @@
       }
     },
     "node_modules/@babel/helpers": {
-      "version": "7.24.6",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.6.tgz",
-      "integrity": "sha512-V2PI+NqnyFu1i0GyTd/O/cTpxzQCYioSkUIRmgo7gFEHKKCg5w46+r/A6WeUR1+P3TeQ49dspGPNd/E3n9AnnA==",
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz",
+      "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/template": "^7.24.6",
-        "@babel/types": "^7.24.6"
+        "@babel/template": "^7.27.0",
+        "@babel/types": "^7.27.0"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.26.2",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz",
-      "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==",
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
+      "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/types": "^7.26.0"
+        "@babel/types": "^7.27.0"
       },
       "bin": {
         "parser": "bin/babel-parser.js"
@@ -2045,6 +2054,38 @@
         "@babel/core": "^7.0.0-0"
       }
     },
+    "node_modules/@babel/plugin-transform-react-jsx-self": {
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz",
+      "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.25.9"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-react-jsx-source": {
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz",
+      "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.25.9"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
     "node_modules/@babel/plugin-transform-react-pure-annotations": {
       "version": "7.25.9",
       "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.25.9.tgz",
@@ -2429,30 +2470,32 @@
       }
     },
     "node_modules/@babel/template": {
-      "version": "7.25.9",
-      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
-      "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
+      "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/code-frame": "^7.25.9",
-        "@babel/parser": "^7.25.9",
-        "@babel/types": "^7.25.9"
+        "@babel/code-frame": "^7.26.2",
+        "@babel/parser": "^7.27.0",
+        "@babel/types": "^7.27.0"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/traverse": {
-      "version": "7.25.9",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz",
-      "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==",
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz",
+      "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@babel/code-frame": "^7.25.9",
-        "@babel/generator": "^7.25.9",
-        "@babel/parser": "^7.25.9",
-        "@babel/template": "^7.25.9",
-        "@babel/types": "^7.25.9",
+        "@babel/code-frame": "^7.26.2",
+        "@babel/generator": "^7.27.0",
+        "@babel/parser": "^7.27.0",
+        "@babel/template": "^7.27.0",
+        "@babel/types": "^7.27.0",
         "debug": "^4.3.1",
         "globals": "^11.1.0"
       },
@@ -2461,10 +2504,11 @@
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.26.0",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz",
-      "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==",
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
+      "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@babel/helper-string-parser": "^7.25.9",
         "@babel/helper-validator-identifier": "^7.25.9"
@@ -5901,6 +5945,13 @@
         "node": ">=18"
       }
     },
+    "node_modules/@polka/url": {
+      "version": "1.0.0-next.29",
+      "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
+      "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@rollup/pluginutils": {
       "version": "5.1.0",
       "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz",
@@ -8786,6 +8837,100 @@
       "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
       "dev": true
     },
+    "node_modules/@vitejs/plugin-react": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz",
+      "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/core": "^7.26.10",
+        "@babel/plugin-transform-react-jsx-self": "^7.25.9",
+        "@babel/plugin-transform-react-jsx-source": "^7.25.9",
+        "@types/babel__core": "^7.20.5",
+        "react-refresh": "^0.17.0"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
+      }
+    },
+    "node_modules/@vitest/browser": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-3.1.2.tgz",
+      "integrity": "sha512-dwL6hQg3NSDP3Z4xzIZL0xHq/AkQAPQ4StFpWVlY2zbRJtK3Y2YqdFZ7YmZjszTETN1BDQZRn/QOrcP+c8ATgg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@testing-library/dom": "^10.4.0",
+        "@testing-library/user-event": "^14.6.1",
+        "@vitest/mocker": "3.1.2",
+        "@vitest/utils": "3.1.2",
+        "magic-string": "^0.30.17",
+        "sirv": "^3.0.1",
+        "tinyrainbow": "^2.0.0",
+        "ws": "^8.18.1"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "playwright": "*",
+        "vitest": "3.1.2",
+        "webdriverio": "^7.0.0 || ^8.0.0 || ^9.0.0"
+      },
+      "peerDependenciesMeta": {
+        "playwright": {
+          "optional": true
+        },
+        "safaridriver": {
+          "optional": true
+        },
+        "webdriverio": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vitest/browser/node_modules/@vitest/pretty-format": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.2.tgz",
+      "integrity": "sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tinyrainbow": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/browser/node_modules/@vitest/utils": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.2.tgz",
+      "integrity": "sha512-5GGd0ytZ7BH3H6JTj9Kw7Prn1Nbg0wZVrIvou+UWxm54d+WoXXgAgjFJ8wn3LdagWLFSEfpPeyYrByZaGEZHLg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "3.1.2",
+        "loupe": "^3.1.3",
+        "tinyrainbow": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/browser/node_modules/tinyrainbow": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
+      "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
     "node_modules/@vitest/expect": {
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz",
@@ -8841,6 +8986,56 @@
         "@types/estree": "^1.0.0"
       }
     },
+    "node_modules/@vitest/mocker": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.2.tgz",
+      "integrity": "sha512-kOtd6K2lc7SQ0mBqYv/wdGedlqPdM/B38paPY+OwJ1XiNi44w3Fpog82UfOibmHaV9Wod18A09I9SCKLyDMqgw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/spy": "3.1.2",
+        "estree-walker": "^3.0.3",
+        "magic-string": "^0.30.17"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "msw": "^2.4.9",
+        "vite": "^5.0.0 || ^6.0.0"
+      },
+      "peerDependenciesMeta": {
+        "msw": {
+          "optional": true
+        },
+        "vite": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vitest/mocker/node_modules/@vitest/spy": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.2.tgz",
+      "integrity": "sha512-OEc5fSXMws6sHVe4kOFyDSj/+4MSwst0ib4un0DlcYgQvRuYQ0+M2HyqGaauUMnjq87tmUaMNDxKQx7wNfVqPA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tinyspy": "^3.0.2"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/mocker/node_modules/estree-walker": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+      "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "^1.0.0"
+      }
+    },
     "node_modules/@vitest/pretty-format": {
       "version": "2.1.9",
       "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz",
@@ -8854,6 +9049,96 @@
         "url": "https://opencollective.com/vitest"
       }
     },
+    "node_modules/@vitest/runner": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.2.tgz",
+      "integrity": "sha512-bhLib9l4xb4sUMPXnThbnhX2Yi8OutBMA8Yahxa7yavQsFDtwY/jrUZwpKp2XH9DhRFJIeytlyGpXCqZ65nR+g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/utils": "3.1.2",
+        "pathe": "^2.0.3"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/runner/node_modules/@vitest/pretty-format": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.2.tgz",
+      "integrity": "sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tinyrainbow": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/runner/node_modules/@vitest/utils": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.2.tgz",
+      "integrity": "sha512-5GGd0ytZ7BH3H6JTj9Kw7Prn1Nbg0wZVrIvou+UWxm54d+WoXXgAgjFJ8wn3LdagWLFSEfpPeyYrByZaGEZHLg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "3.1.2",
+        "loupe": "^3.1.3",
+        "tinyrainbow": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/runner/node_modules/tinyrainbow": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
+      "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/@vitest/snapshot": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.2.tgz",
+      "integrity": "sha512-Q1qkpazSF/p4ApZg1vfZSQ5Yw6OCQxVMVrLjslbLFA1hMDrT2uxtqMaw8Tc/jy5DLka1sNs1Y7rBcftMiaSH/Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "3.1.2",
+        "magic-string": "^0.30.17",
+        "pathe": "^2.0.3"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.2.tgz",
+      "integrity": "sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tinyrainbow": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/snapshot/node_modules/tinyrainbow": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
+      "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
     "node_modules/@vitest/spy": {
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz",
@@ -10258,9 +10543,9 @@
       "dev": true
     },
     "node_modules/browserslist": {
-      "version": "4.23.0",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
-      "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
+      "version": "4.24.4",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
+      "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
       "dev": true,
       "funding": [
         {
@@ -10276,11 +10561,12 @@
           "url": "https://github.com/sponsors/ai"
         }
       ],
+      "license": "MIT",
       "dependencies": {
-        "caniuse-lite": "^1.0.30001587",
-        "electron-to-chromium": "^1.4.668",
-        "node-releases": "^2.0.14",
-        "update-browserslist-db": "^1.0.13"
+        "caniuse-lite": "^1.0.30001688",
+        "electron-to-chromium": "^1.5.73",
+        "node-releases": "^2.0.19",
+        "update-browserslist-db": "^1.1.1"
       },
       "bin": {
         "browserslist": "cli.js"
@@ -10468,9 +10754,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001611",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001611.tgz",
-      "integrity": "sha512-19NuN1/3PjA3QI8Eki55N8my4LzfkMCRLgCVfrl/slbSAchQfV0+GwjPrK3rq37As4UCLlM/DHajbKkAqbv92Q==",
+      "version": "1.0.30001715",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz",
+      "integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==",
       "dev": true,
       "funding": [
         {
@@ -10485,7 +10771,8 @@
           "type": "github",
           "url": "https://github.com/sponsors/ai"
         }
-      ]
+      ],
+      "license": "CC-BY-4.0"
     },
     "node_modules/chai": {
       "version": "5.2.0",
@@ -12391,10 +12678,11 @@
       }
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.4.742",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.742.tgz",
-      "integrity": "sha512-EhE+z1d5RNytAq/qnGAxPR+ie3UzKbv7qqQc0wnEbOh+KDUplgfzkGSCy9d78B+S+nVNTS42BabHXB6Ni+Ud4w==",
-      "dev": true
+      "version": "1.5.141",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.141.tgz",
+      "integrity": "sha512-qS+qH9oqVYc1ooubTiB9l904WVyM6qNYxtOEEGReoZXw3xlqeYdFr5GclNzbkAufWgwWLEPoDi3d9MoRwwIjGw==",
+      "dev": true,
+      "license": "ISC"
     },
     "node_modules/emitter-listener": {
       "version": "1.1.2",
@@ -12720,9 +13008,10 @@
       }
     },
     "node_modules/escalade": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
-      "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "license": "MIT",
       "engines": {
         "node": ">=6"
       }
@@ -13857,6 +14146,16 @@
         "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
       }
     },
+    "node_modules/expect-type": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz",
+      "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
     "node_modules/extend": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -13971,6 +14270,21 @@
         "pend": "~1.2.0"
       }
     },
+    "node_modules/fdir": {
+      "version": "6.4.4",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
+      "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/file-entry-cache": {
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -20856,10 +21170,11 @@
       }
     },
     "node_modules/magic-string": {
-      "version": "0.30.12",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz",
-      "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==",
+      "version": "0.30.17",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+      "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "@jridgewell/sourcemap-codec": "^1.5.0"
       }
@@ -21808,6 +22123,16 @@
       "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==",
       "dev": true
     },
+    "node_modules/mrmime": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
+      "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/ms": {
       "version": "2.1.3",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -21973,10 +22298,11 @@
       "dev": true
     },
     "node_modules/node-releases": {
-      "version": "2.0.14",
-      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
-      "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
-      "dev": true
+      "version": "2.0.19",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
+      "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/normalize-package-data": {
       "version": "2.5.0",
@@ -23579,6 +23905,16 @@
       "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
       "dev": true
     },
+    "node_modules/react-refresh": {
+      "version": "0.17.0",
+      "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+      "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/read": {
       "version": "1.0.7",
       "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz",
@@ -24506,6 +24842,13 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/siginfo": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+      "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+      "dev": true,
+      "license": "ISC"
+    },
     "node_modules/signal-exit": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
@@ -24564,6 +24907,21 @@
         "simple-concat": "^1.0.0"
       }
     },
+    "node_modules/sirv": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz",
+      "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@polka/url": "^1.0.0-next.24",
+        "mrmime": "^2.0.0",
+        "totalist": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/sisteransi": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -24713,6 +25071,13 @@
         "node": ">=8"
       }
     },
+    "node_modules/stackback": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+      "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/statuses": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@@ -24721,6 +25086,13 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/std-env": {
+      "version": "3.9.0",
+      "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz",
+      "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/stoppable": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz",
@@ -25546,6 +25918,60 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/tinybench": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+      "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tinyexec": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+      "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.13",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
+      "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.4.4",
+        "picomatch": "^4.0.2"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/tinyglobby/node_modules/picomatch": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+      "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/tinypool": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz",
+      "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      }
+    },
     "node_modules/tinyrainbow": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz",
@@ -25667,6 +26093,16 @@
         "xtend": "~4.0.1"
       }
     },
+    "node_modules/totalist": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
+      "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/tough-cookie": {
       "version": "4.1.4",
       "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
@@ -26474,9 +26910,9 @@
       }
     },
     "node_modules/update-browserslist-db": {
-      "version": "1.0.13",
-      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
-      "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
+      "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
       "dev": true,
       "funding": [
         {
@@ -26492,9 +26928,10 @@
           "url": "https://github.com/sponsors/ai"
         }
       ],
+      "license": "MIT",
       "dependencies": {
-        "escalade": "^3.1.1",
-        "picocolors": "^1.0.0"
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.1"
       },
       "bin": {
         "update-browserslist-db": "cli.js"
@@ -26889,9 +27326,9 @@
       }
     },
     "node_modules/vite-node": {
-      "version": "3.0.7",
-      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.7.tgz",
-      "integrity": "sha512-2fX0QwX4GkkkpULXdT1Pf4q0tC1i1lFOyseKoonavXUNlQ77KpW2XqBGGNIm/J4Ows4KxgGJzDguYVPKwG/n5A==",
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.2.tgz",
+      "integrity": "sha512-/8iMryv46J3aK13iUXsei5G/A3CUlW4665THCPS+K8xAaqrVWiGB4RfXMQXCLjpK9P2eK//BczrVkn5JLAk6DA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -26911,6 +27348,144 @@
         "url": "https://opencollective.com/vitest"
       }
     },
+    "node_modules/vitest": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.2.tgz",
+      "integrity": "sha512-WaxpJe092ID1C0mr+LH9MmNrhfzi8I65EX/NRU/Ld016KqQNRgxSOlGNP1hHN+a/F8L15Mh8klwaF77zR3GeDQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/expect": "3.1.2",
+        "@vitest/mocker": "3.1.2",
+        "@vitest/pretty-format": "^3.1.2",
+        "@vitest/runner": "3.1.2",
+        "@vitest/snapshot": "3.1.2",
+        "@vitest/spy": "3.1.2",
+        "@vitest/utils": "3.1.2",
+        "chai": "^5.2.0",
+        "debug": "^4.4.0",
+        "expect-type": "^1.2.1",
+        "magic-string": "^0.30.17",
+        "pathe": "^2.0.3",
+        "std-env": "^3.9.0",
+        "tinybench": "^2.9.0",
+        "tinyexec": "^0.3.2",
+        "tinyglobby": "^0.2.13",
+        "tinypool": "^1.0.2",
+        "tinyrainbow": "^2.0.0",
+        "vite": "^5.0.0 || ^6.0.0",
+        "vite-node": "3.1.2",
+        "why-is-node-running": "^2.3.0"
+      },
+      "bin": {
+        "vitest": "vitest.mjs"
+      },
+      "engines": {
+        "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "@edge-runtime/vm": "*",
+        "@types/debug": "^4.1.12",
+        "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+        "@vitest/browser": "3.1.2",
+        "@vitest/ui": "3.1.2",
+        "happy-dom": "*",
+        "jsdom": "*"
+      },
+      "peerDependenciesMeta": {
+        "@edge-runtime/vm": {
+          "optional": true
+        },
+        "@types/debug": {
+          "optional": true
+        },
+        "@types/node": {
+          "optional": true
+        },
+        "@vitest/browser": {
+          "optional": true
+        },
+        "@vitest/ui": {
+          "optional": true
+        },
+        "happy-dom": {
+          "optional": true
+        },
+        "jsdom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vitest/node_modules/@vitest/expect": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.2.tgz",
+      "integrity": "sha512-O8hJgr+zREopCAqWl3uCVaOdqJwZ9qaDwUP7vy3Xigad0phZe9APxKhPcDNqYYi0rX5oMvwJMSCAXY2afqeTSA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/spy": "3.1.2",
+        "@vitest/utils": "3.1.2",
+        "chai": "^5.2.0",
+        "tinyrainbow": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/vitest/node_modules/@vitest/pretty-format": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.2.tgz",
+      "integrity": "sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tinyrainbow": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/vitest/node_modules/@vitest/spy": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.2.tgz",
+      "integrity": "sha512-OEc5fSXMws6sHVe4kOFyDSj/+4MSwst0ib4un0DlcYgQvRuYQ0+M2HyqGaauUMnjq87tmUaMNDxKQx7wNfVqPA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tinyspy": "^3.0.2"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/vitest/node_modules/@vitest/utils": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.2.tgz",
+      "integrity": "sha512-5GGd0ytZ7BH3H6JTj9Kw7Prn1Nbg0wZVrIvou+UWxm54d+WoXXgAgjFJ8wn3LdagWLFSEfpPeyYrByZaGEZHLg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "3.1.2",
+        "loupe": "^3.1.3",
+        "tinyrainbow": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/vitest/node_modules/tinyrainbow": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
+      "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
     "node_modules/vscode-extension-telemetry": {
       "version": "0.1.7",
       "resolved": "https://registry.npmjs.org/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.7.tgz",
@@ -27187,6 +27762,23 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/why-is-node-running": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+      "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "siginfo": "^2.0.0",
+        "stackback": "0.0.2"
+      },
+      "bin": {
+        "why-is-node-running": "cli.js"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/wrap-ansi": {
       "version": "8.1.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
diff --git a/extensions/ql-vscode/package.json b/extensions/ql-vscode/package.json
index 19466ef2f5b..b55ba00f192 100644
--- a/extensions/ql-vscode/package.json
+++ b/extensions/ql-vscode/package.json
@@ -1943,7 +1943,7 @@
     "watch": "gulp watch",
     "test": "npm-run-all test:*",
     "test:unit": "cross-env TZ=UTC LANG=en-US jest --projects test/unit-tests",
-    "test:view": "jest --projects src/view",
+    "test:view": "vitest run --dir src/view",
     "test:vscode-integration": "npm-run-all test:vscode-integration:*",
     "test:vscode-integration:activated-extension": "jest --projects test/vscode-tests/activated-extension",
     "test:vscode-integration:no-workspace": "jest --projects test/vscode-tests/no-workspace",
@@ -2055,6 +2055,8 @@
     "@types/yauzl": "^2.10.3",
     "@typescript-eslint/eslint-plugin": "^8.30.1",
     "@typescript-eslint/parser": "^8.30.1",
+    "@vitejs/plugin-react": "^4.4.1",
+    "@vitest/browser": "^3.1.2",
     "@vscode/test-electron": "^2.3.9",
     "@vscode/vsce": "^3.2.1",
     "ansi-colors": "^4.1.1",
@@ -2100,7 +2102,8 @@
     "typescript": "^5.6.2",
     "typescript-plugin-css-modules": "^5.1.0",
     "vite": "^6.2.6",
-    "vite-node": "^3.0.7"
+    "vite-node": "^3.0.7",
+    "vitest": "^3.1.2"
   },
   "lint-staged": {
     "./**/*.{json,css,scss}": [
diff --git a/extensions/ql-vscode/src/view/common/SuggestBox/__tests__/SuggestBox.test.tsx b/extensions/ql-vscode/src/view/common/SuggestBox/__tests__/SuggestBox.test.tsx
index e7db75d63ba..2e9d3f64928 100644
--- a/extensions/ql-vscode/src/view/common/SuggestBox/__tests__/SuggestBox.test.tsx
+++ b/extensions/ql-vscode/src/view/common/SuggestBox/__tests__/SuggestBox.test.tsx
@@ -77,8 +77,8 @@ const options: TestOption[] = [
 ];
 
 describe("SuggestBox", () => {
-  const onChange = jest.fn();
-  const parseValueToTokens = jest.fn();
+  const onChange = vi.fn();
+  const parseValueToTokens = vi.fn();
   const render = (props?: Partial<SuggestBoxProps<TestOption>>) =>
     reactRender(
       <SuggestBox
diff --git a/extensions/ql-vscode/src/view/common/SuggestBox/__tests__/useEffectEvent.test.ts b/extensions/ql-vscode/src/view/common/SuggestBox/__tests__/useEffectEvent.test.ts
index 7f050d8b3ce..f7c82d3cb8e 100644
--- a/extensions/ql-vscode/src/view/common/SuggestBox/__tests__/useEffectEvent.test.ts
+++ b/extensions/ql-vscode/src/view/common/SuggestBox/__tests__/useEffectEvent.test.ts
@@ -3,8 +3,8 @@ import { useEffectEvent } from "../useEffectEvent";
 
 describe("useEffectEvent", () => {
   it("does not change reference when changing the callback function", () => {
-    const callback1 = jest.fn();
-    const callback2 = jest.fn();
+    const callback1 = vi.fn();
+    const callback2 = vi.fn();
 
     const { result, rerender } = renderHook(
       (callback) => useEffectEvent(callback),
diff --git a/extensions/ql-vscode/src/view/common/SuggestBox/__tests__/useOpenKey.test.ts b/extensions/ql-vscode/src/view/common/SuggestBox/__tests__/useOpenKey.test.ts
index 93a581603a9..702078abf6b 100644
--- a/extensions/ql-vscode/src/view/common/SuggestBox/__tests__/useOpenKey.test.ts
+++ b/extensions/ql-vscode/src/view/common/SuggestBox/__tests__/useOpenKey.test.ts
@@ -5,7 +5,7 @@ import { mockedObject } from "../../../../../test/mocked-object";
 import { useOpenKey } from "../useOpenKey";
 
 describe("useOpenKey", () => {
-  const onOpenChange = jest.fn();
+  const onOpenChange = vi.fn();
 
   beforeEach(() => {
     onOpenChange.mockReset();
@@ -41,7 +41,7 @@ describe("useOpenKey", () => {
     ctrlKey = false,
     metaKey = false,
     shiftKey = false,
-    preventDefault = jest.fn(),
+    preventDefault = vi.fn(),
   }: Partial<KeyboardEvent>) =>
     mockedObject<KeyboardEvent>({
       key,
@@ -183,7 +183,7 @@ describe("useOpenKey", () => {
     rerender(
       mockedObject<FloatingContext>({
         open: true,
-        onOpenChange: jest.fn(),
+        onOpenChange: vi.fn(),
       }),
     );
 
diff --git a/extensions/ql-vscode/src/view/jest-environment-jsdom.ts b/extensions/ql-vscode/src/view/jest-environment-jsdom.ts
deleted file mode 100644
index a43586ddbd3..00000000000
--- a/extensions/ql-vscode/src/view/jest-environment-jsdom.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-// eslint-disable-next-line import/no-namespace -- We need a type of JSDOM so we can't use named imports.
-import * as JSDOM from "jsdom";
-import type {
-  EnvironmentContext,
-  JestEnvironmentConfig,
-} from "@jest/environment";
-import BaseEnv from "@jest/environment-jsdom-abstract";
-
-export default class JSDOMEnvironment extends BaseEnv {
-  constructor(config: JestEnvironmentConfig, context: EnvironmentContext) {
-    super(config, context, JSDOM);
-  }
-}
-
-export const TestEnvironment = JSDOMEnvironment;
diff --git a/extensions/ql-vscode/src/view/jest.config.ts b/extensions/ql-vscode/src/view/jest.config.ts
deleted file mode 100644
index c197b277e62..00000000000
--- a/extensions/ql-vscode/src/view/jest.config.ts
+++ /dev/null
@@ -1,203 +0,0 @@
-import type { Config } from "jest";
-import { transformIgnorePatterns } from "../../test/jest-config";
-
-/*
- * For a detailed explanation regarding each configuration property and type check, visit:
- * https://jestjs.io/docs/configuration
- */
-
-const config: Config = {
-  // All imported modules in your tests should be mocked automatically
-  // automock: false,
-
-  // Stop running tests after `n` failures
-  // bail: 0,
-
-  // The directory where Jest should store its cached dependency information
-  // cacheDirectory: "/private/var/folders/6m/1394pht172qgd7dmw1fwjk100000gn/T/jest_dx",
-
-  // Automatically clear mock calls, instances, contexts and results before every test
-  // clearMocks: true,
-
-  // Indicates whether the coverage information should be collected while executing the test
-  // collectCoverage: false,
-
-  // An array of glob patterns indicating a set of files for which coverage information should be collected
-  // collectCoverageFrom: undefined,
-
-  // The directory where Jest should output its coverage files
-  // coverageDirectory: undefined,
-
-  // An array of regexp pattern strings used to skip coverage collection
-  // coveragePathIgnorePatterns: [
-  //   "/node_modules/"
-  // ],
-
-  // Indicates which provider should be used to instrument code for coverage
-  coverageProvider: "v8",
-
-  // A list of reporter names that Jest uses when writing coverage reports
-  // coverageReporters: [
-  //   "json",
-  //   "text",
-  //   "lcov",
-  //   "clover"
-  // ],
-
-  // An object that configures minimum threshold enforcement for coverage results
-  // coverageThreshold: undefined,
-
-  // A path to a custom dependency extractor
-  // dependencyExtractor: undefined,
-
-  // Make calling deprecated APIs throw helpful error messages
-  // errorOnDeprecated: false,
-
-  // The default configuration for fake timers
-  // fakeTimers: {
-  //   "enableGlobally": false
-  // },
-
-  // Force coverage collection from ignored files using an array of glob patterns
-  // forceCoverageMatch: [],
-
-  // A path to a module which exports an async function that is triggered once before all test suites
-  // globalSetup: undefined,
-
-  // A path to a module which exports an async function that is triggered once after all test suites
-  // globalTeardown: undefined,
-
-  // A set of global variables that need to be available in all test environments
-  // globals: {},
-
-  // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
-  // maxWorkers: "50%",
-
-  // An array of directory names to be searched recursively up from the requiring module's location
-  // moduleDirectories: [
-  //   "node_modules"
-  // ],
-
-  // An array of file extensions your modules use
-  moduleFileExtensions: ["js", "mjs", "cjs", "jsx", "ts", "tsx", "json"],
-
-  // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
-  moduleNameMapper: {
-    "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
-      "<rootDir>/../../test/__mocks__/fileMock.ts",
-    "\\.(css|less)$": "<rootDir>/../../test/__mocks__/styleMock.ts",
-  },
-
-  // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
-  // modulePathIgnorePatterns: [],
-
-  // Activates notifications for test results
-  // notify: false,
-
-  // An enum that specifies notification mode. Requires { notify: true }
-  // notifyMode: "failure-change",
-
-  // A preset that is used as a base for Jest's configuration
-  preset: "ts-jest",
-
-  // Run tests from one or more projects
-  // projects: undefined,
-
-  // Use this configuration option to add custom reporters to Jest
-  // reporters: undefined,
-
-  // Automatically reset mock state before every test
-  // resetMocks: false,
-
-  // Reset the module registry before running each individual test
-  // resetModules: false,
-
-  // A path to a custom resolver
-  // resolver: undefined,
-
-  // Automatically restore mock state and implementation before every test
-  // restoreMocks: false,
-
-  // The root directory that Jest should scan for tests and modules within
-  // rootDir: undefined,
-
-  // A list of paths to directories that Jest should use to search for files in
-  // roots: [
-  //   "<rootDir>"
-  // ],
-
-  // Allows you to use a custom runner instead of Jest's default test runner
-  // runner: "jest-runner",
-
-  // The paths to modules that run some code to configure or set up the testing environment before each test
-  // setupFiles: [],
-
-  // A list of paths to modules that run some code to configure or set up the testing framework before each test
-  setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
-
-  // The number of seconds after which a test is considered as slow and reported as such in the results.
-  // slowTestThreshold: 5,
-
-  // A list of paths to snapshot serializer modules Jest should use for snapshot testing
-  // snapshotSerializers: [],
-
-  // The test environment that will be used for testing
-  testEnvironment: "<rootDir>/jest-environment-jsdom.ts",
-
-  // Options that will be passed to the testEnvironment
-  // testEnvironmentOptions: {},
-
-  // Adds a location field to test results
-  // testLocationInResults: false,
-
-  // The glob patterns Jest uses to detect test files
-  testMatch: ["**/__tests__/**/*.[jt]s?(x)"],
-
-  // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
-  // testPathIgnorePatterns: [
-  //   "/node_modules/"
-  // ],
-
-  // The regexp pattern or array of patterns that Jest uses to detect test files
-  // testRegex: [],
-
-  // This option allows the use of a custom results processor
-  // testResultsProcessor: undefined,
-
-  // This option allows use of a custom test runner
-  // testRunner: "jest-circus/runner",
-
-  // A map from regular expressions to paths to transformers
-  transform: {
-    "^.+\\.tsx?$": [
-      "ts-jest",
-      {
-        tsconfig: "<rootDir>/tsconfig.spec.json",
-      },
-    ],
-    node_modules: [
-      "babel-jest",
-      {
-        presets: ["@babel/preset-env"],
-        plugins: ["@babel/plugin-transform-modules-commonjs"],
-      },
-    ],
-  },
-
-  // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
-  transformIgnorePatterns,
-
-  // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
-  // unmockedModulePathPatterns: undefined,
-
-  // Indicates whether each individual test should be reported during the run
-  // verbose: undefined,
-
-  // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
-  // watchPathIgnorePatterns: [],
-
-  // Whether to use watchman for file crawling
-  // watchman: true,
-};
-
-export default config;
diff --git a/extensions/ql-vscode/src/view/jest.setup.ts b/extensions/ql-vscode/src/view/jest.setup.ts
deleted file mode 100644
index 31dc6657d40..00000000000
--- a/extensions/ql-vscode/src/view/jest.setup.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import "@testing-library/jest-dom";
-
-// https://jestjs.io/docs/26.x/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
-Object.defineProperty(window, "matchMedia", {
-  writable: true,
-  value: jest.fn().mockImplementation((query) => ({
-    matches: false,
-    media: query,
-    onchange: null,
-    addListener: jest.fn(), // deprecated
-    removeListener: jest.fn(), // deprecated
-    addEventListener: jest.fn(),
-    removeEventListener: jest.fn(),
-    dispatchEvent: jest.fn(),
-  })),
-});
-
-// Used by Primer React
-window.CSS.supports = jest.fn().mockResolvedValue(false);
-
-// Functions that are not implemented in jsdom
-window.CSSStyleSheet.prototype.replaceSync = jest
-  .fn()
-  .mockReturnValue(undefined);
-window.ElementInternals.prototype.setFormValue = jest
-  .fn()
-  .mockReturnValue(undefined);
-window.ElementInternals.prototype.setValidity = jest
-  .fn()
-  .mockReturnValue(undefined);
-window.HTMLSlotElement.prototype.assignedElements = jest
-  .fn()
-  .mockReturnValue([]);
-
-// Store this on the window so we can mock it
-window.vsCodeApi = {
-  postMessage: jest.fn(),
-  setState: jest.fn(),
-};
-
-window.acquireVsCodeApi = () => window.vsCodeApi;
diff --git a/extensions/ql-vscode/src/view/method-modeling/__tests__/MethodModeling.spec.tsx b/extensions/ql-vscode/src/view/method-modeling/__tests__/MethodModeling.spec.tsx
index 9a6496aea20..705c72796a8 100644
--- a/extensions/ql-vscode/src/view/method-modeling/__tests__/MethodModeling.spec.tsx
+++ b/extensions/ql-vscode/src/view/method-modeling/__tests__/MethodModeling.spec.tsx
@@ -13,7 +13,7 @@ describe(MethodModeling.name, () => {
   it("renders method modeling panel", () => {
     const method = createMethod();
     const modeledMethod = createSinkModeledMethod();
-    const onChange = jest.fn();
+    const onChange = vi.fn();
 
     render({
       language: QueryLanguage.Java,
diff --git a/extensions/ql-vscode/src/view/method-modeling/__tests__/MethodModelingInputs.spec.tsx b/extensions/ql-vscode/src/view/method-modeling/__tests__/MethodModelingInputs.spec.tsx
index 0734a7064e3..4666583b247 100644
--- a/extensions/ql-vscode/src/view/method-modeling/__tests__/MethodModelingInputs.spec.tsx
+++ b/extensions/ql-vscode/src/view/method-modeling/__tests__/MethodModelingInputs.spec.tsx
@@ -19,7 +19,7 @@ describe(MethodModelingInputs.name, () => {
   const method = createMethod();
   const modeledMethod = createSinkModeledMethod();
   const modelConfig = defaultModelConfig;
-  const onChange = jest.fn();
+  const onChange = vi.fn();
 
   it("renders the method modeling inputs", () => {
     render({
diff --git a/extensions/ql-vscode/src/view/method-modeling/__tests__/MultipleModeledMethodsPanel.spec.tsx b/extensions/ql-vscode/src/view/method-modeling/__tests__/MultipleModeledMethodsPanel.spec.tsx
index 27372287bfc..2c1846878c9 100644
--- a/extensions/ql-vscode/src/view/method-modeling/__tests__/MultipleModeledMethodsPanel.spec.tsx
+++ b/extensions/ql-vscode/src/view/method-modeling/__tests__/MultipleModeledMethodsPanel.spec.tsx
@@ -15,7 +15,7 @@ import { defaultModelConfig } from "../../../model-editor/languages";
 describe(MultipleModeledMethodsPanel.name, () => {
   const language = QueryLanguage.Java;
   const method = createMethod();
-  const onChange = jest.fn<void, [string, ModeledMethod[]]>();
+  const onChange = vi.fn<void, [string, ModeledMethod[]]>();
   const modelConfig = defaultModelConfig;
 
   const baseProps = {
diff --git a/extensions/ql-vscode/src/view/model-editor/__tests__/LibraryRow.spec.tsx b/extensions/ql-vscode/src/view/model-editor/__tests__/LibraryRow.spec.tsx
index 661c9f214c5..1d9fc1d36cc 100644
--- a/extensions/ql-vscode/src/view/model-editor/__tests__/LibraryRow.spec.tsx
+++ b/extensions/ql-vscode/src/view/model-editor/__tests__/LibraryRow.spec.tsx
@@ -7,10 +7,10 @@ import { createMockModelEditorViewState } from "../../../../test/factories/model
 
 describe(LibraryRow.name, () => {
   const method = createMethod();
-  const onChange = jest.fn();
-  const onMethodClick = jest.fn();
-  const onSaveModelClick = jest.fn();
-  const onModelDependencyClick = jest.fn();
+  const onChange = vi.fn();
+  const onMethodClick = vi.fn();
+  const onSaveModelClick = vi.fn();
+  const onModelDependencyClick = vi.fn();
 
   const viewState = createMockModelEditorViewState();
 
@@ -40,7 +40,7 @@ describe(LibraryRow.name, () => {
         onChange={onChange}
         onMethodClick={onMethodClick}
         onSaveModelClick={onSaveModelClick}
-        onGenerateFromSourceClick={jest.fn()}
+        onGenerateFromSourceClick={vi.fn()}
         onModelDependencyClick={onModelDependencyClick}
         {...props}
       />,
diff --git a/extensions/ql-vscode/src/view/model-editor/__tests__/MethodRow.spec.tsx b/extensions/ql-vscode/src/view/model-editor/__tests__/MethodRow.spec.tsx
index 6ce364b4393..4025e5e2937 100644
--- a/extensions/ql-vscode/src/view/model-editor/__tests__/MethodRow.spec.tsx
+++ b/extensions/ql-vscode/src/view/model-editor/__tests__/MethodRow.spec.tsx
@@ -29,8 +29,8 @@ describe(MethodRow.name, () => {
     kind: "taint",
     provenance: "manual",
   };
-  const onChange = jest.fn();
-  const onMethodClick = jest.fn();
+  const onChange = vi.fn();
+  const onMethodClick = vi.fn();
 
   const viewState = createMockModelEditorViewState();
 
diff --git a/extensions/ql-vscode/src/view/model-editor/__tests__/ModelEvaluation.spec.tsx b/extensions/ql-vscode/src/view/model-editor/__tests__/ModelEvaluation.spec.tsx
index 150ace05b53..adb3808744b 100644
--- a/extensions/ql-vscode/src/view/model-editor/__tests__/ModelEvaluation.spec.tsx
+++ b/extensions/ql-vscode/src/view/model-editor/__tests__/ModelEvaluation.spec.tsx
@@ -19,9 +19,9 @@ describe(ModelEvaluation.name, () => {
         viewState={createMockModelEditorViewState({ showEvaluationUi: true })}
         modeledMethods={modeledMethodsMap}
         modifiedSignatures={new Set()}
-        onStartEvaluation={jest.fn()}
-        onStopEvaluation={jest.fn()}
-        openModelAlertsView={jest.fn()}
+        onStartEvaluation={vi.fn()}
+        onStopEvaluation={vi.fn()}
+        openModelAlertsView={vi.fn()}
         evaluationRun={undefined}
         {...props}
       />,
diff --git a/extensions/ql-vscode/src/view/model-editor/__tests__/ModelKindDropdown.spec.tsx b/extensions/ql-vscode/src/view/model-editor/__tests__/ModelKindDropdown.spec.tsx
index 22d4a3a65c7..7869f04bf38 100644
--- a/extensions/ql-vscode/src/view/model-editor/__tests__/ModelKindDropdown.spec.tsx
+++ b/extensions/ql-vscode/src/view/model-editor/__tests__/ModelKindDropdown.spec.tsx
@@ -9,7 +9,7 @@ import {
 import { QueryLanguage } from "../../../common/query-language";
 
 describe(ModelKindDropdown.name, () => {
-  const onChange = jest.fn();
+  const onChange = vi.fn();
 
   beforeEach(() => {
     onChange.mockReset();
diff --git a/extensions/ql-vscode/src/view/model-editor/__tests__/ModelTypeDropdown.spec.tsx b/extensions/ql-vscode/src/view/model-editor/__tests__/ModelTypeDropdown.spec.tsx
index f5ed858f3c1..7fe77bd6874 100644
--- a/extensions/ql-vscode/src/view/model-editor/__tests__/ModelTypeDropdown.spec.tsx
+++ b/extensions/ql-vscode/src/view/model-editor/__tests__/ModelTypeDropdown.spec.tsx
@@ -7,7 +7,7 @@ import { createMethod } from "../../../../test/factories/model-editor/method-fac
 import { defaultModelConfig } from "../../../model-editor/languages";
 
 describe(ModelTypeDropdown.name, () => {
-  const onChange = jest.fn();
+  const onChange = vi.fn();
 
   beforeEach(() => {
     onChange.mockReset();
diff --git a/extensions/ql-vscode/src/view/model-editor/__tests__/ModeledMethodDataGrid.spec.tsx b/extensions/ql-vscode/src/view/model-editor/__tests__/ModeledMethodDataGrid.spec.tsx
index 7a3645cca8f..9fdec40e2d9 100644
--- a/extensions/ql-vscode/src/view/model-editor/__tests__/ModeledMethodDataGrid.spec.tsx
+++ b/extensions/ql-vscode/src/view/model-editor/__tests__/ModeledMethodDataGrid.spec.tsx
@@ -35,8 +35,8 @@ describe(ModeledMethodDataGrid.name, () => {
     methodParameters: "()",
     supported: true,
   });
-  const onChange = jest.fn();
-  const onMethodClick = jest.fn();
+  const onChange = vi.fn();
+  const onMethodClick = vi.fn();
 
   const viewState = createMockModelEditorViewState();
 
diff --git a/extensions/ql-vscode/src/view/model-editor/__tests__/ModeledMethodsList.spec.tsx b/extensions/ql-vscode/src/view/model-editor/__tests__/ModeledMethodsList.spec.tsx
index 6bf0078a3b7..8a7ad236d1b 100644
--- a/extensions/ql-vscode/src/view/model-editor/__tests__/ModeledMethodsList.spec.tsx
+++ b/extensions/ql-vscode/src/view/model-editor/__tests__/ModeledMethodsList.spec.tsx
@@ -32,10 +32,10 @@ describe(ModeledMethodsList.name, () => {
     methodName: "println",
     methodParameters: "(String)",
   });
-  const onChange = jest.fn();
-  const onMethodClick = jest.fn();
-  const onSaveModelClick = jest.fn();
-  const onModelDependencyClick = jest.fn();
+  const onChange = vi.fn();
+  const onMethodClick = vi.fn();
+  const onSaveModelClick = vi.fn();
+  const onModelDependencyClick = vi.fn();
 
   const viewState = createMockModelEditorViewState();
 
@@ -63,7 +63,7 @@ describe(ModeledMethodsList.name, () => {
         onChange={onChange}
         onMethodClick={onMethodClick}
         onSaveModelClick={onSaveModelClick}
-        onGenerateFromSourceClick={jest.fn()}
+        onGenerateFromSourceClick={vi.fn()}
         onModelDependencyClick={onModelDependencyClick}
         {...props}
       />,
diff --git a/extensions/ql-vscode/src/view/model-editor/__tests__/ModelingStatusIndicator.spec.tsx b/extensions/ql-vscode/src/view/model-editor/__tests__/ModelingStatusIndicator.spec.tsx
index 9b47af78072..910b62425e7 100644
--- a/extensions/ql-vscode/src/view/model-editor/__tests__/ModelingStatusIndicator.spec.tsx
+++ b/extensions/ql-vscode/src/view/model-editor/__tests__/ModelingStatusIndicator.spec.tsx
@@ -17,6 +17,6 @@ describe(ModelingStatusIndicator.name, () => {
     },
   ] as const)("renders %s status indicator", ({ status, text }) => {
     render(<ModelingStatusIndicator status={status} />);
-    expect(screen.getByLabelText(text)).toBeVisible();
+    expect(screen.getByLabelText(text)).toBeInTheDocument();
   });
 });
diff --git a/extensions/ql-vscode/src/view/results/__tests__/AlertTablePathRow.spec.tsx b/extensions/ql-vscode/src/view/results/__tests__/AlertTablePathRow.spec.tsx
index ada710bee85..2e4add47eb4 100644
--- a/extensions/ql-vscode/src/view/results/__tests__/AlertTablePathRow.spec.tsx
+++ b/extensions/ql-vscode/src/view/results/__tests__/AlertTablePathRow.spec.tsx
@@ -23,8 +23,8 @@ describe(AlertTablePathRow.name, () => {
         databaseUri={"dbUri"}
         sourceLocationPrefix="src"
         userSettings={{ shouldShowProvenance: false }}
-        updateSelectionCallback={jest.fn()}
-        toggleExpanded={jest.fn()}
+        updateSelectionCallback={vi.fn()}
+        toggleExpanded={vi.fn()}
         {...props}
       />,
     );
diff --git a/extensions/ql-vscode/src/view/results/__tests__/AlertTableResultRow.spec.tsx b/extensions/ql-vscode/src/view/results/__tests__/AlertTableResultRow.spec.tsx
index 0aa7279ae72..e2c0d75d4c2 100644
--- a/extensions/ql-vscode/src/view/results/__tests__/AlertTableResultRow.spec.tsx
+++ b/extensions/ql-vscode/src/view/results/__tests__/AlertTableResultRow.spec.tsx
@@ -18,8 +18,8 @@ describe(AlertTableResultRow.name, () => {
         databaseUri={"dbUri"}
         sourceLocationPrefix="src"
         userSettings={{ shouldShowProvenance: false }}
-        updateSelectionCallback={jest.fn()}
-        toggleExpanded={jest.fn()}
+        updateSelectionCallback={vi.fn()}
+        toggleExpanded={vi.fn()}
         {...props}
       />,
     );
diff --git a/extensions/ql-vscode/src/view/results/__tests__/results.spec.tsx b/extensions/ql-vscode/src/view/results/__tests__/results.spec.tsx
index 0116368cbf7..12b09cd2c2d 100644
--- a/extensions/ql-vscode/src/view/results/__tests__/results.spec.tsx
+++ b/extensions/ql-vscode/src/view/results/__tests__/results.spec.tsx
@@ -5,14 +5,11 @@ import type {
   IntoResultsViewMsg,
 } from "../../../common/interface-types";
 import { SortDirection } from "../../../common/interface-types";
-import { readJSONSync } from "fs-extra";
-import { resolve } from "path";
 import { postMessage } from "../../common/post-message";
 import { ColumnKind } from "../../../common/raw-result-types";
 
-const exampleSarif = readJSONSync(
-  resolve(__dirname, "../../../../test/data/sarif/validSarif.sarif"),
-);
+// eslint-disable-next-line import/no-namespace
+import * as exampleSarif from "./validSarif.sarif.json";
 
 describe(ResultsApp.name, () => {
   const render = () => reactRender(<ResultsApp />);
diff --git a/extensions/ql-vscode/src/view/results/__tests__/validSarif.sarif.json b/extensions/ql-vscode/src/view/results/__tests__/validSarif.sarif.json
new file mode 100644
index 00000000000..2d2fa5cacfa
--- /dev/null
+++ b/extensions/ql-vscode/src/view/results/__tests__/validSarif.sarif.json
@@ -0,0 +1,57 @@
+{
+  "version": "2.1.0",
+  "$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.4",
+  "runs": [
+    {
+      "tool": {
+        "driver": {
+          "name": "ESLint",
+          "informationUri": "https://eslint.org",
+          "rules": [
+            {
+              "id": "no-unused-vars",
+              "shortDescription": {
+                "text": "disallow unused variables"
+              },
+              "helpUri": "https://eslint.org/docs/rules/no-unused-vars",
+              "properties": {
+                "category": "Variables"
+              }
+            }
+          ]
+        }
+      },
+      "artifacts": [
+        {
+          "location": {
+            "uri": "file:///C:/dev/sarif/sarif-tutorials/samples/Introduction/simple-example.js"
+          }
+        }
+      ],
+      "results": [
+        {
+          "level": "error",
+          "message": {
+            "text": "'x' is assigned a value but never used."
+          },
+          "locations": [
+            {
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "file:///C:/dev/sarif/sarif-tutorials/samples/Introduction/simple-example.js",
+                  "index": 0
+                },
+                "region": {
+                  "startLine": 1,
+                  "startColumn": 5
+                }
+              }
+            }
+          ],
+          "ruleId": "no-unused-vars",
+          "ruleIndex": 0
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/extensions/ql-vscode/src/view/tsconfig.json b/extensions/ql-vscode/src/view/tsconfig.json
index d094c039d78..4551643a9c9 100644
--- a/extensions/ql-vscode/src/view/tsconfig.json
+++ b/extensions/ql-vscode/src/view/tsconfig.json
@@ -19,7 +19,8 @@
       {
         "name": "typescript-plugin-css-modules"
       }
-    ]
+    ],
+    "types": ["vitest/globals"]
   },
   "exclude": ["node_modules"]
 }
diff --git a/extensions/ql-vscode/src/view/tsconfig.spec.json b/extensions/ql-vscode/src/view/tsconfig.spec.json
deleted file mode 100644
index 96fce8c40fa..00000000000
--- a/extensions/ql-vscode/src/view/tsconfig.spec.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-  "extends": "./tsconfig.json",
-  "compilerOptions": {
-    "module": "commonjs"
-  },
-  "exclude": []
-}
diff --git a/extensions/ql-vscode/src/view/variant-analysis/__tests__/QueryDetails.spec.tsx b/extensions/ql-vscode/src/view/variant-analysis/__tests__/QueryDetails.spec.tsx
index e50ca9ef567..007cb5f1e3a 100644
--- a/extensions/ql-vscode/src/view/variant-analysis/__tests__/QueryDetails.spec.tsx
+++ b/extensions/ql-vscode/src/view/variant-analysis/__tests__/QueryDetails.spec.tsx
@@ -4,11 +4,11 @@ import type { QueryDetailsProps } from "../QueryDetails";
 import { QueryDetails } from "../QueryDetails";
 
 describe(QueryDetails.name, () => {
-  const onOpenQueryFileClick = jest.fn();
-  const onViewQueryTextClick = jest.fn();
-  const onStopQueryClick = jest.fn();
-  const onCopyRepositoryListClick = jest.fn();
-  const onExportResultsClick = jest.fn();
+  const onOpenQueryFileClick = vi.fn();
+  const onViewQueryTextClick = vi.fn();
+  const onStopQueryClick = vi.fn();
+  const onCopyRepositoryListClick = vi.fn();
+  const onExportResultsClick = vi.fn();
 
   afterEach(() => {
     onOpenQueryFileClick.mockReset();
diff --git a/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisActions.spec.tsx b/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisActions.spec.tsx
index ff0a99099a3..1ddbbc7bab0 100644
--- a/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisActions.spec.tsx
+++ b/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisActions.spec.tsx
@@ -5,9 +5,9 @@ import type { VariantAnalysisActionsProps } from "../VariantAnalysisActions";
 import { VariantAnalysisActions } from "../VariantAnalysisActions";
 
 describe(VariantAnalysisActions.name, () => {
-  const onStopQueryClick = jest.fn();
-  const onCopyRepositoryListClick = jest.fn();
-  const onExportResultsClick = jest.fn();
+  const onStopQueryClick = vi.fn();
+  const onCopyRepositoryListClick = vi.fn();
+  const onExportResultsClick = vi.fn();
 
   afterEach(() => {
     onStopQueryClick.mockReset();
diff --git a/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisOutcomePanels.spec.tsx b/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisOutcomePanels.spec.tsx
index 0ac4ab23df3..c3b3e9cb1d1 100644
--- a/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisOutcomePanels.spec.tsx
+++ b/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisOutcomePanels.spec.tsx
@@ -83,7 +83,7 @@ describe(VariantAnalysisOutcomePanels.name, () => {
           ...variantAnalysis,
         }}
         filterSortState={defaultFilterSortState}
-        setFilterSortState={jest.fn()}
+        setFilterSortState={vi.fn()}
         {...props}
       />,
     );
diff --git a/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisStats.spec.tsx b/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisStats.spec.tsx
index e3a9f3a4abf..a6e5383231a 100644
--- a/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisStats.spec.tsx
+++ b/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisStats.spec.tsx
@@ -5,7 +5,7 @@ import { VariantAnalysisStats } from "../VariantAnalysisStats";
 import { userEvent } from "@testing-library/user-event";
 
 describe(VariantAnalysisStats.name, () => {
-  const onViewLogsClick = jest.fn();
+  const onViewLogsClick = vi.fn();
 
   afterEach(() => {
     onViewLogsClick.mockReset();
diff --git a/extensions/ql-vscode/src/view/vitest.config.ts b/extensions/ql-vscode/src/view/vitest.config.ts
new file mode 100644
index 00000000000..99577384f0b
--- /dev/null
+++ b/extensions/ql-vscode/src/view/vitest.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from "vitest/config";
+import react from "@vitejs/plugin-react";
+
+export default defineConfig({
+  plugins: [react()],
+  test: {
+    globals: true,
+    browser: {
+      enabled: true,
+      provider: "playwright",
+      instances: [{ browser: "chromium" }],
+    },
+    setupFiles: "./vitest.setup.ts",
+  },
+});
diff --git a/extensions/ql-vscode/src/view/vitest.setup.ts b/extensions/ql-vscode/src/view/vitest.setup.ts
new file mode 100644
index 00000000000..60f5858bf81
--- /dev/null
+++ b/extensions/ql-vscode/src/view/vitest.setup.ts
@@ -0,0 +1,7 @@
+// Store this on the window so we can mock it
+window.vsCodeApi = {
+  postMessage: vi.fn(),
+  setState: vi.fn(),
+};
+
+window.acquireVsCodeApi = () => window.vsCodeApi;
diff --git a/extensions/ql-vscode/vitest.workspace.ts b/extensions/ql-vscode/vitest.workspace.ts
new file mode 100644
index 00000000000..d429a40c5b4
--- /dev/null
+++ b/extensions/ql-vscode/vitest.workspace.ts
@@ -0,0 +1,3 @@
+import { defineWorkspace } from "vitest/config";
+
+export default defineWorkspace(["src/view/vitest.config.ts"]);