feat(ui)!: change html reporter default output to .vitest (#10620)

Co-authored-by: Hiroshi Ogawa <[email protected]>
This commit is contained in:
Hiroshi Ogawa
2026-06-19 18:24:43 +09:00
committed by GitHub
co-authored by Hiroshi Ogawa
parent 05ff4ea7bb
commit 26c73e4f09
28 changed files with 91 additions and 101 deletions
+6 -6
View File
@@ -323,10 +323,10 @@ jobs:
run: |
set +e
pnpm --filter=./packages/ui --no-bail test:ui --merge-reports
cp ./packages/ui/.vitest/html/index.html ./packages/ui/.vitest/html/test-ui.html
cp ./packages/ui/.vitest/index.html ./packages/ui/.vitest/test-ui.html
pnpm --filter=./test/unit --filter=./test/e2e --no-bail --sequential test --merge-reports
cp -rf ./test/unit/.vitest/html/index.html ./test/unit/.vitest/html/test-unit.html
cp -rf ./test/e2e/.vitest/html/index.html ./test/e2e/.vitest/html/test-e2e.html
cp -rf ./test/unit/.vitest/index.html ./test/unit/.vitest/test-unit.html
cp -rf ./test/e2e/.vitest/index.html ./test/e2e/.vitest/test-e2e.html
env:
VITEST_CI_MERGE_REPORTS: 'true'
@@ -334,7 +334,7 @@ jobs:
id: upload-ui-report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
path: ./packages/ui/.vitest/html/test-ui.html
path: ./packages/ui/.vitest/test-ui.html
archive: false
retention-days: 7
@@ -342,7 +342,7 @@ jobs:
id: upload-unit-report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
path: ./test/unit/.vitest/html/test-unit.html
path: ./test/unit/.vitest/test-unit.html
archive: false
retention-days: 7
@@ -350,7 +350,7 @@ jobs:
id: upload-e2e-report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
path: ./test/e2e/.vitest/html/test-e2e.html
path: ./test/e2e/.vitest/test-e2e.html
archive: false
retention-days: 7
-3
View File
@@ -30,9 +30,6 @@ test/browser/**/__benchmarks__
test/browser/fixtures/update-snapshot/basic.test.ts
test/e2e/fixtures/browser-multiple/basic-*
*.tsbuildinfo
# exclude static html reporter folder
test/browser/html/
test/unit/html/
.vitest
explainFiles.txt
.vitest-dump
+1 -1
View File
@@ -8,5 +8,5 @@ outline: deep
- **Type:** `string | Record<string, string>`
- **CLI:** `--outputFile=<path>`, `--outputFile.json=./path`
Write test results to a file when the `--reporter=json`, `--reporter=html` or `--reporter=junit` option is also specified.
Write test results to a file when the `--reporter=json` or `--reporter=junit` option is also specified.
By providing an object instead of a string you can define individual outputs when using multiple reporters.
+8
View File
@@ -174,6 +174,14 @@ Vitest no longer serves the browser orchestrator UI from a bare `/__vitest_test_
If you manually opened the browser preview by copying the Vite server URL or visiting `/__vitest_test__/` directly, use the URL opened or printed by Vitest instead.
### Generated Reports and Artifacts Use the `.vitest` Directory
Vitest now uses a single `.vitest` directory at the project root as the shared artifact root, so one `.vitest` entry in `.gitignore` is enough. Defaults that moved this major:
- **Attachments** ([`attachmentsDir`](/config/attachmentsdir)): `.vitest-attachements/` → `.vitest/attachments/`
- **Blob reporter** and `--merge-reports`: `.vitest-reports/blob-*.json` → `.vitest/blob/blob-*.json`
- **HTML reporter** ([`html`](/guide/reporters#html-reporter)): `html/index.html` → `.vitest/index.html`, and its option changed from `outputFile` (a file) to `outputDir` (a directory)
## Migrating to Vitest 4.0 {#vitest-4}
::: warning Prerequisites
+2 -2
View File
@@ -60,7 +60,7 @@ export default defineConfig({
## Reporter Output
By default, Vitest's reporters will print their output to the terminal. When using the `json`, `html` or `junit` reporters, you can instead write your tests' output to a file by including an `outputFile` [configuration option](/config/outputfile) either in your Vite configuration file or via CLI.
By default, Vitest's reporters will print their output to the terminal. When using the `json` or `junit` reporters, you can instead write your tests' output to a file by including an `outputFile` [configuration option](/config/outputfile) either in your Vite configuration file or via CLI. The `html` reporter writes a report directory instead; see its [`outputDir`](#html-reporter) option.
:::code-group
```bash [CLI]
@@ -506,7 +506,7 @@ export default defineConfig({
Generates an HTML file to view test results through an interactive [GUI](/guide/ui). After the file has been generated, Vitest will keep a local development server running and provide a link to view the report in a browser.
Output file can be specified using the [`outputFile`](/config/outputfile) configuration option. If no `outputFile` option is provided, a new HTML file will be created.
The report artifact root can be specified using the reporter's `outputDir` option. The report entry is written to `<outputDir>/index.html` and the UI assets files live under `<outputDir>/ui/`. By default `outputDir` is `.vitest`, the shared Vitest artifact directory, so attachments (`.vitest/attachments`) and coverage (`.vitest/coverage`) are reused without being copied.
:::code-group
```bash [CLI]
+4 -4
View File
@@ -51,10 +51,10 @@ If you still want to see how your tests are running in real time in the terminal
To preview your HTML report, you can use the [vite preview](https://vitejs.dev/guide/cli.html#vite-preview) command:
```sh
npx vite preview --outDir ./html
npx vite preview --outDir .vitest
```
You can configure output with [`outputFile`](/config/outputfile) config option. You need to specify `.html` path there. For example, `./html/index.html` is the default value.
You can configure the output location with the HTML reporter's `outputDir` option. It points to the report artifact root, and the report entry is written to `<outputDir>/index.html`. The default value is `.vitest`, the shared Vitest artifact directory.
:::
If you need a portable report that can be opened or shared as one file, see [`singleFile`](/guide/reporters#html-reporter) in the HTML reporter documentation.
@@ -67,7 +67,7 @@ To view the HTML report from CI, for example in GitHub Actions, upload the outpu
id: upload-report
with:
name: vitest-report
path: html/
path: .vitest/
- name: Viewer link in summary
run: echo "[View HTML report](https://viewer.vitest.dev/?url=${{ steps.upload-report.outputs.artifact-url }})" >> $GITHUB_STEP_SUMMARY
@@ -80,7 +80,7 @@ When you use `singleFile: true`, you can upload the report as a single file and
```yaml
- uses: actions/upload-artifact@v7
with:
path: html/index.html
path: .vitest/index.html
archive: false
```
:::
-1
View File
@@ -1,3 +1,2 @@
__traces__
__screenshots__
html
-1
View File
@@ -1,2 +1 @@
__screenshots__
html/
@@ -7,8 +7,7 @@ export function getAttachmentUrl(attachment: TestAttachment): string {
const contentType = attachment.contentType ?? 'application/octet-stream'
if (attachment.path) {
if (isReport) {
// html reporter copies attachments to /data/ folder
return `./data/${basename(attachment.path)}`
return `./attachments/${basename(attachment.path)}`
}
return `/__vitest_attachment__?path=${encodeURIComponent(attachment.path)}&contentType=${contentType}&token=${(window as any).VITEST_API_TOKEN}`
}
+46 -40
View File
@@ -1,5 +1,5 @@
import type { SerializedError, TestAttachment } from 'vitest'
import type { HTMLOptions, Reporter, ResolvedConfig, RunnerTask, RunnerTestFile, TestModule, Vitest } from 'vitest/node'
import type { HTMLOptions, Reporter, RunnerTask, RunnerTestFile, TestModule, Vitest } from 'vitest/node'
import type { HTMLReportMetadata } from '../client/composables/client/static'
import { existsSync, promises as fs, readFileSync } from 'node:fs'
import { promisify } from 'node:util'
@@ -12,18 +12,6 @@ import { distClientRoot } from './paths'
const gzipAsync = promisify(gzip)
function getOutputFile(config: ResolvedConfig) {
if (!config.outputFile) {
return
}
if (typeof config.outputFile === 'string') {
return config.outputFile
}
return config.outputFile.html
}
export default class HTMLReporter implements Reporter {
ctx!: Vitest
options: HTMLOptions
@@ -36,12 +24,10 @@ export default class HTMLReporter implements Reporter {
async onInit(ctx: Vitest): Promise<void> {
this.ctx = ctx
const htmlFile
= this.options.outputFile
|| getOutputFile(this.ctx.config)
|| 'html/index.html'
const htmlFilePath = resolve(this.ctx.config.root, htmlFile)
this.reporterDir = dirname(htmlFilePath)
this.reporterDir = resolve(
this.ctx.config.root,
this.options.outputDir || '.vitest',
)
}
async onTestRunEnd(
@@ -57,28 +43,46 @@ export default class HTMLReporter implements Reporter {
await inlineAttachments(result.files)
}
// copy ui assets
await fs.cp(distClientRoot, this.reporterDir, { recursive: true })
// create index.html and metadata
const rawData = stringify(result)
const data = await gzipAsync(rawData, {
level: zlibConstants.Z_BEST_COMPRESSION,
})
await handleIndexHtml({
srcDir: distClientRoot,
dstDir: this.reporterDir,
data,
singleFile: this.options.singleFile,
})
// copy attachments
// TODO: unify attachmentsDir and html outputFile, so both live together without extra copy
if (!this.options.singleFile && existsSync(this.ctx.config.attachmentsDir)) {
const destAttachmentsDir = resolve(this.reporterDir, 'data')
await fs.rm(destAttachmentsDir, { recursive: true, force: true })
await fs.mkdir(destAttachmentsDir, { recursive: true })
await fs.cp(this.ctx.config.attachmentsDir, destAttachmentsDir, { recursive: true })
await fs.mkdir(this.reporterDir, { recursive: true })
if (this.options.singleFile) {
// write a single self-contained `<outputDir>/index.html`
await handleIndexHtml({
srcDir: distClientRoot,
dstDir: this.reporterDir,
data,
singleFile: true,
})
}
else {
// copy ui assets into `<outputDir>/ui`
const uiDir = resolve(this.reporterDir, 'ui')
await fs.rm(uiDir, { recursive: true, force: true })
await fs.cp(distClientRoot, uiDir, { recursive: true })
// no need of ui/index.html
await fs.rm(resolve(uiDir, 'index.html'), { force: true })
// create `<outputDir>/index.html` and `<outputDir>/ui/html.meta.json.gz`
await handleIndexHtml({
srcDir: distClientRoot,
dstDir: this.reporterDir,
data,
singleFile: false,
})
// copy attachments into `<outputDir>/attachments` if needed.
// the default location matches so no extra copy.
const attachmentsDir = this.ctx.config.attachmentsDir
const destAttachmentsDir = resolve(this.reporterDir, 'attachments')
if (existsSync(attachmentsDir) && attachmentsDir !== destAttachmentsDir) {
await fs.rm(destAttachmentsDir, { recursive: true, force: true })
await fs.mkdir(destAttachmentsDir, { recursive: true })
await fs.cp(attachmentsDir, destAttachmentsDir, { recursive: true })
}
}
this.ctx.logger.log(
@@ -99,8 +103,8 @@ export default class HTMLReporter implements Reporter {
const destCoverageDir = resolve(this.reporterDir, 'coverage')
if (coverageHtmlDir === destCoverageDir) {
// skip and preserve already generated coverage report.
// this can happen when users configures `outputFile`
// next to `coverage.reportsDirectory`.
// this can happen when the report `outputDir` resolves next to
// `coverage.reportsDirectory` (e.g. default `.vitest/coverage`).
return
}
await fs.rm(destCoverageDir, { recursive: true, force: true })
@@ -204,8 +208,10 @@ async function handleIndexHtml(options: {
}
else {
const dataFile = 'html.meta.json.gz'
await fs.writeFile(resolve(options.dstDir, dataFile), options.data)
metadataCode = `fetch(new URL("./${dataFile}", window.location.href)).then(async res => new Uint8Array(await res.arrayBuffer()))`
await fs.writeFile(resolve(options.dstDir, 'ui', dataFile), options.data)
metadataCode = `fetch(new URL("./ui/${dataFile}", window.location.href)).then(async res => new Uint8Array(await res.arrayBuffer()))`
// rewrite the asset path from `./*` to `./ui/*`
html = html.replace(/\b(href|src)="\.\//g, '$1="./ui/')
}
await fs.writeFile(
+1 -1
View File
@@ -130,7 +130,7 @@ function devHtmlReportPlugin({ htmlDir }: { htmlDir: string }): Plugin {
server.middlewares.use(async (req, res, next) => {
const url = new URL(req.url || '', `http://localhost`)
if (url.pathname === `/${REPORT_FILE}`) {
const data = fs.readFileSync(path.join(htmlDir, REPORT_FILE))
const data = fs.readFileSync(path.join(htmlDir, 'ui', REPORT_FILE))
res.end(data)
return
}
+1 -7
View File
@@ -31,13 +31,7 @@ const testConfig = defineConfig({
? ['blob', { label: process.env.VITEST_CI_BLOB_LABEL }]
: {},
process.env.VITEST_CI_MERGE_REPORTS
? [
'html',
{
outputFile: '.vitest/html/index.html',
singleFile: true,
},
]
? ['html', { singleFile: true }]
: {},
...configDefaults.reporters,
],
+7 -3
View File
@@ -1,10 +1,14 @@
export interface HTMLOptions {
/**
* Path to the generated HTML report.
* Directory used as the report artifact root.
*
* @default 'html/index.html'
* The report entry is written to `<outputDir>/index.html` and the UI
* implementation files live under `<outputDir>/ui/`. By default this is the
* shared `.vitest` artifact directory.
*
* @default '.vitest'
*/
outputFile?: string
outputDir?: string
/**
* Inline report assets, metadata, and attachments into the generated HTML file.
*
+1 -1
View File
@@ -53,7 +53,7 @@ function createReporters(
else if (reporterName in ReportersMap) {
const BuiltinReporter
= ReportersMap[reporterName as BuiltinReporters]
return new BuiltinReporter(reporterOptions)
return new BuiltinReporter(reporterOptions as any)
}
else {
const CustomReporter = await loadCustomReporterModule(
-1
View File
@@ -1 +0,0 @@
html/
+1 -1
View File
@@ -5,7 +5,7 @@ import { instances, provider } from '../../settings'
// TEST_BROWSER=chromium pnpm -C test/browser test-fixtures --root fixtures/trace
// TEST_BROWSER=chromium pnpm -C test/browser test-fixtures --root fixtures/trace --ui --browser.headless
// TEST_BROWSER=chromium pnpm -C test/browser test-fixtures --root fixtures/trace --reporter=html --browser.headless --run
// pnpm dlx serve test/browser/fixtures/trace/html
// pnpm dlx serve test/browser/fixtures/trace/.vitest
export default defineConfig({
test: {
includeTaskLocation: true,
-1
View File
@@ -80,7 +80,6 @@ export default defineConfig({
},
diff: './custom-diff-config.ts',
outputFile: {
html: './html/index.html',
json: './browser.json',
},
onConsoleLog(log) {
-1
View File
@@ -1 +0,0 @@
html
+4 -4
View File
@@ -18,7 +18,7 @@ it('basic', async () => {
}
`)
expect(result.exitCode).toBe(0)
expect(result.fs.statFile('html/index.html').isFile()).toBe(true)
expect(result.fs.statFile('.vitest/index.html').isFile()).toBe(true)
})
it('singleFile', async () => {
@@ -40,7 +40,7 @@ it('singleFile', async () => {
}
`)
expect(result.exitCode).toBe(0)
expect(result.fs.statFile('html/index.html').isFile()).toBe(true)
expect(result.fs.statFile('.vitest/index.html').isFile()).toBe(true)
})
it('browser mode headless', async () => {
@@ -68,7 +68,7 @@ test('basic', () => {});
},
}
`)
expect(result.fs.statFile('html/index.html').isFile()).toBe(true)
expect(result.fs.statFile('.vitest/index.html').isFile()).toBe(true)
})
it('html and coverage already next each other', async () => {
@@ -86,7 +86,7 @@ test('add', () => {
}, {
reporters: [
'default',
['html', { outputFile: './custom-dir/index.html' }],
['html', { outputDir: './custom-dir' }],
],
coverage: {
enabled: true,
@@ -487,7 +487,7 @@ test.for([
expect(result3.stderr).toMatchInlineSnapshot(`""`)
expect(result3.stdout).toMatchInlineSnapshot(`
" HTML Report is generated
You can run npx vite preview --outDir html to see the test results.
You can run npx vite preview --outDir .vitest to see the test results.
"
`)
})
+1 -7
View File
@@ -10,13 +10,7 @@ export default defineConfig({
? ['blob', { label: process.env.VITEST_CI_BLOB_LABEL }]
: {}),
(process.env.VITEST_CI_MERGE_REPORTS
? [
'html',
{
outputFile: '.vitest/html/index.html',
singleFile: true,
},
]
? ['html', { singleFile: true }]
: {}),
],
onConsoleLog(log) {
-1
View File
@@ -1,2 +1 @@
html
test-results
@@ -1,7 +1,7 @@
import { configDefaults, defineConfig } from 'vitest/config'
// pnpm -C test/ui test-fixtures --root fixtures/single-file --run
// pnpm dlx serve test/ui/fixtures/single-file/html/
// pnpm dlx serve test/ui/fixtures/single-file/.vitest/
export default defineConfig({
test: {
+1 -1
View File
@@ -45,7 +45,7 @@ test.describe('html reporter', () => {
},
{
root: linuxRoot,
build: { outDir: 'html' },
build: { outDir: '.vitest' },
},
)
+1 -1
View File
@@ -15,7 +15,7 @@ test.describe('html singleFile', () => {
},
{
root,
build: { outDir: 'html' },
build: { outDir: '.vitest' },
},
)
previewServer = server.previewServer
+1 -1
View File
@@ -83,7 +83,7 @@ test.describe('html reporter', () => {
},
{
root,
build: { outDir: 'html' },
build: { outDir: '.vitest' },
},
)
previewServer = server.previewServer
+1 -1
View File
@@ -167,7 +167,7 @@ test.describe('html report', () => {
{
root: './fixtures/main',
base: '/custom/base/',
build: { outDir: 'html' },
build: { outDir: '.vitest' },
},
)
previewServer = server.previewServer
+1 -7
View File
@@ -133,13 +133,7 @@ export default defineConfig({
? [['blob', { label: process.env.VITEST_CI_BLOB_LABEL }]]
: []),
process.env.VITEST_CI_MERGE_REPORTS
? [
'html',
{
outputFile: '.vitest/html/index.html',
singleFile: true,
},
]
? ['html', { singleFile: true }]
: {},
],
testNamePattern: '^((?!does not include test that).)*$',