Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions docs/.vitepress/contributors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,20 @@ const plainTeamMembers: CoreTeam[] = [
youtube: 'antfu',
sponsor: 'https://github.com/sponsors/antfu',
title: 'A fanatical open sourceror',
org: 'NuxtLabs',
orgLink: 'https://nuxtlabs.com/',
org: 'Vercel',
orgLink: 'https://vercel.com/',
desc: 'Core team member of Vite & Vue',
},
{
avatar: getAvatarUrl('AriPerkkio'),
name: 'Ari Perkkiö',
github: 'AriPerkkio',
bluesky: 'https://bsky.app/profile/ariperkkio.dev',
mastodon: 'https://elk.zone/m.webtoo.ls/@AriPerkkio',
sponsor: 'https://github.com/sponsors/AriPerkkio',
title: 'Open source engineer',
desc: 'Core team member of Vitest',
org: 'StackBlitz',
orgLink: 'https://stackblitz.com/',
org: 'Chromatic',
orgLink: 'https://www.chromatic.com/',
},
{
avatar: getAvatarUrl('hi-ogawa'),
Expand All @@ -96,9 +95,7 @@ const plainTeamMembers: CoreTeam[] = [
bluesky: 'https://bsky.app/profile/patak.dev',
mastodon: 'https://elk.zone/m.webtoo.ls/@patak',
sponsor: 'https://github.com/sponsors/patak-dev',
title: 'A collaborative being',
org: 'StackBlitz',
orgLink: 'https://stackblitz.com/',
title: 'Independent Open Source Adventurer',
desc: 'Core team member of Vite & Vue',
},
{
Expand Down
14 changes: 12 additions & 2 deletions docs/config/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,6 @@ Vitest will delete this directory before running tests if `coverage.clean` is en

Directory to write coverage report to.

To preview the coverage report in the output of [HTML reporter](/guide/reporters.html#html-reporter), this option must be set as a sub-directory of the html report directory (for example `./html/coverage`).

## coverage.reporter

- **Type:** `string | string[] | [string, {}][]`
Expand Down Expand Up @@ -395,3 +393,15 @@ Concurrency limit used when processing the coverage results.
- **CLI:** `--coverage.customProviderModule=<path or module name>`

Specifies the module name or path for the custom coverage provider module. See [Guide - Custom Coverage Provider](/guide/coverage#custom-coverage-provider) for more information.

## coverage.htmlDir

- **Type:** `string`
- **Default:** Automatically inferred from `html`, `html-spa`, or `lcov` coverage reporters
- **CLI:** `--coverage.htmlDir=<path>`

Directory of HTML coverage output to be served in [Vitest UI](/guide/ui) and [HTML reporter](/guide/reporters.html#html-reporter).

This is automatically configured when using builtin coverage reporters that produce HTML output (`html`, `html-spa`, and `lcov`). Use this option to override with a custom coverage reporting location when using custom coverage reporters.

Note that setting this option does not change where coverage HTML report is generated. Configure the `coverage.reporter` option to change the directory instead.
6 changes: 2 additions & 4 deletions docs/guide/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -499,11 +499,9 @@ If code coverage generation is slow on your project, see [Profiling Test Perform

## Vitest UI

You can check your coverage report in [Vitest UI](/guide/ui).
You can check your coverage report in [Vitest UI](/guide/ui) and [HTML reporter](/guide/reporters.html#html-reporter).

Vitest UI will enable coverage report when it is enabled explicitly and the html coverage reporter is present, otherwise it will not be available:
- enable `coverage.enabled=true` in your configuration file or run Vitest with `--coverage.enabled=true` flag
- add `html` to the `coverage.reporter` list: you can also enable `subdir` option to put coverage report in a subdirectory
This is integrated with builtin coverage reporters with HTML output (`html`, `html-spa`, and `lcov` reporters). `html` reporter is enabled by default and this works out of the box. To integrate with custom reporters, you can configure [`coverage.htmlDir`](/config/coverage#coverage-htmldir).

<img alt="html coverage activation in Vitest UI" img-light src="/vitest-ui-show-coverage-light.png">
<img alt="html coverage activation in Vitest UI" img-dark src="/vitest-ui-show-coverage-dark.png">
Expand Down
57 changes: 6 additions & 51 deletions packages/browser/src/node/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import type { HtmlTagDescriptor } from 'vite'
import type { Plugin } from 'vitest/config'
import type { Vitest } from 'vitest/node'
import type { ParentBrowserProject } from './projectParent'
import { createReadStream, readFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { dynamicImportPlugin } from '@vitest/mocker/node'
import { toArray } from '@vitest/utils/helpers'
import MagicString from 'magic-string'
import { basename, dirname, join, resolve } from 'pathe'
import { dirname, join, resolve } from 'pathe'
import sirv from 'sirv'
import { coverageConfigDefaults } from 'vitest/config'
import {
isFileServingAllowed,
isValidApiRequest,
Expand Down Expand Up @@ -63,18 +61,12 @@ export default (parentServer: ParentBrowserProject, base = '/'): Plugin[] => {
},
)

const coverageFolder = resolveCoverageFolder(parentServer.vitest)
const coveragePath = coverageFolder ? coverageFolder[1] : undefined
if (coveragePath && base === coveragePath) {
throw new Error(
`The ui base path and the coverage path cannot be the same: ${base}, change coverage.reportsDirectory`,
)
}

if (coverageFolder) {
// Serve coverage HTML at ./coverage if configured
const coverageHtmlDir = parentServer.vitest.config.coverage?.htmlDir
if (coverageHtmlDir) {
server.middlewares.use(
coveragePath!,
sirv(coverageFolder[0], {
'/__vitest_test__/coverage',
sirv(coverageHtmlDir, {
single: true,
dev: true,
setHeaders: (res) => {
Expand Down Expand Up @@ -604,43 +596,6 @@ function getRequire() {
return _require
}

function resolveCoverageFolder(vitest: Vitest) {
const options = vitest.config
const coverageOptions = vitest._coverageOptions
const htmlReporter = coverageOptions?.enabled
? toArray(options.coverage.reporter).find((reporter) => {
if (typeof reporter === 'string') {
return reporter === 'html'
}

return reporter[0] === 'html'
})
: undefined

if (!htmlReporter) {
return undefined
}

// reportsDirectory not resolved yet
const root = resolve(
options.root || process.cwd(),
coverageOptions.reportsDirectory || coverageConfigDefaults.reportsDirectory,
)

const subdir
= Array.isArray(htmlReporter)
&& htmlReporter.length > 1
&& 'subdir' in htmlReporter[1]
? htmlReporter[1].subdir
: undefined

if (!subdir || typeof subdir !== 'string') {
return [root, `/${basename(root)}/`]
}

return [resolve(root, subdir), `/${basename(root)}/${subdir}/`]
}

const postfixRE = /[?#].*$/
function cleanUrl(url: string): string {
return url.replace(postfixRE, '')
Expand Down
6 changes: 1 addition & 5 deletions packages/ui/client/components/Coverage.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
<script setup lang="ts">
import DetailsHeaderButtons from '~/components/DetailsHeaderButtons.vue'
import { browserState } from '~/composables/client'

defineProps<{
src: string
}>()
</script>

<template>
Expand All @@ -15,7 +11,7 @@ defineProps<{
<DetailsHeaderButtons v-if="browserState" />
</div>
<div flex-auto py-1 bg-white>
<iframe id="vitest-ui-coverage" :src="src" />
<iframe id="vitest-ui-coverage" src="./coverage/index.html" />
</div>
</div>
</template>
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/client/composables/attachments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function getAttachmentUrl(attachment: TestAttachment): string {
if (attachment.path) {
if (isReport) {
// html reporter copies attachments to /data/ folder
return `/data/${basename(attachment.path)}`
return `./data/${basename(attachment.path)}`
}
return `/__vitest_attachment__?path=${encodeURIComponent(attachment.path)}&contentType=${contentType}&token=${(window as any).VITEST_API_TOKEN}`
}
Expand Down
19 changes: 1 addition & 18 deletions packages/ui/client/composables/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const coverageConfigured = computed(() => coverage.value?.enabled)
export const coverageEnabled = computed(() => {
return (
coverageConfigured.value
&& !!coverage.value?.htmlReporter
&& !!coverage.value?.htmlDir
)
})
export const mainSizes = useLocalStorage<[left: number, right: number]>(
Expand Down Expand Up @@ -71,23 +71,6 @@ export const panels = reactive({
},
})

// TODO
// For html report preview, "coverage.reportsDirectory" must be explicitly set as a subdirectory of html report.
// Handling other cases seems difficult, so this limitation is mentioned in the documentation for now.
export const coverageUrl = computed(() => {
if (coverageEnabled.value) {
const idx = coverage.value!.reportsDirectory.lastIndexOf('/')
const htmlReporterSubdir = coverage.value!.htmlReporter?.subdir
return htmlReporterSubdir
? `/${coverage.value!.reportsDirectory.slice(idx + 1)}/${
htmlReporterSubdir
}/index.html`
: `/${coverage.value!.reportsDirectory.slice(idx + 1)}/index.html`
}

return undefined
})

watch(
testRunState,
(state) => {
Expand Down
3 changes: 0 additions & 3 deletions packages/ui/client/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import Navigation from '~/components/Navigation.vue'
import ProgressBar from '~/components/ProgressBar.vue'
import { browserState } from '~/composables/client'
import {
coverageUrl,
coverageVisible,
detailSizes,
detailsPanelVisible,
Expand Down Expand Up @@ -97,7 +96,6 @@ function allowBrowserEvents() {
<Coverage
v-else-if="coverageVisible"
key="coverage"
:src="coverageUrl!"
/>
<FileDetails v-else key="details" />
</transition>
Expand Down Expand Up @@ -127,7 +125,6 @@ function allowBrowserEvents() {
<Coverage
v-else-if="coverageVisible"
key="coverage"
:src="coverageUrl!"
/>
<FileDetails v-else key="details" />
</div>
Expand Down
56 changes: 6 additions & 50 deletions packages/ui/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ import type { Plugin } from 'vite'
import type { Vitest } from 'vitest/node'
import fs from 'node:fs'
import { fileURLToPath } from 'node:url'
import { toArray } from '@vitest/utils/helpers'
import { basename, resolve } from 'pathe'
import { join, resolve } from 'pathe'
import sirv from 'sirv'
import c from 'tinyrainbow'
import { coverageConfigDefaults } from 'vitest/config'
import { isFileServingAllowed, isValidApiRequest } from 'vitest/node'
import { version } from '../package.json'

Expand All @@ -29,18 +27,13 @@ export default (ctx: Vitest): Plugin => {
handler(server) {
const uiOptions = ctx.config
const base = uiOptions.uiBase
const coverageFolder = resolveCoverageFolder(ctx)
const coveragePath = coverageFolder ? coverageFolder[1] : undefined
if (coveragePath && base === coveragePath) {
throw new Error(
`The ui base path and the coverage path cannot be the same: ${base}, change coverage.reportsDirectory`,
)
}

if (coverageFolder) {
// Serve coverage HTML at ./coverage if configured
const coverageHtmlDir = ctx.config.coverage?.htmlDir
if (coverageHtmlDir) {
server.middlewares.use(
coveragePath!,
sirv(coverageFolder[0], {
join(base, 'coverage'),
sirv(coverageHtmlDir, {
single: true,
dev: true,
setHeaders: (res) => {
Expand Down Expand Up @@ -126,40 +119,3 @@ export default (ctx: Vitest): Plugin => {
},
}
}

function resolveCoverageFolder(ctx: Vitest) {
const options = ctx.config
const htmlReporter
= options.api?.port && options.coverage?.enabled
? toArray(options.coverage.reporter).find((reporter) => {
if (typeof reporter === 'string') {
return reporter === 'html'
}

return reporter[0] === 'html'
})
: undefined

if (!htmlReporter) {
return undefined
}

// reportsDirectory not resolved yet
const root = resolve(
ctx.config?.root || options.root || process.cwd(),
options.coverage.reportsDirectory || coverageConfigDefaults.reportsDirectory,
)

const subdir
= Array.isArray(htmlReporter)
&& htmlReporter.length > 1
&& 'subdir' in htmlReporter[1]
? htmlReporter[1].subdir
: undefined

if (!subdir || typeof subdir !== 'string') {
return [root, `/${basename(root)}/`]
}

return [resolve(root, subdir), `/${basename(root)}/${subdir}/`]
}
10 changes: 10 additions & 0 deletions packages/ui/node/reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,14 @@ export default class HTMLReporter implements Reporter {
)}${c.dim(' to see the test results.')}`,
)
}

async onFinishedReportCoverage(): Promise<void> {
if (this.ctx.config.coverage.enabled && this.ctx.config.coverage.htmlDir) {
const coverageHtmlDir = this.ctx.config.coverage.htmlDir
const destCoverageDir = resolve(this.reporterDir, 'coverage')
await fs.rm(destCoverageDir, { recursive: true, force: true })
await fs.mkdir(destCoverageDir, { recursive: true })
await fs.cp(coverageHtmlDir, destCoverageDir, { recursive: true })
}
}
}
25 changes: 25 additions & 0 deletions packages/vitest/src/node/config/resolveConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,31 @@ export function resolveConfig(
`You cannot set "coverage.reportsDirectory" as ${reportsDirectory}. Vitest needs to be able to remove this directory before test run`,
)
}

if (resolved.coverage.htmlDir) {
resolved.coverage.htmlDir = resolve(
resolved.root,
resolved.coverage.htmlDir,
)
}

// infer default htmlDir based on builtin reporter's html output location
if (!resolved.coverage.htmlDir) {
const htmlReporter = resolved.coverage.reporter.find(([name]) => name === 'html' || name === 'html-spa')
if (htmlReporter) {
const [, options] = htmlReporter
const subdir = options && typeof options === 'object' && 'subdir' in options && typeof options.subdir === 'string'
? options.subdir
: undefined
resolved.coverage.htmlDir = resolve(reportsDirectory, subdir || '.')
}
else {
const lcovReporter = resolved.coverage.reporter.find(([name]) => name === 'lcov')
if (lcovReporter) {
resolved.coverage.htmlDir = resolve(reportsDirectory, 'lcov-report')
}
}
}
}

if (resolved.coverage.enabled && resolved.coverage.provider === 'custom' && resolved.coverage.customProviderModule) {
Expand Down
9 changes: 1 addition & 8 deletions packages/vitest/src/node/config/serializeConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,14 @@ export function serializeConfig(project: TestProject): SerializedConfig {
snapshotEnvironment: config.snapshotEnvironment,
passWithNoTests: config.passWithNoTests,
coverage: ((coverage) => {
const htmlReporter = coverage.reporter.find(([reporterName]) => reporterName === 'html') as [
'html',
{ subdir?: string },
] | undefined
const subdir = htmlReporter && htmlReporter[1]?.subdir
return {
reportsDirectory: coverage.reportsDirectory,
provider: coverage.provider,
enabled: coverage.enabled,
htmlReporter: htmlReporter
? { subdir }
: undefined,
customProviderModule: 'customProviderModule' in coverage
? coverage.customProviderModule
: undefined,
htmlDir: coverage.htmlDir,
}
})(config.coverage),
fakeTimers: config.fakeTimers,
Expand Down
Loading
Loading