Skip to content

Commit

Permalink
feat(ui): add html coverage (#3071)
Browse files Browse the repository at this point in the history
Co-authored-by: Vladimir Sheremet <sleuths.slews0s@icloud.com>
  • Loading branch information
userquin and sheremet-va committed May 3, 2023
1 parent 78bad4a commit e24cd9b
Show file tree
Hide file tree
Showing 23 changed files with 216 additions and 20 deletions.
12 changes: 12 additions & 0 deletions .eslintrc
Expand Up @@ -23,6 +23,18 @@
}
]
}
},
{
// these files define vitest as peer dependency
"files": "packages/{coverage-*,ui,browser}/**/*.*",
"rules": {
"no-restricted-imports": [
"error",
{
"paths": ["path"]
}
]
}
}
]
}
2 changes: 2 additions & 0 deletions docs/config/index.md
Expand Up @@ -776,6 +776,8 @@ The reporter has three different types:
}
```

Since Vitest 0.31.0, you can check your coverage report in Vitest UI: check [Vitest UI Coverage](/guide/coverage#vitest-ui) for more details.

#### coverage.skipFull

- **Type:** `boolean`
Expand Down
12 changes: 12 additions & 0 deletions docs/guide/coverage.md
Expand Up @@ -164,3 +164,15 @@ if (condition) {
## Other Options
To see all configurable options for coverage, see the [coverage Config Reference](https://vitest.dev/config/#coverage).
## Vitest UI
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.
<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">
<img alt="html coverage in Vitest UI" img-light src="/vitest-ui-coverage-light.png">
<img alt="html coverage in Vitest UI" img-dark src="/vitest-ui-coverage-dark.png">
2 changes: 2 additions & 0 deletions docs/guide/ui.md
Expand Up @@ -34,6 +34,8 @@ export default {
}
```

Since Vitest 0.31.0, you can check your coverage report in Vitest UI: check [Vitest UI Coverage](/guide/coverage#vitest-ui) for more details.

::: warning
If you still want to see how your tests are running in real time in the terminal, don't forget to add `default` reporter to `reporters` option: `['default', 'html']`.
:::
Expand Down
Binary file added docs/public/vitest-ui-coverage-dark.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/public/vitest-ui-coverage-light.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/public/vitest-ui-show-coverage-dark.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/public/vitest-ui-show-coverage-light.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion packages/browser/src/client/main.ts
@@ -1,5 +1,4 @@
import { createClient } from '@vitest/ws-client'
// eslint-disable-next-line no-restricted-imports
import type { ResolvedConfig } from 'vitest'
import type { CancelReason, VitestRunner } from '@vitest/runner'
import { createBrowserRunner } from './runner'
Expand Down
1 change: 0 additions & 1 deletion packages/coverage-c8/src/provider.ts
Expand Up @@ -9,7 +9,6 @@ import { provider } from 'std-env'
import type { EncodedSourceMap } from 'vite-node'
import { coverageConfigDefaults } from 'vitest/config'
import { BaseCoverageProvider } from 'vitest/coverage'
// eslint-disable-next-line no-restricted-imports
import type { AfterSuiteRunMeta, CoverageC8Options, CoverageProvider, ReportContext, ResolvedCoverageOptions } from 'vitest'
import type { Vitest } from 'vitest/node'
import type { Report } from 'c8'
Expand Down
1 change: 0 additions & 1 deletion packages/coverage-istanbul/src/provider.ts
@@ -1,4 +1,3 @@
/* eslint-disable no-restricted-imports */
import { existsSync, promises as fs } from 'node:fs'
import { relative, resolve } from 'pathe'
import type { TransformPluginContext } from 'rollup'
Expand Down
1 change: 1 addition & 0 deletions packages/ui/client/components.d.ts
Expand Up @@ -11,6 +11,7 @@ declare module '@vue/runtime-core' {
export interface GlobalComponents {
CodeMirror: typeof import('./components/CodeMirror.vue')['default']
ConnectionOverlay: typeof import('./components/ConnectionOverlay.vue')['default']
Coverage: typeof import('./components/Coverage.vue')['default']
Dashboard: typeof import('./components/Dashboard.vue')['default']
DashboardEntry: typeof import('./components/dashboard/DashboardEntry.vue')['default']
DetailsPanel: typeof import('./components/DetailsPanel.vue')['default']
Expand Down
40 changes: 40 additions & 0 deletions packages/ui/client/components/Coverage.vue
@@ -0,0 +1,40 @@
<script setup lang="ts">
defineProps<{
src: string
}>()
</script>

<template>
<div h="full" flex="~ col">
<div
p="3"
h-10
flex="~ gap-2"
items-center
bg-header
border="b base"
>
<div class="i-carbon:folder-details-reference" />
<span
pl-1
font-bold
text-sm
flex-auto
ws-nowrap
overflow-hidden
truncate
>Coverage</span>
</div>
<div flex-auto py-1 bg-white>
<iframe id="vitest-ui-coverage" :src="src" />
</div>
</div>
</template>

<style>
#vitest-ui-coverage {
width: 100%;
height: calc(100vh - 42px);
border: none;
}
</style>
64 changes: 56 additions & 8 deletions packages/ui/client/components/Navigation.vue
@@ -1,8 +1,18 @@
<script setup lang="ts">
import { hasFailedSnapshot } from '@vitest/ws-client'
import { currentModule, dashboardVisible, showDashboard } from '../composables/navigation'
import { Tooltip as VueTooltip } from 'floating-vue'
import {
coverageConfigured,
coverageEnabled,
coverageVisible,
currentModule,
dashboardVisible,
disableCoverage,
showCoverage,
showDashboard,
} from '../composables/navigation'
import { client, findById } from '../composables/client'
import type { Task } from '#types'
import type { File, Task } from '#types'
import { isDark, toggleDark } from '~/composables'
import { files, isReport, runAll } from '~/composables/client'
import { activeFileId } from '~/composables/params'
Expand All @@ -18,39 +28,77 @@ function onItemClick(task: Task) {
showDashboard(false)
}
const toggleMode = computed(() => isDark.value ? 'light' : 'dark')
async function onRunAll(files?: File[]) {
if (coverageEnabled.value) {
disableCoverage.value = true
await nextTick()
if (coverageEnabled.value) {
showDashboard(true)
await nextTick()
}
}
await runAll(files)
}
</script>

<template>
<TasksList border="r base" :tasks="files" :on-item-click="onItemClick" :group-by-type="true" @run="runAll">
<TasksList border="r base" :tasks="files" :on-item-click="onItemClick" :group-by-type="true" @run="onRunAll">
<template #header="{ filteredTests }">
<img w-6 h-6 src="/favicon.svg" alt="Vitest logo">
<span font-light text-sm flex-1>Vitest</span>
<div class="flex text-lg">
<IconButton
v-show="!dashboardVisible"
v-show="(coverageConfigured && !coverageEnabled) || !dashboardVisible"
v-tooltip.bottom="'Dashboard'"
title="Show dashboard"
class="!animate-100ms"
animate-count-1
icon="i-carbon-dashboard"
icon="i-carbon:dashboard"
@click="showDashboard(true)"
/>
<VueTooltip
v-if="coverageConfigured && !coverageEnabled"
title="Coverage enabled but missing html reporter"
class="w-1.4em h-1.4em op100 rounded flex color-red5 dark:color-#f43f5e cursor-help"
>
<div class="i-carbon:folder-off ma" />
<template #popper>
<div class="op100 gap-1 p-y-1" grid="~ items-center cols-[1.5em_1fr]">
<div class="i-carbon:information-square w-1.5em h-1.5em" />
<div>Coverage enabled but missing html reporter.</div>
<div style="grid-column: 2">
Add html reporter to your configuration to see coverage here.
</div>
</div>
</template>
</VueTooltip>
<IconButton
v-if="coverageEnabled"
v-show="!coverageVisible"
v-tooltip.bottom="'Coverage'"
:disabled="disableCoverage"
title="Show coverage"
class="!animate-100ms"
animate-count-1
icon="i-carbon:folder-details-reference"
@click="showCoverage()"
/>
<IconButton
v-if="(failedSnapshot && !isReport)"
v-tooltip.bottom="'Update all failed snapshot(s)'"
icon="i-carbon-result-old"
icon="i-carbon:result-old"
@click="updateSnapshot()"
/>
<IconButton
v-if="!isReport"
v-tooltip.bottom="filteredTests ? (filteredTests.length === 0 ? 'No test to run (clear filter)' : 'Rerun filtered') : 'Rerun all'"
:disabled="filteredTests?.length === 0"
icon="i-carbon-play"
icon="i-carbon:play"
@click="runAll(filteredTests)"
/>
<IconButton
v-tooltip.bottom="`Toggle to ${toggleMode} mode`"
icon="dark:i-carbon-moon i-carbon-sun"
icon="dark:i-carbon-moon i-carbon:sun"
@click="toggleDark()"
/>
</div>
Expand Down
10 changes: 9 additions & 1 deletion packages/ui/client/components/Suites.vue
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { hasFailedSnapshot } from '@vitest/ws-client'
import { coverageEnabled, disableCoverage } from '../composables/navigation'
import { client, current, isReport, runCurrent } from '~/composables/client'
const name = computed(() => current.value?.name.split(/\//g).pop())
Expand All @@ -8,6 +9,13 @@ const failedSnapshot = computed(() => current.value?.tasks && hasFailedSnapshot(
function updateSnapshot() {
return current.value && client.rpc.updateSnapshot(current.value)
}
async function onRunCurrent() {
if (coverageEnabled.value) {
disableCoverage.value = true
await nextTick()
}
await runCurrent()
}
</script>

<template>
Expand All @@ -27,7 +35,7 @@ function updateSnapshot() {
v-if="!isReport"
v-tooltip.bottom="'Rerun file'"
icon="i-carbon-play"
@click="runCurrent()"
@click="onRunCurrent()"
/>
</div>
</template>
Expand Down
36 changes: 35 additions & 1 deletion packages/ui/client/composables/navigation.ts
@@ -1,24 +1,50 @@
import { client, findById } from './client'
import { client, config, findById, testRunState } from './client'
import { activeFileId } from './params'
import type { File } from '#types'

export const currentModule = ref<File>()
export const dashboardVisible = ref(true)
export const coverageVisible = ref(false)
export const disableCoverage = ref(true)
export const coverage = computed(() => config.value?.coverage)
export const coverageConfigured = computed(() => {
if (!config.value?.api?.port)
return false

return coverage.value?.enabled
})
export const coverageEnabled = computed(() => {
return coverageConfigured.value
&& coverage.value.reporter.map(([reporterName]) => reporterName).includes('html')
})
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`
}

return undefined
})
watch(testRunState, (state) => {
disableCoverage.value = state === 'running'
}, { immediate: true })
export function initializeNavigation() {
const file = activeFileId.value
if (file && file.length > 0) {
const current = findById(file)
if (current) {
currentModule.value = current
dashboardVisible.value = false
coverageVisible.value = false
}
else {
watchOnce(
() => client.state.getFiles(),
() => {
currentModule.value = findById(file)
dashboardVisible.value = false
coverageVisible.value = false
},
)
}
Expand All @@ -29,8 +55,16 @@ export function initializeNavigation() {

export function showDashboard(show: boolean) {
dashboardVisible.value = show
coverageVisible.value = false
if (show) {
currentModule.value = undefined
activeFileId.value = ''
}
}

export function showCoverage() {
coverageVisible.value = true
dashboardVisible.value = false
currentModule.value = undefined
activeFileId.value = ''
}
3 changes: 2 additions & 1 deletion packages/ui/client/pages/index.vue
@@ -1,7 +1,7 @@
<script setup lang="ts">
// @ts-expect-error missing types
import { Pane, Splitpanes } from 'splitpanes'
import { initializeNavigation } from '../composables/navigation'
import { coverageUrl, coverageVisible, initializeNavigation } from '../composables/navigation'
const dashboardVisible = initializeNavigation()
const mainSizes = reactive([33, 67])
Expand Down Expand Up @@ -39,6 +39,7 @@ function resizeMain() {
<Pane :size="mainSizes[1]">
<transition>
<Dashboard v-if="dashboardVisible" key="summary" />
<Coverage v-else-if="coverageVisible" key="coverage" :src="coverageUrl" />
<Splitpanes v-else key="detail" @resized="onModuleResized">
<Pane :size="detailSizes[0]">
<Suites />
Expand Down
39 changes: 36 additions & 3 deletions packages/ui/node/index.ts
@@ -1,13 +1,26 @@
import { fileURLToPath } from 'node:url'
import { resolve } from 'pathe'
import { basename, resolve } from 'pathe'
import sirv from 'sirv'
import type { Plugin } from 'vite'
import { coverageConfigDefaults } from 'vitest/config'
import type { ResolvedConfig, Vitest } from 'vitest'

export default (base = '/__vitest__/') => {
export default (ctx: Vitest) => {
return <Plugin>{
name: 'vitest:ui',
apply: 'serve',
async configureServer(server) {
configureServer(server) {
const uiOptions: ResolvedConfig = ctx.config
const base = uiOptions.uiBase
const coverageFolder = resolveCoverageFolder(ctx)
const coveragePath = coverageFolder ? `/${basename(coverageFolder)}/` : 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, {
single: true,
dev: true,
}))
const clientDist = resolve(fileURLToPath(import.meta.url), '../client')
server.middlewares.use(base, sirv(clientDist, {
single: true,
Expand All @@ -16,3 +29,23 @@ export default (base = '/__vitest__/') => {
},
}
}

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

return reporter.length && reporter.includes('html')
})

// reportsDirectory not resolved yet
return enabled
? resolve(
ctx.config?.root || options.root || process.cwd(),
options.coverage.reportsDirectory || coverageConfigDefaults.reportsDirectory,
)
: undefined
}

0 comments on commit e24cd9b

Please sign in to comment.