diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index 2f5c0de140a..cdfee0ad091 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -35,4 +35,11 @@ runs: echo "::group::Integration Tests (docker sandbox)" npm run test:integration:sandbox:docker echo "::endgroup::" + echo "::group::Integration Tests (VS Code)" + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + xvfb-run -a npm run test:integration:vscode + else + npm run test:integration:vscode + fi + echo "::endgroup::" shell: 'bash' diff --git a/.gitignore b/.gitignore index ebb94151e8d..34c572afbed 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ packages/devtools/src/_client-assets.ts .integration-tests/ packages/vscode-ide-companion/*.vsix packages/cli/download-ripgrep*/ +.vscode-test/ # GHA credentials gha-creds-*.json diff --git a/.prettierignore b/.prettierignore index 9009498d8d2..eda965c4d8a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,6 +9,7 @@ .gemini/ .idea .integration-tests/ +**/.vscode-test/ *.iml *.tsbuildinfo *.vsix diff --git a/eslint.config.js b/eslint.config.js index a0a0429119b..29d459a0e38 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -52,6 +52,7 @@ export default tseslint.config( 'bundle/**', 'package/bundle/**', '.integration-tests/**', + '**/.vscode-test/**', 'dist/**', 'evals/**', 'packages/test-utils/**', diff --git a/package-lock.json b/package-lock.json index 0dc1ce49514..d83e2a509c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2195,7 +2195,6 @@ "integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.2", @@ -2376,7 +2375,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -2426,7 +2424,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -2801,7 +2798,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -2835,7 +2831,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz", "integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0" @@ -2890,7 +2885,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz", "integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0", @@ -4054,7 +4048,6 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4329,7 +4322,6 @@ "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.35.0", "@typescript-eslint/types": "8.35.0", @@ -4841,6 +4833,23 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vscode/test-electron": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.5.2.tgz", + "integrity": "sha512-8ukpxv4wYe0iWMRQU18jhzJOHkeGKbnw7xWRX3Zw1WJA4cEKbHcmmLPdPrPtL6rhDcrlCZN+xKRpv09n4gRHYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "jszip": "^3.10.1", + "ora": "^8.1.0", + "semver": "^7.6.2" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@vscode/vsce": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.6.0.tgz", @@ -5203,7 +5212,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7735,7 +7743,6 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -8246,7 +8253,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -9514,7 +9520,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -9713,6 +9718,13 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -9794,7 +9806,6 @@ "resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.11.tgz", "integrity": "sha512-93LQlzT7vvZ1XJcmOMwN4s+6W334QegendeHOMnEJBlhnpIzr8bws6/aOEHG8ZCuVD/vNeeea5m1msHIdAY6ig==", "license": "MIT", - "peer": true, "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.1", "ansi-escapes": "^7.0.0", @@ -10222,6 +10233,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -10867,6 +10891,59 @@ "node": ">=4.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -10992,6 +11069,16 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -11260,6 +11347,49 @@ "dev": true, "license": "MIT" }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -12540,6 +12670,117 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/outvariant": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", @@ -12642,6 +12883,13 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -13073,6 +13321,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -13381,7 +13636,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -13392,7 +13646,6 @@ "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" @@ -14302,6 +14555,13 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -14622,6 +14882,19 @@ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "license": "MIT" }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -15437,7 +15710,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -15661,8 +15933,7 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsx": { "version": "4.20.3", @@ -15670,7 +15941,6 @@ "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -15830,7 +16100,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16053,7 +16322,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -16167,7 +16435,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -16180,7 +16447,6 @@ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -16822,7 +17088,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -17359,7 +17624,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -17446,6 +17710,7 @@ "@types/vscode": "^1.99.0", "@typescript-eslint/eslint-plugin": "^8.31.1", "@typescript-eslint/parser": "^8.31.1", + "@vscode/test-electron": "^2.5.2", "@vscode/vsce": "^3.6.0", "esbuild": "^0.25.3", "eslint": "^9.25.1", diff --git a/package.json b/package.json index 0067054629b..69e4d4380bb 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,8 @@ "test:always_passing_evals": "vitest run --config evals/vitest.config.ts", "test:all_evals": "cross-env RUN_EVALS=1 vitest run --config evals/vitest.config.ts", "test:e2e": "cross-env VERBOSE=true KEEP_OUTPUT=true npm run test:integration:sandbox:none", - "test:integration:all": "npm run test:integration:sandbox:none && npm run test:integration:sandbox:docker && npm run test:integration:sandbox:podman", + "test:integration:vscode": "npm run test:integration --workspace gemini-cli-vscode-ide-companion", + "test:integration:all": "npm run test:integration:sandbox:none && npm run test:integration:sandbox:docker && npm run test:integration:sandbox:podman && npm run test:integration:vscode", "test:integration:sandbox:none": "cross-env GEMINI_SANDBOX=false vitest run --root ./integration-tests", "test:integration:sandbox:docker": "cross-env GEMINI_SANDBOX=docker npm run build:sandbox && cross-env GEMINI_SANDBOX=docker vitest run --root ./integration-tests", "test:integration:sandbox:podman": "cross-env GEMINI_SANDBOX=podman vitest run --root ./integration-tests", diff --git a/packages/vscode-ide-companion/esbuild.js b/packages/vscode-ide-companion/esbuild.js index 228eb27cdf2..0de421a5b3f 100644 --- a/packages/vscode-ide-companion/esbuild.js +++ b/packages/vscode-ide-companion/esbuild.js @@ -33,15 +33,26 @@ const esbuildProblemMatcherPlugin = { async function main() { const ctx = await esbuild.context({ - entryPoints: ['src/extension.ts'], + entryPoints: [ + { in: 'src/extension.ts', out: 'extension' }, + { + in: 'src/integration-tests/run-test.ts', + out: 'integration-tests/run-test', + }, + { + in: 'src/integration-tests/focus.test.ts', + out: 'integration-tests/focus.test', + }, + ], bundle: true, format: 'cjs', minify: production, sourcemap: !production, sourcesContent: false, platform: 'node', - outfile: 'dist/extension.cjs', - external: ['vscode'], + outdir: 'dist', + outExtension: { '.js': '.cjs' }, + external: ['vscode', 'vitest'], logLevel: 'silent', banner: { js: `const import_meta = { url: require('url').pathToFileURL(__filename).href };`, @@ -52,12 +63,10 @@ async function main() { alias: { punycode: 'punycode/', }, - plugins: [ - /* add to the end of plugins array */ - esbuildProblemMatcherPlugin, - ], + plugins: [esbuildProblemMatcherPlugin], loader: { '.node': 'file', '.wasm': 'binary' }, }); + if (watch) { await ctx.watch(); } else { diff --git a/packages/vscode-ide-companion/package.json b/packages/vscode-ide-companion/package.json index ea095429c6c..008c027d97d 100644 --- a/packages/vscode-ide-companion/package.json +++ b/packages/vscode-ide-companion/package.json @@ -120,6 +120,7 @@ "package": "vsce package --no-dependencies", "test": "vitest run", "test:ci": "vitest run --coverage", + "test:integration": "npm run build && node ./scripts/run-integration-tests.js", "validate:notices": "node ./scripts/validate-notices.js" }, "devDependencies": { @@ -129,6 +130,7 @@ "@types/vscode": "^1.99.0", "@typescript-eslint/eslint-plugin": "^8.31.1", "@typescript-eslint/parser": "^8.31.1", + "@vscode/test-electron": "^2.5.2", "@vscode/vsce": "^3.6.0", "esbuild": "^0.25.3", "eslint": "^9.25.1", diff --git a/packages/vscode-ide-companion/scripts/run-integration-tests.js b/packages/vscode-ide-companion/scripts/run-integration-tests.js new file mode 100644 index 00000000000..df97b02cee4 --- /dev/null +++ b/packages/vscode-ide-companion/scripts/run-integration-tests.js @@ -0,0 +1,40 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as path from 'node:path'; +import { runTests } from '@vscode/test-electron'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +async function main() { + try { + // The folder containing the Extension Manifest package.json + // Passed to `--extensionDevelopmentPath` + const extensionDevelopmentPath = path.resolve(__dirname, '../'); + + // The path to test runner + // Passed to --extensionTestsPath + // This script will run in the extension host process + const extensionTestsPath = path.resolve( + __dirname, + '../dist/integration-tests/run-test.cjs', + ); + + // Download VS Code, unzip it and run the integration test + await runTests({ + extensionDevelopmentPath, + extensionTestsPath, + launchArgs: ['--disable-extensions'], + }); + } catch (err) { + console.error('Tests failed', err); + process.exit(1); + } +} + +main(); diff --git a/packages/vscode-ide-companion/src/diff-manager.test.ts b/packages/vscode-ide-companion/src/diff-manager.test.ts new file mode 100644 index 00000000000..93fbd9bcab4 --- /dev/null +++ b/packages/vscode-ide-companion/src/diff-manager.test.ts @@ -0,0 +1,111 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import * as vscode from 'vscode'; +import { DiffManager, DiffContentProvider } from './diff-manager.js'; + +const { vscodeMock } = await vi.hoisted(() => import('./utils/vscode-mock.js')); + +vi.mock('vscode', () => ({ + ...vscodeMock, + workspace: { + ...vscodeMock.workspace, + openTextDocument: vi.fn(() => + Promise.resolve({ + getText: () => 'modified content', + }), + ), + fs: { + ...vscodeMock.workspace.fs, + stat: vi.fn().mockResolvedValue({}), + }, + }, +})); + +describe('DiffManager', () => { + let diffManager: DiffManager; + let diffContentProvider: DiffContentProvider; + let log: (message: string) => void; + + beforeEach(() => { + vi.clearAllMocks(); + log = vi.fn(); + diffContentProvider = new DiffContentProvider(); + diffManager = new DiffManager(log, diffContentProvider); + }); + + function setupMockTab(rightDocUri: vscode.Uri) { + const mockTab = { + input: new vscode.TabInputTextDiff( + vscode.Uri.file('/original/path'), + rightDocUri, + ), + }; + + ( + vscode.window.tabGroups as unknown as { + all: Array<{ tabs: Array<{ input: vscode.TabInputTextDiff }> }>; + } + ).all = [{ tabs: [mockTab] }]; + + return mockTab; + } + + function getRightDocUriFromDiffCall(): vscode.Uri { + const diffCall = vi + .mocked(vscode.commands.executeCommand) + .mock.calls.find((call) => call[0] === 'vscode.diff'); + if (!diffCall) throw new Error('vscode.diff was not called'); + return diffCall[2] as vscode.Uri; + } + + const testCases = [ + { + name: 'closing a diff', + action: (filePath: string) => diffManager.closeDiff(filePath), + }, + { + name: 'accepting a diff', + action: (_filePath: string, rightDocUri: vscode.Uri) => + diffManager.acceptDiff(rightDocUri), + }, + { + name: 'cancelling a diff', + action: (_filePath: string, rightDocUri: vscode.Uri) => + diffManager.cancelDiff(rightDocUri), + }, + ]; + + for (const { name, action } of testCases) { + it(`should call tabGroups.close with preserveFocus=true when ${name}`, async () => { + const filePath = '/test/file.ts'; + const newContent = 'new content'; + + await diffManager.showDiff(filePath, newContent); + + const rightDocUri = getRightDocUriFromDiffCall(); + const mockTab = setupMockTab(rightDocUri); + + await action(filePath, rightDocUri); + + expect(vscode.window.tabGroups.close).toHaveBeenCalledWith(mockTab, true); + }); + } + + it('should clean up internal state after closing a diff', async () => { + const filePath = '/test/file.ts'; + await diffManager.showDiff(filePath, 'content'); + const rightDocUri = getRightDocUriFromDiffCall(); + setupMockTab(rightDocUri); + + expect(diffContentProvider.getContent(rightDocUri)).toBe('content'); + + await diffManager.closeDiff(filePath); + + expect(diffContentProvider.getContent(rightDocUri)).toBeUndefined(); + }); +}); diff --git a/packages/vscode-ide-companion/src/diff-manager.ts b/packages/vscode-ide-companion/src/diff-manager.ts index 83cc97984a0..21bae46cd59 100644 --- a/packages/vscode-ide-companion/src/diff-manager.ts +++ b/packages/vscode-ide-companion/src/diff-manager.ts @@ -247,7 +247,7 @@ export class DiffManager { tab.input instanceof vscode.TabInputTextDiff && tab.input.modified.toString() === rightDocUri.toString() ) { - await vscode.window.tabGroups.close(tab); + await vscode.window.tabGroups.close(tab, true); return; } } diff --git a/packages/vscode-ide-companion/src/extension.test.ts b/packages/vscode-ide-companion/src/extension.test.ts index 49b1b76d432..a7cf3f27e5e 100644 --- a/packages/vscode-ide-companion/src/extension.test.ts +++ b/packages/vscode-ide-companion/src/extension.test.ts @@ -12,6 +12,8 @@ import { detectIdeFromEnv, } from '@google/gemini-cli-core/src/ide/detect-ide.js'; +const { vscodeMock } = await vi.hoisted(() => import('./utils/vscode-mock.js')); + vi.mock('@google/gemini-cli-core/src/ide/detect-ide.js', async () => { const actual = await vi.importActual( '@google/gemini-cli-core/src/ide/detect-ide.js', @@ -23,52 +25,12 @@ vi.mock('@google/gemini-cli-core/src/ide/detect-ide.js', async () => { }); vi.mock('vscode', () => ({ + ...vscodeMock, window: { + ...vscodeMock.window, createOutputChannel: vi.fn(() => ({ appendLine: vi.fn(), })), - showInformationMessage: vi.fn(), - createTerminal: vi.fn(() => ({ - show: vi.fn(), - sendText: vi.fn(), - })), - onDidChangeActiveTextEditor: vi.fn(), - activeTextEditor: undefined, - tabGroups: { - all: [], - close: vi.fn(), - }, - showTextDocument: vi.fn(), - showWorkspaceFolderPick: vi.fn(), - }, - workspace: { - workspaceFolders: [], - onDidCloseTextDocument: vi.fn(), - registerTextDocumentContentProvider: vi.fn(), - onDidChangeWorkspaceFolders: vi.fn(), - onDidGrantWorkspaceTrust: vi.fn(), - getConfiguration: vi.fn(() => ({ - get: vi.fn(), - })), - }, - commands: { - registerCommand: vi.fn(), - executeCommand: vi.fn(), - }, - Uri: { - joinPath: vi.fn(), - }, - ExtensionMode: { - Development: 1, - Production: 2, - }, - EventEmitter: vi.fn(() => ({ - event: vi.fn(), - fire: vi.fn(), - dispose: vi.fn(), - })), - extensions: { - getExtension: vi.fn(), }, })); diff --git a/packages/vscode-ide-companion/src/extension.ts b/packages/vscode-ide-companion/src/extension.ts index e8cef91c2b4..a8ec02916a2 100644 --- a/packages/vscode-ide-companion/src/extension.ts +++ b/packages/vscode-ide-companion/src/extension.ts @@ -134,6 +134,16 @@ export async function activate(context: vscode.ExtensionContext) { DIFF_SCHEME, diffContentProvider, ), + // diff.open is a testing-only command used for integration tests. Only + // load it in non-production contexts. + context.extensionMode === vscode.ExtensionMode.Production + ? { dispose: () => {} } + : vscode.commands.registerCommand( + 'gemini.diff.open', + async (args: { filePath: string; newContent: string }) => { + await diffManager.showDiff(args.filePath, args.newContent); + }, + ), (vscode.commands.registerCommand( 'gemini.diff.accept', (uri?: vscode.Uri) => { diff --git a/packages/vscode-ide-companion/src/ide-server.test.ts b/packages/vscode-ide-companion/src/ide-server.test.ts index b3d39bf8328..1cc170c7a08 100644 --- a/packages/vscode-ide-companion/src/ide-server.test.ts +++ b/packages/vscode-ide-companion/src/ide-server.test.ts @@ -13,6 +13,10 @@ import * as http from 'node:http'; import { IDEServer } from './ide-server.js'; import type { DiffManager } from './diff-manager.js'; +const { vscodeMock: baseVscodeMock } = await vi.hoisted( + () => import('./utils/vscode-mock.js'), +); + vi.mock('node:crypto', () => ({ randomUUID: vi.fn(() => 'test-auth-token'), })); @@ -48,7 +52,9 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { }); const vscodeMock = vi.hoisted(() => ({ + ...baseVscodeMock, workspace: { + ...baseVscodeMock.workspace, workspaceFolders: [ { uri: { diff --git a/packages/vscode-ide-companion/src/integration-tests/focus.test.ts b/packages/vscode-ide-companion/src/integration-tests/focus.test.ts new file mode 100644 index 00000000000..283d6728255 --- /dev/null +++ b/packages/vscode-ide-companion/src/integration-tests/focus.test.ts @@ -0,0 +1,137 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode'; +import * as assert from 'node:assert'; + +/* eslint-disable no-console */ + +/** + * Waits for a specific amount of time. + */ +async function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Checks if a terminal with the given name is active. + */ +function isTerminalActive(name: string): boolean { + return vscode.window.activeTerminal?.name === name; +} + +/** + * Waits for the diff view for the given file to open. + */ +async function waitForDiffVisible(filePath: string): Promise { + const isVisible = () => { + for (const tabGroup of vscode.window.tabGroups.all) { + for (const tab of tabGroup.tabs) { + if ( + tab.input instanceof vscode.TabInputTextDiff && + tab.input.modified.path.endsWith(filePath) + ) { + return true; + } + } + } + return false; + }; + + if (isVisible()) return; + + return new Promise((resolve) => { + const disposable = vscode.window.tabGroups.onDidChangeTabs(() => { + if (isVisible()) { + disposable.dispose(); + resolve(); + } + }); + }); +} + +export async function runTest() { + const TERMINAL_NAME = 'Gemini E2E Terminal'; + + console.log( + 'Running: should maintain focus in the terminal after accepting a diff', + ); + + // 1. Ensure the extension is activated + const extension = vscode.extensions.getExtension( + 'google.gemini-cli-vscode-ide-companion', + ); + if (!extension) { + throw new Error('Extension not found'); + } + await extension.activate(); + + // 2. Open a dummy file and EXPLICITLY FOCUS it. + const dummyUri = vscode.Uri.parse('untitled:dummy.ts'); + const dummyDoc = await vscode.workspace.openTextDocument(dummyUri); + await vscode.window.showTextDocument(dummyDoc, { + viewColumn: vscode.ViewColumn.One, + preview: false, + }); + await sleep(1000); + console.log( + `Dummy editor opened and focused. Active editor: ${vscode.window.activeTextEditor?.document.uri.toString()}`, + ); + + // 3. Create and show a terminal (focusing it) + const terminal = vscode.window.createTerminal(TERMINAL_NAME); + terminal.show(false); // false means 'take focus' + + // Wait for terminal to stabilize and take focus + await sleep(2000); + assert.strictEqual( + isTerminalActive(TERMINAL_NAME), + true, + 'Terminal should be active/focused', + ); + console.log('Terminal focused.'); + + // 4. Open a diff + const testFile = 'e2e-test-file.ts'; + await vscode.commands.executeCommand('gemini.diff.open', { + filePath: testFile, + newContent: '// Modified in E2E test', + }); + + // 5. Wait for the diff tab to appear + await waitForDiffVisible(testFile); + console.log('Diff is visible.'); + + // 6. Accept the diff (triggering the close logic) + console.log('Accepting diff...'); + await vscode.commands.executeCommand('gemini.diff.accept'); + + // 7. Verify focus: Attempt to "type" into the editor area. + await sleep(3000); + + console.log('Attempting to type "FOCUS_TEST" to probe focus...'); + await vscode.commands.executeCommand('default:type', { text: 'FOCUS_TEST' }); + + // Wait for potential document change + await sleep(1000); + + const content = dummyDoc.getText(); + console.log(`Dummy document content after probe: "${content}"`); + + if (content.includes('FOCUS_TEST')) { + throw new Error( + 'Terminal LOST FOCUS! Keyboard input reached the dummy editor.', + ); + } + + console.log('SUCCESS: Terminal maintained focus.'); + + // Cleanup + terminal.dispose(); +} + +/* eslint-disable import/no-default-export */ +export default runTest; diff --git a/packages/vscode-ide-companion/src/integration-tests/run-test.ts b/packages/vscode-ide-companion/src/integration-tests/run-test.ts new file mode 100644 index 00000000000..c41cad823fc --- /dev/null +++ b/packages/vscode-ide-companion/src/integration-tests/run-test.ts @@ -0,0 +1,77 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable no-console */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-type-assertion */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ + +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createRequire, Module } from 'node:module'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const require = createRequire(import.meta.url); + +export async function run(): Promise { + // eslint-disable-next-line no-restricted-syntax + const vscode = require('vscode'); + + // Hack: Intercept require('vscode') globally in this process + // This ensures that tests can just 'import * as vscode from "vscode"' + const originalLoad = (Module as any)._load; + (Module as any)._load = function ( + this: any, + request: string, + ...args: any[] + ) { + if (request === 'vscode') { + return vscode; + } + return originalLoad.apply(this, [request, ...args]); + }; + + // Also inject into global for good measure + (globalThis as any).vscode = vscode; + + console.log('Starting Generic E2E Runner...'); + + const tests = [{ name: 'Focus Management', file: './focus.test.cjs' }]; + + for (const test of tests) { + console.log(`\nRunning Test Suite: ${test.name}`); + try { + const testModulePath = path.resolve(__dirname, test.file); + console.log(`Loading test module from: ${testModulePath}`); + // eslint-disable-next-line no-restricted-syntax + const testModule = require(testModulePath); + + const runFn = + testModule.default || + testModule.runTest || + (typeof testModule === 'function' ? testModule : null); + + if (typeof runFn === 'function') { + console.log(`Executing run function for ${test.name}...`); + await runFn(); + } else { + console.error( + `Could not find a valid run function in ${test.file}. Module keys: ${Object.keys(testModule)}`, + ); + throw new Error(`Invalid test module: ${test.file}`); + } + + console.log(`Test Suite ${test.name} completed successfully.`); + } catch (err) { + console.error(`Test Suite ${test.name} FAILED:`, err); + throw err; + } + } + + console.log('\nAll E2E tests passed!'); +} diff --git a/packages/vscode-ide-companion/src/open-files-manager.test.ts b/packages/vscode-ide-companion/src/open-files-manager.test.ts index b6dc46f6a4b..ff4b172d05f 100644 --- a/packages/vscode-ide-companion/src/open-files-manager.test.ts +++ b/packages/vscode-ide-companion/src/open-files-manager.test.ts @@ -8,86 +8,56 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import * as vscode from 'vscode'; import { OpenFilesManager, MAX_FILES } from './open-files-manager.js'; +const { vscodeMock: baseVscodeMock, createMockTextEditor } = await vi.hoisted( + () => import('./utils/vscode-mock.js'), +); + +let onDidChangeActiveTextEditorListener: ( + editor: vscode.TextEditor | undefined, +) => void; +let onDidChangeTextEditorSelectionListener: ( + e: vscode.TextEditorSelectionChangeEvent, +) => void; +let onDidDeleteFilesListener: (e: vscode.FileDeleteEvent) => void; +let onDidCloseTextDocumentListener: (doc: vscode.TextDocument) => void; +let onDidRenameFilesListener: (e: vscode.FileRenameEvent) => void; + vi.mock('vscode', () => ({ - EventEmitter: vi.fn(() => { - const listeners: Array<(e: void) => unknown> = []; - return { - event: vi.fn((listener) => { - listeners.push(listener); - return { dispose: vi.fn() }; - }), - fire: vi.fn(() => { - listeners.forEach((listener) => listener(undefined)); - }), - dispose: vi.fn(), - }; - }), + ...baseVscodeMock, window: { - onDidChangeActiveTextEditor: vi.fn(), - onDidChangeTextEditorSelection: vi.fn(), + ...baseVscodeMock.window, + onDidChangeActiveTextEditor: vi.fn((listener) => { + onDidChangeActiveTextEditorListener = listener; + return { dispose: vi.fn() }; + }), + onDidChangeTextEditorSelection: vi.fn((listener) => { + onDidChangeTextEditorSelectionListener = listener; + return { dispose: vi.fn() }; + }), }, workspace: { - onDidDeleteFiles: vi.fn(), - onDidCloseTextDocument: vi.fn(), - onDidRenameFiles: vi.fn(), - }, - Uri: { - file: (path: string) => ({ - fsPath: path, - scheme: 'file', + ...baseVscodeMock.workspace, + onDidDeleteFiles: vi.fn((listener) => { + onDidDeleteFilesListener = listener; + return { dispose: vi.fn() }; + }), + onDidCloseTextDocument: vi.fn((listener) => { + onDidCloseTextDocumentListener = listener; + return { dispose: vi.fn() }; + }), + onDidRenameFiles: vi.fn((listener) => { + onDidRenameFilesListener = listener; + return { dispose: vi.fn() }; }), - }, - TextEditorSelectionChangeKind: { - Mouse: 2, }, })); describe('OpenFilesManager', () => { let context: vscode.ExtensionContext; - let onDidChangeActiveTextEditorListener: ( - editor: vscode.TextEditor | undefined, - ) => void; - let onDidChangeTextEditorSelectionListener: ( - e: vscode.TextEditorSelectionChangeEvent, - ) => void; - let onDidDeleteFilesListener: (e: vscode.FileDeleteEvent) => void; - let onDidCloseTextDocumentListener: (doc: vscode.TextDocument) => void; - let onDidRenameFilesListener: (e: vscode.FileRenameEvent) => void; beforeEach(() => { vi.useFakeTimers(); - vi.mocked(vscode.window.onDidChangeActiveTextEditor).mockImplementation( - (listener) => { - onDidChangeActiveTextEditorListener = listener; - return { dispose: vi.fn() }; - }, - ); - vi.mocked(vscode.window.onDidChangeTextEditorSelection).mockImplementation( - (listener) => { - onDidChangeTextEditorSelectionListener = listener; - return { dispose: vi.fn() }; - }, - ); - vi.mocked(vscode.workspace.onDidDeleteFiles).mockImplementation( - (listener) => { - onDidDeleteFilesListener = listener; - return { dispose: vi.fn() }; - }, - ); - vi.mocked(vscode.workspace.onDidCloseTextDocument).mockImplementation( - (listener) => { - onDidCloseTextDocumentListener = listener; - return { dispose: vi.fn() }; - }, - ); - vi.mocked(vscode.workspace.onDidRenameFiles).mockImplementation( - (listener) => { - onDidRenameFilesListener = listener; - return { dispose: vi.fn() }; - }, - ); - context = { subscriptions: [], } as unknown as vscode.ExtensionContext; @@ -98,19 +68,10 @@ describe('OpenFilesManager', () => { vi.useRealTimers(); }); - const getUri = (path: string) => - vscode.Uri.file(path) as unknown as vscode.Uri; + const getUri = (path: string) => vscode.Uri.file(path); const addFile = (uri: vscode.Uri) => { - onDidChangeActiveTextEditorListener({ - document: { - uri, - getText: () => '', - }, - selection: { - active: { line: 0, character: 0 }, - }, - } as unknown as vscode.TextEditor); + onDidChangeActiveTextEditorListener(createMockTextEditor(uri)); }; it('adds a file to the list', async () => { @@ -203,7 +164,7 @@ describe('OpenFilesManager', () => { await vi.advanceTimersByTimeAsync(100); expect(manager.state.workspaceState!.openFiles).toHaveLength(2); - onDidDeleteFilesListener({ files: [uri1] }); + onDidDeleteFilesListener({ files: [uri1] } as vscode.FileDeleteEvent); await vi.advanceTimersByTimeAsync(100); expect(manager.state.workspaceState!.openFiles).toHaveLength(1); @@ -221,7 +182,7 @@ describe('OpenFilesManager', () => { const onDidChangeSpy = vi.fn(); manager.onDidChange(onDidChangeSpy); - onDidDeleteFilesListener({ files: [uri] }); + onDidDeleteFilesListener({ files: [uri] } as vscode.FileDeleteEvent); await vi.advanceTimersByTimeAsync(100); expect(onDidChangeSpy).toHaveBeenCalled(); @@ -238,7 +199,7 @@ describe('OpenFilesManager', () => { await vi.advanceTimersByTimeAsync(100); expect(manager.state.workspaceState!.openFiles).toHaveLength(3); - onDidDeleteFilesListener({ files: [uri1, uri3] }); + onDidDeleteFilesListener({ files: [uri1, uri3] } as vscode.FileDeleteEvent); await vi.advanceTimersByTimeAsync(100); expect(manager.state.workspaceState!.openFiles).toHaveLength(1); @@ -272,7 +233,9 @@ describe('OpenFilesManager', () => { '/test/file1.txt', ); - onDidRenameFilesListener({ files: [{ oldUri, newUri }] }); + onDidRenameFilesListener({ + files: [{ oldUri, newUri }], + } as vscode.FileRenameEvent); await vi.advanceTimersByTimeAsync(100); expect(manager.state.workspaceState!.openFiles).toHaveLength(1); @@ -304,11 +267,11 @@ describe('OpenFilesManager', () => { active: { line: 10, character: 20 }, } as vscode.Selection; + const editor = createMockTextEditor(uri) as vscode.TextEditor; + editor.selection = selection; + onDidChangeTextEditorSelectionListener({ - textEditor: { - document: { uri, getText: () => '' }, - selection, - } as vscode.TextEditor, + textEditor: editor, selections: [selection], kind: vscode.TextEditorSelectionChangeKind.Mouse, }); @@ -326,20 +289,17 @@ describe('OpenFilesManager', () => { active: { line: 10, character: 20 }, } as vscode.Selection; - // We need to override the mock for getText for this test - const textEditor = { - document: { - uri, - getText: vi.fn().mockReturnValue('selected text'), - }, - selection, - } as unknown as vscode.TextEditor; + const editor = createMockTextEditor( + uri, + 'selected text', + ) as vscode.TextEditor; + editor.selection = selection; - onDidChangeActiveTextEditorListener(textEditor); + addFile(uri); await vi.advanceTimersByTimeAsync(100); onDidChangeTextEditorSelectionListener({ - textEditor, + textEditor: editor, selections: [selection], kind: vscode.TextEditorSelectionChangeKind.Mouse, }); @@ -348,7 +308,7 @@ describe('OpenFilesManager', () => { const file = manager.state.workspaceState!.openFiles![0]; expect(file.selectedText).toBe('selected text'); - expect(textEditor.document.getText).toHaveBeenCalledWith(selection); + expect(editor.document.getText).toHaveBeenCalledWith(selection); }); it('truncates long selected text', async () => { @@ -361,19 +321,14 @@ describe('OpenFilesManager', () => { active: { line: 10, character: 20 }, } as vscode.Selection; - const textEditor = { - document: { - uri, - getText: vi.fn().mockReturnValue(longText), - }, - selection, - } as unknown as vscode.TextEditor; + const editor = createMockTextEditor(uri, longText) as vscode.TextEditor; + editor.selection = selection; - onDidChangeActiveTextEditorListener(textEditor); + addFile(uri); await vi.advanceTimersByTimeAsync(100); onDidChangeTextEditorSelectionListener({ - textEditor, + textEditor: editor, selections: [selection], kind: vscode.TextEditorSelectionChangeKind.Mouse, }); @@ -396,11 +351,11 @@ describe('OpenFilesManager', () => { active: { line: 10, character: 20 }, } as vscode.Selection; + const editor = createMockTextEditor(uri1) as vscode.TextEditor; + editor.selection = selection; + onDidChangeTextEditorSelectionListener({ - textEditor: { - document: { uri: uri1, getText: () => '' }, - selection, - } as vscode.TextEditor, + textEditor: editor, selections: [selection], kind: vscode.TextEditorSelectionChangeKind.Mouse, }); diff --git a/packages/vscode-ide-companion/src/utils/vscode-mock.ts b/packages/vscode-ide-companion/src/utils/vscode-mock.ts new file mode 100644 index 00000000000..167d7d960f4 --- /dev/null +++ b/packages/vscode-ide-companion/src/utils/vscode-mock.ts @@ -0,0 +1,172 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { vi } from 'vitest'; +import type * as vscode from 'vscode'; + +/* eslint-disable @typescript-eslint/no-unsafe-type-assertion */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/** + * A mock implementation of vscode.Uri. + */ +export class MockUri { + constructor( + public scheme: string, + public path: string, + public query: string = '', + ) {} + + toString() { + const q = this.query ? `?${this.query}` : ''; + return `${this.scheme}://${this.path}${q}`; + } + + get fsPath() { + return this.path; + } +} + +/** + * Creates a mock EventEmitter. + */ +export const createEventEmitter = () => { + const listeners = new Set<(e: unknown) => unknown>(); + return { + event: vi.fn((listener: (e: unknown) => unknown) => { + listeners.add(listener); + return { dispose: () => listeners.delete(listener) }; + }), + fire: vi.fn((data: unknown) => { + listeners.forEach((l) => l(data)); + }), + dispose: vi.fn(), + }; +}; + +/** + * The base mock object for the vscode module. + * Properties that are frequently overridden in tests are typed as 'any' + * to allow simple object assignment without verbose casting. + */ +export const vscodeMock = { + window: { + onDidChangeActiveTextEditor: vi.fn(() => ({ dispose: vi.fn() })), + onDidChangeTextEditorSelection: vi.fn(() => ({ dispose: vi.fn() })), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + activeTextEditor: undefined as any, + tabGroups: { + all: [] as any[], + close: vi.fn(), + }, + createOutputChannel: vi.fn(() => ({ + appendLine: vi.fn(), + show: vi.fn(), + dispose: vi.fn(), + })), + showInformationMessage: vi.fn(), + showErrorMessage: vi.fn(), + showTextDocument: vi.fn(), + showWorkspaceFolderPick: vi.fn(), + createTerminal: vi.fn(() => ({ + show: vi.fn(), + sendText: vi.fn(), + })), + }, + workspace: { + workspaceFolders: [] as any[], + isTrusted: true, + onDidCloseTextDocument: vi.fn(() => ({ dispose: vi.fn() })), + onDidDeleteFiles: vi.fn(() => ({ dispose: vi.fn() })), + onDidRenameFiles: vi.fn(() => ({ dispose: vi.fn() })), + registerTextDocumentContentProvider: vi.fn(), + onDidChangeWorkspaceFolders: vi.fn(() => ({ dispose: vi.fn() })), + onDidGrantWorkspaceTrust: vi.fn(() => ({ dispose: vi.fn() })), + getConfiguration: vi.fn(() => ({ + get: vi.fn(), + update: vi.fn(), + })), + openTextDocument: vi.fn(), + fs: { + stat: vi.fn(), + }, + }, + commands: { + executeCommand: vi.fn(), + registerCommand: vi.fn(), + }, + Uri: { + file: vi.fn( + (path: string) => new MockUri('file', path) as unknown as vscode.Uri, + ), + from: vi.fn( + (args: { scheme: string; path: string; query?: string }) => + new MockUri( + args.scheme, + args.path, + args.query || '', + ) as unknown as vscode.Uri, + ), + parse: vi.fn((uri: string) => { + const [scheme, rest] = uri.split('://'); + const [path, query] = (rest || '').split('?'); + return new MockUri(scheme, path, query || '') as unknown as vscode.Uri; + }), + joinPath: vi.fn( + (uri: vscode.Uri, ...parts: string[]) => + new MockUri( + uri.scheme, + [uri.fsPath, ...parts].join('/'), + (uri as any).query || '', + ) as unknown as vscode.Uri, + ), + }, + EventEmitter: vi.fn( + () => createEventEmitter() as unknown as vscode.EventEmitter, + ), + TabInputTextDiff: class { + constructor( + public original: vscode.Uri | null, + public modified: vscode.Uri, + ) {} + }, + ExtensionMode: { + Development: 1, + Production: 2, + }, + TextEditorSelectionChangeKind: { + Mouse: 2, + }, + extensions: { + getExtension: vi.fn(), + }, +}; + +/** + * Helper to create a mock TextDocument. + */ +export const createMockTextDocument = ( + uri: vscode.Uri, + content: string = '', +): any => ({ + uri, + getText: vi.fn().mockReturnValue(content), + fileName: uri.fsPath, +}); + +/** + * Helper to create a mock TextEditor. + */ +export const createMockTextEditor = ( + uri: vscode.Uri, + content: string = '', +): any => ({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + document: createMockTextDocument(uri, content), + selection: { + active: { line: 0, character: 0 }, + }, +}); diff --git a/packages/vscode-ide-companion/tsconfig.json b/packages/vscode-ide-companion/tsconfig.json index f1357064852..52d91a02fd8 100644 --- a/packages/vscode-ide-companion/tsconfig.json +++ b/packages/vscode-ide-companion/tsconfig.json @@ -12,11 +12,13 @@ * type checking error in ../../node_modules/@types/request/index.d.ts. */ "skipLibCheck": true, - "rootDir": "src", + "rootDir": ".", "strict": true /* enable all strict type-checking options */ /* Additional Checks */ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */ - } + }, + "include": ["src", "vitest.config.ts"], + "exclude": ["dist", "node_modules", "src/integration-tests"] } diff --git a/packages/vscode-ide-companion/vitest.config.ts b/packages/vscode-ide-companion/vitest.config.ts new file mode 100644 index 00000000000..a2c89e86e84 --- /dev/null +++ b/packages/vscode-ide-companion/vitest.config.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + exclude: [ + '**/node_modules/**', + '**/dist/**', + '**/src/integration-tests/**', + ], + }, +});