diff --git a/docs/guide/coverage.md b/docs/guide/coverage.md
index 19ab602b4cb6..e09a2e21034c 100644
--- a/docs/guide/coverage.md
+++ b/docs/guide/coverage.md
@@ -169,7 +169,9 @@ To see all configurable options for coverage, see the [coverage Config Reference
Since Vitest 0.31.0, you can check your coverage report in [Vitest UI](./ui).
-If you have configured coverage reporters, don't forget to add `html` reporter to the list, Vitest UI will only enable html coverage report if it is present.
+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 or run Vitest with `--coverage.enabled=true` flag
+- add `html` to the `coverage.reporters` list: you can also enable `subdir` option to put coverage report in a subdirectory
diff --git a/packages/ui/client/composables/navigation.ts b/packages/ui/client/composables/navigation.ts
index cb6fd3186cfc..a459969e6618 100644
--- a/packages/ui/client/composables/navigation.ts
+++ b/packages/ui/client/composables/navigation.ts
@@ -21,7 +21,15 @@ export const coverageUrl = computed(() => {
if (coverageEnabled.value) {
const url = `${window.location.protocol}//${window.location.hostname}:${config.value!.api!.port!}`
const idx = coverage.value!.reportsDirectory.lastIndexOf('/')
- return `${url}/${coverage.value!.reportsDirectory.slice(idx + 1)}/index.html`
+ const htmlReporter = coverage.value!.reporter.find((reporter) => {
+ if (reporter[0] !== 'html')
+ return undefined
+
+ return reporter
+ })
+ return htmlReporter && 'subdir' in htmlReporter[1]
+ ? `${url}/${coverage.value!.reportsDirectory.slice(idx + 1)}/${htmlReporter[1].subdir}/index.html`
+ : `${url}/${coverage.value!.reportsDirectory.slice(idx + 1)}/index.html`
}
return undefined
diff --git a/packages/ui/node/index.ts b/packages/ui/node/index.ts
index 1ca2f972213c..c00bab66b350 100644
--- a/packages/ui/node/index.ts
+++ b/packages/ui/node/index.ts
@@ -13,11 +13,11 @@ export default (ctx: Vitest) => {
const uiOptions = ctx.config
const base = uiOptions.uiBase
const coverageFolder = resolveCoverageFolder(ctx)
- const coveragePath = coverageFolder ? `/${basename(coverageFolder)}/` : undefined
+ 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`)
- coverageFolder && server.middlewares.use(coveragePath!, sirv(coverageFolder, {
+ coverageFolder && server.middlewares.use(coveragePath!, sirv(coverageFolder[0], {
single: true,
dev: true,
setHeaders: (res) => {
@@ -35,20 +35,30 @@ export default (ctx: Vitest) => {
function resolveCoverageFolder(ctx: Vitest) {
const options = ctx.config
- const enabled = options.api?.port
- && options.coverage?.enabled
- && options.coverage.reporter.some((reporter) => {
+ const htmlReporter = (options.api?.port && options.coverage?.enabled)
+ ? options.coverage.reporter.find((reporter) => {
if (typeof reporter === 'string')
return reporter === 'html'
- return reporter.length && reporter.includes('html')
+ return reporter[0] === 'html'
})
+ : undefined
+
+ if (!htmlReporter)
+ return undefined
// reportsDirectory not resolved yet
- return enabled
- ? resolve(
- ctx.config?.root || options.root || process.cwd(),
- options.coverage.reportsDirectory || coverageConfigDefaults.reportsDirectory,
- )
+ 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)
+ return [root, `/${basename(root)}/`]
+
+ return [resolve(root, subdir), `/${basename(root)}/${subdir}/`]
}