Skip to content

feat(ui): add action to explorer item to show the test/suite line in the source code tab #5948

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

Merged
merged 11 commits into from
Jun 27, 2024
4 changes: 4 additions & 0 deletions docs/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2283,6 +2283,10 @@ Should `location` property be included when Vitest API receives tasks in [report

The `location` property has `column` and `line` values that correspond to the `test` or `describe` position in the original file.

This option will be auto enabled if you don't disable it explicitly, and you are running Vitest with:
- [Vitest UI](/guide/ui)
- or using the [Browser Mode](/guide/browser) without [headless](/guide/browser#headless) mode

::: tip
This option has no effect if you do not use custom code that relies on this.
:::
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/client/auto-imports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ declare global {
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const calcExternalLabels: typeof import('./composables/module-graph')['calcExternalLabels']
const codemirrorRef: typeof import('./composables/codemirror')['codemirrorRef']
const computed: typeof import('vue')['computed']
const computedAsync: typeof import('@vueuse/core')['computedAsync']
const computedEager: typeof import('@vueuse/core')['computedEager']
Expand Down Expand Up @@ -63,6 +64,7 @@ declare global {
const isRef: typeof import('vue')['isRef']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const markRaw: typeof import('vue')['markRaw']
const navigateTo: typeof import('./composables/navigation')['navigateTo']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
Expand Down Expand Up @@ -117,6 +119,8 @@ declare global {
const shouldOpenInEditor: typeof import('./composables/error')['shouldOpenInEditor']
const showCoverage: typeof import('./composables/navigation')['showCoverage']
const showDashboard: typeof import('./composables/navigation')['showDashboard']
const showLine: typeof import('./composables/codemirror')['showLine']
const showSource: typeof import('./composables/codemirror')['showSource']
const syncRef: typeof import('@vueuse/core')['syncRef']
const syncRefs: typeof import('@vueuse/core')['syncRefs']
const templateRef: typeof import('@vueuse/core')['templateRef']
Expand Down
16 changes: 7 additions & 9 deletions packages/ui/client/components/CodeMirror.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import type CodeMirror from 'codemirror'
import { codemirrorRef } from '~/composables/codemirror'

const { mode, readOnly } = defineProps<{
mode?: string
Expand Down Expand Up @@ -30,12 +30,9 @@ const modeMap: Record<string, any> = {

const el = ref<HTMLTextAreaElement>()

const cm = shallowRef<CodeMirror.EditorFromTextArea>()

defineExpose({ cm })

onMounted(async () => {
cm.value = useCodeMirror(el, modelValue as unknown as Ref<string>, {
// useCodeMirror will remove the codemirrorRef.value on onUnmounted callback
const codemirror = useCodeMirror(el, modelValue as unknown as Ref<string>, {
...attrs,
mode: modeMap[mode || ''] || mode,
readOnly: readOnly ? true : undefined,
Expand All @@ -48,9 +45,10 @@ onMounted(async () => {
},
},
})
cm.value.setSize('100%', '100%')
cm.value.clearHistory()
setTimeout(() => cm.value!.refresh(), 100)
codemirror.setSize('100%', '100%')
codemirror.clearHistory()
codemirrorRef.value = codemirror
setTimeout(() => codemirrorRef.value!.refresh(), 100)
})
</script>

Expand Down
15 changes: 4 additions & 11 deletions packages/ui/client/components/Navigation.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
<script setup lang="ts">
import { Tooltip as VueTooltip } from 'floating-vue'
import type { File, Task } from 'vitest'
import type { File } from 'vitest'
import {
coverageConfigured,
coverageEnabled,
coverageVisible,
currentModule,
dashboardVisible,
disableCoverage,
navigateTo,
showCoverage,
showDashboard,
} from '~/composables/navigation'
import { client, findById, isReport, runAll, runFiles } from '~/composables/client'
import { client, isReport, runAll, runFiles } from '~/composables/client'
import { isDark, toggleDark } from '~/composables'
import { activeFileId } from '~/composables/params'
import { explorerTree } from '~/composables/explorer'
import { initialized, shouldShowExpandAll } from '~/composables/explorer/state'

Expand All @@ -23,12 +22,6 @@ function updateSnapshot() {

const toggleMode = computed(() => isDark.value ? 'light' : 'dark')

function onItemClick(task: Task) {
activeFileId.value = task.file.id
currentModule.value = findById(task.file.id)
showDashboard(false)
}

async function onRunAll(files?: File[]) {
if (coverageEnabled.value) {
disableCoverage.value = true
Expand Down Expand Up @@ -57,7 +50,7 @@ function expandTests() {

<template>
<!-- TODO: have test tree so the folders are also nested: test -> filename -> suite -> test -->
<Explorer border="r base" :on-item-click="onItemClick" :nested="true" @run="onRunAll">
<Explorer border="r base" :on-item-click="navigateTo" :nested="true" @run="onRunAll">
<template #header="{ filteredFiles }">
<img w-6 h-6 src="/favicon.svg" alt="Vitest logo">
<span font-light text-sm flex-1>Vitest</span>
Expand Down
6 changes: 5 additions & 1 deletion packages/ui/client/components/explorer/Explorer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { activeFileId } from '~/composables/params'
import { useSearch } from '~/composables/explorer/search'

import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import { config } from '~/composables/client'

defineOptions({ inheritAttrs: false })

Expand All @@ -21,6 +22,8 @@ const emit = defineEmits<{
(event: 'run', files?: File[]): void
}>()

const includeTaskLocation = computed(() => config.value.includeTaskLocation)

const searchBox = ref<HTMLInputElement | undefined>()

const {
Expand Down Expand Up @@ -197,6 +200,7 @@ useResizeObserver(testExplorerRef, (entries) => {
>
<template #default="{ item }">
<ExplorerItem
class="h-28px m-0 p-0"
:task-id="item.id"
:expandable="item.expandable"
:type="item.type"
Expand All @@ -209,7 +213,7 @@ useResizeObserver(testExplorerRef, (entries) => {
:state="item.state"
:duration="item.duration"
:opened="item.expanded"
class="h-28px m-0 p-0"
:disable-task-location="!includeTaskLocation"
:class="activeFileId === item.id ? 'bg-active' : ''"
:on-item-click="onItemClick"
/>
Expand Down
76 changes: 62 additions & 14 deletions packages/ui/client/components/explorer/ExplorerItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
import type { Task, TaskState } from '@vitest/runner'
import { nextTick } from 'vue'
import { hasFailedSnapshot } from '@vitest/ws-client'
import { Tooltip as VueTooltip } from 'floating-vue'
import { client, isReport, runFiles } from '~/composables/client'
import { coverageEnabled } from '~/composables/navigation'
import type { TaskTreeNodeType } from '~/composables/explorer/types'
import { explorerTree } from '~/composables/explorer'
import { search } from '~/composables/explorer/state'
import { showSource } from '~/composables/codemirror'

// TODO: better handling of "opened" - it means to forcefully open the tree item and set in TasksList right now
const {
Expand All @@ -19,6 +21,7 @@ const {
expandable,
typecheck,
type,
disableTaskLocation,
onItemClick,
} = defineProps<{
taskId: string
Expand All @@ -34,6 +37,7 @@ const {
search?: string
projectName?: string
projectNameColor: string
disableTaskLocation?: boolean
onItemClick?: (task: Task) => void
}>()

Expand Down Expand Up @@ -86,10 +90,11 @@ const gridStyles = computed(() => {
}
// text content
gridColumns.push('minmax(0, 1fr)')
// buttons
if (type === 'file') {
// action buttons
if (!isReport || type === 'file') {
gridColumns.push('min-content')
}

// all the vertical lines with width 1rem and mx-2: always centered
return `grid-template-columns: ${
entries.map(() => '1rem').join(' ')
Expand All @@ -107,6 +112,25 @@ const highlighted = computed(() => {
? name.replace(regex, match => `<span class="highlight">${match}</span>`)
: name
})

const disableShowDetails = computed(() => type !== 'file' && disableTaskLocation)
const showDetailsTooltip = computed(() => {
return type === 'file'
? 'Open test details'
: type === 'suite'
? 'View Suite Source Code'
: 'View Test Source Code'
})

function showDetails() {
const t = task.value!
if (type === 'file') {
onItemClick?.(t)
}
else {
showSource(t)
}
}
</script>

<template>
Expand Down Expand Up @@ -136,7 +160,6 @@ const highlighted = computed(() => {
<div v-if="type === 'suite' && typecheck" class="i-logos:typescript-icon" flex-shrink-0 mr-2 />
<div flex items-end gap-2 :text="state === 'fail' ? 'red-500' : ''" overflow-hidden>
<span text-sm truncate font-light>
<!-- only show [] in files view -->
<span v-if="type === 'file' && projectName" :style="{ color: projectNameColor }">
[{{ projectName }}]
</span>
Expand All @@ -146,29 +169,55 @@ const highlighted = computed(() => {
{{ duration > 0 ? duration : '< 1' }}ms
</span>
</div>
<div v-if="type === 'file'" gap-1 justify-end flex-grow-1 pl-1 class="test-actions">
<div v-if="isReport && type === 'file'" gap-1 justify-end flex-grow-1 pl-1 class="test-actions">
<IconAction
v-if="!isReport && failedSnapshot"
v-tooltip.bottom="'Open test details'"
data-testid="btn-open-details"
title="Open test details"
icon="i-carbon:intrusion-prevention"
@click.prevent.stop="onItemClick?.(task)"
/>
</div>
<div v-if="!isReport" gap-1 justify-end flex-grow-1 pl-1 class="test-actions">
<IconAction
v-if="failedSnapshot"
v-tooltip.bottom="'Fix failed snapshot(s)'"
data-testid="btn-fix-snapshot"
title="Fix failed snapshot(s)"
icon="i-carbon-result-old"
icon="i-carbon:result-old"
@click.prevent.stop="updateSnapshot(task)"
/>
<IconAction
v-tooltip.bottom="'Open test details'"
<VueTooltip
v-if="disableShowDetails"
:title="showDetailsTooltip"
class="w-1.4em h-1.4em op100 rounded flex color-red5 dark:color-#f43f5e cursor-help"
>
<div class="i-carbon:intrusion-prevention 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>This feature is not available, you have configured <span class="text-[#add467]">includeTaskLocation: false</span> in your configuration file.</div>
<div style="grid-column: 2">
Check the documentation for further details about <span class="text-[#add467]">includeTaskLocation</span>.
</div>
</div>
</template>
</VueTooltip>
<IconButton
v-else
v-tooltip.bottom="showDetailsTooltip"
data-testid="btn-open-details"
title="Open test details"
icon="i-carbon-intrusion-prevention"
@click.prevent.stop="onItemClick?.(task)"
icon="i-carbon:intrusion-prevention"
@click.prevent.stop="showDetails"
/>
<IconAction
v-if="!isReport"
<IconButton
v-tooltip.bottom="'Run current test'"
data-testid="btn-run-test"
title="Run current test"
icon="i-carbon:play-filled-alt"
text-green5
:disabled="type !== 'file'"

Choose a reason for hiding this comment

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

added disabled run test/suite for future usage: we'll need to change the logic to allow run individual tests/suites.

Sorry if I am late, but could you please clarify why the ability to run individual test was disabled?

Is is something that is coming in the future? Is there a feature request for that or should I open one?

Thanks!

Copy link
Member

Choose a reason for hiding this comment

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

This was never supported. It is possible to support - feel free to create an issue or a PR

@click.prevent.stop="onRun(task)"
/>
</div>
Expand All @@ -185,8 +234,7 @@ const highlighted = computed(() => {
.test-actions {
display: none;
}
.item-wrapper:hover .test-actions,
.item-wrapper[data-current="true"] .test-actions {
.item-wrapper:hover .test-actions {
display: flex;
}
</style>
11 changes: 6 additions & 5 deletions packages/ui/client/components/views/ViewEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { ErrorWithDiff, File } from 'vitest'
import { createTooltip, destroyTooltip } from 'floating-vue'
import { openInEditor } from '~/composables/error'
import { client } from '~/composables/client'
import { codemirrorRef } from '~/composables/codemirror'

const props = defineProps<{
file?: File
Expand Down Expand Up @@ -56,11 +57,11 @@ function clearListeners() {
}

useResizeObserver(editor, () => {
cm.value?.refresh()
codemirrorRef.value?.refresh()
})

function codemirrorChanges() {
draft.value = serverCode.value !== cm.value!.getValue()
draft.value = serverCode.value !== codemirrorRef.value!.getValue()
}

watch(
Expand Down Expand Up @@ -105,8 +106,8 @@ function createErrorElement(e: ErrorWithDiff) {
}
div.appendChild(span)
listeners.push([span, el, () => destroyTooltip(span)])
handles.push(cm.value!.addLineClass(stack.line - 1, 'wrap', 'bg-red-500/10'))
widgets.push(cm.value!.addLineWidget(stack.line - 1, div))
handles.push(codemirrorRef.value!.addLineClass(stack.line - 1, 'wrap', 'bg-red-500/10'))
widgets.push(codemirrorRef.value!.addLineWidget(stack.line - 1, div))
}

watch(
Expand All @@ -120,7 +121,7 @@ watch(
setTimeout(() => {
clearListeners()
widgets.forEach(widget => widget.clear())
handles.forEach(h => cm.value?.removeLineClass(h, 'wrap'))
handles.forEach(h => codemirrorRef.value?.removeLineClass(h, 'wrap'))
widgets.length = 0
handles.length = 0

Expand Down
26 changes: 26 additions & 0 deletions packages/ui/client/composables/codemirror.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import 'codemirror/mode/jsx/jsx'
import 'codemirror/addon/display/placeholder'
import 'codemirror/addon/scroll/simplescrollbars'
import 'codemirror/addon/scroll/simplescrollbars.css'
import type { Task } from '@vitest/runner'
import { navigateTo } from '~/composables/navigation'

export const codemirrorRef = shallowRef<CodeMirror.EditorFromTextArea>()

export function useCodeMirror(
textarea: Ref<HTMLTextAreaElement | null | undefined>,
Expand Down Expand Up @@ -50,5 +54,27 @@ export function useCodeMirror(
{ immediate: true },
)

onUnmounted(() => {
codemirrorRef.value = undefined
})

return markRaw(cm)
}

export async function showSource(task: Task) {
const codeMirror = codemirrorRef.value
if (!codeMirror || activeFileId.value !== task.file.id) {
navigateTo(task, true)
// we need to await, CodeMirrow will take some time to initialize
await new Promise(r => setTimeout(r, 256))
}

nextTick(() => {
const line = { line: task.location?.line ?? 0, ch: 0 }
codemirrorRef.value?.scrollIntoView(line)
nextTick(() => {
codemirrorRef.value?.focus()
codemirrorRef.value?.setCursor(line)
})
})
}
Loading
Loading