[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat(ui): add html coverage (#3071)

Co-authored-by: Vladimir Sheremet <sleuths.slews0s@icloud.com>

authored by

Joaquín Sánchez
Vladimir Sheremet
and committed by
GitHub
(May 3, 2023, 11:23 AM +0200) e24cd9b2 78bad4ab

+216 -20
+12
.eslintrc
··· 23 23 } 24 24 ] 25 25 } 26 + }, 27 + { 28 + // these files define vitest as peer dependency 29 + "files": "packages/{coverage-*,ui,browser}/**/*.*", 30 + "rules": { 31 + "no-restricted-imports": [ 32 + "error", 33 + { 34 + "paths": ["path"] 35 + } 36 + ] 37 + } 26 38 } 27 39 ] 28 40 }
+3
pnpm-lock.yaml
··· 1036 1036 sirv: 1037 1037 specifier: ^2.0.2 1038 1038 version: 2.0.2 1039 + vitest: 1040 + specifier: '>=0.30.1 <1' 1041 + version: link:../vitest 1039 1042 devDependencies: 1040 1043 '@faker-js/faker': 1041 1044 specifier: ^7.6.0
+2
docs/config/index.md
··· 776 776 } 777 777 ``` 778 778 779 + 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. 780 + 779 781 #### coverage.skipFull 780 782 781 783 - **Type:** `boolean`
+12
docs/guide/coverage.md
··· 164 164 ## Other Options 165 165 166 166 To see all configurable options for coverage, see the [coverage Config Reference](https://vitest.dev/config/#coverage). 167 + 168 + ## Vitest UI 169 + 170 + Since Vitest 0.31.0, you can check your coverage report in [Vitest UI](./ui). 171 + 172 + 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. 173 + 174 + <img alt="html coverage activation in Vitest UI" img-light src="/vitest-ui-show-coverage-light.png"> 175 + <img alt="html coverage activation in Vitest UI" img-dark src="/vitest-ui-show-coverage-dark.png"> 176 + 177 + <img alt="html coverage in Vitest UI" img-light src="/vitest-ui-coverage-light.png"> 178 + <img alt="html coverage in Vitest UI" img-dark src="/vitest-ui-coverage-dark.png">
+2
docs/guide/ui.md
··· 34 34 } 35 35 ``` 36 36 37 + 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. 38 + 37 39 ::: warning 38 40 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']`. 39 41 :::
docs/public/vitest-ui-coverage-dark.png

This is a binary file and will not be displayed.

docs/public/vitest-ui-coverage-light.png

This is a binary file and will not be displayed.

docs/public/vitest-ui-show-coverage-dark.png

This is a binary file and will not be displayed.

docs/public/vitest-ui-show-coverage-light.png

This is a binary file and will not be displayed.

+3
packages/ui/package.json
··· 43 43 "test:open": "cypress open --component", 44 44 "prepublishOnly": "pnpm build" 45 45 }, 46 + "peerDependencies": { 47 + "vitest": ">=0.30.1 <1" 48 + }, 46 49 "dependencies": { 47 50 "@vitest/utils": "workspace:*", 48 51 "fast-glob": "^3.2.12",
+2 -1
packages/ui/rollup.config.js
··· 14 14 'worker_threads', 15 15 'node:worker_threads', 16 16 'vitest/node', 17 - 'vitest', 17 + 'vitest/config', 18 + 'vite', 18 19 ] 19 20 20 21 export default () => [
-1
packages/coverage-c8/src/provider.ts
··· 9 9 import type { EncodedSourceMap } from 'vite-node' 10 10 import { coverageConfigDefaults } from 'vitest/config' 11 11 import { BaseCoverageProvider } from 'vitest/coverage' 12 - // eslint-disable-next-line no-restricted-imports 13 12 import type { AfterSuiteRunMeta, CoverageC8Options, CoverageProvider, ReportContext, ResolvedCoverageOptions } from 'vitest' 14 13 import type { Vitest } from 'vitest/node' 15 14 import type { Report } from 'c8'
-1
packages/coverage-istanbul/src/provider.ts
··· 1 - /* eslint-disable no-restricted-imports */ 2 1 import { existsSync, promises as fs } from 'node:fs' 3 2 import { relative, resolve } from 'pathe' 4 3 import type { TransformPluginContext } from 'rollup'
+1
packages/ui/client/components.d.ts
··· 11 11 export interface GlobalComponents { 12 12 CodeMirror: typeof import('./components/CodeMirror.vue')['default'] 13 13 ConnectionOverlay: typeof import('./components/ConnectionOverlay.vue')['default'] 14 + Coverage: typeof import('./components/Coverage.vue')['default'] 14 15 Dashboard: typeof import('./components/Dashboard.vue')['default'] 15 16 DashboardEntry: typeof import('./components/dashboard/DashboardEntry.vue')['default'] 16 17 DetailsPanel: typeof import('./components/DetailsPanel.vue')['default']
+36 -3
packages/ui/node/index.ts
··· 1 1 import { fileURLToPath } from 'node:url' 2 - import { resolve } from 'pathe' 2 + import { basename, resolve } from 'pathe' 3 3 import sirv from 'sirv' 4 4 import type { Plugin } from 'vite' 5 + import { coverageConfigDefaults } from 'vitest/config' 6 + import type { ResolvedConfig, Vitest } from 'vitest' 5 7 6 - export default (base = '/__vitest__/') => { 8 + export default (ctx: Vitest) => { 7 9 return <Plugin>{ 8 10 name: 'vitest:ui', 9 11 apply: 'serve', 10 - async configureServer(server) { 12 + configureServer(server) { 13 + const uiOptions: ResolvedConfig = ctx.config 14 + const base = uiOptions.uiBase 15 + const coverageFolder = resolveCoverageFolder(ctx) 16 + const coveragePath = coverageFolder ? `/${basename(coverageFolder)}/` : undefined 17 + if (coveragePath && base === coveragePath) 18 + throw new Error(`The ui base path and the coverage path cannot be the same: ${base}, change coverage.reportsDirectory`) 19 + 20 + coverageFolder && server.middlewares.use(coveragePath!, sirv(coverageFolder, { 21 + single: true, 22 + dev: true, 23 + })) 11 24 const clientDist = resolve(fileURLToPath(import.meta.url), '../client') 12 25 server.middlewares.use(base, sirv(clientDist, { 13 26 single: true, ··· 15 28 })) 16 29 }, 17 30 } 31 + } 32 + 33 + function resolveCoverageFolder(ctx: Vitest) { 34 + const options: ResolvedConfig = ctx.config 35 + const enabled = options.api?.port 36 + && options.coverage?.enabled 37 + && options.coverage.reporter.some((reporter) => { 38 + if (typeof reporter === 'string') 39 + return reporter === 'html' 40 + 41 + return reporter.length && reporter.includes('html') 42 + }) 43 + 44 + // reportsDirectory not resolved yet 45 + return enabled 46 + ? resolve( 47 + ctx.config?.root || options.root || process.cwd(), 48 + options.coverage.reportsDirectory || coverageConfigDefaults.reportsDirectory, 49 + ) 50 + : undefined 18 51 }
-1
packages/ui/node/reporter.ts
··· 6 6 import c from 'picocolors' 7 7 import fg from 'fast-glob' 8 8 import { stringify } from 'flatted' 9 - // eslint-disable-next-line no-restricted-imports 10 9 import type { File, ModuleGraphData, Reporter, ResolvedConfig, Vitest } from 'vitest' 11 10 import { getModuleGraph } from '../../vitest/src/utils/graph' 12 11 import { getOutputFile } from '../../vitest/src/utils/config-helpers'
-1
packages/browser/src/client/main.ts
··· 1 1 import { createClient } from '@vitest/ws-client' 2 - // eslint-disable-next-line no-restricted-imports 3 2 import type { ResolvedConfig } from 'vitest' 4 3 import type { CancelReason, VitestRunner } from '@vitest/runner' 5 4 import { createBrowserRunner } from './runner'
+40
packages/ui/client/components/Coverage.vue
··· 1 + <script setup lang="ts"> 2 + defineProps<{ 3 + src: string 4 + }>() 5 + </script> 6 + 7 + <template> 8 + <div h="full" flex="~ col"> 9 + <div 10 + p="3" 11 + h-10 12 + flex="~ gap-2" 13 + items-center 14 + bg-header 15 + border="b base" 16 + > 17 + <div class="i-carbon:folder-details-reference" /> 18 + <span 19 + pl-1 20 + font-bold 21 + text-sm 22 + flex-auto 23 + ws-nowrap 24 + overflow-hidden 25 + truncate 26 + >Coverage</span> 27 + </div> 28 + <div flex-auto py-1 bg-white> 29 + <iframe id="vitest-ui-coverage" :src="src" /> 30 + </div> 31 + </div> 32 + </template> 33 + 34 + <style> 35 + #vitest-ui-coverage { 36 + width: 100%; 37 + height: calc(100vh - 42px); 38 + border: none; 39 + } 40 + </style>
+56 -8
packages/ui/client/components/Navigation.vue
··· 1 1 <script setup lang="ts"> 2 2 import { hasFailedSnapshot } from '@vitest/ws-client' 3 - import { currentModule, dashboardVisible, showDashboard } from '../composables/navigation' 3 + import { Tooltip as VueTooltip } from 'floating-vue' 4 + import { 5 + coverageConfigured, 6 + coverageEnabled, 7 + coverageVisible, 8 + currentModule, 9 + dashboardVisible, 10 + disableCoverage, 11 + showCoverage, 12 + showDashboard, 13 + } from '../composables/navigation' 4 14 import { client, findById } from '../composables/client' 5 - import type { Task } from '#types' 15 + import type { File, Task } from '#types' 6 16 import { isDark, toggleDark } from '~/composables' 7 17 import { files, isReport, runAll } from '~/composables/client' 8 18 import { activeFileId } from '~/composables/params' ··· 18 28 showDashboard(false) 19 29 } 20 30 const toggleMode = computed(() => isDark.value ? 'light' : 'dark') 31 + async function onRunAll(files?: File[]) { 32 + if (coverageEnabled.value) { 33 + disableCoverage.value = true 34 + await nextTick() 35 + if (coverageEnabled.value) { 36 + showDashboard(true) 37 + await nextTick() 38 + } 39 + } 40 + await runAll(files) 41 + } 21 42 </script> 22 43 23 44 <template> 24 - <TasksList border="r base" :tasks="files" :on-item-click="onItemClick" :group-by-type="true" @run="runAll"> 45 + <TasksList border="r base" :tasks="files" :on-item-click="onItemClick" :group-by-type="true" @run="onRunAll"> 25 46 <template #header="{ filteredTests }"> 26 47 <img w-6 h-6 src="/favicon.svg" alt="Vitest logo"> 27 48 <span font-light text-sm flex-1>Vitest</span> 28 49 <div class="flex text-lg"> 29 50 <IconButton 30 - v-show="!dashboardVisible" 51 + v-show="(coverageConfigured && !coverageEnabled) || !dashboardVisible" 31 52 v-tooltip.bottom="'Dashboard'" 32 53 title="Show dashboard" 33 54 class="!animate-100ms" 34 55 animate-count-1 35 - icon="i-carbon-dashboard" 56 + icon="i-carbon:dashboard" 36 57 @click="showDashboard(true)" 58 + /> 59 + <VueTooltip 60 + v-if="coverageConfigured && !coverageEnabled" 61 + title="Coverage enabled but missing html reporter" 62 + class="w-1.4em h-1.4em op100 rounded flex color-red5 dark:color-#f43f5e cursor-help" 63 + > 64 + <div class="i-carbon:folder-off ma" /> 65 + <template #popper> 66 + <div class="op100 gap-1 p-y-1" grid="~ items-center cols-[1.5em_1fr]"> 67 + <div class="i-carbon:information-square w-1.5em h-1.5em" /> 68 + <div>Coverage enabled but missing html reporter.</div> 69 + <div style="grid-column: 2"> 70 + Add html reporter to your configuration to see coverage here. 71 + </div> 72 + </div> 73 + </template> 74 + </VueTooltip> 75 + <IconButton 76 + v-if="coverageEnabled" 77 + v-show="!coverageVisible" 78 + v-tooltip.bottom="'Coverage'" 79 + :disabled="disableCoverage" 80 + title="Show coverage" 81 + class="!animate-100ms" 82 + animate-count-1 83 + icon="i-carbon:folder-details-reference" 84 + @click="showCoverage()" 37 85 /> 38 86 <IconButton 39 87 v-if="(failedSnapshot && !isReport)" 40 88 v-tooltip.bottom="'Update all failed snapshot(s)'" 41 - icon="i-carbon-result-old" 89 + icon="i-carbon:result-old" 42 90 @click="updateSnapshot()" 43 91 /> 44 92 <IconButton 45 93 v-if="!isReport" 46 94 v-tooltip.bottom="filteredTests ? (filteredTests.length === 0 ? 'No test to run (clear filter)' : 'Rerun filtered') : 'Rerun all'" 47 95 :disabled="filteredTests?.length === 0" 48 - icon="i-carbon-play" 96 + icon="i-carbon:play" 49 97 @click="runAll(filteredTests)" 50 98 /> 51 99 <IconButton 52 100 v-tooltip.bottom="`Toggle to ${toggleMode} mode`" 53 - icon="dark:i-carbon-moon i-carbon-sun" 101 + icon="dark:i-carbon-moon i-carbon:sun" 54 102 @click="toggleDark()" 55 103 /> 56 104 </div>
+9 -1
packages/ui/client/components/Suites.vue
··· 1 1 <script setup lang="ts"> 2 2 import { hasFailedSnapshot } from '@vitest/ws-client' 3 + import { coverageEnabled, disableCoverage } from '../composables/navigation' 3 4 import { client, current, isReport, runCurrent } from '~/composables/client' 4 5 5 6 const name = computed(() => current.value?.name.split(/\//g).pop()) ··· 7 8 const failedSnapshot = computed(() => current.value?.tasks && hasFailedSnapshot(current.value?.tasks)) 8 9 function updateSnapshot() { 9 10 return current.value && client.rpc.updateSnapshot(current.value) 11 + } 12 + async function onRunCurrent() { 13 + if (coverageEnabled.value) { 14 + disableCoverage.value = true 15 + await nextTick() 16 + } 17 + await runCurrent() 10 18 } 11 19 </script> 12 20 ··· 27 35 v-if="!isReport" 28 36 v-tooltip.bottom="'Rerun file'" 29 37 icon="i-carbon-play" 30 - @click="runCurrent()" 38 + @click="onRunCurrent()" 31 39 /> 32 40 </div> 33 41 </template>
+35 -1
packages/ui/client/composables/navigation.ts
··· 1 - import { client, findById } from './client' 1 + import { client, config, findById, testRunState } from './client' 2 2 import { activeFileId } from './params' 3 3 import type { File } from '#types' 4 4 5 5 export const currentModule = ref<File>() 6 6 export const dashboardVisible = ref(true) 7 + export const coverageVisible = ref(false) 8 + export const disableCoverage = ref(true) 9 + export const coverage = computed(() => config.value?.coverage) 10 + export const coverageConfigured = computed(() => { 11 + if (!config.value?.api?.port) 12 + return false 7 13 14 + return coverage.value?.enabled 15 + }) 16 + export const coverageEnabled = computed(() => { 17 + return coverageConfigured.value 18 + && coverage.value.reporter.map(([reporterName]) => reporterName).includes('html') 19 + }) 20 + export const coverageUrl = computed(() => { 21 + if (coverageEnabled.value) { 22 + const url = `${window.location.protocol}//${window.location.hostname}:${config.value!.api!.port!}` 23 + const idx = coverage.value!.reportsDirectory.lastIndexOf('/') 24 + return `${url}/${coverage.value!.reportsDirectory.slice(idx + 1)}/index.html` 25 + } 26 + 27 + return undefined 28 + }) 29 + watch(testRunState, (state) => { 30 + disableCoverage.value = state === 'running' 31 + }, { immediate: true }) 8 32 export function initializeNavigation() { 9 33 const file = activeFileId.value 10 34 if (file && file.length > 0) { ··· 12 36 if (current) { 13 37 currentModule.value = current 14 38 dashboardVisible.value = false 39 + coverageVisible.value = false 15 40 } 16 41 else { 17 42 watchOnce( ··· 19 44 () => { 20 45 currentModule.value = findById(file) 21 46 dashboardVisible.value = false 47 + coverageVisible.value = false 22 48 }, 23 49 ) 24 50 } ··· 29 55 30 56 export function showDashboard(show: boolean) { 31 57 dashboardVisible.value = show 58 + coverageVisible.value = false 32 59 if (show) { 33 60 currentModule.value = undefined 34 61 activeFileId.value = '' 35 62 } 63 + } 64 + 65 + export function showCoverage() { 66 + coverageVisible.value = true 67 + dashboardVisible.value = false 68 + currentModule.value = undefined 69 + activeFileId.value = '' 36 70 }
+2 -1
packages/ui/client/pages/index.vue
··· 1 1 <script setup lang="ts"> 2 2 // @ts-expect-error missing types 3 3 import { Pane, Splitpanes } from 'splitpanes' 4 - import { initializeNavigation } from '../composables/navigation' 4 + import { coverageUrl, coverageVisible, initializeNavigation } from '../composables/navigation' 5 5 6 6 const dashboardVisible = initializeNavigation() 7 7 const mainSizes = reactive([33, 67]) ··· 39 39 <Pane :size="mainSizes[1]"> 40 40 <transition> 41 41 <Dashboard v-if="dashboardVisible" key="summary" /> 42 + <Coverage v-else-if="coverageVisible" key="coverage" :src="coverageUrl" /> 42 43 <Splitpanes v-else key="detail" @resized="onModuleResized"> 43 44 <Pane :size="detailSizes[0]"> 44 45 <Suites />
+1 -1
packages/vitest/src/node/plugins/index.ts
··· 20 20 21 21 async function UIPlugin() { 22 22 await ensurePackageInstalled('@vitest/ui', getRoot()) 23 - return (await import('@vitest/ui')).default(options.uiBase) 23 + return (await import('@vitest/ui')).default(ctx) 24 24 } 25 25 26 26 return [