From 5b4408e3a8ecb566b1d8065667588fbc62b6442c Mon Sep 17 00:00:00 2001 From: Francisco Alejandro Garcia Cebada Date: Mon, 12 Oct 2026 19:22:50 -0700 Subject: [PATCH 1/3] dev: Adding unit test coverage and continuos integration pipeline. --- .github/workflows/ci.yml | 40 + FASTBuildMonitorVSCode/.gitignore | 2 +- FASTBuildMonitorVSCode/.vscodeignore | 7 +- FASTBuildMonitorVSCode/CONTRIBUTING.md | 56 +- FASTBuildMonitorVSCode/package-lock.json | 1811 +++++++++++++++-- FASTBuildMonitorVSCode/package.json | 8 + .../extension/buildMonitorService.test.ts | 143 ++ .../tests/extension/buildWatcher.test.ts | 109 + .../tests/extension/extension.test.ts | 192 ++ .../tests/extension/logWatcher.test.ts | 145 ++ .../tests/extension/models.test.ts | 59 + .../extension/monitorPanelProvider.test.ts | 182 ++ .../tests/extension/utilities.test.ts | 132 ++ FASTBuildMonitorVSCode/tests/mocks/vscode.ts | 120 ++ FASTBuildMonitorVSCode/tests/setup.ts | 12 + FASTBuildMonitorVSCode/tsconfig.test.json | 20 + FASTBuildMonitorVSCode/vitest.config.mts | 52 + .../webview-ui/package-lock.json | 245 +++ .../webview-ui/package.json | 2 + .../webview-ui/tests/app.test.tsx | 99 + .../webview-ui/tests/components.test.tsx | 104 + .../webview-ui/tests/fixtures.ts | 22 + .../webview-ui/tests/index.test.tsx | 32 + .../webview-ui/tests/progressBar.test.tsx | 72 + .../webview-ui/tests/setup.ts | 72 + .../webview-ui/tests/timeline.test.tsx | 172 ++ .../webview-ui/tests/types.test.ts | 58 + 27 files changed, 3836 insertions(+), 132 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 FASTBuildMonitorVSCode/tests/extension/buildMonitorService.test.ts create mode 100644 FASTBuildMonitorVSCode/tests/extension/buildWatcher.test.ts create mode 100644 FASTBuildMonitorVSCode/tests/extension/extension.test.ts create mode 100644 FASTBuildMonitorVSCode/tests/extension/logWatcher.test.ts create mode 100644 FASTBuildMonitorVSCode/tests/extension/models.test.ts create mode 100644 FASTBuildMonitorVSCode/tests/extension/monitorPanelProvider.test.ts create mode 100644 FASTBuildMonitorVSCode/tests/extension/utilities.test.ts create mode 100644 FASTBuildMonitorVSCode/tests/mocks/vscode.ts create mode 100644 FASTBuildMonitorVSCode/tests/setup.ts create mode 100644 FASTBuildMonitorVSCode/tsconfig.test.json create mode 100644 FASTBuildMonitorVSCode/vitest.config.mts create mode 100644 FASTBuildMonitorVSCode/webview-ui/tests/app.test.tsx create mode 100644 FASTBuildMonitorVSCode/webview-ui/tests/components.test.tsx create mode 100644 FASTBuildMonitorVSCode/webview-ui/tests/fixtures.ts create mode 100644 FASTBuildMonitorVSCode/webview-ui/tests/index.test.tsx create mode 100644 FASTBuildMonitorVSCode/webview-ui/tests/progressBar.test.tsx create mode 100644 FASTBuildMonitorVSCode/webview-ui/tests/setup.ts create mode 100644 FASTBuildMonitorVSCode/webview-ui/tests/timeline.test.tsx create mode 100644 FASTBuildMonitorVSCode/webview-ui/tests/types.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..810dc46 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: CI + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + +concurrency: + group: ci-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + build_and_test: + name: Build and Test + runs-on: ubuntu-latest + defaults: + run: + working-directory: FASTBuildMonitorVSCode + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 24 + cache: npm + cache-dependency-path: | + FASTBuildMonitorVSCode/package-lock.json + FASTBuildMonitorVSCode/webview-ui/package-lock.json + - run: npm run ci:all + - run: npm run build:all + - run: npm run test:typecheck + - run: npm run test:coverage + - name: Upload coverage report + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage + path: FASTBuildMonitorVSCode/coverage + if-no-files-found: ignore diff --git a/FASTBuildMonitorVSCode/.gitignore b/FASTBuildMonitorVSCode/.gitignore index e6890d5..0149683 100644 --- a/FASTBuildMonitorVSCode/.gitignore +++ b/FASTBuildMonitorVSCode/.gitignore @@ -3,4 +3,4 @@ node_modules/ .vscode-test/ *.vsix artifacts/ - +coverage/ diff --git a/FASTBuildMonitorVSCode/.vscodeignore b/FASTBuildMonitorVSCode/.vscodeignore index c179e3a..eb9c5a5 100644 --- a/FASTBuildMonitorVSCode/.vscodeignore +++ b/FASTBuildMonitorVSCode/.vscodeignore @@ -4,6 +4,8 @@ artifacts/** scripts/** +tests/** +coverage/** src/** **/.gitignore .yarnrc @@ -14,10 +16,13 @@ src/** **/*.ts webview-ui/src/** +webview-ui/tests/** webview-ui/node_modules/** node_modules/** # Config files esbuild.js -*.config.js \ No newline at end of file +*.config.js +vitest.config.mts +tsconfig.test.json \ No newline at end of file diff --git a/FASTBuildMonitorVSCode/CONTRIBUTING.md b/FASTBuildMonitorVSCode/CONTRIBUTING.md index 3f164f8..cae41bb 100644 --- a/FASTBuildMonitorVSCode/CONTRIBUTING.md +++ b/FASTBuildMonitorVSCode/CONTRIBUTING.md @@ -1,3 +1,15 @@ +## Development and tests + +From `FASTBuildMonitorVSCode`, using Node.js 24: + +```bash +npm run ci:all +npm test +npm run test:coverage +``` + +Vitest covers the extension core and React web UI. Pull requests automatically build both parts, type-check tests, and validate coverage. + # Release Process This repository uses [semantic-release](https://github.com/semantic-release/semantic-release) for it's releases. That means that any commit that goes into `mojang/main` will potentially trigger a release and contribute to the changelog. @@ -15,17 +27,53 @@ Some examples of this are: # Local Development -Initialize +Use Node.js 24. Run the following commands from `FASTBuildMonitorVSCode`. + +Install the extension and webview dependencies from the committed lockfiles: ```bash -npm install:all +npm run ci:all ``` -Build +When changing dependencies, use `npm run install:all` and commit both updated lockfiles. + +Build: ```bash npm run build:all ``` -To test: press F5 in VS Code to launch the Extension Development Host. +## Automated tests + +The project uses [Vitest](https://vitest.dev/) with two projects: + +- `extension`: Node-based tests in `tests/extension` covering log polling and parsing, build sessions and workers, configuration and utilities, activation, commands, status updates, and webview messaging. +- `webview`: React Testing Library tests in `webview-ui/tests`, running in jsdom, covering snapshot updates, controls, statistics, job lists, themes, progress contrast, and timeline rendering, core allocation, scrolling, zooming, and tooltips. + +Run both suites once: + +```bash +npm test +``` + +Run a single project or use watch mode: + +```bash +npm test -- --project extension +npm test -- --project webview +npm test -- --watch +``` + +Type-check the tests and collect coverage: + +```bash +npm run test:typecheck +npm run test:coverage +``` + +Coverage includes all extension and webview TypeScript sources, including files not imported by a test. Declaration files are excluded. Every source file must meet at least **80% statements, lines, and functions**, and **75% branches**. Reports are written to `coverage`; open `coverage/index.html` for the detailed report. Add behavior-focused regression tests alongside changes rather than excluding uncovered production code. + +The extension tests mock the VS Code API and file polling boundaries, and use fake timers instead of real polling delays. Webview tests mock the VS Code bridge, canvas operations/pixel samples, and resize observers. They do not require VS Code, a running FASTBuild process, or a browser. These tests do not replace visual checks of actual canvas rendering: press F5 in VS Code to launch the Extension Development Host for manual validation. + +The [CI workflow](../.github/workflows/ci.yml) runs on pull requests when opened, updated, or reopened. It installs locked dependencies, builds both parts, type-checks tests, enforces coverage thresholds, and uploads the coverage report. It uses read-only repository permissions and does not require release credentials. To enforce it before merging, configure the **Build and Test** check as required in branch protection. # Architecture diff --git a/FASTBuildMonitorVSCode/package-lock.json b/FASTBuildMonitorVSCode/package-lock.json index 288feb5..4d6047b 100644 --- a/FASTBuildMonitorVSCode/package-lock.json +++ b/FASTBuildMonitorVSCode/package-lock.json @@ -25,18 +25,36 @@ "@types/node": "^24.0.0", "@types/semver": "^7.7.1", "@types/vscode": "^1.85.0", + "@vitest/coverage-v8": "~4.0.18", "@vscode/vsce": "^3.2.1", "esbuild": "^0.25.11", "esbuild-plugin-copy": "^2.1.1", + "jsdom": "^26.1.0", "semantic-release": "^24.1.1", "semantic-release-vsce": "^6.1.3", - "typescript": "^5.3.0" + "typescript": "^5.3.0", + "vite": "^6.4.3", + "vitest": "~4.0.18" }, "engines": { "node": "^24.0.0", "vscode": "^1.85.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha1-zEL1uFxZP3nx+k8l0rmzIeYdF5Q=", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, "node_modules/@azu/format-text": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz", @@ -237,15 +255,65 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha1-fwhx2Zgk0jE31g+G/PYTD9WhtR8=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha1-vYcITO0MeW7Ea9pJLeboPSnon8I=", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha1-Einu8x2FFW1w+j9M2Fk3bQ6vaGM=", + "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha1-u+EtyltO+YOg0K9LB7m8kOoKuro=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -257,6 +325,121 @@ "node": ">=0.1.90" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha1-EGxUyAjKv9GrTGAthQXuWEwplu8=", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha1-hHP2Pi/NbkWYON1BJAHVlI8iTGU=", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha1-Tjhq86md02xG/vATz+TBw0Hu1vA=", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha1-V1U3Cpopq67FUVtDyLPyz5wuMHY=", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha1-Mz/tq8P9Go5dAQABNzHPGeaoxdM=", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -928,6 +1111,51 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha1-eg7mAfYPmaIMfHxf8MgDiMEYm9Y=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha1-9MZj6GLwbcmMpNRThixGkCeJoY0=", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha1-2xXWeByTHzolGj2sOVAcmKYIL9A=", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha1-5X1DBpZgeGYgOAlPs465FG3Drqk=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -1466,93 +1694,443 @@ "node": ">=12" } }, - "node_modules/@sec-ant/readable-stream": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", - "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@secretlint/config-creator": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", - "integrity": "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.0.tgz", + "integrity": "sha512-FLLpX941CD/7aBvX3GqLGBM9OvY5PGXTryPJkG5r5/OuBpZpV5k45Uo3RxAvqd35tsyc3hH1bhtz921IKzoIzg==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@secretlint/types": "^10.2.2" - }, - "engines": { - "node": ">=20.0.0" - } + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@secretlint/config-loader": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-10.2.2.tgz", - "integrity": "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==", + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.0.tgz", + "integrity": "sha512-iI32XM/jnDYJsvx5ai/za/7gWO9XoSeFFtU06/ZCaRgyilfJOjaopD3nOpk2WrTCGOyeDt4hleUiGOAnd5tHNg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@secretlint/profiler": "^10.2.2", - "@secretlint/resolver": "^10.2.2", - "@secretlint/types": "^10.2.2", - "ajv": "^8.17.1", - "debug": "^4.4.1", - "rc-config-loader": "^4.1.3" - }, - "engines": { - "node": ">=20.0.0" - } + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@secretlint/config-loader/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.0.tgz", + "integrity": "sha512-dRBZlbt2sEtF8Mn4MODThze9fIPdDoOhVSlXMAKy5dQbOYK9pOOoPY4Fat3OTtr0+8zbJo5TTZwlsfxY/kDNJg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@secretlint/config-loader/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.0.tgz", + "integrity": "sha512-Y3whrJOMucfkhgNkvcM93M6j8C53iEAMmPMYD99C6G1kUy4ELigLWk9ar2/4aCBSRgsql0zdlOemLgW2m5GZgQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@secretlint/core": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-10.2.2.tgz", - "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==", + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.0.tgz", + "integrity": "sha512-S6S1GEzPjFF6IONYJX3AKt/799+8RgBgCcwRnUo+Ep1IvBxUbw5M51iU0TDdK2NWWVnSvCQ8yi6Em0fGASMOng==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@secretlint/profiler": "^10.2.2", - "@secretlint/types": "^10.2.2", - "debug": "^4.4.1", - "structured-source": "^4.0.0" - }, - "engines": { - "node": ">=20.0.0" - } + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@secretlint/formatter": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.2.2.tgz", - "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==", + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.0.tgz", + "integrity": "sha512-Ed1DPoFENkXj4IGzSB12SesV40iP6LxCCrbTIkSGIQBpOOxpBM+0kj62lmaeIHfMWTDoddqTOtqc7s8LYfn+Pw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@secretlint/resolver": "^10.2.2", - "@secretlint/types": "^10.2.2", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.0.tgz", + "integrity": "sha512-J/6xhutJPtFyIkmHdf4olOdHHKH5kOJNumojdWn+Elv5ICEYTVOa362Vw27TxsK0BLzb1le3EDlO3UbvFK1lnQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.0.tgz", + "integrity": "sha512-G4L/Nnx59d2LvDoV/P4S5lZ7oVLT6hodOHMacqjiq2LL2gts86KbkEBTYL53+fBt0odhf7XPIbIATGoYiJctxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.0.tgz", + "integrity": "sha512-zF16pvJDEUksGYRbfnXne7pdklAyj2XDmJeYKgP93oq0cUyX9Y8O7lUo1R+IY1GagP/YBvCEDCe7eEcAKpw//g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.0.tgz", + "integrity": "sha512-nIggWn8n+w12+R5lsV+PYYoUY8vuObxPAQqJI7WES+9v0qspYuAHoZMfYM0yLxlipasT5BY9ht5W8t5c/rSCkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.0.tgz", + "integrity": "sha512-WLOTiHPzszP0dxIMsHpFeDSOSB5I49Mj8K4+yKE3H1X/D0JRG5XZ5jrctJLMjnUc/LUzaKiwq6EHWNJ8pup8PA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.0.tgz", + "integrity": "sha512-35RK9aRhh7g7Pmeu5gQ01aCvrhXzj9LoBE6xtUcEH1ns+nyTaSzxSpADmBQINu4QiTDGvmQYwo1j5hlg1rRXFw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.0.tgz", + "integrity": "sha512-RqTfnpoTjuWfPAmqK5p2edt2AMCDJWm6oTn7sKfmyx+6tRga0ip05SJ977o6udTcIiDeF/CJMUfkaN4gtWt6Aw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.0.tgz", + "integrity": "sha512-RQjZPigjxFvFZDijG6rjlSXxhRaTf9zLYVpGK+lVcga64oMqP1GbgVqCb46eHTTceSIgp89Ja0y5aPe3GFBYHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.0.tgz", + "integrity": "sha512-tjL58mgQcTRnDRpeky+Sth1MKQeYi2Ct6r3Luq9E5aTSjnM/i/JbozqZkKqp9thM5+oDSZxo+05f9kdQedfTGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.0.tgz", + "integrity": "sha512-ictoRyVaoZ8C67myHef+8hhCa6IplXJJ/xUCkVCxziV4zxMZh+jvhkhGsPBLqj1S72fnqD0QH5zL4lcaa4X7DQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.0.tgz", + "integrity": "sha512-8aVvZ2hfr9RLFzBQkQNmYeHLDja8fLeQAmLt757zdKN/B+8L809KlLJKctQjS22wZotptV8G6o7fVPuAs8S1tQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.0.tgz", + "integrity": "sha512-2dEF8GAcDKwshUfydD+GhosppNyBzhVUHkdFF3CXd3FpOkzj/RZzF5aoEtzklujklT5qFVRU2GK/cFeLaTNKVg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.0.tgz", + "integrity": "sha512-3XHTvoo2hKkh0MiOfS7SkhHjMaaWF7P4F3drocZZaho/Z8w7h/jpnuBHm91BOQLfvrKHs1yR+zir4E8RI3XzTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.0.tgz", + "integrity": "sha512-HKtkzodL4a3KFWDXDoO61cRynAWTR3LnAE0oGtfG9lcNFpNuVYUFOByxDgNqEvic9rvaQbAj+SFwTEWDptRaIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.0.tgz", + "integrity": "sha512-JBsxqiY16uyyCujtoC3s7zL+SrYyNElptD7K4V+KUoJ2V8/XG3hPWpheK+SuPwNh1cikJvIZmIdsrYXvD6PLJw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.0.tgz", + "integrity": "sha512-S5Qrijh37qhnUYQ0ay8WN3LuWeKjow7wTUyCOPMZtnXiBXYXdsZETI6ZLfYnQAKovltckPLBFdNeD61P2rJc7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.0.tgz", + "integrity": "sha512-zbZ6iMmLFEsCtTdc2/y1F/cDFVNNCPF7Ae8PCT/wdPE//hGwYp6r+MyCLMFX+kOeIe09Pz6ASfnnfpSTN1FJQQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.0.tgz", + "integrity": "sha512-L6Hw69oNpuYaOr1zCG30v0/DqKYCSRwodPGwIcVZ/6f8SOzq9rxtL/xNVI5MO4wjli+Ko0ErK9uHJL3mNPt8wQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.0.tgz", + "integrity": "sha512-nl+CTWIgOGszQmUz/kanfgrfFZ4qiwxKIfT/ryl3N3Ewk5tqizmYtNXiqb7Bu/fDtNgu1kyxdFiAUJNur3iysg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/config-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", + "integrity": "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/config-loader": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-10.2.2.tgz", + "integrity": "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "ajv": "^8.17.1", + "debug": "^4.4.1", + "rc-config-loader": "^4.1.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/config-loader/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@secretlint/config-loader/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/core": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-10.2.2.tgz", + "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "structured-source": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/formatter": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.2.2.tgz", + "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", "@textlint/linter-formatter": "^15.2.0", "@textlint/module-interop": "^15.2.0", "@textlint/types": "^15.2.0", @@ -2099,6 +2677,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha1-p5tV26+GBIEvUtFAssmrQbwVC7g=", + "dev": true, + "license": "MIT" + }, "node_modules/@textlint/ast-node-types": { "version": "15.6.0", "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.6.0.tgz", @@ -2171,10 +2756,28 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha1-jpzZ4cNYH6azQaWu1ViOsoW+C0o=", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha1-M0MRlx06BxIefrkbaEpgXn7qnL0=", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha1-zz8Oh2177hWpOrkluCv1cKOQSiQ=", "license": "MIT" }, "node_modules/@types/json-schema": { @@ -2458,74 +3061,216 @@ "node": "18 || 20 || >=22" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz", + "integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==", + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz", + "integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.5.tgz", + "integrity": "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.0.18", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", + "integrity": "sha1-ucTbdHms1R1fDO2RsoU8KcPQzac=", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.0.18", + "ast-v8-to-istanbul": "^0.3.10", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.1", + "obug": "^2.1.1", + "std-env": "^3.10.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.0.18", + "vitest": "4.0.18" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha1-NhUQ2Z+/IOuBQiLkr8uFOdedyU0=", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha1-uXNdoRTvZeqVZSxb3xMVnG+rSGU=", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha1-+8zU2RB3QHLsFUY1U+24ylzlMhg=", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz", - "integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==", + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha1-wsCj7SJuyF6TEvnMjEPFs6iTqLE=", + "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.0", - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/typescript-estree": "8.59.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz", - "integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==", + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha1-vLQP1tdCZ5wqySe6KVtmrxxsNMU=", + "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha1-ug8gUD+20IuvMwnWkLPvq9+oh2I=", + "dev": true, + "license": "MIT", "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@typespec/ts-http-runtime": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.5.tgz", - "integrity": "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw==", + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha1-ljaxbYakFS7Gio1oWc/3AolkM9Q=", "dev": true, "license": "MIT", "dependencies": { - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "tslib": "^2.6.2" + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" }, - "engines": { - "node": ">=20.0.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, "node_modules/@vscode/vsce": { @@ -3015,6 +3760,35 @@ "node": ">=8" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha1-9kGhlrM1aQsQcL8AtudZP+wZC/c=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha1-jrG3yG74SZhZvnYbF//ZFAbAw28=", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha1-3/51mbSou3/jCv+NAjUjTf+3mDE=", + "dev": true, + "license": "MIT" + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -3356,6 +4130,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3935,6 +4719,34 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha1-6hgAcCTjFn9PEFMV8+wtmCv0jtk=", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha1-L3aQa84YJEKf/stpIPRaCzDwDd4=", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -3952,6 +4764,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha1-5kmkPjq5U6chkv9Zg4ZeUJ837Zo=", + "dev": true, + "license": "MIT" + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -4449,6 +5268,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha1-kVlgFWGICoXyc0VgqQmbLDHlNyo=", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -4862,6 +5688,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha1-Z8PlSexAKkh7T8GT0ZU6UkdSNA0=", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -4926,6 +5762,16 @@ "node": ">=6" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha1-JO338MxppE0AhWe6RZSrlvPDo9Y=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-content-type-parse": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", @@ -5632,6 +6478,26 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha1-aW31KafP2CRGNp3FGT5ZCjc1tEg=", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha1-39YAJ9o2o238viNiYsAKWCJoFFM=", + "dev": true, + "license": "MIT" + }, "node_modules/htmlparser2": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", @@ -6013,6 +6879,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha1-Fx7W8Z46xVQ5Tt94yqBXhKRb67U=", + "dev": true, + "license": "MIT" + }, "node_modules/is-stream": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", @@ -6085,6 +6958,45 @@ "node": "^18.17 || >=20.6.1" } }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha1-LRZsSwZE1Do58Ev2wu3R5YXzF1Y=", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha1-kIMFusmlvRdaxqdEier9D8JEWn0=", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha1-y0U1FitXhKpiPO4hpyUs8sgHrJM=", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/istextorbinary": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", @@ -6132,6 +7044,72 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha1-q18cHK/AS9h4clSQl06l6L8McrM=", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/entities/-/entities-6.0.1.tgz", + "integrity": "sha1-wow0pDN5yn9h0HQTCy9fcCCjBpQ=", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/jsdom/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha1-1+Ik+nI5nHoXUJn0X8KtAksF7AU=", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -6473,6 +7451,28 @@ "dev": true, "license": "ISC" }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha1-VnY+wJoPqAkd8nh5/ZTRkHjADZE=", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/magicast/-/magicast-0.5.5.tgz", + "integrity": "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" + } + }, "node_modules/make-asynchronous": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/make-asynchronous/-/make-asynchronous-1.1.0.tgz", @@ -6491,6 +7491,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha1-w8IwencSd82WODBfkVwprnQbYU4=", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/markdown-it": { "version": "14.1.1", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", @@ -6749,6 +7765,25 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", @@ -9450,6 +10485,13 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/nwsapi": { + "version": "2.2.28", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/nwsapi/-/nwsapi-2.2.28.tgz", + "integrity": "sha512-IlVB7OS7qrOsVYlpnFIkETjMwT9jwvmocJmmM+GZU/PAB3uGi9Ezd7vcWhWBUnSc0ya4ppmQITOyP1ez9gg8cg==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -9483,6 +10525,20 @@ "node": ">= 0.4" } }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/obug/-/obug-2.2.1.tgz", + "integrity": "sha1-nEU+/u2oIgUOuXdCQulpx4TWTXg=", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -9902,6 +10958,13 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha1-PsvsVUIWhbcKnahyss/z4cvtFxY=", + "dev": true, + "license": "MIT" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -10021,7 +11084,36 @@ "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", "license": "MIT", "engines": { - "node": ">=4" + "node": ">=4" + } + }, + "node_modules/postcss": { + "version": "8.5.29", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.19", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" } }, "node_modules/prebuild-install": { @@ -10396,6 +11488,59 @@ "node": ">=0.10.0" } }, + "node_modules/rollup": { + "version": "4.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/rollup/-/rollup-4.64.0.tgz", + "integrity": "sha512-gjA/kFeDfffII8COoPsvLPvE16hWDTEc1+0QKQNKUkuV8iJjqsC4xXs8/FzuL0v9EV39kuxTbAgcFoTRyy2TGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.64.0", + "@rollup/rollup-android-arm64": "4.64.0", + "@rollup/rollup-darwin-arm64": "4.64.0", + "@rollup/rollup-darwin-x64": "4.64.0", + "@rollup/rollup-freebsd-arm64": "4.64.0", + "@rollup/rollup-freebsd-x64": "4.64.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.64.0", + "@rollup/rollup-linux-arm-musleabihf": "4.64.0", + "@rollup/rollup-linux-arm64-gnu": "4.64.0", + "@rollup/rollup-linux-arm64-musl": "4.64.0", + "@rollup/rollup-linux-loong64-gnu": "4.64.0", + "@rollup/rollup-linux-loong64-musl": "4.64.0", + "@rollup/rollup-linux-ppc64-gnu": "4.64.0", + "@rollup/rollup-linux-ppc64-musl": "4.64.0", + "@rollup/rollup-linux-riscv64-gnu": "4.64.0", + "@rollup/rollup-linux-riscv64-musl": "4.64.0", + "@rollup/rollup-linux-s390x-gnu": "4.64.0", + "@rollup/rollup-linux-x64-gnu": "4.64.0", + "@rollup/rollup-linux-x64-musl": "4.64.0", + "@rollup/rollup-openbsd-x64": "4.64.0", + "@rollup/rollup-openharmony-arm64": "4.64.0", + "@rollup/rollup-win32-arm64-msvc": "4.64.0", + "@rollup/rollup-win32-ia32-msvc": "4.64.0", + "@rollup/rollup-win32-x64-gnu": "4.64.0", + "@rollup/rollup-win32-x64-msvc": "4.64.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha1-MCHRtDUvvzthSq7tC8DVc5q+C8I=", + "dev": true, + "license": "MIT" + }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -10457,6 +11602,19 @@ "node": ">=11.0.0" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha1-/ltKR2jfTxSiAbG6amXB89mYjMU=", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/secretlint": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-10.2.2.tgz", @@ -10692,6 +11850,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha1-MudscLeXJOO7Vny51UPrhYzPrzA=", + "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", @@ -10924,6 +12089,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/spawn-error-forwarder": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/spawn-error-forwarder/-/spawn-error-forwarder-1.0.0.tgz", @@ -10977,6 +12152,20 @@ "through2": "~2.0.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha1-Gsig2Ug4SNFpXkGLbQMaPDzmjjs=", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha1-2BCyfjoHMEeyteQANIgfXqb5yDs=", + "dev": true, + "license": "MIT" + }, "node_modules/stream-combiner2": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", @@ -11140,6 +12329,13 @@ "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha1-QwY30ki6d+B4iDlR+5qg7tfGP6I=", + "dev": true, + "license": "MIT" + }, "node_modules/table": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", @@ -11374,6 +12570,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha1-EDyfi6bXI3pHq23R3P93JRhjQms=", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha1-FqLjxuI/r85yZA5njmUds2FFucY=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -11419,6 +12632,36 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyrainbow": { + "version": "3.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tinyrainbow/-/tinyrainbow-3.2.0.tgz", + "integrity": "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha1-CH4FVbMblyXuSMp+d+3FYRXNgvc=", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha1-qT5u2dUFy1TFQs5D/rFMc5EyZdg=", + "dev": true, + "license": "MIT" + }, "node_modules/tmp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", @@ -11442,6 +12685,32 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha1-Ztd0tKHZ4S3HUIlyWvOsdewxvtc=", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha1-lq6GfN24/bZKScwwWajUKLzyOMo=", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/traverse": { "version": "0.6.8", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.8.tgz", @@ -11752,6 +13021,216 @@ "url": "https://bevry.me/fund" } }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vite/-/vite-6.4.3.tgz", + "integrity": "sha1-haFk23znBvKndoEu+is0DxchhY4=", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha1-7Sq5Z6MxreYvGNB32uGSaE1Q01A=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha1-YxM2ADTMs2s9xh7L3/eBIfkP4h8=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha1-VvlmNT7KC1D033VAzUNQym1FSgU=", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha1-YxM2ADTMs2s9xh7L3/eBIfkP4h8=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha1-+SW6JoVRWFlNkHMTzt0UdsWWf2w=", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/web-worker": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", @@ -11759,6 +13238,16 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha1-JWtOGIK+feu/AdBfCqIDl3jqCAo=", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", @@ -11783,6 +13272,20 @@ "node": ">=18" } }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha1-TuAtXXJRVdrgBPaulcc+fvXZVmM=", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -11798,6 +13301,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha1-o/aalxB/SUs83Dvd3Yg6fWXOvwQ=", + "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/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -11840,6 +13360,28 @@ "license": "ISC", "optional": true }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/wsl-utils": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", @@ -11856,6 +13398,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha1-gr6blX96/az5YeWYDxvyJ8C/dnM=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/xml2js": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", @@ -11880,6 +13432,13 @@ "node": ">=4.0" } }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha1-Bg/hvLf5x2/ioX24apvDq4lCEMs=", + "dev": true, + "license": "MIT" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/FASTBuildMonitorVSCode/package.json b/FASTBuildMonitorVSCode/package.json index 1046397..b2ebb97 100644 --- a/FASTBuildMonitorVSCode/package.json +++ b/FASTBuildMonitorVSCode/package.json @@ -96,6 +96,10 @@ }, "scripts": { "install:all": "npm install && cd webview-ui && npm install", + "ci:all": "npm ci && cd webview-ui && npm ci", + "test": "vitest", + "test:coverage": "vitest run --coverage", + "test:typecheck": "tsc --project tsconfig.test.json", "build:webview": "cd webview-ui && npm run build && cd ..", "build:all": "npm run build:webview && npm run compile", "vscode:prepublish": "npm run clean || npm run build:all", @@ -122,6 +126,10 @@ "typescript-eslint": "^8.18.1" }, "devDependencies": { + "@vitest/coverage-v8": "~4.0.18", + "jsdom": "^26.1.0", + "vite": "^6.4.3", + "vitest": "~4.0.18", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "esbuild": "^0.25.11", diff --git a/FASTBuildMonitorVSCode/tests/extension/buildMonitorService.test.ts b/FASTBuildMonitorVSCode/tests/extension/buildMonitorService.test.ts new file mode 100644 index 0000000..694fd1d --- /dev/null +++ b/FASTBuildMonitorVSCode/tests/extension/buildMonitorService.test.ts @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { BuildMonitorService } from '../../src/buildMonitorService'; +import { BuildWatcher } from '../../src/buildWatcher'; +import { BuildJobStatus } from '../../src/models'; +import { Logger } from '../../src/utilities/logger'; + +vi.mock('../../src/buildWatcher', async () => { + const { EventEmitter } = await import('node:events'); + return { + BuildWatcher: vi.fn(class extends EventEmitter { + on = super.on.bind(this); + emit = super.emit.bind(this); + getLogPath = vi.fn(() => 'build.log'); + getIsRestoringHistory = vi.fn(() => false); + start = vi.fn(); + stop = vi.fn(); + }), + }; +}); + +let service: BuildMonitorService; +let watcher: BuildWatcher; +beforeEach(() => { + service = new BuildMonitorService(Logger.getInstance(), 'build.log'); + watcher = vi.mocked(BuildWatcher).mock.instances[0]; +}); +afterEach(() => Logger.getInstance().dispose()); + +function startSession(time = 1000) { + watcher.emit('sessionStarted', { time, logVersion: 1, processId: 42 }); +} +function startJob(hostName = 'local', jobName = 'a.cpp', time = 1100) { + watcher.emit('jobStarted', { time, hostName, jobName }); +} +function finishJob(hostName: string, jobName: string, result: BuildJobStatus, time = 1200) { + watcher.emit('jobFinished', { time, hostName, jobName, result, message: 'details' }); +} + +describe('build monitor state', () => { + it('starts and stops idempotently, forwarding polling and restoration state', () => { + const changed = vi.fn(); + service.on('stateChanged', changed); + expect(service.getSnapshot()).toEqual({ + isMonitoring: false, logPath: 'build.log', session: undefined, workers: [], + }); + expect(service.getLogPath()).toBe('build.log'); + service.stop(); + service.start(250); + service.start(); + expect(service.isMonitoring()).toBe(true); + expect(watcher.start).toHaveBeenCalledExactlyOnceWith(250); + service.stop(); + service.stop(); + expect(service.isMonitoring()).toBe(false); + expect(watcher.stop).toHaveBeenCalledOnce(); + expect(changed).toHaveBeenCalledTimes(2); + expect(service.getIsRestoringHistory()).toBe(false); + vi.mocked(watcher.getIsRestoringHistory).mockReturnValue(true); + expect(service.getIsRestoringHistory()).toBe(true); + }); + + it('ignores jobs and stops before a session but still forwards progress events', () => { + const progress = vi.fn(); + service.on('progressChanged', progress); + startJob(); + finishJob('local', 'a.cpp', BuildJobStatus.Success); + watcher.emit('sessionStopped', { time: 2000 }); + watcher.emit('progressChanged', { time: 1500, progress: 20 }); + expect(service.getCurrentSession()).toBeUndefined(); + expect(service.getWorkers().size).toBe(0); + expect(progress).toHaveBeenCalledWith(20); + }); + + it('tracks job identity, case-insensitive hosts and cached/active totals', () => { + const started = vi.fn(); + const finished = vi.fn(); + const sessionStarted = vi.fn(); + service.on('jobStarted', started); + service.on('jobFinished', finished); + service.on('sessionStarted', sessionStarted); + startSession(); + startJob('Worker-A', 'a.cpp'); + startJob('WORKER-A', 'b.cpp'); + startJob('local', 'c.cpp'); + finishJob('worker-a', 'a.cpp', BuildJobStatus.SuccessCached); + watcher.emit('progressChanged', { time: 1500, progress: 55.5 }); + expect(service.getCurrentSession()).toMatchObject({ processId: 42, progress: 55.5 }); + expect(sessionStarted).toHaveBeenCalledWith(service.getCurrentSession()); + expect(started).toHaveBeenCalledTimes(3); + expect(finished).toHaveBeenCalledWith(expect.objectContaining({ + eventName: 'a.cpp', endTime: 1200, status: BuildJobStatus.SuccessCached, message: 'details', + })); + expect(service.getSnapshot().workers).toEqual([ + { hostName: 'Worker-A', activeJobs: 1, totalJobs: 2, cachedJobs: 1 }, + { hostName: 'local', activeJobs: 1, totalJobs: 1, cachedJobs: 0 }, + ]); + expect(service.getWorkers().get('worker-a')?.jobs[0]).toBe(service.getCurrentSession()?.jobs[0]); + expect(JSON.parse(JSON.stringify(service.getSnapshot())).workers).toHaveLength(2); + }); + + it('finishes the most recent matching active job and ignores unmatched completions', () => { + startSession(); + startJob('worker', 'same.cpp', 1100); + startJob('worker', 'same.cpp', 1150); + startJob('different', 'same.cpp', 1170); + finishJob('WORKER', 'same.cpp', BuildJobStatus.Error); + const jobs = service.getCurrentSession()!.jobs; + expect(jobs.map(j => j.status)).toEqual([ + BuildJobStatus.Building, BuildJobStatus.Error, BuildJobStatus.Building, + ]); + finishJob('worker', 'missing.cpp', BuildJobStatus.Success); + finishJob('worker', 'same.cpp', BuildJobStatus.Success); + finishJob('worker', 'same.cpp', BuildJobStatus.Failed); + expect(jobs[0].status).toBe(BuildJobStatus.Success); + expect(jobs[1].status).toBe(BuildJobStatus.Error); + expect(jobs[2].status).toBe(BuildJobStatus.Building); + }); + + it('stops unfinished jobs, preserves finished jobs and keeps prior session history', () => { + const stopped = vi.fn(); + service.on('sessionStopped', stopped); + startSession(); + startJob('local', 'completed'); + finishJob('local', 'completed', BuildJobStatus.Success); + startJob('remote', 'active'); + watcher.emit('sessionStopped', { time: 2000 }); + const previous = service.getCurrentSession()!; + expect(previous).toMatchObject({ endTime: 2000, progress: 100 }); + expect(previous.jobs[0]).toMatchObject({ status: BuildJobStatus.Success, endTime: 1200 }); + expect(previous.jobs[1]).toMatchObject({ status: BuildJobStatus.Stopped, endTime: 2000 }); + expect(stopped).toHaveBeenCalledWith(previous); + startJob('local', 'late job', 2100); + expect(previous.endTime).toBeUndefined(); + startSession(3000); + expect(service.getSessions()).toEqual([previous, service.getCurrentSession()]); + expect(service.getCurrentSession()?.jobs).toEqual([]); + expect(service.getWorkers().size).toBe(0); + expect(service.getCurrentSession()?.progress).toBe(0); + }); +}); diff --git a/FASTBuildMonitorVSCode/tests/extension/buildWatcher.test.ts b/FASTBuildMonitorVSCode/tests/extension/buildWatcher.test.ts new file mode 100644 index 0000000..f1f9b92 --- /dev/null +++ b/FASTBuildMonitorVSCode/tests/extension/buildWatcher.test.ts @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { BuildWatcher } from '../../src/buildWatcher'; +import { LogWatcher } from '../../src/logWatcher'; +import { BuildJobStatus } from '../../src/models'; + +vi.mock('../../src/logWatcher', async () => { + const { EventEmitter } = await import('node:events'); + return { + LogWatcher: vi.fn(class extends EventEmitter { + on = super.on.bind(this); + emit = super.emit.bind(this); + getLogPath = vi.fn(() => 'build.log'); + getIsRestoringHistory = vi.fn(() => false); + start = vi.fn(); + stop = vi.fn(); + }), + }; +}); + +const timestamp = 1700000000123; +const filetime = (BigInt(timestamp) * 10000n + 116444736000000000n).toString(); +let watcher: BuildWatcher; +let log: LogWatcher; + +beforeEach(() => { + watcher = new BuildWatcher('build.log'); + log = vi.mocked(LogWatcher).mock.instances[0]; +}); + +describe('FASTBuild log parser', () => { + it('delegates polling, paths and restoration state', () => { + expect(LogWatcher).toHaveBeenCalledWith('build.log'); + watcher.start(250); + watcher.stop(); + expect(log.start).toHaveBeenCalledWith(250); + expect(log.stop).toHaveBeenCalledOnce(); + expect(watcher.getLogPath()).toBe('build.log'); + expect(watcher.getIsRestoringHistory()).toBe(false); + vi.mocked(log.getIsRestoringHistory).mockReturnValue(true); + expect(watcher.getIsRestoringHistory()).toBe(true); + }); + + it('forwards history events and ignores resets', () => { + const started = vi.fn(); + const ended = vi.fn(); + watcher.on('historyRestorationStarted', started); + watcher.on('historyRestorationEnded', ended); + log.emit('historyRestorationStarted'); + log.emit('historyRestorationEnded'); + log.emit('logReset'); + expect(started).toHaveBeenCalledOnce(); + expect(ended).toHaveBeenCalledOnce(); + }); + + it.each([ + ['START_BUILD 3 42', 'sessionStarted', { logVersion: 3, processId: 42 }], + ['STOP_BUILD', 'sessionStopped', {}], + ['START_JOB worker "C:\\build path\\file.cpp"', 'jobStarted', { hostName: 'worker', jobName: 'C:\\build path\\file.cpp' }], + ['PROGRESS_STATUS 42.75', 'progressChanged', { progress: 42.75 }], + ['GRAPH "CPU group" usage percent 17.5', 'counterReported', { groupName: 'CPU group', counterName: 'usage', unitTag: 'percent', value: 17.5 }], + ])('parses %s', (line, event, expected) => { + const listener = vi.fn(); + watcher.on(event, listener); + log.emit('logReceived', `${filetime} ${line}`); + expect(listener).toHaveBeenCalledExactlyOnceWith({ time: timestamp, ...expected }); + expect(watcher.getLastMessageTime()).toBe(timestamp); + }); + + it.each([ + ['FAILED', BuildJobStatus.Failed], ['ERROR', BuildJobStatus.Error], + ['SUCCESS', BuildJobStatus.Success], ['SUCCESS_COMPLETE', BuildJobStatus.Success], + ['SUCCESS_CACHED', BuildJobStatus.SuccessCached], + ['SUCCESS_PREPROCESSED', BuildJobStatus.SuccessPreprocessed], + ['TIMEOUT', BuildJobStatus.Timeout], ['UNKNOWN', BuildJobStatus.Error], + ])('maps %s job results and preserves quoted messages', (result, status) => { + const listener = vi.fn(); + watcher.on('jobFinished', listener); + log.emit('logReceived', `${filetime} FINISH_JOB ${result} local "a b.obj" "compiler message with spaces"`); + expect(listener).toHaveBeenCalledExactlyOnceWith({ + time: timestamp, result: status, hostName: 'local', + jobName: 'a b.obj', message: 'compiler message with spaces', + }); + }); + + it('accepts finish events without an optional message', () => { + const listener = vi.fn(); + watcher.on('jobFinished', listener); + log.emit('logReceived', `${filetime} FINISH_JOB SUCCESS local output.obj`); + expect(listener.mock.calls[0][0]).toEqual({ + time: timestamp, result: BuildJobStatus.Success, hostName: 'local', + jobName: 'output.obj', message: undefined, + }); + }); + + it('ignores empty, unknown, and invalid timestamp records without advancing time', () => { + const listener = vi.fn(); + watcher.on('sessionStopped', listener); + for (const line of ['', 'one-token', `${filetime} UNKNOWN`, 'not-a-time STOP_BUILD']) { + log.emit('logReceived', line); + } + expect(listener).not.toHaveBeenCalled(); + expect(watcher.getLastMessageTime()).toBe(0); + log.emit('logReceived', `${filetime} STOP_BUILD`); + expect(listener).toHaveBeenCalledOnce(); + }); +}); diff --git a/FASTBuildMonitorVSCode/tests/extension/extension.test.ts b/FASTBuildMonitorVSCode/tests/extension/extension.test.ts new file mode 100644 index 0000000..ce4ec12 --- /dev/null +++ b/FASTBuildMonitorVSCode/tests/extension/extension.test.ts @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { activate, deactivate } from '../../src/extension'; +import { BuildMonitorService } from '../../src/buildMonitorService'; +import { MonitorPanelProvider } from '../../src/monitorPanelProvider'; +import { ConfigManager, type ConfigIssue } from '../../src/utilities/configManager'; +import { Logger } from '../../src/utilities/logger'; +import { + commands, configuration, configurationChanged, createContext, outputChannel, + registeredCommands, statusBar, window, +} from '../mocks/vscode'; +import { BuildJobStatus, type BuildSession } from '../../src/models'; + +const panel = vi.hoisted(() => ({ + show: vi.fn(), start: vi.fn(), stop: vi.fn(), refresh: vi.fn(), +})); +vi.mock('../../src/monitorPanelProvider', () => ({ + MonitorPanelProvider: { + getOrCreate: vi.fn(() => panel), + getInstance: vi.fn(() => panel), + }, +})); +vi.mock('../../src/utilities/configManager', () => ({ + ConfigManager: vi.fn(class { + onConfigurationError = vi.fn(); + checkConfigAndSetDefaults = vi.fn<() => Promise>().mockResolvedValue([]); + dispose = vi.fn(); + }), +})); +vi.mock('../../src/buildMonitorService', async () => { + const { EventEmitter } = await import('node:events'); + return { + BuildMonitorService: vi.fn(class extends EventEmitter { + on = super.on.bind(this); + emit = super.emit.bind(this); + getLogPath = vi.fn(() => 'build.log'); + getCurrentSession = vi.fn<() => BuildSession | undefined>(); + stop = vi.fn(); + }), + }; +}); + +let context = createContext(); +beforeEach(() => { context = createContext(); }); +afterEach(() => { + deactivate(); + for (const disposable of context.subscriptions) disposable.dispose(); + Logger.getInstance().dispose(); +}); + +function configResult(issues: ConfigIssue[]) { + vi.mocked(ConfigManager).mockImplementationOnce(function () { + return { + onConfigurationError: vi.fn(), + checkConfigAndSetDefaults: vi.fn().mockResolvedValue(issues), + dispose: vi.fn(), + } as Partial as ConfigManager; + }); +} + +describe('extension lifecycle', () => { + it('registers commands and disposables, uses the configured log path, and stops on deactivation', async () => { + configuration.set('logPath', 'custom.log'); + await activate(context); + expect(BuildMonitorService).toHaveBeenCalledWith(Logger.getInstance(), 'custom.log'); + expect([...registeredCommands.keys()]).toEqual([ + 'fbuildMonitor.show', 'fbuildMonitor.start', 'fbuildMonitor.stop', + ]); + expect(statusBar).toMatchObject({ + text: '$(tools) FASTBuild', tooltip: 'Open FASTBuild Monitor', command: 'fbuildMonitor.show', + }); + expect(statusBar.show).toHaveBeenCalledOnce(); + expect(context.subscriptions).toContain(outputChannel); + registeredCommands.get('fbuildMonitor.show')!(); + expect(panel.show).toHaveBeenCalledOnce(); + registeredCommands.get('fbuildMonitor.start')!(); + expect(panel.start).toHaveBeenCalledOnce(); + expect(panel.show).toHaveBeenCalledTimes(2); + registeredCommands.get('fbuildMonitor.stop')!(); + expect(panel.stop).toHaveBeenCalledOnce(); + deactivate(); + expect(vi.mocked(BuildMonitorService).mock.instances[0].stop).toHaveBeenCalledOnce(); + }); + + it('updates the status bar for running, failed, successful, and absent sessions', async () => { + await activate(context); + expect(BuildMonitorService).toHaveBeenCalledWith(Logger.getInstance(), undefined); + const service = vi.mocked(BuildMonitorService).mock.instances[0]; + const session: BuildSession = { processId: 1, logVersion: 1, startTime: 100, progress: 42.75, jobs: [] }; + vi.mocked(service.getCurrentSession).mockReturnValue(session); + service.emit('stateChanged'); + expect(statusBar.text).toBe('$(sync~spin) FASTBuild 42.8%'); + session.endTime = 200; + service.emit('stateChanged'); + expect(statusBar.text).toBe('$(check) FASTBuild Done'); + for (const status of [BuildJobStatus.Failed, BuildJobStatus.Error]) { + session.jobs = [{ hostName: 'local', eventName: 'a', startTime: 100, status }]; + service.emit('stateChanged'); + expect(statusBar.text).toBe('$(error) FASTBuild FAILED'); + } + vi.mocked(service.getCurrentSession).mockReturnValue(undefined); + service.emit('stateChanged'); + expect(statusBar.text).toBe('$(tools) FASTBuild'); + }); + + it('refreshes only on theme or color-setting changes', async () => { + await activate(context); + configuration.set('useLegacyColors', true); + for (const key of ['workbench.colorTheme', 'fbuildMonitor.useLegacyColors', 'editor.fontSize']) { + configurationChanged.fire({ affectsConfiguration: setting => setting === key }); + } + expect(panel.refresh).toHaveBeenCalledTimes(2); + expect(panel.refresh).toHaveBeenLastCalledWith(true); + configuration.delete('useLegacyColors'); + configurationChanged.fire({ affectsConfiguration: setting => setting === 'workbench.colorTheme' }); + expect(panel.refresh).toHaveBeenLastCalledWith(false); + }); + + it('reports configuration errors and offers to open the affected setting', async () => { + await activate(context); + window.showErrorMessage.mockResolvedValue('Open Settings'); + const manager = vi.mocked(ConfigManager).mock.instances[0]; + const listener = vi.mocked(manager.onConfigurationError).mock.calls[0][0]; + listener({ setting: 'fbuildMonitor.logPath', value: 42, message: 'Expected a string' }); + await Promise.resolve(); + expect(window.showErrorMessage).toHaveBeenCalledWith( + 'Invalid fbuildMonitor.logPath setting: Expected a string', 'Open Settings', + ); + expect(commands.executeCommand).toHaveBeenCalledWith('workbench.action.openSettings', 'fbuildMonitor.logPath'); + commands.executeCommand.mockClear(); + window.showErrorMessage.mockResolvedValue(undefined); + listener({ setting: 'fbuildMonitor.logPath', value: 42, message: 'Expected a string' }); + await Promise.resolve(); + expect(commands.executeCommand).not.toHaveBeenCalled(); + }); + + it('allows warnings and recommendations while showing a detailed report on request', async () => { + configResult([ + { severity: 'warning', setting: 'one', message: 'First warning' }, + { severity: 'warning', setting: '', message: 'Second warning' }, + { severity: 'info', setting: 'two', message: 'Recommendation' }, + { severity: 'info', setting: '', message: 'Another recommendation' }, + ]); + window.showWarningMessage.mockResolvedValue('View Details'); + await activate(context); + expect(window.showWarningMessage).toHaveBeenCalledWith('First warning\nSecond warning', 'View Details'); + expect(outputChannel.appendLine).toHaveBeenCalledWith(expect.stringContaining('Overall Status: PASSED')); + expect(outputChannel.appendLine).toHaveBeenCalledWith(expect.stringContaining('RECOMMENDATIONS:')); + expect(outputChannel.show).toHaveBeenCalledOnce(); + expect(MonitorPanelProvider.getOrCreate).toHaveBeenCalled(); + }); + + it('uses a single warning message and does not open details when dismissed', async () => { + configResult([{ severity: 'warning', setting: '', message: 'Warning' }]); + await activate(context); + expect(window.showWarningMessage).toHaveBeenCalledWith('Warning', 'View Details'); + expect(outputChannel.show).not.toHaveBeenCalled(); + }); + + it('allows info-only configuration and missing extension versions', async () => { + configResult([{ severity: 'info', setting: '', message: 'Recommendation' }]); + context = createContext(''); + await activate(context); + expect(window.showWarningMessage).not.toHaveBeenCalled(); + expect(outputChannel.appendLine).toHaveBeenCalledWith(expect.stringContaining('vunknown')); + }); + + it.each([1, 2])('blocks activation for %i configuration errors and offers the output log', async count => { + configResult(Array.from({ length: count }, (_, i) => ({ + severity: 'error', setting: i === 0 ? 'logPath' : '', message: `Invalid ${i}`, + }))); + window.showErrorMessage.mockResolvedValueOnce('View Details').mockResolvedValueOnce('View Output'); + await expect(activate(context)).rejects.toThrow('Extension activation blocked'); + expect(BuildMonitorService).not.toHaveBeenCalled(); + expect(window.showErrorMessage.mock.calls[0][0]).toContain('Invalid 0'); + expect(outputChannel.appendLine).toHaveBeenCalledWith(expect.stringContaining('Overall Status: FAILED')); + expect(outputChannel.show).toHaveBeenCalledTimes(2); + }); + + it('logs and rethrows activation failures even when notifications are dismissed', async () => { + const error = new Error('Registration failed'); + vi.mocked(MonitorPanelProvider.getOrCreate).mockImplementationOnce(() => { throw error; }); + await expect(activate(context)).rejects.toBe(error); + expect(window.showErrorMessage).toHaveBeenCalledWith( + 'FASTBuild Monitor failed to activate. Check the output panel for details.', 'View Output', + ); + expect(outputChannel.appendLine).toHaveBeenCalledWith(expect.stringContaining('ERROR: Failed to activate')); + expect(outputChannel.show).not.toHaveBeenCalled(); + }); +}); diff --git a/FASTBuildMonitorVSCode/tests/extension/logWatcher.test.ts b/FASTBuildMonitorVSCode/tests/extension/logWatcher.test.ts new file mode 100644 index 0000000..44f5e99 --- /dev/null +++ b/FASTBuildMonitorVSCode/tests/extension/logWatcher.test.ts @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { LogWatcher } from '../../src/logWatcher'; + +const file = vi.hoisted(() => ({ + text: '', exists: true, directoryExists: true, mtime: 1, locked: false, +})); +const logPath = path.join(os.tmpdir(), 'unit-test-build', 'build.log'); + +vi.mock('fs', () => ({ + existsSync: vi.fn((name: string) => name.endsWith('build.log') ? file.exists : file.directoryExists), + mkdirSync: vi.fn(), + statSync: vi.fn(() => ({ size: Buffer.byteLength(file.text), mtimeMs: file.mtime })), + openSync: vi.fn(() => { + if (file.locked) throw new Error('File is locked'); + return 42; + }), + readSync: vi.fn((_fd: number, buffer: Buffer, offset: number, length: number, position: number) => + Buffer.from(file.text).copy(buffer, offset, position, position + length)), + closeSync: vi.fn(), +})); + +beforeEach(() => { + Object.assign(file, { text: '', exists: true, directoryExists: true, mtime: 1, locked: false }); + vi.useFakeTimers(); + vi.stubEnv('FASTBUILD_TEMP_PATH', ''); +}); + +describe('log file polling', () => { + it('uses explicit paths, existing FASTBUILD_TEMP_PATH, or the OS temp directory', () => { + expect(new LogWatcher(logPath).getLogPath()).toBe(logPath); + expect(new LogWatcher().getLogPath()).toBe(path.join(os.tmpdir(), 'FASTBuild', 'FastBuildLog.log')); + const custom = path.join(os.tmpdir(), 'fastbuild-tests'); + vi.stubEnv('FASTBUILD_TEMP_PATH', custom); + expect(new LogWatcher().getLogPath()).toBe(path.join(custom, 'FASTBuild', 'FastBuildLog.log')); + file.directoryExists = false; + expect(new LogWatcher().getLogPath()).toBe(path.join(os.tmpdir(), 'FASTBuild', 'FastBuildLog.log')); + }); + + it('creates a missing directory, waits for a file, and stops polling', () => { + file.exists = file.directoryExists = false; + const watcher = new LogWatcher(logPath); + const received = vi.fn(); + watcher.on('logReceived', received); + watcher.start(100); + expect(fs.mkdirSync).toHaveBeenCalledWith(path.dirname(logPath), { recursive: true }); + file.exists = true; + file.text = 'first\n'; + vi.advanceTimersByTime(100); + expect(received).toHaveBeenCalledExactlyOnceWith('first'); + watcher.stop(); + watcher.stop(); + file.text += 'second\n'; + vi.advanceTimersByTime(1000); + expect(received).toHaveBeenCalledOnce(); + expect(vi.getTimerCount()).toBe(0); + }); + + it('restores history in order and handles CRLF, blank lines, UTF-8 and partial messages', () => { + file.text = 'first\r\n\ncaf\u00e9\npart'; + const watcher = new LogWatcher(logPath); + const events: string[] = []; + watcher.on('historyRestorationStarted', () => { + expect(watcher.getIsRestoringHistory()).toBe(true); + events.push('start'); + }); + watcher.on('logReceived', (message: string) => events.push(message)); + watcher.on('historyRestorationEnded', () => events.push('end')); + watcher.start(); + expect(events).toEqual(['start', 'first', 'caf\u00e9', 'end']); + expect(watcher.getIsRestoringHistory()).toBe(false); + file.text += 'ial\n'; + vi.advanceTimersByTime(500); + expect(events.at(-1)).toBe('partial'); + vi.advanceTimersByTime(500); + expect(events).toHaveLength(5); + expect(fs.closeSync).toHaveBeenCalledTimes(2); + watcher.stop(); + }); + + it('ends restoration for an empty file without opening it', () => { + const watcher = new LogWatcher(logPath); + const ended = vi.fn(); + watcher.on('historyRestorationEnded', ended); + watcher.start(); + expect(ended).toHaveBeenCalledOnce(); + expect(watcher.getIsRestoringHistory()).toBe(false); + expect(fs.openSync).not.toHaveBeenCalled(); + watcher.stop(); + }); + + it('detects truncation and reads the replacement from the beginning', () => { + file.text = 'long first line\n'; + const watcher = new LogWatcher(logPath); + const reset = vi.fn(); + const received = vi.fn(); + watcher.on('logReset', reset); + watcher.on('logReceived', received); + watcher.start(); + file.text = 'new\n'; + file.mtime++; + vi.advanceTimersByTime(500); + expect(reset).toHaveBeenCalledOnce(); + expect(received.mock.calls).toEqual([['long first line'], ['new']]); + watcher.stop(); + }); + + it('retries locked files and resumes when a deleted file is recreated', () => { + file.locked = true; + file.text = 'retry\n'; + const watcher = new LogWatcher(logPath); + const received = vi.fn(); + watcher.on('logReceived', received); + watcher.start(); + expect(received).not.toHaveBeenCalled(); + file.locked = false; + vi.advanceTimersByTime(500); + expect(received).toHaveBeenCalledWith('retry'); + file.exists = false; + vi.advanceTimersByTime(500); + file.exists = true; + file.text = 'recreated\n'; + vi.advanceTimersByTime(500); + expect(received).toHaveBeenLastCalledWith('recreated'); + watcher.stop(); + }); + + it('closes an opened file even if reading throws', () => { + file.text = 'retry\n'; + vi.mocked(fs.readSync).mockImplementationOnce(() => { throw new Error('Read failed'); }); + const watcher = new LogWatcher(logPath); + const received = vi.fn(); + watcher.on('logReceived', received); + watcher.start(); + expect(fs.closeSync).toHaveBeenCalledWith(42); + vi.advanceTimersByTime(500); + expect(received).toHaveBeenCalledWith('retry'); + watcher.stop(); + }); +}); diff --git a/FASTBuildMonitorVSCode/tests/extension/models.test.ts b/FASTBuildMonitorVSCode/tests/extension/models.test.ts new file mode 100644 index 0000000..7eb1ef1 --- /dev/null +++ b/FASTBuildMonitorVSCode/tests/extension/models.test.ts @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, expect, it, vi } from 'vitest'; +import { + BuildJobStatus, type BuildSession, formatDuration, getJobDisplayName, + getJobDurationMs, getStatusColor, sessionActiveJobs, sessionCachedJobs, + sessionFailedJobs, sessionSuccessJobs, sessionTotalJobs, +} from '../../src/models'; + +describe('build models', () => { + it.each([ + ['file.cpp', 'file.cpp'], ['src/file.cpp', 'file.cpp'], + ['C:\\src\\file.cpp', 'file.cpp'], ['src\\nested/file.cpp', 'file.cpp'], ['', ''], + ])('extracts the display name from %s', (input, expected) => { + expect(getJobDisplayName(input)).toBe(expected); + }); + + it.each([[0, '0.0s'], [1234, '1.2s'], [60000, '1:00.0'], [125600, '2:05.6']])( + 'formats %i milliseconds', (input, expected) => expect(formatDuration(input)).toBe(expected), + ); + + it('uses the current time only for unfinished jobs, including a zero end time', () => { + vi.useFakeTimers(); + vi.setSystemTime(5000); + const job = { hostName: 'local', eventName: 'a', startTime: -1000, status: BuildJobStatus.Building }; + expect(getJobDurationMs(job)).toBe(6000); + expect(getJobDurationMs({ ...job, endTime: 0 })).toBe(1000); + expect(getJobDurationMs({ ...job, endTime: 2000 })).toBe(3000); + }); + + it.each([ + [BuildJobStatus.Building, '#2196F3'], [BuildJobStatus.Success, '#4CAF50'], + [BuildJobStatus.SuccessCached, '#8BC34A'], [BuildJobStatus.SuccessPreprocessed, '#CDDC39'], + [BuildJobStatus.Failed, '#F44336'], [BuildJobStatus.Error, '#E91E63'], + [BuildJobStatus.Timeout, '#FF9800'], [BuildJobStatus.RacedOut, '#9E9E9E'], + [BuildJobStatus.Stopped, '#607D8B'], + ])('keeps the %s status color stable', (status, color) => { + expect(getStatusColor(status)).toBe(color); + }); + + it('counts successful variants, failures, and active jobs independently', () => { + const session: BuildSession = { + processId: 1, logVersion: 1, startTime: 0, progress: 0, + jobs: Object.values(BuildJobStatus).map(status => ({ + status, hostName: 'local', eventName: status, startTime: 0, + })), + }; + expect(sessionTotalJobs(session)).toBe(9); + expect(sessionActiveJobs(session)).toBe(1); + expect(sessionSuccessJobs(session)).toBe(3); + expect(sessionCachedJobs(session)).toBe(1); + expect(sessionFailedJobs(session)).toBe(2); + session.jobs = []; + for (const count of [sessionTotalJobs, sessionActiveJobs, sessionSuccessJobs, sessionCachedJobs, sessionFailedJobs]) { + expect(count(session)).toBe(0); + } + }); +}); diff --git a/FASTBuildMonitorVSCode/tests/extension/monitorPanelProvider.test.ts b/FASTBuildMonitorVSCode/tests/extension/monitorPanelProvider.test.ts new file mode 100644 index 0000000..5e18f7a --- /dev/null +++ b/FASTBuildMonitorVSCode/tests/extension/monitorPanelProvider.test.ts @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type * as VSCode from 'vscode'; +import * as vscodeMock from '../mocks/vscode'; +import { BuildMonitorService } from '../../src/buildMonitorService'; +import { Logger } from '../../src/utilities/logger'; +import type { MonitorPanelProvider as Provider } from '../../src/monitorPanelProvider'; +import type { BuildSession } from '../../src/models'; + +let provider: Provider; +let service: BuildMonitorService; +let monitoring: boolean; +let session: BuildSession | undefined; +let restoring: boolean; +let context: VSCode.ExtensionContext; +let messages: vscodeMock.EventEmitter<{ command: string }>; +let visibility: vscodeMock.EventEmitter; +let disposed: vscodeMock.EventEmitter; +let view: VSCode.WebviewView; +const postMessage = vi.fn().mockResolvedValue(true); + +beforeEach(async () => { + vi.useFakeTimers(); + vi.resetModules(); + vi.doMock('vscode', () => vscodeMock); + const { MonitorPanelProvider } = await import('../../src/monitorPanelProvider'); + monitoring = false; + session = undefined; + restoring = false; + context = vscodeMock.createContext(); + service = new BuildMonitorService(Logger.getInstance(), 'test.log'); + vi.spyOn(service, 'start').mockImplementation(() => { monitoring = true; }); + vi.spyOn(service, 'stop').mockImplementation(() => { monitoring = false; }); + vi.spyOn(service, 'isMonitoring').mockImplementation(() => monitoring); + vi.spyOn(service, 'getCurrentSession').mockImplementation(() => session); + vi.spyOn(service, 'getIsRestoringHistory').mockImplementation(() => restoring); + vi.spyOn(service, 'getSnapshot').mockImplementation(() => ({ + isMonitoring: monitoring, session, workers: [], logPath: 'test.log', + })); + expect(MonitorPanelProvider.getInstance()).toBeUndefined(); + provider = MonitorPanelProvider.getOrCreate(context, service); + expect(MonitorPanelProvider.getInstance()).toBe(provider); + expect(MonitorPanelProvider.getOrCreate(context, service)).toBe(provider); + messages = new vscodeMock.EventEmitter(); + visibility = new vscodeMock.EventEmitter(); + disposed = new vscodeMock.EventEmitter(); + const webview: Partial = { + options: {}, html: '', cspSource: 'https://webview', + asWebviewUri: uri => uri, postMessage, onDidReceiveMessage: messages.event, + }; + view = { + webview: webview as VSCode.Webview, visible: true, + onDidDispose: disposed.event, onDidChangeVisibility: visibility.event, + viewType: 'fbuildMonitor.fbuildMonitorView', show: vi.fn(), + }; +}); + +afterEach(() => { + provider.dispose(); + service.removeAllListeners(); + messages.dispose(); + visibility.dispose(); + disposed.dispose(); + for (const subscription of context.subscriptions) subscription.dispose(); + Logger.getInstance().dispose(); +}); + +function resolve() { + provider.resolveWebviewView(view, { state: undefined }, { + isCancellationRequested: false, onCancellationRequested: new vscodeMock.EventEmitter().event, + }); +} + +describe('monitor webview provider', () => { + it('registers once and generates restricted HTML with matching script nonces and asset URIs', () => { + expect(vscodeMock.window.registerWebviewViewProvider).toHaveBeenCalledOnce(); + resolve(); + expect(view.webview.options.enableScripts).toBe(true); + expect(view.webview.options.localResourceRoots?.map(uri => uri.toString())).toEqual([ + 'file:///extension/out', 'file:///extension/webview-ui/build', + ]); + expect(view.webview.html).toContain("default-src 'none'"); + expect(view.webview.html).toContain('file:///extension/webview-ui/build/assets/monitorPanel.js'); + expect(view.webview.html).toContain('file:///extension/webview-ui/build/assets/monitorPanel.css'); + const nonce = view.webview.html.match(/script-src 'nonce-([A-Za-z0-9]{32})'/)?.[1]; + expect(nonce).toHaveLength(32); + expect(view.webview.html).toContain(`nonce="${nonce}"`); + expect(service.start).toHaveBeenCalledWith(500); + vi.advanceTimersByTime(100); + expect(postMessage).toHaveBeenCalledWith({ + type: 'snapshot', data: { isMonitoring: true, session: undefined, workers: [], logPath: 'test.log', useLegacyColors: false }, + }); + }); + + it('honors auto-start, poll intervals, start/stop commands and explicit snapshot requests', () => { + vscodeMock.configuration.set('autoStart', false); + vscodeMock.configuration.set('pollIntervalMs', 250); + vscodeMock.configuration.set('useLegacyColors', true); + resolve(); + expect(service.start).not.toHaveBeenCalled(); + messages.fire({ command: 'start' }); + expect(service.start).toHaveBeenCalledWith(250); + messages.fire({ command: 'stop' }); + expect(service.stop).toHaveBeenCalledOnce(); + messages.fire({ command: 'requestSnapshot' }); + expect(postMessage).toHaveBeenLastCalledWith(expect.objectContaining({ + data: expect.objectContaining({ isMonitoring: false, useLegacyColors: true }), + })); + postMessage.mockClear(); + messages.fire({ command: 'unknown' }); + expect(postMessage).not.toHaveBeenCalled(); + provider.show(); + expect(vscodeMock.commands.executeCommand).toHaveBeenCalledWith('fbuildMonitor.fbuildMonitorView.focus'); + }); + + it('defers hidden starts, resumes on visibility, and respects an explicit stop', () => { + view = { ...view, visible: false }; + resolve(); + expect(service.start).not.toHaveBeenCalled(); + view = Object.assign(view, { visible: true }); + visibility.fire(); + expect(service.start).toHaveBeenCalledOnce(); + Object.assign(view, { visible: false }); + visibility.fire(); + expect(service.stop).toHaveBeenCalledOnce(); + Object.assign(view, { visible: true }); + visibility.fire(); + expect(service.start).toHaveBeenCalledTimes(2); + provider.stop(); + Object.assign(view, { visible: false }); + visibility.fire(); + Object.assign(view, { visible: true }); + visibility.fire(); + expect(service.start).toHaveBeenCalledTimes(2); + }); + + it('coalesces state updates, suppresses history updates, and refreshes active-build timers', () => { + vscodeMock.configuration.set('autoStart', false); + resolve(); + vi.advanceTimersByTime(100); + postMessage.mockClear(); + service.emit('stateChanged'); + service.emit('stateChanged'); + vi.advanceTimersByTime(100); + expect(postMessage).toHaveBeenCalledOnce(); + postMessage.mockClear(); + restoring = true; + service.emit('stateChanged'); + vi.advanceTimersByTime(200); + expect(postMessage).not.toHaveBeenCalled(); + restoring = false; + monitoring = true; + session = { processId: 1, logVersion: 1, startTime: 1, progress: 0, jobs: [] }; + vi.advanceTimersByTime(300); + expect(postMessage).toHaveBeenCalledTimes(3); + session.endTime = 100; + postMessage.mockClear(); + vi.advanceTimersByTime(200); + expect(postMessage).not.toHaveBeenCalled(); + provider.refresh(true); + expect(postMessage).toHaveBeenCalledWith(expect.objectContaining({ + data: expect.objectContaining({ useLegacyColors: true }), + })); + }); + + it('replaces refresh timers when resolved again and disposes timers and message listeners', () => { + provider.start(); + expect(service.start).not.toHaveBeenCalled(); + provider.refresh(false); + expect(postMessage).not.toHaveBeenCalled(); + resolve(); + resolve(); + expect(vi.getTimerCount()).toBe(1); + disposed.fire(); + expect(vi.getTimerCount()).toBe(0); + postMessage.mockClear(); + messages.fire({ command: 'requestSnapshot' }); + expect(postMessage).not.toHaveBeenCalled(); + }); +}); diff --git a/FASTBuildMonitorVSCode/tests/extension/utilities.test.ts b/FASTBuildMonitorVSCode/tests/extension/utilities.test.ts new file mode 100644 index 0000000..480912a --- /dev/null +++ b/FASTBuildMonitorVSCode/tests/extension/utilities.test.ts @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { afterEach, describe, expect, it, vi } from 'vitest'; +import * as vscode from 'vscode'; +import { ConfigManager } from '../../src/utilities/configManager'; +import { ErrorFormatter } from '../../src/utilities/errorFormatter'; +import { VsCodeFileSystem } from '../../src/utilities/fileSystem'; +import { getNonce } from '../../src/utilities/getNonce'; +import { getUri } from '../../src/utilities/getUri'; +import { Logger } from '../../src/utilities/logger'; +import { configuration, createContext, outputChannel, workspace } from '../mocks/vscode'; + +afterEach(() => Logger.getInstance().dispose()); + +describe('error formatting', () => { + it('formats messages with optional prefix and context', () => { + expect(ErrorFormatter.formatError('failed')).toBe('FASTBuild Monitor: failed'); + expect(ErrorFormatter.formatError('failed', false)).toBe('failed'); + expect(ErrorFormatter.formatErrorObject(new Error('failed'), 'read')).toBe('FASTBuild Monitor: read: failed'); + expect(ErrorFormatter.formatErrorObject('failed', undefined, false)).toBe('failed'); + expect(ErrorFormatter.formatErrorObject(42)).toBe('FASTBuild Monitor: 42'); + expect(ErrorFormatter.getErrorMessage(new Error('failed'))).toBe('failed'); + expect(ErrorFormatter.getErrorMessage(undefined)).toBe('undefined'); + }); +}); + +describe('logger', () => { + it('uses the configured channel, timestamps all levels, and shares one instance', () => { + configuration.set('logChannelName', 'Custom log'); + vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-01-01T00:00:00Z')); + const logger = Logger.getInstance(); + expect(Logger.getInstance()).toBe(logger); + expect(vscode.window.createOutputChannel).toHaveBeenCalledWith('Custom log'); + logger.info('info'); + logger.warn('warn'); + logger.debug('debug'); + logger.error('error'); + expect(outputChannel.appendLine.mock.calls.map(([line]) => line)).toEqual([ + '[2026-01-01T00:00:00.000Z] INFO: info', + '[2026-01-01T00:00:00.000Z] WARN: warn', + '[2026-01-01T00:00:00.000Z] DEBUG: debug', + '[2026-01-01T00:00:00.000Z] ERROR: error', + ]); + expect(logger.getOutputChannel()).toBe(outputChannel); + logger.show(); + expect(outputChannel.show).toHaveBeenCalledOnce(); + }); + + it('logs Error details, optional stacks and non-Error values', () => { + const logger = Logger.getInstance(); + const error = new Error('detail'); + error.stack = 'stack'; + logger.error('failed', error); + expect(outputChannel.appendLine).toHaveBeenCalledWith(' detail'); + expect(outputChannel.appendLine).toHaveBeenCalledWith(' Stack trace:\nstack'); + error.stack = undefined; + logger.error('failed without stack', error); + logger.error('failed', 42); + expect(outputChannel.appendLine).toHaveBeenCalledWith(' 42'); + }); + + it('disposes only once and recreates a disposed singleton', () => { + const logger = Logger.getInstance(); + logger.dispose(); + logger.dispose(); + expect(outputChannel.dispose).toHaveBeenCalledOnce(); + expect(Logger.getInstance()).not.toBe(logger); + expect(vscode.window.createOutputChannel).toHaveBeenLastCalledWith('FASTBuild Monitor'); + }); +}); + +describe('VS Code utilities', () => { + it('delegates file operations and propagates failures', async () => { + const fs = new VsCodeFileSystem(); + const uri = vscode.Uri.parse('file:///test'); + const contents = new Uint8Array([1, 2, 3]); + const stat = { type: vscode.FileType.File, ctime: 1, mtime: 2, size: 3 }; + workspace.fs.readFile.mockResolvedValue(contents); + workspace.fs.stat.mockResolvedValue(stat); + await fs.writeFile(uri, contents); + expect(workspace.fs.writeFile).toHaveBeenCalledWith(uri, contents); + expect(await fs.readFile(uri)).toBe(contents); + expect(workspace.fs.readFile).toHaveBeenCalledWith(uri); + expect(await fs.stat(uri)).toBe(stat); + expect(workspace.fs.stat).toHaveBeenCalledWith(uri); + await fs.delete(uri); + expect(workspace.fs.delete).toHaveBeenCalledWith(uri); + const error = new Error('Permission denied'); + workspace.fs.readFile.mockRejectedValue(error); + workspace.fs.writeFile.mockRejectedValue(error); + workspace.fs.stat.mockRejectedValue(error); + workspace.fs.delete.mockRejectedValue(error); + await expect(fs.readFile(uri)).rejects.toBe(error); + await expect(fs.writeFile(uri, contents)).rejects.toBe(error); + await expect(fs.stat(uri)).rejects.toBe(error); + await expect(fs.delete(uri)).rejects.toBe(error); + }); + + it('creates 32-character alphanumeric nonces', () => { + vi.spyOn(Math, 'random').mockReturnValueOnce(0).mockReturnValue(0.999); + expect(getNonce()).toBe(`A${'9'.repeat(31)}`); + expect(getNonce()).toMatch(/^[A-Za-z0-9]{32}$/); + }); + + it('joins resource paths before converting to a webview URI', () => { + const converted = vscode.Uri.parse('https://webview/resource'); + const webview = { asWebviewUri: vi.fn(() => converted) } as Pick; + expect(getUri(webview as vscode.Webview, vscode.Uri.parse('file:///extension'), ['build', 'main.js'])).toBe(converted); + expect(webview.asWebviewUri).toHaveBeenCalledWith(vscode.Uri.parse('file:///extension/build/main.js')); + }); + + it('reports configuration checks and update repository metadata', async () => { + const config = new ConfigManager(Logger.getInstance(), createContext()); + const listener = vi.fn(); + const subscription = config.onConfigurationError(listener); + expect(await config.checkConfigAndSetDefaults()).toEqual([]); + expect(config.getTagPrefix()).toBe('VS-Code-v'); + expect(ConfigManager.getRepositoryInfo()).toEqual({ owner: 'Mojang', name: 'FASTBuildMonitor' }); + expect(outputChannel.appendLine).toHaveBeenCalledWith(expect.stringContaining('Errors: false, Warnings: false, Total issues: 0')); + expect(listener).not.toHaveBeenCalled(); + subscription.dispose(); + config.dispose(); + }); + + it('reads update-check versions from the extension context without rewriting them', () => { + const config = new ConfigManager(Logger.getInstance(), createContext('1.2.3-preview.4')); + expect(config['getCurrentVersion']()).toBe('1.2.3-preview.4'); + config.dispose(); + }); +}); diff --git a/FASTBuildMonitorVSCode/tests/mocks/vscode.ts b/FASTBuildMonitorVSCode/tests/mocks/vscode.ts new file mode 100644 index 0000000..60797f1 --- /dev/null +++ b/FASTBuildMonitorVSCode/tests/mocks/vscode.ts @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { posix } from 'node:path'; +import { vi } from 'vitest'; +import type * as VSCode from 'vscode'; + +export class Disposable { + constructor(public dispose = vi.fn()) {} +} + +export class EventEmitter { + private listeners = new Set<(event: T) => void>(); + event = (listener: (event: T) => void, thisArg?: unknown, disposables?: VSCode.Disposable[]) => { + const bound = listener.bind(thisArg); + this.listeners.add(bound); + const disposable = new Disposable(vi.fn(() => this.listeners.delete(bound))); + disposables?.push(disposable); + return disposable; + }; + fire(event: T) { + for (const listener of this.listeners) listener(event); + } + dispose() { + this.listeners.clear(); + } +} + +export class Uri implements VSCode.Uri { + private constructor(private value: string) {} + static parse(value: string) { return new Uri(value); } + static file(value: string) { return new Uri(`file://${value.replaceAll('\\', '/')}`); } + static joinPath(base: Uri, ...parts: string[]) { + return new Uri(`${base.toString().replace(/\/$/, '')}/${posix.join(...parts)}`); + } + get scheme() { return new URL(this.value).protocol.slice(0, -1); } + get authority() { return new URL(this.value).host; } + get path() { return new URL(this.value).pathname; } + get query() { return new URL(this.value).search.slice(1); } + get fragment() { return new URL(this.value).hash.slice(1); } + get fsPath() { return this.path; } + with(change: { scheme?: string; authority?: string; path?: string; query?: string; fragment?: string }) { + const uri = new URL(this.value); + if (change.scheme !== undefined) uri.protocol = change.scheme; + if (change.authority !== undefined) uri.host = change.authority; + if (change.path !== undefined) uri.pathname = change.path; + if (change.query !== undefined) uri.search = change.query; + if (change.fragment !== undefined) uri.hash = change.fragment; + return new Uri(uri.toString()); + } + toString() { return this.value; } + toJSON() { return { scheme: this.scheme, authority: this.authority, path: this.path, query: this.query, fragment: this.fragment }; } +} + +export const configuration = new Map(); +export const configurationChanged = new EventEmitter(); +export const outputChannel = { + name: 'FASTBuild Monitor', + append: vi.fn(), appendLine: vi.fn(), replace: vi.fn(), clear: vi.fn(), + show: vi.fn(), hide: vi.fn(), dispose: vi.fn(), +}; +export const statusBar = { + text: '', tooltip: '', command: '', show: vi.fn(), hide: vi.fn(), dispose: vi.fn(), +}; +export const StatusBarAlignment = { Left: 1, Right: 2 }; +export const FileType = { File: 1 }; +export const workspace = { + getConfiguration: vi.fn(() => ({ + get: (key: string, fallback?: T): T | undefined => + configuration.has(key) ? configuration.get(key) as T : fallback, + })), + onDidChangeConfiguration: configurationChanged.event, + fs: { + writeFile: vi.fn<(uri: VSCode.Uri, content: Uint8Array) => Promise>(), + delete: vi.fn<(uri: VSCode.Uri) => Promise>(), + readFile: vi.fn<(uri: VSCode.Uri) => Promise>(), + stat: vi.fn<(uri: VSCode.Uri) => Promise>(), + }, +}; +export const window = { + createOutputChannel: vi.fn(() => outputChannel), + createStatusBarItem: vi.fn(() => statusBar), + registerWebviewViewProvider: vi.fn(() => new Disposable()), + showErrorMessage: vi.fn<(...args: string[]) => Promise>(), + showWarningMessage: vi.fn<(...args: string[]) => Promise>(), +}; +export const registeredCommands = new Map void>(); +export const commands = { + registerCommand: vi.fn((name: string, callback: () => void) => { + registeredCommands.set(name, callback); + return new Disposable(vi.fn(() => registeredCommands.delete(name))); + }), + executeCommand: vi.fn<(name: string, ...args: unknown[]) => Promise>(), +}; + +// Tests supply only the VS Code context fields the extension consumes. +export function createContext(version = '1.2.3'): VSCode.ExtensionContext { + const context: Partial = { + extensionUri: Uri.parse('file:///extension'), + subscriptions: [], + extension: { packageJSON: { version } } as VSCode.Extension, + }; + return context as VSCode.ExtensionContext; +} + +export function resetVSCode() { + configuration.clear(); + configurationChanged.dispose(); + registeredCommands.clear(); + statusBar.text = ''; + statusBar.tooltip = ''; + statusBar.command = ''; + window.showErrorMessage.mockResolvedValue(undefined); + window.showWarningMessage.mockResolvedValue(undefined); + commands.executeCommand.mockResolvedValue(undefined); + workspace.fs.writeFile.mockReset().mockResolvedValue(undefined); + workspace.fs.delete.mockReset().mockResolvedValue(undefined); + workspace.fs.readFile.mockReset().mockResolvedValue(new Uint8Array()); + workspace.fs.stat.mockReset(); +} diff --git a/FASTBuildMonitorVSCode/tests/setup.ts b/FASTBuildMonitorVSCode/tests/setup.ts new file mode 100644 index 0000000..e9f88a9 --- /dev/null +++ b/FASTBuildMonitorVSCode/tests/setup.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { afterEach, beforeEach, vi } from 'vitest'; +import { resetVSCode } from './mocks/vscode'; + +beforeEach(resetVSCode); +afterEach(() => { + vi.useRealTimers(); + vi.unstubAllEnvs(); + vi.unstubAllGlobals(); +}); diff --git a/FASTBuildMonitorVSCode/tsconfig.test.json b/FASTBuildMonitorVSCode/tsconfig.test.json new file mode 100644 index 0000000..85a9a47 --- /dev/null +++ b/FASTBuildMonitorVSCode/tsconfig.test.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "rootDir": ".", + "noEmit": true, + "jsx": "react-jsx", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["node"] + }, + "include": [ + "tests/**/*.ts", + "webview-ui/tests/**/*.ts", + "webview-ui/tests/**/*.tsx", + "webview-ui/src/**/*.d.ts", + "vitest.config.mts" + ], + "exclude": ["node_modules", "out", "webview-ui/node_modules", "webview-ui/build"] +} diff --git a/FASTBuildMonitorVSCode/vitest.config.mts b/FASTBuildMonitorVSCode/vitest.config.mts new file mode 100644 index 0000000..ea24dd2 --- /dev/null +++ b/FASTBuildMonitorVSCode/vitest.config.mts @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + watch: false, + projects: [ + { + resolve: { + alias: { + vscode: fileURLToPath(new URL('./tests/mocks/vscode.ts', import.meta.url)), + }, + }, + test: { + name: 'extension', + environment: 'node', + include: ['tests/extension/**/*.test.ts'], + setupFiles: ['tests/setup.ts'], + clearMocks: true, + restoreMocks: true, + }, + }, + { + esbuild: { jsx: 'automatic' }, + test: { + name: 'webview', + environment: 'jsdom', + include: ['webview-ui/tests/**/*.test.{ts,tsx}'], + setupFiles: ['webview-ui/tests/setup.ts'], + clearMocks: true, + restoreMocks: true, + }, + }, + ], + coverage: { + provider: 'v8', + include: ['src/**/*.ts', 'webview-ui/src/**/*.{ts,tsx}'], + exclude: ['**/*.d.ts'], + reporter: ['text', 'html', 'lcov', 'json-summary'], + thresholds: { + perFile: true, + statements: 80, + branches: 75, + functions: 80, + lines: 80, + }, + }, + }, +}); diff --git a/FASTBuildMonitorVSCode/webview-ui/package-lock.json b/FASTBuildMonitorVSCode/webview-ui/package-lock.json index 7f158d0..1478edd 100644 --- a/FASTBuildMonitorVSCode/webview-ui/package-lock.json +++ b/FASTBuildMonitorVSCode/webview-ui/package-lock.json @@ -12,6 +12,8 @@ "react-dom": "^18.3.1" }, "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.0", "@types/vscode-webview": "^1.57.0", @@ -20,6 +22,13 @@ "vite": "^6.2.6" } }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha1-tbcaJaTRavokglkt36YvzMYLx9E=", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -254,6 +263,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha1-EgIkUMRaTabY2Ch7GKT/Ldsj92g=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -1151,6 +1170,90 @@ "win32" ] }, + "node_modules/@testing-library/dom": { + "version": "10.4.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@testing-library/dom/-/dom-10.4.2.tgz", + "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha1-dhOgThRt0pdtJN3wGXMNV6idVsI=", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha1-mT6SXMHXPyxmLn113VpURSWaj9g=", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha1-QmkH53FvNwONq4qmnY8EWfnsJLI=", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha1-GjHD03iFDSd42rtjdNA23LpLpwg=", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1259,6 +1362,41 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha1-CCyyyJyf6GWaMRpTvWpNxTAdswQ=", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha1-B0SWkK1Fd30ZJKwquy/IiV26g2s=", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha1-ZQxWnkGtkLUbPX315e7Rx1ScED4=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.10.10", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", @@ -1334,6 +1472,13 @@ "dev": true, "license": "MIT" }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1359,6 +1504,24 @@ } } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha1-JkQhTxmX057Q7g7OcjNUkKesZ74=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha1-WnQp5gZus2ZNkR4z+w5F3o6whFM=", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/electron-to-chromium": { "version": "1.5.325", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.325.tgz", @@ -1461,6 +1624,16 @@ "node": ">=6.9.0" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha1-Yk+PRJfWGbLZdoUx1Y9BIoVNclE=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1515,6 +1688,27 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha1-watQ93iHtxJiEgG6n9Tjpu0JmUE=", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha1-pj9oFnOzBXH76LwlaGrnRu76mGk=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1597,6 +1791,22 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha1-IYGHn96lGnpYUfs52SD6pj8B2I4=", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -1622,6 +1832,14 @@ "react": "^18.3.1" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha1-5pHUqOnHiTZWVVOas3J2Kw77VPA=", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -1632,6 +1850,20 @@ "node": ">=0.10.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/redent/-/redent-3.0.0.tgz", + "integrity": "sha1-5Ve3mYMWu1PJ8fVvpiY1LGljBZ8=", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/rollup": { "version": "4.60.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", @@ -1706,6 +1938,19 @@ "node": ">=0.10.0" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha1-wy4c7pQLazQyx3G8LFS8znPNMAE=", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", diff --git a/FASTBuildMonitorVSCode/webview-ui/package.json b/FASTBuildMonitorVSCode/webview-ui/package.json index 13b874b..8130018 100644 --- a/FASTBuildMonitorVSCode/webview-ui/package.json +++ b/FASTBuildMonitorVSCode/webview-ui/package.json @@ -13,6 +13,8 @@ "react-dom": "^18.3.1" }, "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.0", "@types/vscode-webview": "^1.57.0", diff --git a/FASTBuildMonitorVSCode/webview-ui/tests/app.test.tsx b/FASTBuildMonitorVSCode/webview-ui/tests/app.test.tsx new file mode 100644 index 0000000..6495900 --- /dev/null +++ b/FASTBuildMonitorVSCode/webview-ui/tests/app.test.tsx @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { act, fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import App from '../src/monitor_panel/App'; +import { BuildJobStatus, type MonitorSnapshot } from '../src/monitor_panel/types'; +import { job, session, snapshot } from './fixtures'; +import { vscodeApi } from './setup'; + +const timeline = vi.hoisted(() => vi.fn()); +vi.mock('../src/monitor_panel/components/Timeline', () => ({ + default: (props: { timeScale: number; onTimeScaleChange: (value: number) => void }) => { + timeline(props); + return ; + }, +})); + +function sendSnapshot(data: MonitorSnapshot) { + act(() => { + window.dispatchEvent(new MessageEvent('message', { data: { type: 'snapshot', data } })); + }); +} + +describe('monitor application', () => { + it('requests initial state and sends start/stop commands for the current monitoring state', () => { + render(); + expect(vscodeApi.postMessage).toHaveBeenCalledExactlyOnceWith({ command: 'requestSnapshot' }); + expect(screen.getByText('Idle - Waiting for FASTBuild...')).toBeInTheDocument(); + expect(screen.getByText('Log: (unknown)')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: /Start Monitoring/ })); + expect(vscodeApi.postMessage).toHaveBeenLastCalledWith({ command: 'start' }); + sendSnapshot(snapshot({ isMonitoring: true, logPath: 'C:\\build\\FastBuildLog.log' })); + fireEvent.click(screen.getByRole('button', { name: /Stop Monitoring/ })); + expect(vscodeApi.postMessage).toHaveBeenLastCalledWith({ command: 'stop' }); + expect(screen.getByText('Log: C:\\build\\FastBuildLog.log')).toBeInTheDocument(); + }); + + it('updates progress, statistics, jobs, elapsed time and workers from snapshots', () => { + vi.useFakeTimers(); + vi.setSystemTime(6500); + render(); + sendSnapshot(snapshot({ + isMonitoring: true, + session: session({ + endTime: undefined, progress: 42.5, + jobs: [job({ status: BuildJobStatus.Building, endTime: undefined })], + }), + workers: [{ hostName: 'worker', totalJobs: 5, activeJobs: 1, cachedJobs: 2 }], + })); + expect(screen.getByText('Building (PID: 42)...')).toBeInTheDocument(); + expect(screen.getByText('00:00:05')).toBeInTheDocument(); + expect(screen.getAllByText('42.5%')).toHaveLength(2); + expect(screen.getByText('file.cpp')).toBeInTheDocument(); + expect(screen.getByText('5 total / 2 cached')).toBeInTheDocument(); + sendSnapshot(snapshot({ session: session() })); + expect(screen.getByText('00:00:10')).toBeInTheDocument(); + expect(screen.getByText('Build completed - 1 jobs - 00:00:10')).toBeInTheDocument(); + sendSnapshot(snapshot({ session: session({ processId: 43, jobs: [], progress: 0, endTime: undefined }) })); + expect(screen.queryByText('file.cpp')).not.toBeInTheDocument(); + expect(screen.getAllByText('0.0%')).toHaveLength(2); + }); + + it('applies and removes legacy colors and ignores unrelated messages', () => { + render(); + sendSnapshot(snapshot({ useLegacyColors: true })); + expect(document.body).toHaveClass('legacy-colors'); + expect(document.querySelector('.using-legacy-colors')).toBeInTheDocument(); + act(() => { + window.dispatchEvent(new MessageEvent('message', { data: { type: 'unrelated', data: {} } })); + }); + expect(document.body).toHaveClass('legacy-colors'); + sendSnapshot(snapshot({ useLegacyColors: false })); + expect(document.body).not.toHaveClass('legacy-colors'); + expect(document.querySelector('.not-using-legacy-colors')).toBeInTheDocument(); + }); + + it('clamps toolbar zoom, resets it, and accepts timeline zoom changes', () => { + render(); + for (let i = 0; i < 20; i++) fireEvent.click(screen.getByTitle('Zoom In')); + expect(timeline).toHaveBeenLastCalledWith(expect.objectContaining({ timeScale: 200 })); + for (let i = 0; i < 30; i++) fireEvent.click(screen.getByTitle('Zoom Out')); + expect(timeline).toHaveBeenLastCalledWith(expect.objectContaining({ timeScale: 1 })); + fireEvent.click(screen.getByTitle('Reset Zoom')); + expect(timeline).toHaveBeenLastCalledWith(expect.objectContaining({ timeScale: 20 })); + fireEvent.click(screen.getByRole('button', { name: 'Timeline zoom' })); + expect(timeline).toHaveBeenLastCalledWith(expect.objectContaining({ timeScale: 75 })); + }); + + it('removes its message listener when unmounted', () => { + const add = vi.spyOn(window, 'addEventListener'); + const remove = vi.spyOn(window, 'removeEventListener'); + const { unmount } = render(); + const listener = add.mock.calls.find(([name]) => name === 'message')?.[1]; + expect(listener).toBeDefined(); + unmount(); + expect(remove).toHaveBeenCalledWith('message', listener); + }); +}); diff --git a/FASTBuildMonitorVSCode/webview-ui/tests/components.test.tsx b/FASTBuildMonitorVSCode/webview-ui/tests/components.test.tsx new file mode 100644 index 0000000..cc8e892 --- /dev/null +++ b/FASTBuildMonitorVSCode/webview-ui/tests/components.test.tsx @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { fireEvent, render, screen, within } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import Toolbar, { getStatusText } from '../src/monitor_panel/components/Toolbar'; +import SidePanel from '../src/monitor_panel/components/SidePanel'; +import RecentJobs from '../src/monitor_panel/components/RecentJobs'; +import { BuildJobStatus, STATUS_TEXT_COLORS } from '../src/monitor_panel/types'; +import { job, session, snapshot } from './fixtures'; + +describe('toolbar', () => { + it('describes idle, waiting, running, completed and failed builds', () => { + expect(getStatusText(snapshot())).toBe('Idle - Waiting for FASTBuild...'); + expect(getStatusText(snapshot({ isMonitoring: true }))).toBe('Monitoring - Waiting for FASTBuild...'); + expect(getStatusText(snapshot({ session: session({ endTime: undefined }) }))).toBe('Building (PID: 42)...'); + expect(getStatusText(snapshot({ session: session() }))).toBe('Build completed - 1 jobs - 00:00:10'); + const jobs = [ + job({ status: BuildJobStatus.Failed }), job({ status: BuildJobStatus.Error }), + job({ status: BuildJobStatus.Success }), + ]; + expect(getStatusText(snapshot({ session: session({ jobs }) }))).toBe('Build FAILED - 2 error(s) - 00:00:10'); + }); + + it('renders status and elapsed time, updates its toggle and invokes each action', () => { + const props = { + isMonitoring: false, statusText: 'Ready', elapsedTime: '01:02:03', + onToggle: vi.fn(), onZoomIn: vi.fn(), onZoomOut: vi.fn(), onZoomReset: vi.fn(), + }; + const { rerender } = render(); + expect(screen.getByText('Ready')).toBeInTheDocument(); + expect(screen.getByText('01:02:03')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: /Start Monitoring/ })); + fireEvent.click(screen.getByTitle('Zoom In')); + fireEvent.click(screen.getByTitle('Zoom Out')); + fireEvent.click(screen.getByTitle('Reset Zoom')); + for (const callback of [props.onToggle, props.onZoomIn, props.onZoomOut, props.onZoomReset]) { + expect(callback).toHaveBeenCalledOnce(); + } + rerender(); + expect(screen.getByRole('button', { name: /Stop Monitoring/ })).toBeInTheDocument(); + }); +}); + +describe('statistics and worker panel', () => { + it('shows zero counts before a build', () => { + const { container } = render(); + expect([...container.querySelectorAll('.stat-value')].map(el => el.textContent)).toEqual(['0', '0', '0', '0', '0']); + expect(container.querySelectorAll('.worker-card')).toHaveLength(0); + }); + + it('counts every status correctly and renders per-worker totals and theme-aware colors', () => { + const jobs = Object.values(BuildJobStatus).map(status => job({ status })); + const { container } = render(); + expect([...container.querySelectorAll('.stat-value')].map(el => el.textContent)).toEqual(['9', '1', '3', '1', '2']); + expect(screen.getByText('10 total / 2 cached')).toBeInTheDocument(); + expect(screen.getByText('7 total / 0 cached')).toBeInTheDocument(); + expect(screen.getByText('3 active')).toBeInTheDocument(); + expect(container.querySelectorAll('.worker-card')).toHaveLength(2); + expect(container.querySelectorAll('.stat-value')[1].style.color).toBe(STATUS_TEXT_COLORS.Building); + }); +}); + +describe('recent jobs', () => { + it('renders an empty list before a build', () => { + const { container } = render(); + expect(screen.getByRole('heading', { name: 'Recent Jobs' })).toBeInTheDocument(); + expect(container.querySelectorAll('.job-row')).toHaveLength(0); + }); + + it('shows the newest 100 jobs first without changing the original order', () => { + const jobs = Array.from({ length: 105 }, (_, i) => job({ + eventName: `src/job-${i}.cpp`, startTime: 1000 + i, + })); + const original = [...jobs]; + const { container } = render(); + const rows = container.querySelectorAll('.job-row'); + expect(rows).toHaveLength(100); + expect(within(rows[0]).getByText('job-104.cpp')).toBeInTheDocument(); + expect(within(rows[99]).getByText('job-5.cpp')).toBeInTheDocument(); + expect(screen.queryByText('job-4.cpp')).not.toBeInTheDocument(); + expect(jobs).toEqual(original); + }); + + it('updates active durations and displays job status, host and filename', () => { + vi.useFakeTimers(); + vi.setSystemTime(6500); + const build = session({ jobs: [job({ + eventName: 'C:\\src\\file.cpp', hostName: 'remote', + endTime: undefined, status: BuildJobStatus.Building, + })] }); + const { container, rerender } = render(); + expect(screen.getByText('file.cpp')).toBeInTheDocument(); + expect(screen.getByText('remote')).toBeInTheDocument(); + expect(screen.getByText('5.5s')).toBeInTheDocument(); + expect(container.querySelector('.job-status')?.style.color).toBe(STATUS_TEXT_COLORS.Building); + vi.setSystemTime(7500); + rerender(); + expect(screen.getByText('6.5s')).toBeInTheDocument(); + }); +}); diff --git a/FASTBuildMonitorVSCode/webview-ui/tests/fixtures.ts b/FASTBuildMonitorVSCode/webview-ui/tests/fixtures.ts new file mode 100644 index 0000000..a4bf696 --- /dev/null +++ b/FASTBuildMonitorVSCode/webview-ui/tests/fixtures.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { BuildJobStatus, type BuildJob, type BuildSession, type MonitorSnapshot } from '../src/monitor_panel/types'; + +export function job(overrides: Partial = {}): BuildJob { + return { + hostName: 'local', eventName: 'src/file.cpp', startTime: 1000, + endTime: 3000, status: BuildJobStatus.Success, ...overrides, + }; +} + +export function session(overrides: Partial = {}): BuildSession { + return { + processId: 42, logVersion: 1, startTime: 1000, endTime: 11000, + progress: 100, jobs: [job()], ...overrides, + }; +} + +export function snapshot(overrides: Partial = {}): MonitorSnapshot { + return { isMonitoring: false, logPath: '', workers: [], ...overrides }; +} diff --git a/FASTBuildMonitorVSCode/webview-ui/tests/index.test.tsx b/FASTBuildMonitorVSCode/webview-ui/tests/index.test.tsx new file mode 100644 index 0000000..3571db9 --- /dev/null +++ b/FASTBuildMonitorVSCode/webview-ui/tests/index.test.tsx @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { act } from '@testing-library/react'; +import { expect, it, vi } from 'vitest'; +import type { Root } from 'react-dom/client'; +import * as ReactDOM from 'react-dom/client'; + +vi.mock('react-dom/client', async importOriginal => { + const original = await importOriginal(); + return { ...original, createRoot: vi.fn(original.createRoot) }; +}); + +it('mounts the monitor application into the webview root', async () => { + const container = document.createElement('div'); + container.id = 'root'; + document.body.append(container); + const { createRoot } = await vi.importActual('react-dom/client'); + let root: Root | undefined; + vi.mocked(ReactDOM.createRoot).mockImplementation((element, options) => { + root = createRoot(element, options); + return root; + }); + try { + await act(async () => { await import('../src/monitor_panel/index'); }); + expect(container.querySelector('.toolbar')).toBeInTheDocument(); + expect(container.querySelector('.timeline-container')).toBeInTheDocument(); + } finally { + act(() => root?.unmount()); + container.remove(); + } +}); diff --git a/FASTBuildMonitorVSCode/webview-ui/tests/progressBar.test.tsx b/FASTBuildMonitorVSCode/webview-ui/tests/progressBar.test.tsx new file mode 100644 index 0000000..9e087d7 --- /dev/null +++ b/FASTBuildMonitorVSCode/webview-ui/tests/progressBar.test.tsx @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { act, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import ProgressBar from '../src/monitor_panel/components/ProgressBar'; +import { createCanvasContext, getContext } from './setup'; + +describe('progress bar', () => { + it.each([0, 42.5, 100])('renders %s%% with complementary clips and one accessible label', progress => { + const { container } = render(); + expect(container.querySelector('.progress-bar')).toHaveStyle({ width: `${progress}%` }); + const labels = screen.getAllByText(`${progress.toFixed(1)}%`); + expect(labels).toHaveLength(2); + expect(labels[0]).toHaveStyle({ clipPath: `inset(0 0 0 ${progress}%)` }); + expect(labels[1]).toHaveStyle({ clipPath: `inset(0 ${100 - progress}% 0 0)` }); + expect(labels[1]).toHaveAttribute('aria-hidden', 'true'); + expect(labels[0]).not.toHaveAttribute('aria-hidden'); + }); + + it.each([ + [[0, 0, 0], '#fff'], [[255, 255, 255], '#000'], + [[117, 117, 117], '#fff'], [[118, 118, 118], '#000'], + [[33, 150, 243], '#000'], [[10, 10, 10], '#fff'], + ] as const)('chooses the higher-contrast text color for RGB %s', (rgb, expected) => { + const context = createCanvasContext(); + context.getImageData.mockReturnValue({ data: new Uint8ClampedArray([...rgb, 255]) }); + getContext.mockReturnValueOnce(context); + const { container } = render(); + const track = container.querySelector('.progress-container')!; + expect(track.style.getPropertyValue('--progress-track-text')).toBe(expected); + expect(track.style.getPropertyValue('--progress-fill-text')).toBe(expected); + }); + + it('samples track and fill independently and recomputes after theme changes', async () => { + const context = createCanvasContext(); + context.getImageData + .mockReturnValueOnce({ data: new Uint8ClampedArray([255, 255, 255, 255]) }) + .mockReturnValueOnce({ data: new Uint8ClampedArray([0, 0, 0, 255]) }); + getContext.mockReturnValueOnce(context); + const { container, unmount } = render(); + const track = container.querySelector('.progress-container')!; + expect(track.style.getPropertyValue('--progress-track-text')).toBe('#000'); + expect(track.style.getPropertyValue('--progress-fill-text')).toBe('#fff'); + expect(context.fillRect.mock.calls.length).toBeGreaterThan(3); + await act(async () => { document.body.classList.add('legacy-colors'); }); + expect(track.style.getPropertyValue('--progress-track-text')).toBe('#fff'); + const reads = context.getImageData.mock.calls.length; + const themeStyle = document.createElement('style'); + await act(async () => { document.head.append(themeStyle); }); + expect(context.getImageData.mock.calls.length).toBeGreaterThan(reads); + unmount(); + const afterUnmount = context.getImageData.mock.calls.length; + await act(async () => { themeStyle.remove(); }); + expect(context.getImageData).toHaveBeenCalledTimes(afterUnmount); + }); + + it('surfaces a missing canvas context rather than using an unverified contrast color', () => { + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}); + const onError = (event: ErrorEvent) => event.preventDefault(); + window.addEventListener('error', onError); + getContext.mockReturnValueOnce(null); + try { + expect(() => render()).toThrow( + 'Unable to calculate progress label contrast: canvas context unavailable.', + ); + } finally { + window.removeEventListener('error', onError); + consoleError.mockRestore(); + } + }); +}); diff --git a/FASTBuildMonitorVSCode/webview-ui/tests/setup.ts b/FASTBuildMonitorVSCode/webview-ui/tests/setup.ts new file mode 100644 index 0000000..6cb6167 --- /dev/null +++ b/FASTBuildMonitorVSCode/webview-ui/tests/setup.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import '@testing-library/jest-dom/vitest'; +import { cleanup } from '@testing-library/react'; +import { afterEach, beforeEach, vi } from 'vitest'; + +export const vscodeApi = { + postMessage: vi.fn(), + getState: vi.fn(), + setState: vi.fn(), +}; +vi.stubGlobal('acquireVsCodeApi', () => vscodeApi); + +export class ResizeObserverMock implements ResizeObserver { + static instances: ResizeObserverMock[] = []; + constructor(private callback: ResizeObserverCallback) { + ResizeObserverMock.instances.push(this); + } + observe = vi.fn(); + unobserve = vi.fn(); + disconnect = vi.fn(); + trigger() { this.callback([], this); } +} + +export function createCanvasContext() { + return { + fillStyle: '', strokeStyle: '', lineWidth: 1, font: '', textAlign: '', textBaseline: '', + beginPath: vi.fn(), closePath: vi.fn(), moveTo: vi.fn(), lineTo: vi.fn(), + quadraticCurveTo: vi.fn(), rect: vi.fn(), clip: vi.fn(), fill: vi.fn(), + stroke: vi.fn(), fillRect: vi.fn(), save: vi.fn(), restore: vi.fn(), + setTransform: vi.fn(), translate: vi.fn(), fillText: vi.fn(), + getImageData: vi.fn(() => ({ data: new Uint8ClampedArray([0, 0, 0, 255]) })), + }; +} + +let contexts = new WeakMap>(); +export function canvasContext(canvas: HTMLCanvasElement) { + const context = contexts.get(canvas); + if (!context) throw new Error('The component has not requested a 2D context for this canvas'); + return context; +} + +const originalGetContext = Object.getOwnPropertyDescriptor(HTMLCanvasElement.prototype, 'getContext')!; +export const getContext = vi.fn(function (this: HTMLCanvasElement, kind: string): ReturnType | null { + if (kind !== '2d') throw new Error(`Unsupported test canvas context: ${kind}`); + let context = contexts.get(this); + if (!context) { + context = createCanvasContext(); + contexts.set(this, context); + } + return context; +}); + +beforeEach(() => { + contexts = new WeakMap(); + ResizeObserverMock.instances = []; + vi.stubGlobal('acquireVsCodeApi', () => vscodeApi); + vi.stubGlobal('ResizeObserver', ResizeObserverMock); + Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', { + configurable: true, writable: true, value: getContext, + }); + document.body.className = ''; + document.body.removeAttribute('style'); +}); + +afterEach(() => { + cleanup(); + vi.useRealTimers(); + vi.unstubAllGlobals(); + Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', originalGetContext); +}); diff --git a/FASTBuildMonitorVSCode/webview-ui/tests/timeline.test.tsx b/FASTBuildMonitorVSCode/webview-ui/tests/timeline.test.tsx new file mode 100644 index 0000000..bee1c51 --- /dev/null +++ b/FASTBuildMonitorVSCode/webview-ui/tests/timeline.test.tsx @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { act, fireEvent, render } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import Timeline from '../src/monitor_panel/components/Timeline'; +import { BuildJobStatus } from '../src/monitor_panel/types'; +import { canvasContext, getContext, ResizeObserverMock } from './setup'; +import { job, session } from './fixtures'; + +beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(11000); + vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(1000); +}); + +function canvas(container: HTMLElement, selector: string) { + const element = container.querySelector(selector); + if (!element) throw new Error(`Missing canvas: ${selector}`); + return element; +} + +describe('timeline rendering and interaction', () => { + it('renders an empty state and releases resize observers on unmount', () => { + const { container, unmount } = render(); + const body = canvas(container, '.timeline-body canvas'); + expect(canvasContext(body).fillText).toHaveBeenCalledWith('Waiting for FASTBuild data...', 410, 84); + const observer = ResizeObserverMock.instances[0]; + expect(observer.observe).toHaveBeenCalledWith(container.firstChild); + act(() => observer.trigger()); + expect(canvasContext(body).fillText).toHaveBeenCalledTimes(2); + fireEvent.mouseMove(body, { clientX: 10, clientY: 10 }); + expect(body.title).toBe(''); + unmount(); + expect(observer.disconnect).toHaveBeenCalledOnce(); + }); + + it('allocates cores by peak concurrency, merges host casing and puts local rows first', () => { + const jobs = [ + job({ hostName: 'worker-b', startTime: 3000, endTime: 6000 }), + job({ hostName: 'local', startTime: 2000, endTime: 5000 }), + job({ hostName: 'worker-a', startTime: 1000, endTime: 3000 }), + job({ hostName: 'LOCAL', startTime: 1000, endTime: 4000 }), + job({ hostName: 'WORKER-A', startTime: 3000, endTime: 5000 }), + job({ hostName: 'worker-b', startTime: 4000, endTime: 6000 }), + ]; + const original = [...jobs]; + const { container } = render(); + const labels = canvasContext(canvas(container, '.timeline-labels canvas')); + expect(labels.fillText.mock.calls.map(([text]) => text)).toEqual([ + 'LOCAL (Core # 0)', 'LOCAL (Core # 1)', 'worker-a (Core # 0)', + 'worker-b (Core # 0)', 'worker-b (Core # 1)', + ]); + expect(jobs).toEqual(original); + expect(canvasContext(canvas(container, '.timeline-body canvas')).fill).toHaveBeenCalledTimes(6); + }); + + it('keeps separate rows for overlapping active jobs and preserves rows after completion', () => { + const jobs = [ + job({ endTime: undefined, status: BuildJobStatus.Building }), + job({ startTime: 2000, endTime: undefined, status: BuildJobStatus.Building }), + ]; + const { container, rerender } = render( + , + ); + let labels = canvasContext(canvas(container, '.timeline-labels canvas')); + expect(labels.fillText).toHaveBeenCalledTimes(2); + labels.fillText.mockClear(); + rerender( ({ ...j, endTime: 11000, status: BuildJobStatus.Stopped })) })} + timeScale={20} onTimeScaleChange={vi.fn()} />); + labels = canvasContext(canvas(container, '.timeline-labels canvas')); + expect(labels.fillText).toHaveBeenCalledTimes(2); + }); + + it('shows and clears tooltips using the hovered row, filename, host, status and duration', () => { + const { container } = render(); + const body = canvas(container, '.timeline-body canvas'); + fireEvent.mouseMove(body, { clientX: 10, clientY: 10 }); + expect(body.title).toBe('file.cpp (local) - Success - 5.0s'); + fireEvent.mouseMove(body, { clientX: 10, clientY: 40 }); + expect(body.title).toBe('file.cpp (remote) - Success - 5.0s'); + fireEvent.mouseMove(body, { clientX: 300, clientY: 40 }); + expect(body.title).toBe(''); + fireEvent.mouseMove(body, { clientX: 10, clientY: 120 }); + expect(body.title).toBe(''); + }); + + it('converts viewport hover coordinates into scrolled content coordinates', () => { + const { container } = render(); + const scroller = container.querySelector('.timeline-container')!; + scroller.scrollLeft = 500; + const body = canvas(container, '.timeline-body canvas'); + fireEvent.mouseMove(body, { clientX: 10, clientY: 10 }); + expect(body.title).toContain('file.cpp (local)'); + }); + + it('zooms only with Ctrl+wheel and clamps scales to 1 through 200', () => { + const change = vi.fn(); + const { container, rerender } = render(); + const scroller = container.querySelector('.timeline-container')!; + fireEvent.wheel(scroller, { deltaY: -1 }); + expect(change).not.toHaveBeenCalled(); + fireEvent.wheel(scroller, { deltaY: -1, ctrlKey: true }); + expect(change).toHaveBeenLastCalledWith(23); + fireEvent.wheel(scroller, { deltaY: 1, ctrlKey: true }); + expect(change).toHaveBeenLastCalledWith(20 / 1.15); + rerender(); + fireEvent.wheel(scroller, { deltaY: -1, ctrlKey: true }); + expect(change).toHaveBeenLastCalledWith(200); + rerender(); + fireEvent.wheel(scroller, { deltaY: 1, ctrlKey: true }); + expect(change).toHaveBeenLastCalledWith(1); + }); + + it.each([1, 7, 15, 30, 50])('renders time-axis ticks at scale %i', timeScale => { + const { container } = render(); + const header = canvasContext(canvas(container, '.timeline-header canvas')); + expect(header.fillText).toHaveBeenCalledWith('0:00', 0, 16); + }); + + it('keeps large, zoomed timelines viewport-sized and formats hour labels', () => { + vi.stubGlobal('devicePixelRatio', 2); + const { container } = render(); + const body = canvas(container, '.timeline-body canvas'); + const header = canvas(container, '.timeline-header canvas'); + expect(body.width).toBe(1640); + expect(header.width).toBe(1640); + expect(container.querySelector('.timeline-body')?.style.width).toBe('1440100px'); + expect(canvasContext(body).setTransform).toHaveBeenCalledWith(2, 0, 0, 2, 0, 0); + expect(canvasContext(header).fillText.mock.calls.some(([text]) => String(text).startsWith('2:00:'))).toBe(true); + expect(canvasContext(body).fill).not.toHaveBeenCalled(); + }); + + it('stops auto-scroll when the user scrolls back and resumes near the right edge', () => { + vi.spyOn(HTMLElement.prototype, 'scrollWidth', 'get').mockReturnValue(10000); + const build = session({ endTime: 101000 }); + const { container, rerender } = render(); + const scroller = container.querySelector('.timeline-container')!; + expect(scroller.scrollLeft).toBe(1280); + const body = canvasContext(canvas(container, '.timeline-body canvas')); + const calls = body.translate.mock.calls.length; + fireEvent.scroll(scroller); + expect(body.translate).toHaveBeenCalledTimes(calls); + scroller.scrollLeft = 50; + fireEvent.scroll(scroller); + rerender(); + expect(scroller.scrollLeft).toBe(50); + scroller.scrollLeft = 9000; + fireEvent.scroll(scroller); + expect(scroller.scrollLeft).toBe(3280); + }); + + it('handles sub-second jobs and job end times beyond the reported session end', () => { + const { container } = render(); + const body = canvasContext(canvas(container, '.timeline-body canvas')); + expect(body.fill).toHaveBeenCalledTimes(2); + expect(body.fillText).toHaveBeenCalledWith('file.cpp', 3.5, 42); + expect(body.fillStyle).toBe('#000'); + }); + + it('does not draw when the browser cannot provide a canvas context', () => { + getContext.mockReturnValueOnce(null); + const { container } = render(); + expect(canvasContext(canvas(container, '.timeline-body canvas')).fillText).not.toHaveBeenCalled(); + }); +}); diff --git a/FASTBuildMonitorVSCode/webview-ui/tests/types.test.ts b/FASTBuildMonitorVSCode/webview-ui/tests/types.test.ts new file mode 100644 index 0000000..551258c --- /dev/null +++ b/FASTBuildMonitorVSCode/webview-ui/tests/types.test.ts @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, expect, it } from 'vitest'; +import { + BuildJobStatus, formatDuration, formatElapsed, getDisplayName, resolveColor, + STATUS_COLORS, STATUS_TEXT_COLORS, TIMELINE_COLORS, +} from '../src/monitor_panel/types'; +import { formatElapsedCompact } from '../src/monitor_panel/components/Toolbar'; +import { BuildJobStatus as ExtensionStatus, getStatusColor } from '../../src/models'; + +describe('webview formatting and colors', () => { + it.each([ + ['file.cpp', 'file.cpp'], ['src/file.cpp', 'file.cpp'], + ['C:\\src\\file.cpp', 'file.cpp'], ['src\\nested/file.cpp', 'file.cpp'], ['', ''], + ])('extracts %s filenames', (input, expected) => expect(getDisplayName(input)).toBe(expected)); + + it.each([[0, '0.0s'], [1250, '1.3s'], [60000, '1:00.0'], [125600, '2:05.6']])( + 'formats %i millisecond durations', (input, expected) => expect(formatDuration(input)).toBe(expected), + ); + + it.each([[0, '00:00:00'], [59999, '00:00:59'], [61000, '00:01:01'], [3661000, '01:01:01'], [360000000, '100:00:00']])( + 'formats %i elapsed milliseconds consistently', (input, expected) => { + expect(formatElapsed(input)).toBe(expected); + expect(formatElapsedCompact(input)).toBe(expected); + }, + ); + + it('resolves theme variables and provides explicit fallbacks', () => { + const style = document.createElement('div').style; + style.setProperty('--text', ' #abcdef '); + expect(resolveColor(style, '--text')).toBe('#abcdef'); + expect(resolveColor(style, '--bg')).toBe('#00000000'); + expect(resolveColor(style, '--unknown')).toBe('#000'); + for (const variable of Object.values(TIMELINE_COLORS)) { + expect(variable).toMatch(/^--/); + } + }); + + it('keeps status names and fill colors synchronized with the extension model', () => { + expect(Object.values(BuildJobStatus)).toEqual(Object.values(ExtensionStatus)); + for (const status of Object.values(ExtensionStatus)) { + expect(STATUS_COLORS[status]).toBe(getStatusColor(status)); + expect(STATUS_TEXT_COLORS[status]).toMatch(/^var\(--status-/); + } + }); + + it('keeps black chart labels at a contrast ratio of at least 4.5 for every job color', () => { + for (const [status, color] of Object.entries(STATUS_COLORS)) { + const [r, g, b] = [1, 3, 5].map(offset => { + const channel = parseInt(color.slice(offset, offset + 2), 16) / 255; + return channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4; + }); + const ratio = (0.2126 * r + 0.7152 * g + 0.0722 * b + 0.05) / 0.05; + expect(ratio, status).toBeGreaterThanOrEqual(4.5); + } + }); +}); From 09b108719ff5e562461d3c60b60f2f3c88b1b9ba Mon Sep 17 00:00:00 2001 From: Francisco Alejandro Garcia Cebada Date: Mon, 12 Oct 2026 19:23:23 -0700 Subject: [PATCH 2/3] - Updating readme file --- FASTBuildMonitorVSCode/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/FASTBuildMonitorVSCode/README.md b/FASTBuildMonitorVSCode/README.md index 51dea37..292aac2 100644 --- a/FASTBuildMonitorVSCode/README.md +++ b/FASTBuildMonitorVSCode/README.md @@ -51,3 +51,4 @@ | `fbuildmonitor.logPath` | `""` | Custom path to `FastBuildLog.log`. Empty = auto-detect from `%TEMP%\FASTBuild\` or `FASTBUILD_TEMP_PATH`. | | `fbuildmonitor.pollInterval` | `500` | Log file poll interval in milliseconds (100–5000). | | `fbuildmonitor.autoStart` | `true` | Auto-start monitoring when the panel opens. | +| `fbuildMonitor.useLegacyColors` | `false` | Use the original fixed dark color scheme instead of colors derived from the active VS Code theme. | From ed8aaf50e12fcbbd8c2651c8aed4b6cb959010f8 Mon Sep 17 00:00:00 2001 From: Francisco Alejandro Garcia Cebada Date: Mon, 12 Oct 2026 19:39:53 -0700 Subject: [PATCH 3/3] - Updating actions/upload-artifact to v7 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 810dc46..884fc87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: - run: npm run test:coverage - name: Upload coverage report if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: coverage path: FASTBuildMonitorVSCode/coverage