[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat: support merge reports for non-sharded multi-environment runs (take 2) (#10031)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Ari Perkkiö <ari.perkkio@gmail.com>

authored by

Hiroshi Ogawa
Claude Sonnet 4.6
Codex
Ari Perkkiö
and committed by
GitHub
(May 4, 2026, 7:27 AM +0200) e60b2f49 c99d18f8

+902 -203
+59
.github/workflows/ci.yml
··· 116 116 117 117 - name: Test 118 118 run: pnpm run test:ci 119 + env: 120 + VITEST_CI_BLOB_LABEL: ${{ matrix.os }}-node-${{ matrix.node_version }} 119 121 120 122 - name: Test Examples 121 123 run: pnpm run test:examples ··· 129 131 name: playwright-report-${{ matrix.os }}-node-${{ matrix.node_version }} 130 132 path: test/ui/test-results/ 131 133 retention-days: 30 134 + 135 + - uses: actions/upload-artifact@v7 136 + if: ${{ !cancelled() }} 137 + with: 138 + name: vitest-blob-${{ matrix.os }}-node-${{ matrix.node_version }} 139 + path: | 140 + README.md 141 + test/unit/.vitest-reports 142 + test/e2e/.vitest-reports 143 + retention-days: 1 144 + include-hidden-files: true 132 145 133 146 test-cached: 134 147 needs: changed ··· 251 264 name: playwright-report-rolldown 252 265 path: rolldown/test/ui/test-results/ 253 266 retention-days: 30 267 + 268 + merge-reports: 269 + needs: test 270 + if: ${{ !cancelled() }} 271 + runs-on: ubuntu-latest 272 + name: Merge Reports 273 + timeout-minutes: 10 274 + steps: 275 + - uses: actions/checkout@v6 276 + 277 + - uses: ./.github/actions/setup-and-cache 278 + 279 + - name: Install 280 + run: pnpm i 281 + 282 + - name: Build 283 + run: pnpm run build 284 + 285 + - uses: actions/download-artifact@v4 286 + with: 287 + pattern: vitest-blob-* 288 + merge-multiple: true 289 + 290 + - name: Merge reports 291 + continue-on-error: true 292 + run: pnpm --filter=./test/unit --filter=./test/e2e --no-bail --sequential test --merge-reports --reporter=html 293 + 294 + - name: Merge reports html 295 + id: merge-html 296 + run: | 297 + mkdir -p html-all 298 + cp -rf test/unit/html html-all/unit 299 + cp -rf test/e2e/html html-all/e2e 300 + echo "short_sha=${GITHUB_SHA:0:7}" >> $GITHUB_OUTPUT 301 + 302 + - uses: actions/upload-artifact@v7 303 + id: upload-report 304 + with: 305 + name: vitest-ci-report-${{ steps.merge-html.outputs.short_sha }} 306 + path: html-all 307 + retention-days: 7 308 + 309 + - name: Link report viewer 310 + run: | 311 + echo "::notice title=Vitest HTML report::View HTML report: https://viewer.vitest.dev/?url=${{ steps.upload-report.outputs.artifact-url }}" 312 + echo "[View HTML report](https://viewer.vitest.dev/?url=${{ steps.upload-report.outputs.artifact-url }})" >> $GITHUB_STEP_SUMMARY
+1 -1
docs/guide/cli.md
··· 225 225 ::: 226 226 227 227 ::: tip 228 - If `--reporter=blob` is used without an output file, the default path will include the current shard config to avoid collisions with other Vitest processes. 228 + If `--reporter=blob` is used without an output file, the default path will include the current shard config and blob label from `VITEST_BLOB_LABEL` or the blob reporter `label` option to avoid collisions with other Vitest processes. 229 229 ::: 230 230 231 231 ### merge-reports
+12 -3
docs/guide/improving-performance.md
··· 132 132 vitest run --merge-reports 133 133 ``` 134 134 135 + When running the same shards across multiple environments, set the `VITEST_BLOB_LABEL` environment variable so merged reports can display them separately: 136 + 137 + ```sh 138 + VITEST_BLOB_LABEL=linux vitest run --reporter=blob --shard=1/3 139 + ``` 140 + 135 141 ::: details GitHub Actions example 136 142 This setup is also used at https://github.com/vitest-tests/test-sharding. 137 143 ··· 144 150 - main 145 151 jobs: 146 152 tests: 147 - runs-on: ubuntu-latest 153 + runs-on: ${{ matrix.os }} 148 154 strategy: 149 155 matrix: 156 + os: [ubuntu-latest, macos-latest] 150 157 shardIndex: [1, 2, 3, 4] 151 158 shardTotal: [4] 152 159 steps: ··· 163 170 164 171 - name: Run tests 165 172 run: pnpm run test --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} 173 + env: 174 + VITEST_BLOB_LABEL: ${{ matrix.os }} 166 175 167 176 - name: Upload blob report to GitHub Actions Artifacts 168 177 if: ${{ !cancelled() }} 169 178 uses: actions/upload-artifact@v4 170 179 with: 171 - name: blob-report-${{ matrix.shardIndex }} 180 + name: blob-report-${{ matrix.os }}-${{ matrix.shardIndex }} 172 181 path: .vitest-reports/* 173 182 include-hidden-files: true 174 183 retention-days: 1 ··· 177 186 if: ${{ !cancelled() }} 178 187 uses: actions/upload-artifact@v4 179 188 with: 180 - name: blob-attachments-${{ matrix.shardIndex }} 189 + name: blob-attachments-${{ matrix.os }}-${{ matrix.shardIndex }} 181 190 path: .vitest/** 182 191 include-hidden-files: true 183 192 retention-days: 1
+21 -2
docs/guide/reporters.md
··· 748 748 npx vitest --reporter=blob --outputFile=reports/blob-1.json 749 749 ``` 750 750 751 - We recommend using this reporter if you are running Vitest on different machines with the [`--shard`](/guide/cli#shard) flag. 752 - All blob reports can be merged into any report by using `--merge-reports` command at the end of your CI pipeline: 751 + We recommend using this reporter if you are running Vitest on different machines with the [`--shard`](/guide/cli#shard) flag or across multiple environments (e.g., linux/macos/windows). All blob reports can be merged into any report by using `--merge-reports` command at the end of your CI pipeline: 753 752 754 753 ```bash 755 754 npx vitest --merge-reports=reports --reporter=json --reporter=default 755 + ``` 756 + 757 + When running the same tests across multiple environments, use the `VITEST_BLOB_LABEL` environment variable to distinguish each environment's blob. Vitest reads labels at merge time and displays results separately: 758 + 759 + ```bash 760 + VITEST_BLOB_LABEL=linux vitest run --reporter=blob 761 + ``` 762 + 763 + You can also provide the label via the blob reporter option. This has higher priority than `VITEST_BLOB_LABEL`. 764 + 765 + ```ts [vitest.config.ts] 766 + import { defineConfig } from 'vitest/config' 767 + 768 + export default defineConfig({ 769 + test: { 770 + reporters: [ 771 + ['blob', { label: 'linux' }], 772 + ], 773 + }, 774 + }) 756 775 ``` 757 776 758 777 Blob reporter output doesn't include file-based [attachments](/api/advanced/artifacts.html#testattachment).
+6 -1
test/e2e/vitest.config.ts
··· 4 4 test: { 5 5 include: ['test/**/**.{test,spec}.ts'], 6 6 includeTaskLocation: true, 7 - reporters: ['verbose'], 7 + reporters: [ 8 + 'verbose', 9 + ...(process.env.VITEST_CI_BLOB_LABEL 10 + ? [['blob', { label: process.env.VITEST_CI_BLOB_LABEL }] as any] 11 + : []), 12 + ], 8 13 testTimeout: 60_000, 9 14 isolate: false, 10 15 fileParallelism: false,
+16 -6
test/test-utils/index.ts
··· 1 1 import type { Options } from 'tinyexec' 2 2 import type { UserConfig as ViteUserConfig } from 'vite' 3 - import type { SerializedConfig, TestContext, WorkerGlobalState } from 'vitest' 3 + import type { ParsedStack, SerializedConfig, TestContext, WorkerGlobalState } from 'vitest' 4 4 import type { TestProjectConfiguration } from 'vitest/config' 5 5 import type { 6 6 TestCase, ··· 243 243 get results() { 244 244 return ctx?.state.getTestModules() || [] 245 245 }, 246 - errorTree(options?: { project?: boolean; stackTrace?: boolean; diff?: boolean }) { 246 + errorTree(options?: BuildErrorTreeOptions & { project?: boolean }) { 247 247 const modules = ctx?.state.getTestModules() || [] 248 248 const tree = options?.project 249 249 ? buildErrorProjectTree(modules, options) ··· 572 572 } 573 573 } 574 574 575 - export function buildErrorTree(testModules: TestModule[], options?: { stackTrace?: boolean; diff?: boolean }) { 575 + export interface BuildErrorTreeOptions { 576 + stackTrace?: boolean 577 + diff?: boolean 578 + fileLabel?: boolean 579 + } 580 + 581 + export function buildErrorTree(testModules: TestModule[], options?: BuildErrorTreeOptions) { 576 582 const root = testModules[0]?.project.config.root 577 583 578 - function mapError(e: { message: string; diff?: string; stacks?: { file: string; line: number; column: number; method: string }[] }) { 584 + function mapError(e: { message: string; diff?: string; stacks?: ParsedStack[] }) { 579 585 let message = e.message 580 586 if (options?.diff && e.diff) { 581 587 message = [message, stripVTControlCharacters(e.diff)].join('\n') ··· 619 625 } 620 626 return moduleChildren 621 627 }, 628 + options?.fileLabel 629 + ? module => `${module.relativeModuleId} (${module.meta().__vitest_label__})` 630 + : undefined, 622 631 ) 623 632 } 624 633 ··· 627 636 onTestCase?: (result: TestCase) => unknown, 628 637 onTestSuite?: (testSuite: TestSuite, suiteChildren: Record<string, any>) => unknown, 629 638 onTestModule?: (testModule: TestModule, moduleChildren: Record<string, any>) => unknown, 639 + onTestModuleKey?: (testModule: TestModule) => string, 630 640 ) { 631 641 type TestTree = Record<string, any> 632 642 ··· 657 667 658 668 for (const module of testModules) { 659 669 // Use relative module ID for cleaner output 660 - const key = module.relativeModuleId 670 + const key = onTestModuleKey ? onTestModuleKey(module) : module.relativeModuleId 661 671 const moduleChildren = walkCollection(module.children) 662 672 tree[key] = onTestModule ? onTestModule(module, moduleChildren) : moduleChildren 663 673 } ··· 679 689 return projectTree 680 690 } 681 691 682 - export function buildErrorProjectTree(testModules: TestModule[], options?: { stackTrace?: boolean; diff?: boolean }) { 692 + export function buildErrorProjectTree(testModules: TestModule[], options?: BuildErrorTreeOptions) { 683 693 const projectTree: Record<string, Record<string, any>> = {} 684 694 685 695 for (const testModule of testModules) {
+8 -3
test/unit/vite.config.ts
··· 125 125 }, 126 126 }, 127 127 includeTaskLocation: true, 128 - reporters: process.env.GITHUB_ACTIONS 129 - ? ['default', ['github-actions', { displayAnnotations: false }]] 130 - : [['default', { summary: true }], 'hanging-process'], 128 + reporters: [ 129 + ...(process.env.GITHUB_ACTIONS 130 + ? [['default'], ['github-actions', { displayAnnotations: false }] as any] 131 + : [['default', { summary: true }], ['hanging-process']]), 132 + ...(process.env.VITEST_CI_BLOB_LABEL 133 + ? [['blob', { label: process.env.VITEST_CI_BLOB_LABEL }]] 134 + : []), 135 + ], 131 136 testNamePattern: '^((?!does not include test that).)*$', 132 137 coverage: { 133 138 provider: 'istanbul',
+8 -1
packages/runner/src/collect.ts
··· 47 47 48 48 const fileTags: string[] = typeof spec === 'string' ? [] : (spec.fileTags || []) 49 49 50 - const file = createFileTask(filepath, config.root, config.name, runner.pool, runner.viteEnvironment) 50 + const file = createFileTask( 51 + filepath, 52 + config.root, 53 + config.name, 54 + runner.pool, 55 + runner.viteEnvironment, 56 + { __vitest_label__: config.mergeReportsLabel }, 57 + ) 51 58 file.tags = fileTags 52 59 file.shuffle = config.sequence.shuffle 53 60
+12
packages/runner/src/suite.ts
··· 347 347 ...options, 348 348 } 349 349 const timeout = options.timeout ?? runner.config.testTimeout 350 + // TODO: should this be `parentTask.meta`? 351 + // currently we don't inherit 352 + // file.meta -> task.meta 353 + // file.meta -> suite.meta (see initSuite) 354 + // but we do inherit 355 + // suite.meta -> task.meta 356 + // suite.meta -> suite.meta 357 + // and also 358 + // file.tags -> task.tags 359 + // file.tags -> suite.tags 360 + // suite.tags -> suite.tags 361 + // suite.tags -> task.tags 350 362 const parentMeta = currentSuite?.meta 351 363 const tagMeta = tagsOptions.meta 352 364 const testMeta = Object.create(null)
+5
packages/utils/src/helpers.ts
··· 429 429 export function unique<T>(array: T[]): T[] { 430 430 return Array.from(new Set(array)) 431 431 } 432 + 433 + export function sanitizeFilePath(s: string): string { 434 + // eslint-disable-next-line no-control-regex 435 + return s.replace(/[\x00-\x2C\x2E\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-') 436 + }
+15 -9
test/e2e/test/reported-tasks.test.ts
··· 320 320 const testSuite = [...testModule.children.suites()][0] 321 321 const suiteSpec = testSuite.toTestSpecification() 322 322 expect(suiteSpec.moduleId).toBe(testModule.moduleId) 323 - expect(suiteSpec.testIds).toEqual([ 324 - '-1008553841_11_0', 325 - '-1008553841_11_1', 326 - '-1008553841_11_2_0', 327 - '-1008553841_11_2_1', 328 - '-1008553841_11_2_2', 329 - '-1008553841_11_2_3', 330 - ]) 323 + expect(suiteSpec.testIds).toMatchInlineSnapshot(` 324 + [ 325 + "1763725585_11_0", 326 + "1763725585_11_1", 327 + "1763725585_11_2_0", 328 + "1763725585_11_2_1", 329 + "1763725585_11_2_2", 330 + "1763725585_11_2_3", 331 + ] 332 + `) 331 333 expect(suiteSpec.project).toBe(testModule.project) 332 334 333 335 const testCase = testSuite.children.at(0) as TestCase 334 336 const caseSpec = testCase.toTestSpecification() 335 337 expect(caseSpec.moduleId).toBe(testModule.moduleId) 336 - expect(caseSpec.testIds).toEqual(['-1008553841_11_0']) 338 + expect(caseSpec.testIds).toMatchInlineSnapshot(` 339 + [ 340 + "1763725585_11_0", 341 + ] 342 + `) 337 343 expect(caseSpec.project).toBe(testModule.project) 338 344 }) 339 345
+104 -104
test/e2e/test/static-collect.test.ts
··· 24 24 "adds numbers": { 25 25 "errors": [], 26 26 "fullName": "math operations > adds numbers", 27 - "id": "-1732721377_0_0", 27 + "id": "1709388417_0_0", 28 28 "location": "5:6", 29 29 "mode": "run", 30 30 "state": "pending", ··· 32 32 "subtracts numbers": { 33 33 "errors": [], 34 34 "fullName": "math operations > subtracts numbers", 35 - "id": "-1732721377_0_1", 35 + "id": "1709388417_0_1", 36 36 "location": "9:6", 37 37 "mode": "skip", 38 38 "state": "skipped", ··· 57 57 "works with testAnother": { 58 58 "errors": [], 59 59 "fullName": "custom test functions > works with testAnother", 60 - "id": "-1732721377_0_1", 60 + "id": "1709388417_0_1", 61 61 "location": "6:6", 62 62 "mode": "run", 63 63 "state": "pending", ··· 65 65 "works with testSomething": { 66 66 "errors": [], 67 67 "fullName": "custom test functions > works with testSomething", 68 - "id": "-1732721377_0_0", 68 + "id": "1709388417_0_0", 69 69 "location": "5:6", 70 70 "mode": "run", 71 71 "state": "pending", ··· 90 90 "works with integrationTest": { 91 91 "errors": [], 92 92 "fullName": "custom test functions > works with integrationTest", 93 - "id": "-1732721377_0_1", 93 + "id": "1709388417_0_1", 94 94 "location": "6:6", 95 95 "mode": "run", 96 96 "state": "pending", ··· 98 98 "works with unitTest": { 99 99 "errors": [], 100 100 "fullName": "custom test functions > works with unitTest", 101 - "id": "-1732721377_0_0", 101 + "id": "1709388417_0_0", 102 102 "location": "5:6", 103 103 "mode": "run", 104 104 "state": "pending", ··· 124 124 "barTest with only": { 125 125 "errors": [], 126 126 "fullName": "only tests > barTest with only", 127 - "id": "-1732721377_0_2", 127 + "id": "1709388417_0_2", 128 128 "location": "7:6", 129 129 "mode": "run", 130 130 "state": "pending", ··· 132 132 "regular test with only": { 133 133 "errors": [], 134 134 "fullName": "only tests > regular test with only", 135 - "id": "-1732721377_0_0", 135 + "id": "1709388417_0_0", 136 136 "location": "5:6", 137 137 "mode": "run", 138 138 "state": "pending", ··· 140 140 "testFoo with only": { 141 141 "errors": [], 142 142 "fullName": "only tests > testFoo with only", 143 - "id": "-1732721377_0_1", 143 + "id": "1709388417_0_1", 144 144 "location": "6:6", 145 145 "mode": "run", 146 146 "state": "pending", ··· 166 166 "barTest with skip": { 167 167 "errors": [], 168 168 "fullName": "skip tests > barTest with skip", 169 - "id": "-1732721377_0_2", 169 + "id": "1709388417_0_2", 170 170 "location": "7:6", 171 171 "mode": "skip", 172 172 "state": "skipped", ··· 174 174 "regular test with skip": { 175 175 "errors": [], 176 176 "fullName": "skip tests > regular test with skip", 177 - "id": "-1732721377_0_0", 177 + "id": "1709388417_0_0", 178 178 "location": "5:6", 179 179 "mode": "skip", 180 180 "state": "skipped", ··· 182 182 "testFoo with skip": { 183 183 "errors": [], 184 184 "fullName": "skip tests > testFoo with skip", 185 - "id": "-1732721377_0_1", 185 + "id": "1709388417_0_1", 186 186 "location": "6:6", 187 187 "mode": "skip", 188 188 "state": "skipped", ··· 208 208 "barTest with todo": { 209 209 "errors": [], 210 210 "fullName": "todo tests > barTest with todo", 211 - "id": "-1732721377_0_2", 211 + "id": "1709388417_0_2", 212 212 "location": "7:6", 213 213 "mode": "todo", 214 214 "state": "skipped", ··· 216 216 "regular test with todo": { 217 217 "errors": [], 218 218 "fullName": "todo tests > regular test with todo", 219 - "id": "-1732721377_0_0", 219 + "id": "1709388417_0_0", 220 220 "location": "5:6", 221 221 "mode": "todo", 222 222 "state": "skipped", ··· 224 224 "testFoo with todo": { 225 225 "errors": [], 226 226 "fullName": "todo tests > testFoo with todo", 227 - "id": "-1732721377_0_1", 227 + "id": "1709388417_0_1", 228 228 "location": "6:6", 229 229 "mode": "todo", 230 230 "state": "skipped", ··· 258 258 "integration test": { 259 259 "errors": [], 260 260 "fullName": "outer suite > inner suite > deeply nested > integration test", 261 - "id": "-1732721377_0_1_1_0", 261 + "id": "1709388417_0_1_1_0", 262 262 "location": "11:10", 263 263 "mode": "run", 264 264 "state": "pending", ··· 267 267 "unit test": { 268 268 "errors": [], 269 269 "fullName": "outer suite > inner suite > unit test", 270 - "id": "-1732721377_0_1_0", 270 + "id": "1709388417_0_1_0", 271 271 "location": "8:8", 272 272 "mode": "run", 273 273 "state": "pending", ··· 276 276 "regular test": { 277 277 "errors": [], 278 278 "fullName": "outer suite > regular test", 279 - "id": "-1732721377_0_0", 279 + "id": "1709388417_0_0", 280 280 "location": "5:6", 281 281 "mode": "run", 282 282 "state": "pending", ··· 311 311 "only extended test": { 312 312 "errors": [], 313 313 "fullName": "extended tests > only extended test", 314 - "id": "-1732721377_0_2", 314 + "id": "1709388417_0_2", 315 315 "location": "16:6", 316 316 "mode": "run", 317 317 "state": "pending", ··· 319 319 "skips extended test": { 320 320 "errors": [], 321 321 "fullName": "extended tests > skips extended test", 322 - "id": "-1732721377_0_1", 322 + "id": "1709388417_0_1", 323 323 "location": "15:6", 324 324 "mode": "skip", 325 325 "state": "skipped", ··· 327 327 "uses extended test": { 328 328 "errors": [], 329 329 "fullName": "extended tests > uses extended test", 330 - "id": "-1732721377_0_0", 330 + "id": "1709388417_0_0", 331 331 "location": "14:6", 332 332 "mode": "skip", 333 333 "state": "skipped", ··· 359 359 "only extended test": { 360 360 "errors": [], 361 361 "fullName": "extended tests > only extended test", 362 - "id": "-1732721377_0_2", 362 + "id": "1709388417_0_2", 363 363 "location": "13:6", 364 364 "mode": "run", 365 365 "state": "pending", ··· 367 367 "skips extended test": { 368 368 "errors": [], 369 369 "fullName": "extended tests > skips extended test", 370 - "id": "-1732721377_0_1", 370 + "id": "1709388417_0_1", 371 371 "location": "12:6", 372 372 "mode": "skip", 373 373 "state": "skipped", ··· 375 375 "uses extended test": { 376 376 "errors": [], 377 377 "fullName": "extended tests > uses extended test", 378 - "id": "-1732721377_0_0", 378 + "id": "1709388417_0_0", 379 379 "location": "11:6", 380 380 "mode": "skip", 381 381 "state": "skipped", ··· 401 401 "only imported test": { 402 402 "errors": [], 403 403 "fullName": "imported test function > only imported test", 404 - "id": "-1732721377_0_2", 404 + "id": "1709388417_0_2", 405 405 "location": "7:6", 406 406 "mode": "run", 407 407 "state": "pending", ··· 409 409 "skips imported test": { 410 410 "errors": [], 411 411 "fullName": "imported test function > skips imported test", 412 - "id": "-1732721377_0_1", 412 + "id": "1709388417_0_1", 413 413 "location": "6:6", 414 414 "mode": "skip", 415 415 "state": "skipped", ··· 417 417 "uses imported test": { 418 418 "errors": [], 419 419 "fullName": "imported test function > uses imported test", 420 - "id": "-1732721377_0_0", 420 + "id": "1709388417_0_0", 421 421 "location": "5:6", 422 422 "mode": "skip", 423 423 "state": "skipped", ··· 431 431 const testModule = await collectTests(` 432 432 import { describe } from 'vitest'; 433 433 import { it } from './Utils/test-extend.ts'; 434 - 434 + 435 435 vi.mock('@/composables/test.js', async (importOriginal) => { }); 436 - 436 + 437 437 describe('should included', () => { 438 438 it('is included', () => {}); 439 439 }); ··· 450 450 "is included": { 451 451 "errors": [], 452 452 "fullName": "should included > is included", 453 - "id": "-1732721377_0_0", 453 + "id": "1709388417_0_0", 454 454 "location": "8:6", 455 455 "mode": "run", 456 456 "state": "pending", ··· 461 461 "is included": { 462 462 "errors": [], 463 463 "fullName": "top level describe > nested describe > is included", 464 - "id": "-1732721377_1_0_0", 464 + "id": "1709388417_1_0_0", 465 465 "location": "13:8", 466 466 "mode": "run", 467 467 "state": "pending", ··· 476 476 const testModule = await collectTests(` 477 477 import { describe } from 'vitest'; 478 478 import { it } from './Utils/test-extend.ts'; 479 - 479 + 480 480 // vi.mock('@/composables/test.js', async (importOriginal) => { }); 481 - 481 + 482 482 describe('should included', () => { 483 483 it('is included', () => {}); 484 484 }); ··· 496 496 "is included": { 497 497 "errors": [], 498 498 "fullName": "should included > is included", 499 - "id": "-1732721377_0_0", 499 + "id": "1709388417_0_0", 500 500 "location": "8:6", 501 501 "mode": "run", 502 502 "state": "pending", ··· 507 507 "is included": { 508 508 "errors": [], 509 509 "fullName": "top level describe > nested describe > is included", 510 - "id": "-1732721377_1_0_0", 510 + "id": "1709388417_1_0_0", 511 511 "location": "13:8", 512 512 "mode": "run", 513 513 "state": "pending", ··· 535 535 "classic it syntax": { 536 536 "errors": [], 537 537 "fullName": "mixed tests > classic it syntax", 538 - "id": "-1732721377_0_0", 538 + "id": "1709388417_0_0", 539 539 "location": "5:6", 540 540 "mode": "run", 541 541 "state": "pending", ··· 543 543 "ends with Test": { 544 544 "errors": [], 545 545 "fullName": "mixed tests > ends with Test", 546 - "id": "-1732721377_0_3", 546 + "id": "1709388417_0_3", 547 547 "location": "8:6", 548 548 "mode": "run", 549 549 "state": "pending", ··· 551 551 "standard test": { 552 552 "errors": [], 553 553 "fullName": "mixed tests > standard test", 554 - "id": "-1732721377_0_1", 554 + "id": "1709388417_0_1", 555 555 "location": "6:6", 556 556 "mode": "run", 557 557 "state": "pending", ··· 559 559 "starts with test": { 560 560 "errors": [], 561 561 "fullName": "mixed tests > starts with test", 562 - "id": "-1732721377_0_2", 562 + "id": "1709388417_0_2", 563 563 "location": "7:6", 564 564 "mode": "run", 565 565 "state": "pending", ··· 586 586 "normal custom test": { 587 587 "errors": [], 588 588 "fullName": "custom test with modifiers > normal custom test", 589 - "id": "-1732721377_0_0", 589 + "id": "1709388417_0_0", 590 590 "location": "5:6", 591 591 "mode": "skip", 592 592 "state": "skipped", ··· 594 594 "only custom test": { 595 595 "errors": [], 596 596 "fullName": "custom test with modifiers > only custom test", 597 - "id": "-1732721377_0_2", 597 + "id": "1709388417_0_2", 598 598 "location": "7:6", 599 599 "mode": "run", 600 600 "state": "pending", ··· 602 602 "skipped custom test": { 603 603 "errors": [], 604 604 "fullName": "custom test with modifiers > skipped custom test", 605 - "id": "-1732721377_0_1", 605 + "id": "1709388417_0_1", 606 606 "location": "6:6", 607 607 "mode": "skip", 608 608 "state": "skipped", ··· 610 610 "todo custom test": { 611 611 "errors": [], 612 612 "fullName": "custom test with modifiers > todo custom test", 613 - "id": "-1732721377_0_3", 613 + "id": "1709388417_0_3", 614 614 "location": "8:6", 615 615 "mode": "todo", 616 616 "state": "skipped", ··· 636 636 "barTest in skipped suite": { 637 637 "errors": [], 638 638 "fullName": "skipped suite > barTest in skipped suite", 639 - "id": "-1732721377_0_2", 639 + "id": "1709388417_0_2", 640 640 "location": "7:6", 641 641 "mode": "skip", 642 642 "state": "skipped", ··· 644 644 "regular test in skipped suite": { 645 645 "errors": [], 646 646 "fullName": "skipped suite > regular test in skipped suite", 647 - "id": "-1732721377_0_0", 647 + "id": "1709388417_0_0", 648 648 "location": "5:6", 649 649 "mode": "skip", 650 650 "state": "skipped", ··· 652 652 "testFoo in skipped suite": { 653 653 "errors": [], 654 654 "fullName": "skipped suite > testFoo in skipped suite", 655 - "id": "-1732721377_0_1", 655 + "id": "1709388417_0_1", 656 656 "location": "6:6", 657 657 "mode": "skip", 658 658 "state": "skipped", ··· 678 678 "barTest in only suite": { 679 679 "errors": [], 680 680 "fullName": "only suite > barTest in only suite", 681 - "id": "-1732721377_0_2", 681 + "id": "1709388417_0_2", 682 682 "location": "7:6", 683 683 "mode": "run", 684 684 "state": "pending", ··· 686 686 "regular test in only suite": { 687 687 "errors": [], 688 688 "fullName": "only suite > regular test in only suite", 689 - "id": "-1732721377_0_0", 689 + "id": "1709388417_0_0", 690 690 "location": "5:6", 691 691 "mode": "run", 692 692 "state": "pending", ··· 694 694 "testFoo in only suite": { 695 695 "errors": [], 696 696 "fullName": "only suite > testFoo in only suite", 697 - "id": "-1732721377_0_1", 697 + "id": "1709388417_0_1", 698 698 "location": "6:6", 699 699 "mode": "run", 700 700 "state": "pending", ··· 722 722 "each": true, 723 723 "errors": [], 724 724 "fullName": "each tests > barTest with each %i", 725 - "id": "-1732721377_0_2-dynamic", 725 + "id": "1709388417_0_2-dynamic", 726 726 "location": "7:28", 727 727 "mode": "run", 728 728 "state": "pending", ··· 732 732 "each": true, 733 733 "errors": [], 734 734 "fullName": "each tests > test with each %i", 735 - "id": "-1732721377_0_0-dynamic", 735 + "id": "1709388417_0_0-dynamic", 736 736 "location": "5:25", 737 737 "mode": "run", 738 738 "state": "pending", ··· 742 742 "each": true, 743 743 "errors": [], 744 744 "fullName": "each tests > testFoo with each %i", 745 - "id": "-1732721377_0_1-dynamic", 745 + "id": "1709388417_0_1-dynamic", 746 746 "location": "6:28", 747 747 "mode": "run", 748 748 "state": "pending", ··· 782 782 "first integration": { 783 783 "errors": [], 784 784 "fullName": "root suite > integration tests > first integration", 785 - "id": "-1732721377_0_2_0", 785 + "id": "1709388417_0_2_0", 786 786 "location": "17:8", 787 787 "mode": "run", 788 788 "state": "pending", ··· 790 790 "focused integration": { 791 791 "errors": [], 792 792 "fullName": "root suite > integration tests > focused integration", 793 - "id": "-1732721377_0_2_1", 793 + "id": "1709388417_0_2_1", 794 794 "location": "18:8", 795 795 "mode": "run", 796 796 "state": "pending", ··· 798 798 "planned integration": { 799 799 "errors": [], 800 800 "fullName": "root suite > integration tests > planned integration", 801 - "id": "-1732721377_0_2_2", 801 + "id": "1709388417_0_2_2", 802 802 "location": "19:8", 803 803 "mode": "run", 804 804 "state": "pending", ··· 807 807 "root test": { 808 808 "errors": [], 809 809 "fullName": "root suite > root test", 810 - "id": "-1732721377_0_0", 810 + "id": "1709388417_0_0", 811 811 "location": "5:6", 812 812 "mode": "skip", 813 813 "state": "skipped", ··· 816 816 "first unit test": { 817 817 "errors": [], 818 818 "fullName": "root suite > unit tests > first unit test", 819 - "id": "-1732721377_0_1_0", 819 + "id": "1709388417_0_1_0", 820 820 "location": "8:8", 821 821 "mode": "skip", 822 822 "state": "skipped", ··· 825 825 "test in skipped suite": { 826 826 "errors": [], 827 827 "fullName": "root suite > unit tests > skipped nested > test in skipped suite", 828 - "id": "-1732721377_0_1_2_0", 828 + "id": "1709388417_0_1_2_0", 829 829 "location": "12:10", 830 830 "mode": "skip", 831 831 "state": "skipped", ··· 834 834 "skipped unit test": { 835 835 "errors": [], 836 836 "fullName": "root suite > unit tests > skipped unit test", 837 - "id": "-1732721377_0_1_1", 837 + "id": "1709388417_0_1_1", 838 838 "location": "9:8", 839 839 "mode": "skip", 840 840 "state": "skipped", ··· 865 865 "it as global": { 866 866 "errors": [], 867 867 "fullName": "global test functions > it as global", 868 - "id": "-1732721377_0_1", 868 + "id": "1709388417_0_1", 869 869 "location": "4:6", 870 870 "mode": "skip", 871 871 "state": "skipped", ··· 873 873 "myTest as global": { 874 874 "errors": [], 875 875 "fullName": "global test functions > myTest as global", 876 - "id": "-1732721377_0_3", 876 + "id": "1709388417_0_3", 877 877 "location": "6:6", 878 878 "mode": "skip", 879 879 "state": "skipped", ··· 882 882 "skipped global test": { 883 883 "errors": [], 884 884 "fullName": "global test functions > nested > skipped global test", 885 - "id": "-1732721377_0_4_0", 885 + "id": "1709388417_0_4_0", 886 886 "location": "9:8", 887 887 "mode": "skip", 888 888 "state": "skipped", ··· 890 890 "testUnit.only as global": { 891 891 "errors": [], 892 892 "fullName": "global test functions > nested > testUnit.only as global", 893 - "id": "-1732721377_0_4_1", 893 + "id": "1709388417_0_4_1", 894 894 "location": "10:8", 895 895 "mode": "run", 896 896 "state": "pending", ··· 899 899 "test as global": { 900 900 "errors": [], 901 901 "fullName": "global test functions > test as global", 902 - "id": "-1732721377_0_0", 902 + "id": "1709388417_0_0", 903 903 "location": "3:6", 904 904 "mode": "skip", 905 905 "state": "skipped", ··· 907 907 "testSomething as global": { 908 908 "errors": [], 909 909 "fullName": "global test functions > testSomething as global", 910 - "id": "-1732721377_0_2", 910 + "id": "1709388417_0_2", 911 911 "location": "5:6", 912 912 "mode": "skip", 913 913 "state": "skipped", ··· 930 930 "Service": { 931 931 "errors": [], 932 932 "fullName": "Service", 933 - "id": "-1732721377_0", 933 + "id": "1709388417_0", 934 934 "location": "4:4", 935 935 "mode": "run", 936 936 "state": "pending", ··· 954 954 "test with single tag": { 955 955 "errors": [], 956 956 "fullName": "tagged tests > test with single tag", 957 - "id": "-1732721377_0_0", 957 + "id": "1709388417_0_0", 958 958 "location": "5:6", 959 959 "mode": "run", 960 960 "state": "pending", ··· 965 965 "test without tags": { 966 966 "errors": [], 967 967 "fullName": "tagged tests > test without tags", 968 - "id": "-1732721377_0_1", 968 + "id": "1709388417_0_1", 969 969 "location": "6:6", 970 970 "mode": "run", 971 971 "state": "pending", ··· 990 990 "test with empty tags": { 991 991 "errors": [], 992 992 "fullName": "tagged tests > test with empty tags", 993 - "id": "-1732721377_0_1", 993 + "id": "1709388417_0_1", 994 994 "location": "6:6", 995 995 "mode": "run", 996 996 "state": "pending", ··· 998 998 "test with multiple tags": { 999 999 "errors": [], 1000 1000 "fullName": "tagged tests > test with multiple tags", 1001 - "id": "-1732721377_0_0", 1001 + "id": "1709388417_0_0", 1002 1002 "location": "5:6", 1003 1003 "mode": "run", 1004 1004 "state": "pending", ··· 1026 1026 "test in tagged suite": { 1027 1027 "errors": [], 1028 1028 "fullName": "tagged suite > test in tagged suite", 1029 - "id": "-1732721377_0_0", 1029 + "id": "1709388417_0_0", 1030 1030 "location": "5:6", 1031 1031 "mode": "run", 1032 1032 "state": "pending", ··· 1059 1059 "test inherits both tags": { 1060 1060 "errors": [], 1061 1061 "fullName": "outer suite > inner suite > test inherits both tags", 1062 - "id": "-1732721377_0_1_0", 1062 + "id": "1709388417_0_1_0", 1063 1063 "location": "8:8", 1064 1064 "mode": "run", 1065 1065 "state": "pending", ··· 1071 1071 "test with own tag": { 1072 1072 "errors": [], 1073 1073 "fullName": "outer suite > inner suite > test with own tag", 1074 - "id": "-1732721377_0_1_1", 1074 + "id": "1709388417_0_1_1", 1075 1075 "location": "9:8", 1076 1076 "mode": "run", 1077 1077 "state": "pending", ··· 1085 1085 "test inherits parent tag": { 1086 1086 "errors": [], 1087 1087 "fullName": "outer suite > test inherits parent tag", 1088 - "id": "-1732721377_0_0", 1088 + "id": "1709388417_0_0", 1089 1089 "location": "5:6", 1090 1090 "mode": "run", 1091 1091 "state": "pending", ··· 1113 1113 "skipped test with tags": { 1114 1114 "errors": [], 1115 1115 "fullName": "tests with options > skipped test with tags", 1116 - "id": "-1732721377_0_1", 1116 + "id": "1709388417_0_1", 1117 1117 "location": "6:6", 1118 1118 "mode": "skip", 1119 1119 "state": "skipped", ··· 1124 1124 "test with tags and timeout": { 1125 1125 "errors": [], 1126 1126 "fullName": "tests with options > test with tags and timeout", 1127 - "id": "-1732721377_0_0", 1127 + "id": "1709388417_0_0", 1128 1128 "location": "5:6", 1129 1129 "mode": "run", 1130 1130 "state": "pending", ··· 1161 1161 "concurrent": true, 1162 1162 "errors": [], 1163 1163 "fullName": "concurrent suite > inherits concurrent", 1164 - "id": "-1732721377_0_0", 1164 + "id": "1709388417_0_0", 1165 1165 "location": "5:6", 1166 1166 "mode": "run", 1167 1167 "state": "pending", ··· 1170 1170 "not concurrent": { 1171 1171 "errors": [], 1172 1172 "fullName": "concurrent suite > non-concurrent nested > not concurrent", 1173 - "id": "-1732721377_0_2_0", 1173 + "id": "1709388417_0_2_0", 1174 1174 "location": "9:8", 1175 1175 "mode": "run", 1176 1176 "state": "pending", ··· 1179 1179 "not concurrent via options": { 1180 1180 "errors": [], 1181 1181 "fullName": "concurrent suite > not concurrent via options", 1182 - "id": "-1732721377_0_1", 1182 + "id": "1709388417_0_1", 1183 1183 "location": "6:6", 1184 1184 "mode": "run", 1185 1185 "state": "pending", ··· 1189 1189 "concurrent": true, 1190 1190 "errors": [], 1191 1191 "fullName": "concurrent suite > regular nested > still concurrent", 1192 - "id": "-1732721377_0_3_0", 1192 + "id": "1709388417_0_3_0", 1193 1193 "location": "13:8", 1194 1194 "mode": "run", 1195 1195 "state": "pending", ··· 1217 1217 "concurrent": true, 1218 1218 "errors": [], 1219 1219 "fullName": "concurrent suite > test in concurrent suite", 1220 - "id": "-1732721377_0_0", 1220 + "id": "1709388417_0_0", 1221 1221 "location": "5:6", 1222 1222 "mode": "skip", 1223 1223 "state": "skipped", ··· 1226 1226 "test outside concurrent suite": { 1227 1227 "errors": [], 1228 1228 "fullName": "test outside concurrent suite", 1229 - "id": "-1732721377_1", 1229 + "id": "1709388417_1", 1230 1230 "location": "8:4", 1231 1231 "mode": "run", 1232 1232 "state": "pending", ··· 1255 1255 "concurrent": true, 1256 1256 "errors": [], 1257 1257 "fullName": "options tests > concurrent via options", 1258 - "id": "-1732721377_0_3", 1258 + "id": "1709388417_0_3", 1259 1259 "location": "8:6", 1260 1260 "mode": "skip", 1261 1261 "state": "skipped", ··· 1263 1263 "not concurrent via options": { 1264 1264 "errors": [], 1265 1265 "fullName": "options tests > not concurrent via options", 1266 - "id": "-1732721377_0_4", 1266 + "id": "1709388417_0_4", 1267 1267 "location": "9:6", 1268 1268 "mode": "skip", 1269 1269 "state": "skipped", ··· 1271 1271 "only via options": { 1272 1272 "errors": [], 1273 1273 "fullName": "options tests > only via options", 1274 - "id": "-1732721377_0_1", 1274 + "id": "1709388417_0_1", 1275 1275 "location": "6:6", 1276 1276 "mode": "run", 1277 1277 "state": "pending", ··· 1280 1280 "concurrent": true, 1281 1281 "errors": [], 1282 1282 "fullName": "options tests > skip and concurrent via options", 1283 - "id": "-1732721377_0_5", 1283 + "id": "1709388417_0_5", 1284 1284 "location": "10:6", 1285 1285 "mode": "skip", 1286 1286 "state": "skipped", ··· 1288 1288 "skipped via options": { 1289 1289 "errors": [], 1290 1290 "fullName": "options tests > skipped via options", 1291 - "id": "-1732721377_0_0", 1291 + "id": "1709388417_0_0", 1292 1292 "location": "5:6", 1293 1293 "mode": "skip", 1294 1294 "state": "skipped", ··· 1296 1296 "todo via options": { 1297 1297 "errors": [], 1298 1298 "fullName": "options tests > todo via options", 1299 - "id": "-1732721377_0_2", 1299 + "id": "1709388417_0_2", 1300 1300 "location": "7:6", 1301 1301 "mode": "todo", 1302 1302 "state": "skipped", ··· 1324 1324 "concurrent": true, 1325 1325 "errors": [], 1326 1326 "fullName": "concurrent suite > skipped in concurrent suite", 1327 - "id": "-1732721377_0_1", 1327 + "id": "1709388417_0_1", 1328 1328 "location": "6:6", 1329 1329 "mode": "skip", 1330 1330 "state": "skipped", ··· 1333 1333 "concurrent": true, 1334 1334 "errors": [], 1335 1335 "fullName": "concurrent suite > test in concurrent suite", 1336 - "id": "-1732721377_0_0", 1336 + "id": "1709388417_0_0", 1337 1337 "location": "5:6", 1338 1338 "mode": "run", 1339 1339 "state": "pending", ··· 1343 1343 "concurrent": true, 1344 1344 "errors": [], 1345 1345 "fullName": "concurrent test", 1346 - "id": "-1732721377_1", 1346 + "id": "1709388417_1", 1347 1347 "location": "9:4", 1348 1348 "mode": "run", 1349 1349 "state": "pending", ··· 1367 1367 "concurrent": true, 1368 1368 "errors": [], 1369 1369 "fullName": "concurrent each %i > test inside concurrent each", 1370 - "id": "-1732721377_0_0", 1370 + "id": "1709388417_0_0", 1371 1371 "location": "5:6", 1372 1372 "mode": "run", 1373 1373 "state": "pending", ··· 1394 1394 "each": true, 1395 1395 "errors": [], 1396 1396 "fullName": "suite > concurrent each test %i", 1397 - "id": "-1732721377_0_0-dynamic", 1397 + "id": "1709388417_0_0-dynamic", 1398 1398 "location": "5:36", 1399 1399 "mode": "run", 1400 1400 "state": "pending", ··· 1442 1442 "test 1": { 1443 1443 "errors": [], 1444 1444 "fullName": "suite 1 > test 1", 1445 - "id": "492646822_0_0", 1445 + "id": "538223770_0_0", 1446 1446 "location": "10:2", 1447 1447 "mode": "run", 1448 1448 "state": "pending", ··· 1497 1497 "also conditional": { 1498 1498 "errors": [], 1499 1499 "fullName": "runIf tests > also conditional", 1500 - "id": "-1732721377_0_1", 1500 + "id": "1709388417_0_1", 1501 1501 "location": "6:22", 1502 1502 "mode": "skip", 1503 1503 "state": "skipped", ··· 1505 1505 "runs conditionally": { 1506 1506 "errors": [], 1507 1507 "fullName": "runIf tests > runs conditionally", 1508 - "id": "-1732721377_0_0", 1508 + "id": "1709388417_0_0", 1509 1509 "location": "5:21", 1510 1510 "mode": "skip", 1511 1511 "state": "skipped", ··· 1530 1530 "also conditional skip": { 1531 1531 "errors": [], 1532 1532 "fullName": "skipIf tests > also conditional skip", 1533 - "id": "-1732721377_0_1", 1533 + "id": "1709388417_0_1", 1534 1534 "location": "6:23", 1535 1535 "mode": "skip", 1536 1536 "state": "skipped", ··· 1538 1538 "skips conditionally": { 1539 1539 "errors": [], 1540 1540 "fullName": "skipIf tests > skips conditionally", 1541 - "id": "-1732721377_0_0", 1541 + "id": "1709388417_0_0", 1542 1542 "location": "5:22", 1543 1543 "mode": "skip", 1544 1544 "state": "skipped", ··· 1565 1565 "each": true, 1566 1566 "errors": [], 1567 1567 "fullName": "for tests > skipped for %i", 1568 - "id": "-1732721377_0_1-dynamic", 1568 + "id": "1709388417_0_1-dynamic", 1569 1569 "location": "6:26", 1570 1570 "mode": "skip", 1571 1571 "state": "skipped", ··· 1575 1575 "each": true, 1576 1576 "errors": [], 1577 1577 "fullName": "for tests > test with for %i", 1578 - "id": "-1732721377_0_0-dynamic", 1578 + "id": "1709388417_0_0-dynamic", 1579 1579 "location": "5:24", 1580 1580 "mode": "run", 1581 1581 "state": "pending", ··· 1602 1602 "each": true, 1603 1603 "errors": [], 1604 1604 "fullName": "actual 2 test", 1605 - "id": "-1732721377_1-dynamic", 1605 + "id": "1709388417_1-dynamic", 1606 1606 "location": "5:15", 1607 1607 "mode": "run", 1608 1608 "state": "pending", ··· 1610 1610 "actual 3 test": { 1611 1611 "errors": [], 1612 1612 "fullName": "actual 3 test", 1613 - "id": "-1732721377_2", 1613 + "id": "1709388417_2", 1614 1614 "location": "6:4", 1615 1615 "mode": "run", 1616 1616 "state": "pending", ··· 1618 1618 "actual 4 test": { 1619 1619 "errors": [], 1620 1620 "fullName": "actual 4 test", 1621 - "id": "-1732721377_3", 1621 + "id": "1709388417_3", 1622 1622 "location": "7:4", 1623 1623 "mode": "run", 1624 1624 "state": "pending", ··· 1626 1626 "actual test": { 1627 1627 "errors": [], 1628 1628 "fullName": "actual test", 1629 - "id": "-1732721377_0", 1629 + "id": "1709388417_0", 1630 1630 "location": "4:4", 1631 1631 "mode": "run", 1632 1632 "state": "pending",
+17 -3
test/e2e/test/test-specifications.test.ts
··· 2 2 import { expect, test } from 'vitest' 3 3 import { runInlineTests } from '../../test-utils' 4 4 5 - test.each( 5 + test.for( 6 6 [ 7 7 { testNamePattern: /two/ }, 8 8 { testLines: [8] }, 9 - { testIds: ['-109630875_1'] }, 9 + { testIds: ['-1838252165_1'] }, 10 10 ] satisfies TestSpecificationOptions[], 11 - )('runs with options %o', async (options) => { 11 + )('runs with options %o', async (options, { onTestFailed }) => { 12 12 const { fs, ctx, errorTree } = await runInlineTests({ 13 13 'basic.test.js': /* js */ ` 14 14 import { test, expect } from 'vitest' ··· 33 33 ) 34 34 35 35 await vitest.runTestSpecifications([specification]) 36 + onTestFailed(() => { 37 + // eslint-disable-next-line no-console 38 + console.log('⚠️⚠️⚠️ Failed options', options) 39 + for (const [id, task] of vitest.state.idMap.entries()) { 40 + // eslint-disable-next-line no-console 41 + console.log({ 42 + id, 43 + name: task.name, 44 + mode: task.mode, 45 + type: task.type, 46 + location: task.location, 47 + }) 48 + } 49 + }) 36 50 37 51 expect(errorTree()).toEqual({ 38 52 'basic.test.js': {
+28 -1
packages/browser/src/client/orchestrator.ts
··· 464 464 function generateFileId(file: string) { 465 465 const config = getConfig() 466 466 const path = relative(config.root, file) 467 - return generateHash(`${path}${config.name || ''}`) 467 + return generateFileHash( 468 + path, 469 + config.name, 470 + { 471 + typecheck: config.pool === 'typescript', 472 + __vitest_label__: config.mergeReportsLabel, 473 + }, 474 + ) 475 + } 476 + 477 + // TODO: copied from packages/runner/src/utils/collect.ts 478 + interface HashMeta { 479 + typecheck?: boolean 480 + __vitest_label__?: string 481 + } 482 + 483 + function generateFileHash( 484 + file: string, 485 + projectName: string | undefined, 486 + meta?: HashMeta, 487 + ): string { 488 + const seed = [ 489 + file, 490 + projectName || '', 491 + meta?.typecheck ? '__typecheck__' : '', 492 + meta?.__vitest_label__ || '', 493 + ].join('\0') 494 + return generateHash(seed) 468 495 } 469 496 470 497 function generateHash(str: string): string {
+1
packages/runner/src/types/runner.ts
··· 46 46 tags: TestTagDefinition[] 47 47 tagsFilter: string[] | undefined 48 48 strictTags: boolean 49 + mergeReportsLabel: string | undefined 49 50 } 50 51 51 52 /**
+16 -3
packages/runner/src/utils/collect.ts
··· 193 193 }) 194 194 } 195 195 196 + interface HashMeta { 197 + typecheck?: boolean 198 + __vitest_label__?: string 199 + } 200 + 196 201 export function createFileTask( 197 202 filepath: string, 198 203 root: string, 199 204 projectName: string | undefined, 200 205 pool?: string, 201 206 viteEnvironment?: string, 207 + meta?: HashMeta, 202 208 ): File { 203 209 const path = relative(root, filepath) 204 210 const file: File = { 205 - id: generateFileHash(path, projectName), 211 + id: generateFileHash(path, projectName, meta), 206 212 name: path, 207 213 fullName: path, 208 214 type: 'suite', 209 215 mode: 'queued', 210 216 filepath, 211 217 tasks: [], 212 - meta: Object.create(null), 218 + meta: Object.assign(Object.create(null), meta), 213 219 projectName, 214 220 file: undefined!, 215 221 pool, ··· 228 234 export function generateFileHash( 229 235 file: string, 230 236 projectName: string | undefined, 237 + meta?: HashMeta, 231 238 ): string { 232 - return /* @__PURE__ */ generateHash(`${file}${projectName || ''}`) 239 + const seed = [ 240 + file, 241 + projectName || '', 242 + meta?.typecheck ? '__typecheck__' : '', 243 + meta?.__vitest_label__ || '', 244 + ].join('\0') 245 + return generateHash(seed) 233 246 } 234 247 235 248 export function findTestFileStackTrace(testFilePath: string, error: string): ParsedStack | undefined {
+3
packages/ui/client/components/FileDetails.vue
··· 60 60 return !!current.value?.meta?.typecheck 61 61 }) 62 62 63 + const label = computed(() => current.value?.meta?.__vitest_label__) 64 + 63 65 function open() { 64 66 const filePath = current.value?.filepath 65 67 if (filePath) { ··· 206 208 <div p="2" h-10 flex="~ gap-2" items-center bg-header border="b base"> 207 209 <StatusIcon :state="current.result?.state" :mode="current.mode" :failed-snapshot="failedSnapshot" /> 208 210 <div v-if="isTypecheck" v-tooltip.bottom="'This is a typecheck test. It won\'t report results of the runtime tests'" class="i-logos:typescript-icon" flex-shrink-0 /> 211 + <span v-if="label" class="rounded-sm px-1 text-xs font-light bg-cyan-500/20 text-cyan-700 dark:text-cyan-300" flex-shrink-0>{{ label }}</span> 209 212 <span 210 213 v-if="current?.file.projectName" 211 214 class="rounded-full py-0.5 px-2 text-xs font-light"
+23 -25
packages/vitest/src/node/ast-collect.ts
··· 1 1 import type { File, Suite, Task, Test } from '@vitest/runner' 2 - import type { SerializedConfig } from '../runtime/config' 3 2 import type { TestError } from '../types/general' 4 3 import type { TestProject } from './project' 5 4 import { promises as fs } from 'node:fs' 6 5 import { originalPositionFor, TraceMap } from '@jridgewell/trace-mapping' 7 6 import { 8 7 calculateSuiteHash, 8 + createFileTask as createFileTaskOriginal, 9 9 createTaskName, 10 - generateHash, 11 10 validateTags, 12 11 } from '@vitest/runner/utils' 13 12 import { unique } from '@vitest/utils/helpers' ··· 279 278 } 280 279 281 280 export function createFailedFileTask(project: TestProject, filepath: string, error: Error): File { 282 - const testFilepath = relative(project.config.root, filepath) 283 - const file: ParsedFile = { 281 + const config = project.serializedConfig 282 + const baseFile = createFileTaskOriginal( 284 283 filepath, 285 - type: 'suite', 286 - id: /* @__PURE__ */ generateHash(`${testFilepath}${project.config.name || ''}`), 287 - name: testFilepath, 288 - fullName: testFilepath, 284 + config.root, 285 + config.name, 286 + config.pool, 287 + undefined, 288 + { typecheck: config.pool === 'typescript', __vitest_label__: config.mergeReportsLabel }, 289 + ) 290 + const file: ParsedFile = { 291 + ...baseFile, 289 292 mode: 'run', 290 - tasks: [], 291 293 start: 0, 292 294 end: 0, 293 - projectName: project.name, 294 - meta: {}, 295 - pool: project.browser ? 'browser' : project.config.pool, 296 - file: null!, 297 295 result: { 298 296 state: 'fail', 299 297 errors: serializeError(project, error), ··· 326 324 } 327 325 328 326 function createFileTask( 327 + project: TestProject, 329 328 testFilepath: string, 330 329 code: string, 331 330 requestMap: any, 332 - config: SerializedConfig, 333 331 filepath: string, 334 332 fileTags: string[] | undefined, 335 333 ) { 336 334 const { definitions, ast } = astParseFile(testFilepath, code) 337 - const file: ParsedFile = { 335 + const config = project.serializedConfig 336 + const baseFile = createFileTaskOriginal( 338 337 filepath, 339 - type: 'suite', 340 - id: /* @__PURE__ */ generateHash(`${testFilepath}${config.name || ''}`), 341 - name: testFilepath, 342 - fullName: testFilepath, 338 + config.root, 339 + config.name, 340 + config.pool, 341 + undefined, 342 + { typecheck: config.pool === 'typescript', __vitest_label__: config.mergeReportsLabel }, 343 + ) 344 + const file: ParsedFile = { 345 + ...baseFile, 343 346 mode: 'run', 344 - tasks: [], 345 347 start: ast.start, 346 348 end: ast.end, 347 - projectName: config.name, 348 - meta: {}, 349 - pool: 'browser', 350 - file: null!, 351 349 tags: fileTags || [], 352 350 } 353 351 file.file = file ··· 494 492 ) 495 493 } 496 494 return createFileTask( 495 + project, 497 496 testFilepath, 498 497 request.code, 499 498 request.map, 500 - project.serializedConfig, 501 499 filepath, 502 500 request.fileTags, 503 501 )
+1 -1
packages/vitest/src/node/core.ts
··· 622 622 const specifications: TestSpecification[] = [] 623 623 for (const file of files) { 624 624 const project = this.getProjectByName(file.projectName || '') 625 - const specification = project.createSpecification(file.filepath, undefined, file.pool) 625 + const specification = project.createSpecification(file.filepath, undefined, file.pool, file.meta) 626 626 specifications.push(specification) 627 627 } 628 628
+5
packages/vitest/src/node/project.ts
··· 1 + import type { TaskMeta } from '@vitest/runner/types' 1 2 import type { GlobOptions } from 'tinyglobby' 2 3 import type { DevEnvironment, ViteDevServer, InlineConfig as ViteInlineConfig } from 'vite' 3 4 import type { ModuleRunner } from 'vite/module-runner' ··· 152 153 locationsOrOptions?: number[] | TestSpecificationOptions | undefined, 153 154 /** @internal */ 154 155 pool?: string, 156 + /** @internal */ 157 + metaOverride?: TaskMeta, 155 158 ): TestSpecification { 156 159 return new TestSpecification( 157 160 this, 158 161 moduleId, 159 162 pool || getFilePoolName(this), 160 163 locationsOrOptions, 164 + metaOverride, 161 165 ) 162 166 } 163 167 ··· 551 555 server.config, 552 556 ) 553 557 this._config.api.token = this.vitest.config.api.token 558 + this._config.mergeReportsLabel = this.vitest.config.mergeReportsLabel 554 559 this._setHash() 555 560 for (const _providedKey in this.config.provide) { 556 561 const providedKey = _providedKey as keyof ProvidedContext
+4 -4
packages/vitest/src/node/state.ts
··· 145 145 collectFiles(project: TestProject, files: File[] = []): void { 146 146 files.forEach((file) => { 147 147 const existing = this.filesMap.get(file.filepath) || [] 148 - const otherFiles = existing.filter( 149 - i => i.projectName !== file.projectName || i.meta.typecheck !== file.meta.typecheck, 150 - ) 151 148 const currentFile = existing.find( 152 - i => i.projectName === file.projectName, 149 + i => i.projectName === file.projectName 150 + && i.meta.typecheck === file.meta.typecheck 151 + && i.meta.__vitest_label__ === file.meta.__vitest_label__, 153 152 ) 154 153 // keep logs for the previous file because it should always be initiated before the collections phase 155 154 // which means that all logs are collected during the collection and not inside tests 156 155 if (currentFile) { 157 156 file.logs = currentFile.logs 158 157 } 158 + const otherFiles = existing.filter(i => i !== currentFile) 159 159 otherFiles.push(file) 160 160 this.filesMap.set(file.filepath, otherFiles) 161 161 this.updateId(file, project)
+1 -6
packages/vitest/src/node/test-run.ts
··· 17 17 import { createHash } from 'node:crypto' 18 18 import { existsSync, readFileSync } from 'node:fs' 19 19 import { copyFile, mkdir, writeFile } from 'node:fs/promises' 20 - import { isPrimitive } from '@vitest/utils/helpers' 20 + import { isPrimitive, sanitizeFilePath } from '@vitest/utils/helpers' 21 21 import { serializeValue } from '@vitest/utils/serialize' 22 22 import { parseErrorStacktrace } from '@vitest/utils/source-map' 23 23 import { extractSourcemapFromFile } from '@vitest/utils/source-map/node' ··· 310 310 } 311 311 } 312 312 } 313 - } 314 - 315 - function sanitizeFilePath(s: string): string { 316 - // eslint-disable-next-line no-control-regex 317 - return s.replace(/[\x00-\x2C\x2E\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-') 318 313 }
+5 -7
packages/vitest/src/node/test-specification.ts
··· 1 + import type { TaskMeta } from '@vitest/runner/types' 1 2 import type { SerializedTestSpecification } from '../runtime/types/utils' 2 3 import type { TestProject } from './project' 3 4 import type { TestModule } from './reporters/reported-tasks' ··· 55 56 moduleId: string, 56 57 pool: Pool, 57 58 testLinesOrOptions?: number[] | TestSpecificationOptions | undefined, 59 + // merge-reports uses the original `file.meta` from the test run 60 + metaOverride?: TaskMeta, 58 61 ) { 59 62 const projectName = project.config.name 60 - const hashName = pool !== 'typescript' 61 - ? projectName 62 - : projectName 63 - // https://github.com/vitest-dev/vitest/blob/main/packages/vitest/src/typecheck/collect.ts#L58 64 - ? `${projectName}:__typecheck__` 65 - : '__typecheck__' 66 63 this.taskId = generateFileHash( 67 64 relative(project.config.root, moduleId), 68 - hashName, 65 + projectName, 66 + metaOverride ?? { typecheck: pool === 'typescript', __vitest_label__: project.config.mergeReportsLabel }, 69 67 ) 70 68 this.project = project 71 69 this.moduleId = moduleId
+1
packages/vitest/src/runtime/config.ts
··· 154 154 tags: TestTagDefinition[] 155 155 tagsFilter: string[] | undefined 156 156 strictTags: boolean 157 + mergeReportsLabel: string | undefined 157 158 slowTestThreshold: number | undefined 158 159 isAgent: boolean 159 160 }
+10 -14
packages/vitest/src/typecheck/collect.ts
··· 3 3 import type { TestProject } from '../node/project' 4 4 import { 5 5 calculateSuiteHash, 6 + createFileTask, 6 7 createTaskName, 7 - generateHash, 8 8 interpretTaskModes, 9 9 someTasksAreOnly, 10 10 } from '@vitest/runner/utils' 11 11 import { ancestor as walkAst } from 'acorn-walk' 12 - import { relative } from 'pathe' 13 12 import { parseAstAsync } from 'vite' 14 13 15 14 interface ParsedFile extends File { ··· 53 52 return null 54 53 } 55 54 const ast = await parseAstAsync(request.code) 56 - const testFilepath = relative(ctx.config.root, filepath) 57 55 const projectName = ctx.name 58 - const typecheckSubprojectName = projectName ? `${projectName}:__typecheck__` : '__typecheck__' 59 56 const file: ParsedFile = { 60 - filepath, 61 - type: 'suite', 62 - id: generateHash(`${testFilepath}${typecheckSubprojectName}`), 63 - name: testFilepath, 64 - fullName: testFilepath, 65 - mode: 'run', 66 - tasks: [], 57 + ...createFileTask( 58 + filepath, 59 + ctx.config.root, 60 + projectName, 61 + undefined, 62 + undefined, 63 + { typecheck: true }, 64 + ), 67 65 start: ast.start, 68 66 end: ast.end, 69 - projectName, 70 - meta: { typecheck: true }, 71 - file: null!, 67 + mode: 'run', 72 68 } 73 69 file.file = file 74 70 const definitions: LocalCallDefinition[] = []
+1
packages/vitest/src/types/global.ts
··· 110 110 interface TaskMeta { 111 111 typecheck?: boolean 112 112 benchmark?: boolean 113 + __vitest_label__?: string 113 114 } 114 115 115 116 interface File {
+476 -1
test/e2e/test/reporters/merge-reports.test.ts
··· 1 + import type { RunVitestConfig } from '#test-utils' 1 2 import type { File, Test } from '@vitest/runner/types' 2 3 import type { TestUserConfig, Vitest } from 'vitest/node' 3 4 import { rmSync } from 'node:fs' 4 5 import { resolve } from 'node:path' 5 - import { runVitest } from '#test-utils' 6 + import { buildTestTree, runVitest, useFS } from '#test-utils' 6 7 import { playwright } from '@vitest/browser-playwright' 7 8 import { createFileTask } from '@vitest/runner/utils' 8 9 import { beforeEach, expect, test } from 'vitest' ··· 452 453 const rows = report 453 454 .replace(/\d+ms/g, '<time>') 454 455 .replace(/\d+\.\d+s/g, '<time>') 456 + .replace(/blob report written to (.*)/g, 'blob report written to <path>') 455 457 .split('\n') 456 458 457 459 // Trim start and end, capture just rendered tree ··· 481 483 context: {} as any, 482 484 } 483 485 } 486 + 487 + test('merge reports with labels', async () => { 488 + const root = resolve(process.cwd(), `vitest-test-${crypto.randomUUID()}`) 489 + useFS(root, { 490 + 'first.test.ts': ` 491 + test("always good", () => {}) 492 + 493 + test("works on linux", () => { 494 + expect(process.env.TEST_LABEL_ENV === 'linux').toBe(true) 495 + }) 496 + 497 + test("works on macos", () => { 498 + expect(process.env.TEST_LABEL_ENV === 'macos').toBe(true) 499 + }) 500 + `, 501 + 'second.test.ts': ` 502 + test("linux only", () => {}) 503 + `, 504 + 'third.test.ts': ` 505 + test("macos only", () => {}) 506 + `, 507 + }) 508 + process.env.TEST_LABEL_ENV = 'linux' 509 + const result1 = await runVitest({ 510 + root, 511 + globals: true, 512 + reporters: [['blob', { label: 'linux' }]], 513 + }, ['first', 'second']) 514 + expect(result1.stderr).toMatchInlineSnapshot(`""`) 515 + expect(result1.errorTree()).toMatchInlineSnapshot(` 516 + { 517 + "first.test.ts": { 518 + "always good": "passed", 519 + "works on linux": "passed", 520 + "works on macos": [ 521 + "expected false to be true // Object.is equality", 522 + ], 523 + }, 524 + "second.test.ts": { 525 + "linux only": "passed", 526 + }, 527 + } 528 + `) 529 + process.env.TEST_LABEL_ENV = 'macos' 530 + process.env.VITEST_BLOB_LABEL = 'macos' // test VITEST_BLOB_LABEL 531 + const result2 = await runVitest({ 532 + root, 533 + globals: true, 534 + reporters: [ 535 + 'blob', 536 + // test the original run's reporter doesn't include label 537 + 'verbose', 538 + ], 539 + }, ['first', 'third']) 540 + delete process.env.VITEST_BLOB_LABEL 541 + expect(trimReporterOutput(result2.stdout)).toMatchInlineSnapshot(` 542 + "✓ first.test.ts > always good <time> 543 + × first.test.ts > works on linux <time> 544 + → expected false to be true // Object.is equality 545 + ✓ first.test.ts > works on macos <time> 546 + ✓ third.test.ts > macos only <time> 547 + 548 + Test Files 1 failed | 1 passed (2) 549 + Tests 1 failed | 3 passed (4) 550 + Duration <time> (transform <time>, setup <time>, import <time>, tests <time>, environment <time>) 551 + 552 + blob report written to <path>" 553 + `) 554 + expect(result2.stderr).toMatchInlineSnapshot(` 555 + " 556 + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ 557 + 558 + FAIL first.test.ts > works on linux 559 + AssertionError: expected false to be true // Object.is equality 560 + 561 + - Expected 562 + + Received 563 + 564 + - true 565 + + false 566 + 567 + ❯ first.test.ts:5:50 568 + 3| 569 + 4| test("works on linux", () => { 570 + 5| expect(process.env.TEST_LABEL_ENV === 'linux').toBe(true) 571 + | ^ 572 + 6| }) 573 + 7| 574 + 575 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 576 + 577 + " 578 + `) 579 + expect(result2.errorTree()).toMatchInlineSnapshot(` 580 + { 581 + "first.test.ts": { 582 + "always good": "passed", 583 + "works on linux": [ 584 + "expected false to be true // Object.is equality", 585 + ], 586 + "works on macos": "passed", 587 + }, 588 + "third.test.ts": { 589 + "macos only": "passed", 590 + }, 591 + } 592 + `) 593 + const result = await runVitest({ 594 + root, 595 + mergeReports: resolve(root, '.vitest-reports'), 596 + }) 597 + expect(trimReporterOutput(result.stdout)).toMatchInlineSnapshot(` 598 + "✓ linux first.test.ts > always good <time> 599 + ✓ linux first.test.ts > works on linux <time> 600 + × linux first.test.ts > works on macos <time> 601 + → expected false to be true // Object.is equality 602 + ✓ linux second.test.ts > linux only <time> 603 + ✓ macos first.test.ts > always good <time> 604 + × macos first.test.ts > works on linux <time> 605 + → expected false to be true // Object.is equality 606 + ✓ macos first.test.ts > works on macos <time> 607 + ✓ macos third.test.ts > macos only <time> 608 + 609 + Test Files 2 failed | 2 passed (4) 610 + Tests 2 failed | 6 passed (8) 611 + Duration <time> (transform <time>, setup <time>, import <time>, tests <time>, environment <time>) 612 + Per blob <time> <time>" 613 + `) 614 + expect(result.stderr).toMatchInlineSnapshot(` 615 + " 616 + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯ 617 + 618 + FAIL linux first.test.ts > works on macos 619 + AssertionError: expected false to be true // Object.is equality 620 + 621 + - Expected 622 + + Received 623 + 624 + - true 625 + + false 626 + 627 + ❯ first.test.ts:9:50 628 + 7| 629 + 8| test("works on macos", () => { 630 + 9| expect(process.env.TEST_LABEL_ENV === 'macos').toBe(true) 631 + | ^ 632 + 10| }) 633 + 11| 634 + 635 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯ 636 + 637 + FAIL macos first.test.ts > works on linux 638 + AssertionError: expected false to be true // Object.is equality 639 + 640 + - Expected 641 + + Received 642 + 643 + - true 644 + + false 645 + 646 + ❯ first.test.ts:5:50 647 + 3| 648 + 4| test("works on linux", () => { 649 + 5| expect(process.env.TEST_LABEL_ENV === 'linux').toBe(true) 650 + | ^ 651 + 6| }) 652 + 7| 653 + 654 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯ 655 + 656 + " 657 + `) 658 + expect(result.errorTree({ fileLabel: true })).toMatchInlineSnapshot(` 659 + { 660 + "first.test.ts (linux)": { 661 + "always good": "passed", 662 + "works on linux": "passed", 663 + "works on macos": [ 664 + "expected false to be true // Object.is equality", 665 + ], 666 + }, 667 + "first.test.ts (macos)": { 668 + "always good": "passed", 669 + "works on linux": [ 670 + "expected false to be true // Object.is equality", 671 + ], 672 + "works on macos": "passed", 673 + }, 674 + "second.test.ts (linux)": { 675 + "linux only": "passed", 676 + }, 677 + "third.test.ts (macos)": { 678 + "macos only": "passed", 679 + }, 680 + } 681 + `) 682 + }) 683 + 684 + test('merge reports with projects and labels', async () => { 685 + const root = resolve(process.cwd(), `vitest-test-${crypto.randomUUID()}`) 686 + useFS(root, { 687 + 'basic.test.ts': ` 688 + import { test, expect } from "vitest"; 689 + 690 + test("always good", () => {}) 691 + 692 + test("works on node", () => { 693 + expect(typeof window).toBe('undefined') 694 + }) 695 + 696 + test("works on browser", () => { 697 + expect(typeof window).not.toBe('undefined') 698 + }) 699 + `, 700 + }) 701 + const baseConfig: RunVitestConfig = { 702 + root, 703 + projects: [ 704 + { 705 + extends: true, 706 + test: { 707 + name: 'node', 708 + sequence: { 709 + groupOrder: 0, 710 + }, 711 + }, 712 + }, 713 + { 714 + extends: true, 715 + test: { 716 + name: 'browser', 717 + sequence: { 718 + groupOrder: 1, 719 + }, 720 + browser: { 721 + enabled: true, 722 + headless: true, 723 + screenshotFailures: false, 724 + provider: playwright(), 725 + instances: [ 726 + { 727 + browser: 'chromium', 728 + }, 729 + ], 730 + }, 731 + }, 732 + }, 733 + ], 734 + } 735 + const result1 = await runVitest({ 736 + ...baseConfig, 737 + reporters: [['blob', { label: 'linux' }]], 738 + }) 739 + expect(result1.stderr).toMatchInlineSnapshot(`""`) 740 + expect(result1.errorTree({ project: true })).toMatchInlineSnapshot(` 741 + { 742 + "browser (chromium)": { 743 + "basic.test.ts": { 744 + "always good": "passed", 745 + "works on browser": "passed", 746 + "works on node": [ 747 + "expected 'object' to be 'undefined' // Object.is equality", 748 + ], 749 + }, 750 + }, 751 + "node": { 752 + "basic.test.ts": { 753 + "always good": "passed", 754 + "works on browser": [ 755 + "expected 'undefined' not to be 'undefined' // Object.is equality", 756 + ], 757 + "works on node": "passed", 758 + }, 759 + }, 760 + } 761 + `) 762 + const result2 = await runVitest({ 763 + ...baseConfig, 764 + reporters: [['blob', { label: 'macos' }]], 765 + }) 766 + expect(result2.stderr).toMatchInlineSnapshot(`""`) 767 + expect(result2.errorTree({ project: true })).toMatchInlineSnapshot(` 768 + { 769 + "browser (chromium)": { 770 + "basic.test.ts": { 771 + "always good": "passed", 772 + "works on browser": "passed", 773 + "works on node": [ 774 + "expected 'object' to be 'undefined' // Object.is equality", 775 + ], 776 + }, 777 + }, 778 + "node": { 779 + "basic.test.ts": { 780 + "always good": "passed", 781 + "works on browser": [ 782 + "expected 'undefined' not to be 'undefined' // Object.is equality", 783 + ], 784 + "works on node": "passed", 785 + }, 786 + }, 787 + } 788 + `) 789 + const result = await runVitest({ 790 + ...baseConfig, 791 + mergeReports: resolve(root, '.vitest-reports'), 792 + }) 793 + expect(trimReporterOutput(result.stdout)).toMatchInlineSnapshot(` 794 + "✓ |node| linux basic.test.ts > always good <time> 795 + ✓ |node| linux basic.test.ts > works on node <time> 796 + × |node| linux basic.test.ts > works on browser <time> 797 + → expected 'undefined' not to be 'undefined' // Object.is equality 798 + ✓ |browser (chromium)| linux basic.test.ts > always good <time> 799 + × |browser (chromium)| linux basic.test.ts > works on node <time> 800 + → expected 'object' to be 'undefined' // Object.is equality 801 + ✓ |browser (chromium)| linux basic.test.ts > works on browser <time> 802 + ✓ |node| macos basic.test.ts > always good <time> 803 + ✓ |node| macos basic.test.ts > works on node <time> 804 + × |node| macos basic.test.ts > works on browser <time> 805 + → expected 'undefined' not to be 'undefined' // Object.is equality 806 + ✓ |browser (chromium)| macos basic.test.ts > always good <time> 807 + × |browser (chromium)| macos basic.test.ts > works on node <time> 808 + → expected 'object' to be 'undefined' // Object.is equality 809 + ✓ |browser (chromium)| macos basic.test.ts > works on browser <time> 810 + 811 + Test Files 4 failed (4) 812 + Tests 4 failed | 8 passed (12) 813 + Duration <time> (transform <time>, setup <time>, import <time>, tests <time>, environment <time>) 814 + Per blob <time> <time>" 815 + `) 816 + expect(result.stderr).toMatchInlineSnapshot(` 817 + " 818 + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 4 ⎯⎯⎯⎯⎯⎯⎯ 819 + 820 + FAIL |node| linux basic.test.ts > works on browser 821 + FAIL |node| macos basic.test.ts > works on browser 822 + AssertionError: expected 'undefined' not to be 'undefined' // Object.is equality 823 + ❯ basic.test.ts:11:29 824 + 9| 825 + 10| test("works on browser", () => { 826 + 11| expect(typeof window).not.toBe('undefined') 827 + | ^ 828 + 12| }) 829 + 13| 830 + 831 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/4]⎯ 832 + 833 + FAIL |browser (chromium)| linux basic.test.ts > works on node 834 + AssertionError: expected 'object' to be 'undefined' // Object.is equality 835 + 836 + Expected: "undefined" 837 + Received: "object" 838 + 839 + ❯ basic.test.ts:7:24 840 + 5| 841 + 6| test("works on node", () => { 842 + 7| expect(typeof window).toBe('undefined') 843 + | ^ 844 + 8| }) 845 + 9| 846 + 847 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/4]⎯ 848 + 849 + FAIL |browser (chromium)| macos basic.test.ts > works on node 850 + AssertionError: expected 'object' to be 'undefined' // Object.is equality 851 + 852 + Expected: "undefined" 853 + Received: "object" 854 + 855 + ❯ basic.test.ts:7:24 856 + 5| 857 + 6| test("works on node", () => { 858 + 7| expect(typeof window).toBe('undefined') 859 + | ^ 860 + 8| }) 861 + 9| 862 + 863 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/4]⎯ 864 + 865 + " 866 + `) 867 + expect(result.errorTree({ project: true, fileLabel: true })).toMatchInlineSnapshot(` 868 + { 869 + "browser (chromium)": { 870 + "basic.test.ts (linux)": { 871 + "always good": "passed", 872 + "works on browser": "passed", 873 + "works on node": [ 874 + "expected 'object' to be 'undefined' // Object.is equality", 875 + ], 876 + }, 877 + "basic.test.ts (macos)": { 878 + "always good": "passed", 879 + "works on browser": "passed", 880 + "works on node": [ 881 + "expected 'object' to be 'undefined' // Object.is equality", 882 + ], 883 + }, 884 + }, 885 + "node": { 886 + "basic.test.ts (linux)": { 887 + "always good": "passed", 888 + "works on browser": [ 889 + "expected 'undefined' not to be 'undefined' // Object.is equality", 890 + ], 891 + "works on node": "passed", 892 + }, 893 + "basic.test.ts (macos)": { 894 + "always good": "passed", 895 + "works on browser": [ 896 + "expected 'undefined' not to be 'undefined' // Object.is equality", 897 + ], 898 + "works on node": "passed", 899 + }, 900 + }, 901 + } 902 + `) 903 + }) 904 + 905 + // currently file level meta is not inherited 906 + test('label meta', async () => { 907 + const root = resolve(process.cwd(), `vitest-test-${crypto.randomUUID()}`) 908 + useFS(root, { 909 + 'basic.test.ts': ` 910 + describe("top-suite", () => { 911 + test("inner-test", () => {}) 912 + }) 913 + test("top-test", () => {}) 914 + `, 915 + }) 916 + const result = await runVitest({ 917 + root, 918 + globals: true, 919 + reporters: [['blob', { label: 'windows' }]], 920 + }) 921 + expect(result.stderr).toMatchInlineSnapshot(`""`) 922 + expect(result.errorTree()).toMatchInlineSnapshot(` 923 + { 924 + "basic.test.ts": { 925 + "top-suite": { 926 + "inner-test": "passed", 927 + }, 928 + "top-test": "passed", 929 + }, 930 + } 931 + `) 932 + const tree = buildTestTree( 933 + result.results, 934 + t => ({ '@META': t.meta(), 'state': t.result().state }), 935 + (suite, children) => ({ '@META': suite.meta(), ...children }), 936 + (file, children) => ({ '@META': file.meta(), ...children }), 937 + ) 938 + expect(tree).toMatchInlineSnapshot(` 939 + { 940 + "basic.test.ts": { 941 + "@META": { 942 + "__vitest_label__": "windows", 943 + }, 944 + "top-suite": { 945 + "@META": {}, 946 + "inner-test": { 947 + "@META": {}, 948 + "state": "passed", 949 + }, 950 + }, 951 + "top-test": { 952 + "@META": {}, 953 + "state": "passed", 954 + }, 955 + }, 956 + } 957 + `) 958 + })
+1
packages/ui/client/components/explorer/Explorer.vue
··· 331 331 :indent="item.indent" 332 332 :name="item.name" 333 333 :typecheck="item.typecheck === true" 334 + :label="item.label" 334 335 :project-name="item.projectName ?? ''" 335 336 :project-name-color="item.projectNameColor ?? ''" 336 337 :state="item.state"
+3
packages/ui/client/components/explorer/ExplorerItem.vue
··· 25 25 opened, 26 26 expandable, 27 27 typecheck, 28 + label, 28 29 type, 29 30 disableTaskLocation, 30 31 onItemClick, ··· 35 36 name: string 36 37 indent: number 37 38 typecheck?: boolean 39 + label?: string 38 40 duration?: number 39 41 slow?: boolean 40 42 state?: TaskState ··· 216 218 <StatusIcon :state="state" :mode="task.mode" :failed-snapshot="failedSnapshot" w-4 /> 217 219 <div flex items-baseline gap-2 overflow-hidden> 218 220 <div v-if="type === 'file' && typecheck" v-tooltip.bottom="'This is a typecheck test. It won\'t report results of the runtime tests'" class="i-logos:typescript-icon" flex-shrink-0 /> 221 + <span v-if="type === 'file' && label" class="rounded-sm px-1 text-xs font-light bg-cyan-500/20 text-cyan-700 dark:text-cyan-300" flex-shrink-0>{{ label }}</span> 219 222 <span text-sm truncate font-light> 220 223 <span v-if="type === 'file' && projectName" class="rounded-full py-0.5 px-2 mr-1 text-xs" :style="{ backgroundColor: projectNameColor, color: projectNameTextColor }"> 221 224 {{ projectName }}
+3
packages/ui/client/components/views/ViewTestReport.vue
··· 22 22 return mapLeveledTaskStacks(isDark.value, [props.test])[0] as RunnerTestCase | null 23 23 }) 24 24 25 + // filter out internal TaskMeta 26 + // packages/vitest/src/types/global.ts 25 27 const kWellKnownMeta = new Set([ 26 28 'benchmark', 27 29 'typecheck', 30 + '__vitest_label__', 28 31 ]) 29 32 const meta = computed(() => { 30 33 return Object.entries(props.test.meta).filter(([name]) => {
+1
packages/ui/client/composables/explorer/types.ts
··· 63 63 type: 'file' 64 64 filepath: string 65 65 typecheck: boolean | undefined 66 + label?: string 66 67 projectName?: string 67 68 projectNameColor: string 68 69 collectDuration?: number
+2
packages/ui/client/composables/explorer/utils.ts
··· 84 84 85 85 if (fileNode) { 86 86 fileNode.typecheck = !!file.meta && 'typecheck' in file.meta 87 + fileNode.label = file.meta?.__vitest_label__ 87 88 fileNode.state = file.result?.state 88 89 fileNode.mode = file.mode 89 90 fileNode.duration = typeof file.result?.duration === 'number' ? Math.round(file.result.duration) : undefined ··· 106 107 children: new Set(), 107 108 tasks: [], 108 109 typecheck: !!file.meta && 'typecheck' in file.meta, 110 + label: file.meta?.__vitest_label__, 109 111 indent: 0, 110 112 duration: typeof file.result?.duration === 'number' ? Math.round(file.result.duration) : undefined, 111 113 slow: false,
+10
packages/vitest/src/node/config/resolveConfig.ts
··· 760 760 } 761 761 } 762 762 763 + resolved.mergeReportsLabel = process.env.VITEST_BLOB_LABEL 764 + for (const reporter of resolved.reporters) { 765 + if (Array.isArray(reporter) && reporter[0] === 'blob') { 766 + const options = reporter[1] as any 767 + if (options && typeof options.label === 'string') { 768 + resolved.mergeReportsLabel = options.label 769 + } 770 + } 771 + } 772 + 763 773 if (resolved.changed) { 764 774 resolved.passWithNoTests ??= true 765 775 }
+1
packages/vitest/src/node/config/serializeConfig.ts
··· 150 150 tags: config.tags || [], 151 151 tagsFilter: config.tagsFilter, 152 152 strictTags: config.strictTags ?? true, 153 + mergeReportsLabel: config.mergeReportsLabel, 153 154 slowTestThreshold: 154 155 config.slowTestThreshold 155 156 ?? globalConfig.slowTestThreshold
+7 -1
packages/vitest/src/node/reporters/base.ts
··· 335 335 title += ` ${c.bgBlue(c.bold(' TS '))}` 336 336 } 337 337 338 + const label = this.ctx.state.blobs && entity.task.file.meta.__vitest_label__ 339 + if (label) { 340 + title += ` ${c.bgCyan(c.bold(` ${label} `))}` 341 + } 342 + 338 343 return title 339 344 } 340 345 ··· 936 941 name += c.dim(` [ ${this.relative(filepath)} ]`) 937 942 } 938 943 944 + const label = this.ctx.state.blobs && task.file?.meta?.__vitest_label__ 939 945 this.ctx.logger.error( 940 - `${c.bgRed(c.bold(' FAIL '))} ${formatProjectName(project)}${name}`, 946 + `${c.bgRed(c.bold(' FAIL '))} ${formatProjectName(project)}${label ? `${c.bgCyan(c.bold(` ${label} `))} ` : ''}${name}`, 941 947 ) 942 948 } 943 949
+8 -3
packages/vitest/src/node/reporters/blob.ts
··· 7 7 import type { TestModule } from './reported-tasks' 8 8 import { existsSync } from 'node:fs' 9 9 import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises' 10 + import { sanitizeFilePath } from '@vitest/utils/helpers' 10 11 import { parse, stringify } from 'flatted' 11 12 import { dirname, resolve } from 'pathe' 12 13 import { getOutputFile } from '../../utils/config-helpers' 13 14 14 15 export interface BlobOptions { 15 16 outputFile?: string 17 + label?: string 16 18 } 17 19 18 20 export class BlobReporter implements Reporter { ··· 50 52 = this.options.outputFile ?? getOutputFile(this.ctx.config, 'blob') 51 53 if (!outputFile) { 52 54 const shard = this.ctx.config.shard 53 - outputFile = shard 54 - ? `.vitest-reports/blob-${shard.index}-${shard.count}.json` 55 - : '.vitest-reports/blob.json' 55 + const filename = [ 56 + 'blob', 57 + this.ctx.config.mergeReportsLabel, 58 + shard && `${shard.index}-${shard.count}`, 59 + ].filter(Boolean).join('-') 60 + outputFile = `.vitest-reports/${sanitizeFilePath(filename)}.json` 56 61 } 57 62 58 63 const environmentModules: MergeReportEnvironmentModules = {}
+5 -3
packages/vitest/src/node/reporters/summary.ts
··· 38 38 projectColor: TestModule['project']['color'] 39 39 hook?: Omit<SlowTask, 'hook'> 40 40 tests: Map<TestCase['id'], SlowTask> 41 - typecheck: boolean 41 + meta: TestModule['task']['meta'] 42 42 } 43 43 44 44 /** ··· 288 288 const summary = [''] 289 289 290 290 for (const testFile of Array.from(this.runningModules.values()).sort(sortRunningModules)) { 291 - const typecheck = testFile.typecheck ? `${c.bgBlue(c.bold(' TS '))} ` : '' 291 + const typecheck = testFile.meta.typecheck ? `${c.bgBlue(c.bold(' TS '))} ` : '' 292 + const label = this.ctx.state.blobs && testFile.meta.__vitest_label__ ? `${c.bgCyan(c.bold(` ${testFile.meta.__vitest_label__} `))} ` : '' 292 293 summary.push( 293 294 c.bold(c.yellow(` ${F_POINTER} `)) 294 295 + formatProjectName({ name: testFile.projectName, color: testFile.projectColor }) 295 296 + typecheck 297 + + label 296 298 + testFile.filename 297 299 + c.dim(!testFile.completed && !testFile.total 298 300 ? ' [queued]' ··· 398 400 projectName: module.project.name, 399 401 projectColor: module.project.color, 400 402 tests: new Map(), 401 - typecheck: !!module.task.meta.typecheck, 403 + meta: module.task.meta, 402 404 } 403 405 }
+1
packages/vitest/src/node/types/config.ts
··· 1226 1226 vmMemoryLimit?: UserConfig['vmMemoryLimit'] 1227 1227 dumpDir?: string 1228 1228 tagsFilter?: string[] 1229 + mergeReportsLabel?: string 1229 1230 1230 1231 experimental: Omit<Required<UserConfig>['experimental'], 'importDurations'> & { 1231 1232 importDurations: {
+1 -1
test/e2e/test/reporters/__snapshots__/html.test.ts.snap
··· 182 182 "file": [Circular], 183 183 "fullName": "all-passing-or-skipped.test.ts > 3 + 3 = 6", 184 184 "fullTestName": "3 + 3 = 6", 185 - "id": "1111755131_1", 185 + "id": "1804288165_1", 186 186 "location": { 187 187 "column": 6, 188 188 "line": 7,