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 7 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
1 change: 1 addition & 0 deletions packages/ui/client/components.d.ts
Expand Up @@ -9,6 +9,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 @@ -16,10 +24,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 @@ -30,25 +49,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
11 changes: 10 additions & 1 deletion packages/ui/client/components/Suites.vue
@@ -1,11 +1,20 @@
<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())

const failedSnapshot = computed(() => current.value?.tasks && hasFailedSnapshot(current.value?.tasks))
const updateSnapshot = () => current.value && client.rpc.updateSnapshot(current.value)

async function onRunCurrent() {
if (coverageEnabled.value) {
disableCoverage.value = true
await nextTick()
}
await runCurrent()
}
</script>

<template>
Expand All @@ -25,7 +34,7 @@ const updateSnapshot = () => current.value && client.rpc.updateSnapshot(current.
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 @@ const 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 '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 '#types'

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
}