Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): add html coverage #3071

Merged
merged 20 commits into from May 3, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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`, you only need to add `html` reporter: check [Vitest UI Coverage](/guide/coverage#vitest-ui) for more details.

#### coverage.skipFull

- **Type:** `boolean`
Expand Down
11 changes: 11 additions & 0 deletions docs/guide/coverage.md
Expand Up @@ -163,3 +163,14 @@ 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), you only need to add `html` reporter.


<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`, you only need to add `html` reporter: 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>
44 changes: 37 additions & 7 deletions packages/ui/client/components/Navigation.vue
@@ -1,8 +1,16 @@
<script setup lang="ts">
import { hasFailedSnapshot } from '@vitest/ws-client'
import { currentModule, dashboardVisible, showDashboard } from '../composables/navigation'
import {
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,10 +26,21 @@ 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>
Expand All @@ -32,25 +51,36 @@ const toggleMode = computed(() => isDark.value ? 'light' : 'dark')
title="Show dashboard"
class="!animate-100ms"
animate-count-1
icon="i-carbon-dashboard"
icon="i-carbon:dashboard"
@click="showDashboard(true)"
/>
<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
33 changes: 32 additions & 1 deletion packages/ui/client/composables/navigation.ts
@@ -1,24 +1,47 @@
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 coverageEnabled = computed(() => {
if (!config.value?.api?.port)
return false

const cov = coverage.value
return cov?.enabled && cov.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 +52,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, {
sheremet-va marked this conversation as resolved.
Show resolved Hide resolved
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
}
1 change: 0 additions & 1 deletion packages/ui/node/reporter.ts
Expand Up @@ -6,7 +6,6 @@ import { basename, dirname, relative, resolve } from 'pathe'
import c from 'picocolors'
import fg from 'fast-glob'
import { stringify } from 'flatted'
// eslint-disable-next-line no-restricted-imports
import type { File, ModuleGraphData, Reporter, ResolvedConfig, Vitest } from 'vitest'
import { getModuleGraph } from '../../vitest/src/utils/graph'
import { getOutputFile } from '../../vitest/src/utils/config-helpers'
Expand Down
3 changes: 3 additions & 0 deletions packages/ui/package.json
Expand Up @@ -43,6 +43,9 @@
"test:open": "cypress open --component",
"prepublishOnly": "pnpm build"
},
"peerDependencies": {
"vitest": ">=0.30.1 <1"
},
Comment on lines +46 to +48
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this makes the feature a breaking change - which is OK, but should be indicated in release notes. Maybe even in the PR description here.

"dependencies": {
"@vitest/utils": "workspace:*",
"fast-glob": "^3.2.12",
Expand Down
3 changes: 2 additions & 1 deletion packages/ui/rollup.config.js
Expand Up @@ -14,7 +14,8 @@ const external = [
'worker_threads',
'node:worker_threads',
'vitest/node',
'vitest',
'vitest/config',
'vite',
]

export default () => [
Expand Down