[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: add `createReport` and `.vitest` report directory convention (#9993)

authored by

Ari Perkkiö and committed by
GitHub
(Apr 22, 2026, 6:50 AM +0200) 72a6dc25 3f802da4

+460 -1
+7
docs/guide/index.md
··· 46 46 47 47 The `npx` tool will execute the specified command. By default, `npx` will first check if the command exists in the local project's binaries. If it is not found there, `npx` will look in the system's `$PATH` and execute it if found. If the command is not found in either location, `npx` will install it in a temporary location prior to execution. 48 48 49 + Vitest and third party integrations can use `.vitest` directory to store generated artifacts. It's recommended to add this in your `.gitignore`. 50 + 51 + ``` sh [.gitignore] 52 + # Vitest reports and artifacts 53 + .vitest/ 54 + ``` 55 + 49 56 ## Writing Tests 50 57 51 58 As an example, we will write a simple test that verifies the output of a function that adds two numbers.
+119
docs/api/advanced/vitest.md
··· 687 687 ::: warning 688 688 At the moment, the [browser](/guide/browser/) modules are not supported. 689 689 ::: 690 + 691 + ## createReport <Version>5.0.0</Version> {#createreport} 692 + 693 + ```ts 694 + function createReport(scope: string): Report 695 + ``` 696 + 697 + Creates a report that is limited to the given scope. `Report` follows Vitest's rules around [Storing artifacts on file system](/guide/advanced/reporters.html#storing-artifacts-on-file-system). 698 + 699 + `Report` provides collection of utilities for writing test results, temporary files and other artifacts on the file system. It's especially intended for third party integrations like custom reporters. 700 + 701 + All operations of `Report` are limited to given `scope`. A single report cannot interfere with other reports. Internally Vitest creates `.vitest` directory where each `scope` creates their own directory. This convention of `.vitest` directory reduces the amount of entries end-users need to specify in their `.gitignore`. 702 + 703 + ```ts 704 + import type { Report } from 'vitest/node' 705 + 706 + const scope = 'example-yaml-reporter' 707 + 708 + // Automatically creates `<project-root>/.vitest/example-yaml-reporter/` 709 + // directory if it does not exist already 710 + const report: Report = vitest.createReport(scope) 711 + ``` 712 + 713 + ### Report.root 714 + 715 + ```ts 716 + const root: string 717 + ``` 718 + 719 + The root directory for this scope. 720 + 721 + ```ts 722 + const report = vitest.createReport('my-json-reporter') 723 + 724 + // Is <project-root>/.vitest/my-json-reporter 725 + const root = report.root 726 + ``` 727 + 728 + 729 + ### Report.clean 730 + 731 + ```ts 732 + function clean(): Promise<void> 733 + ``` 734 + 735 + Clean up the report directory for this scope. 736 + 737 + ```ts 738 + const report = vitest.createReport('my-json-reporter') 739 + 740 + // Removes everything inside <project-root>/.vitest/my-json-reporter/ 741 + await report.clean() 742 + ``` 743 + 744 + ### Report.writeFile 745 + 746 + ```ts 747 + function writeFile( 748 + filename: string, 749 + content: string | Uint8Array, 750 + encoding?: BufferEncoding 751 + ): Promise<void> 752 + ``` 753 + 754 + Write a file to the report directory for this scope. By default the file will be written with UTF-8 encoding. The filename is relative to the scope directory. 755 + 756 + ```ts 757 + const report = vitest.createReport('my-json-reporter') 758 + 759 + // Writes file to .vitest/my-json-reporter/test-report.json 760 + await report.writeFile('test-report.json', JSON.stringify(results)) 761 + ``` 762 + 763 + ### Report.readFile 764 + 765 + ```ts 766 + function readFile(filename: string, encoding?: BufferEncoding): Promise<string> 767 + ``` 768 + 769 + Read a file from the report directory for this scope. 770 + 771 + ```ts 772 + const report = vitest.createReport('my-json-reporter') 773 + 774 + // Reads file from .vitest/my-json-reporter/test-report.json 775 + const content: string = await report.readFile('test-report.json') 776 + ``` 777 + 778 + ### Report.readdir 779 + 780 + ```ts 781 + function readdir(): Promise<string[]> 782 + ``` 783 + 784 + Read contents of the report directory for this scope. 785 + 786 + ```ts 787 + const report = vitest.createReport('my-json-reporter') 788 + 789 + // Reads contents from .vitest/my-json-reporter 790 + const filenames: string[] = await report.readdir() 791 + ``` 792 + 793 + ### Report.delete 794 + 795 + <!-- eslint-skip --> 796 + ```ts 797 + function delete(filename: string): Promise<void> 798 + ``` 799 + 800 + Delete a file from the report directory for this scope. 801 + 802 + ```ts 803 + const report = vitest.createReport('my-json-reporter') 804 + 805 + // Deletes file from .vitest/my-json-reporter/test-report.json 806 + await report.delete('test-report.json') 807 + ``` 808 +
+27
docs/guide/advanced/reporters.md
··· 72 72 } 73 73 ``` 74 74 75 + ## Storing artifacts on file system 76 + 77 + ::: tip 78 + Vitest provides [`vitest.createReport`](/api/advanced/vitest.html#createreport) that exposes collection of utilities for writing artifacts on file system conveniently. 79 + ::: 80 + 81 + If your custom reporter needs to store any artifacts on file system it should place them inside `.vitest` directory. This directory is a convention that Vitest reporters and third party integrations can use to co-locate their results in a single directory. This way users of your custom reporter do not need to add multiple exclusion in their `.gitignore`. Only the `.vitest` is needed. 82 + 83 + Reporters and other integrations should respect following rules around `.vitest` directory: 84 + 85 + - `.vitest` directory is placed in [the `root` of the project](/config/root) 86 + - Reporter can create `.vitest` directory if it does not already exist 87 + - Reporter should never remove `.vitest` directory 88 + - Reporter should create their own directory inside `.vitest`, for example `.vitest/yaml-reporter/` 89 + - Reporter can remove their own specific directory inside `.vitest`, for example `.vitest/yaml-reporter/` 90 + 91 + ```ansi 92 + .vitest 93 + 94 + ├── yaml-reporter 95 + │ ├── results.yaml 96 + │ └── summary.yaml 97 + 98 + └── junit-reporter 99 + └── report.xml 100 + ``` 101 + 75 102 ## Exported Reporters 76 103 77 104 `vitest` comes with a few [built-in reporters](/guide/reporters) that you can use out of the box.
+9
packages/vitest/src/node/core.ts
··· 9 9 import type { CliOptions } from './cli/cli-api' 10 10 import type { VitestFetchFunction } from './environments/fetchModule' 11 11 import type { ProcessPool } from './pool' 12 + import type { Report } from './reporters/report' 12 13 import type { TestModule } from './reporters/reported-tasks' 13 14 import type { TestSpecification } from './test-specification' 14 15 import type { ResolvedConfig, TestProjectConfiguration, UserConfig, VitestRunMode } from './types/config' ··· 46 47 import { getDefaultTestProject, resolveBrowserProjects, resolveProjects } from './projects/resolveProjects' 47 48 import { BlobReporter, readBlobs } from './reporters/blob' 48 49 import { HangingProcessReporter } from './reporters/hanging-process' 50 + import { createReport } from './reporters/report' 49 51 import { createBenchmarkReporters, createReporters } from './reporters/utils' 50 52 import { VitestResolver } from './resolver' 51 53 import { VitestSpecifications } from './specifications' ··· 1664 1666 const positivePattern = project.slice(1) 1665 1667 return wildcardPatternToRegExp(positivePattern).test(name) 1666 1668 }) 1669 + } 1670 + 1671 + /** 1672 + * Create a report that's scoped to a specific reporter directory. 1673 + */ 1674 + createReport(scope: string): Report { 1675 + return createReport(this, scope) 1667 1676 } 1668 1677 } 1669 1678
+1 -1
packages/vitest/src/public/node.ts
··· 73 73 } from '../node/reporters' 74 74 export type { HTMLOptions } from '../node/reporters/html' 75 75 export type { JsonOptions } from '../node/reporters/json' 76 - 77 76 export type { JUnitOptions } from '../node/reporters/junit' 78 77 78 + export type { Report } from '../node/reporters/report' 79 79 export type { 80 80 ModuleDiagnostic, 81 81 TaskOptions,
+165
test/cli/test/reporters/report.test.ts
··· 1 + import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' 2 + import { normalize, resolve } from 'pathe' 3 + import { assert, expect, onTestFinished, test } from 'vitest' 4 + import { runVitest } from '../../../test-utils' 5 + 6 + test('creates .vitest when initialized', async () => { 7 + const vitest = await run() 8 + const directory = resolve(vitest.config.root, '.vitest') 9 + 10 + rmSync(directory, { recursive: true, force: true }) 11 + vitest.createReport('example-reporter') 12 + 13 + expect(existsSync(directory)).toBe(true) 14 + }) 15 + 16 + test('sets root', async () => { 17 + const vitest = await run() 18 + 19 + const report = vitest.createReport('example-reporter') 20 + 21 + expect(normalize(report.root)).toBe( 22 + resolve(vitest.config.root, '.vitest', 'example-reporter'), 23 + ) 24 + }) 25 + 26 + test('writeFile() creates file in scoped directory', async () => { 27 + const vitest = await run() 28 + 29 + const report = vitest.createReport('example-reporter') 30 + await report.writeFile('example-report.txt', 'Example report here!') 31 + 32 + expect(readFileSync(resolve(vitest.config.root, '.vitest', 'example-reporter', 'example-report.txt'), 'utf-8')).toBe('Example report here!') 33 + }) 34 + 35 + test('readFile() reads a file in scoped directory', async () => { 36 + const vitest = await run() 37 + 38 + const report = vitest.createReport('example-reporter') 39 + writeFileSync(resolve(vitest.config.root, '.vitest', 'example-reporter', 'example-report.txt'), 'Example report here!') 40 + 41 + await expect(report.readFile('example-report.txt')).resolves.toBe('Example report here!') 42 + }) 43 + 44 + test('readdir() reads contents of scoped directory', async () => { 45 + const vitest = await run() 46 + 47 + const report = vitest.createReport('example-reporter') 48 + const scopedDir = resolve(vitest.config.root, '.vitest', 'example-reporter') 49 + mkdirSync(scopedDir, { recursive: true }) 50 + writeFileSync(resolve(scopedDir, 'report-1'), 'anything') 51 + writeFileSync(resolve(scopedDir, 'report-2'), 'anything') 52 + 53 + await expect(report.readdir()).resolves.toMatchInlineSnapshot(` 54 + [ 55 + "report-1", 56 + "report-2", 57 + ] 58 + `) 59 + }) 60 + 61 + test('delete() removes file in scoped directory', async () => { 62 + const vitest = await run() 63 + 64 + const report = vitest.createReport('example-reporter') 65 + const scopedDir = resolve(vitest.config.root, '.vitest', 'example-reporter') 66 + mkdirSync(scopedDir, { recursive: true }) 67 + writeFileSync(resolve(scopedDir, 'report-1'), 'anything') 68 + writeFileSync(resolve(scopedDir, 'report-2'), 'anything') 69 + 70 + expect(readdirSync(scopedDir)).toMatchInlineSnapshot(` 71 + [ 72 + "report-1", 73 + "report-2", 74 + ] 75 + `) 76 + 77 + await report.delete('report-1') 78 + 79 + expect(readdirSync(scopedDir)).toMatchInlineSnapshot(` 80 + [ 81 + "report-2", 82 + ] 83 + `) 84 + }) 85 + 86 + test('clean() clears only the scoped directory', async () => { 87 + const vitest = await run() 88 + 89 + // This directory should not be affected by scoped report writer 90 + const unrelatedDir = resolve(vitest.config.root, '.vitest', 'unrelated-reporter') 91 + mkdirSync(unrelatedDir, { recursive: true }) 92 + writeFileSync(resolve(unrelatedDir, 'report-1'), 'anything') 93 + 94 + const report = vitest.createReport('example-reporter') 95 + const scopedDir = resolve(vitest.config.root, '.vitest', 'example-reporter') 96 + mkdirSync(scopedDir, { recursive: true }) 97 + writeFileSync(resolve(scopedDir, 'report-2'), 'anything') 98 + writeFileSync(resolve(scopedDir, 'report-3'), 'anything') 99 + 100 + expect(readdirSync(scopedDir)).toMatchInlineSnapshot(` 101 + [ 102 + "report-2", 103 + "report-3", 104 + ] 105 + `) 106 + expect(readdirSync(unrelatedDir)).toMatchInlineSnapshot(` 107 + [ 108 + "report-1", 109 + ] 110 + `) 111 + 112 + await report.clean() 113 + 114 + expect(readdirSync(scopedDir)).toMatchInlineSnapshot(`[]`) 115 + expect(readdirSync(unrelatedDir)).toMatchInlineSnapshot(` 116 + [ 117 + "report-1", 118 + ] 119 + `) 120 + }) 121 + 122 + test('clean() does not clear results when --merge-reports is used, unless forced to', async () => { 123 + const vitest = await run({ mergeReports: '.vitest/blob', watch: false, standalone: false }) 124 + 125 + const report = vitest.createReport('example-reporter') 126 + const scopedDir = resolve(vitest.config.root, '.vitest', 'example-reporter') 127 + mkdirSync(scopedDir, { recursive: true }) 128 + writeFileSync(resolve(scopedDir, 'report-1'), 'anything') 129 + 130 + expect(readdirSync(scopedDir)).toMatchInlineSnapshot(` 131 + [ 132 + "report-1", 133 + ] 134 + `) 135 + 136 + await report.clean() 137 + 138 + expect(readdirSync(scopedDir)).toMatchInlineSnapshot(` 139 + [ 140 + "report-1", 141 + ] 142 + `) 143 + 144 + await report.clean(true) 145 + 146 + expect(readdirSync(scopedDir)).toMatchInlineSnapshot(`[]`) 147 + }) 148 + 149 + async function run(options?: Partial<Parameters<typeof runVitest>[0]>) { 150 + const vitest = await runVitest({ 151 + root: './fixtures/basic', 152 + standalone: true, 153 + watch: true, 154 + ...options, 155 + }) 156 + 157 + assert(vitest.ctx) 158 + const root = vitest.ctx.config.root 159 + 160 + onTestFinished(() => { 161 + rmSync(resolve(root, '.vitest'), { recursive: true, force: true }) 162 + }) 163 + 164 + return vitest.ctx 165 + }
+132
packages/vitest/src/node/reporters/report.ts
··· 1 + import type { Vitest } from '../core' 2 + import * as fsSync from 'node:fs' 3 + import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises' 4 + import { resolve } from 'node:path' 5 + 6 + export interface Report { 7 + /** 8 + * The root directory for this scope. 9 + * 10 + * ```ts 11 + * const report = vitest.createReport('my-json-reporter'); 12 + * 13 + * // Is <project-root>/.vitest/my-json-reporter 14 + * const root = report.root 15 + * ``` 16 + */ 17 + root: string 18 + 19 + /** 20 + * Clean up the report directory for this scope. 21 + * 22 + * By default, if `--merge-reports` is used, this method will not delete existing reports. 23 + * To force deletion of existing reports, pass `true` as an argument. 24 + * 25 + * ```ts 26 + * const report = vitest.createReport('my-json-reporter'); 27 + * 28 + * // Removes everything inside <project-root>/.vitest/my-json-reporter/ 29 + * await report.clean() 30 + * ``` 31 + */ 32 + clean: (force?: boolean) => Promise<void> 33 + 34 + /** 35 + * Write a file to the report directory for this scope. 36 + * By default the file will be written with UTF-8 encoding. 37 + * The filename is relative to the scope directory. 38 + * 39 + * ```ts 40 + * const report = vitest.createReport('my-json-reporter'); 41 + * 42 + * // Writes file to .vitest/my-json-reporter/test-report.json 43 + * await report.writeFile('test-report.json', JSON.stringify(results)) 44 + * ``` 45 + */ 46 + writeFile: (filename: string, content: Parameters<typeof writeFile>[1], encoding?: BufferEncoding) => Promise<void> 47 + 48 + /** 49 + * Read a file from the report directory for this scope. 50 + * 51 + * ```ts 52 + * const report = vitest.createReport('my-json-reporter'); 53 + * 54 + * // Reads file from .vitest/my-json-reporter/test-report.json 55 + * const content: string = await report.readFile('test-report.json') 56 + * ``` 57 + */ 58 + readFile: (filename: string, encoding?: BufferEncoding) => Promise<string> 59 + 60 + /** 61 + * Read contents of the report directory for this scope. 62 + * 63 + * ```ts 64 + * const report = vitest.createReport('my-json-reporter'); 65 + * 66 + * // Reads contents from .vitest/my-json-reporter 67 + * const filenames: string[] = await report.readdir() 68 + * ``` 69 + */ 70 + readdir: () => Promise<string[]> 71 + 72 + /** 73 + * Delete a file from the report directory for this scope. 74 + * 75 + * ```ts 76 + * const report = vitest.createReport('my-json-reporter'); 77 + * 78 + * // Deletes file from .vitest/my-json-reporter/test-report.json 79 + * await report.delete('test-report.json') 80 + * ``` 81 + */ 82 + delete: (filename: string) => Promise<void> 83 + } 84 + 85 + export function createReport(ctx: Vitest, scope: string): Report { 86 + const root = ctx.config.root 87 + const vitestDir = resolve(root, '.vitest') 88 + const reportDir = resolve(vitestDir, scope) 89 + 90 + if (!fsSync.existsSync(vitestDir)) { 91 + fsSync.mkdirSync(vitestDir) 92 + } 93 + 94 + if (!fsSync.existsSync(reportDir)) { 95 + fsSync.mkdirSync(reportDir) 96 + } 97 + 98 + return { 99 + root: reportDir, 100 + 101 + async clean(force = false) { 102 + if (fsSync.existsSync(reportDir)) { 103 + // Do not delete results when run with --merge-reports, unless forced to. 104 + // In test runs with --shard, it's possible that users do some other handling for 105 + // the reports after 'vitest --merge-reports' run. For example upload all the '.vitest/attachments'. 106 + if (ctx.config.mergeReports && !force) { 107 + return 108 + } 109 + 110 + await rm(reportDir, { recursive: true, force: true }) 111 + } 112 + 113 + await mkdir(reportDir) 114 + }, 115 + 116 + async readFile(filename, encoding = 'utf8') { 117 + return await readFile(resolve(vitestDir, scope, filename), encoding) 118 + }, 119 + 120 + async readdir() { 121 + return await readdir(resolve(vitestDir, scope)) 122 + }, 123 + 124 + async writeFile(filename, content, encoding = 'utf8') { 125 + await writeFile(resolve(vitestDir, scope, filename), content, encoding) 126 + }, 127 + 128 + async delete(filename) { 129 + await rm(resolve(vitestDir, scope, filename), { recursive: true, force: true }) 130 + }, 131 + } 132 + }