mirror of
https://github.com/hanzoai/vitest.git
synced 2026-09-24 17:04:25 +00:00
feat(ui)!: change html reporter default output to .vitest (#10620)
Co-authored-by: Hiroshi Ogawa <[email protected]>
This commit is contained in:
committed by
GitHub
co-authored by
Hiroshi Ogawa
parent
05ff4ea7bb
commit
26c73e4f09
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,3 +1,2 @@
|
||||
__traces__
|
||||
__screenshots__
|
||||
html
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
html/
|
||||
@@ -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,
|
||||
|
||||
@@ -80,7 +80,6 @@ export default defineConfig({
|
||||
},
|
||||
diff: './custom-diff-config.ts',
|
||||
outputFile: {
|
||||
html: './html/index.html',
|
||||
json: './browser.json',
|
||||
},
|
||||
onConsoleLog(log) {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
html
|
||||
@@ -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.
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
@@ -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,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: {
|
||||
|
||||
@@ -45,7 +45,7 @@ test.describe('html reporter', () => {
|
||||
},
|
||||
{
|
||||
root: linuxRoot,
|
||||
build: { outDir: 'html' },
|
||||
build: { outDir: '.vitest' },
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ test.describe('html singleFile', () => {
|
||||
},
|
||||
{
|
||||
root,
|
||||
build: { outDir: 'html' },
|
||||
build: { outDir: '.vitest' },
|
||||
},
|
||||
)
|
||||
previewServer = server.previewServer
|
||||
|
||||
@@ -83,7 +83,7 @@ test.describe('html reporter', () => {
|
||||
},
|
||||
{
|
||||
root,
|
||||
build: { outDir: 'html' },
|
||||
build: { outDir: '.vitest' },
|
||||
},
|
||||
)
|
||||
previewServer = server.previewServer
|
||||
|
||||
@@ -167,7 +167,7 @@ test.describe('html report', () => {
|
||||
{
|
||||
root: './fixtures/main',
|
||||
base: '/custom/base/',
|
||||
build: { outDir: 'html' },
|
||||
build: { outDir: '.vitest' },
|
||||
},
|
||||
)
|
||||
previewServer = server.previewServer
|
||||
|
||||
@@ -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).)*$',
|
||||
|
||||
Reference in New Issue
Block a user