Skip to content

Commit 7ec298e

Browse files
userquindammy001
andauthored
feat(ui): add action to explorer item to show the test/suite line in the source code tab (#5948)
Co-authored-by: Anjorin Damilare <[email protected]>
1 parent caef40a commit 7ec298e

File tree

17 files changed

+248
-68
lines changed

17 files changed

+248
-68
lines changed

docs/config/index.md

+4
Original file line numberDiff line numberDiff line change
@@ -2297,6 +2297,10 @@ Should `location` property be included when Vitest API receives tasks in [report
22972297

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

2300+
This option will be auto-enabled if you don't disable it explicitly, and you are running Vitest with:
2301+
- [Vitest UI](/guide/ui)
2302+
- or using the [Browser Mode](/guide/browser) without [headless](/guide/browser#headless) mode
2303+
23002304
::: tip
23012305
This option has no effect if you do not use custom code that relies on this.
23022306
:::

packages/ui/client/auto-imports.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ declare global {
1010
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
1111
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
1212
const calcExternalLabels: typeof import('./composables/module-graph')['calcExternalLabels']
13+
const codemirrorRef: typeof import('./composables/codemirror')['codemirrorRef']
1314
const computed: typeof import('vue')['computed']
1415
const computedAsync: typeof import('@vueuse/core')['computedAsync']
1516
const computedEager: typeof import('@vueuse/core')['computedEager']
@@ -61,8 +62,10 @@ declare global {
6162
const isReactive: typeof import('vue')['isReactive']
6263
const isReadonly: typeof import('vue')['isReadonly']
6364
const isRef: typeof import('vue')['isRef']
65+
const lineNumber: typeof import('./composables/params')['lineNumber']
6466
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
6567
const markRaw: typeof import('vue')['markRaw']
68+
const navigateTo: typeof import('./composables/navigation')['navigateTo']
6669
const nextTick: typeof import('vue')['nextTick']
6770
const onActivated: typeof import('vue')['onActivated']
6871
const onBeforeMount: typeof import('vue')['onBeforeMount']
@@ -117,6 +120,8 @@ declare global {
117120
const shouldOpenInEditor: typeof import('./composables/error')['shouldOpenInEditor']
118121
const showCoverage: typeof import('./composables/navigation')['showCoverage']
119122
const showDashboard: typeof import('./composables/navigation')['showDashboard']
123+
const showLine: typeof import('./composables/codemirror')['showLine']
124+
const showSource: typeof import('./composables/codemirror')['showSource']
120125
const syncRef: typeof import('@vueuse/core')['syncRef']
121126
const syncRefs: typeof import('@vueuse/core')['syncRefs']
122127
const templateRef: typeof import('@vueuse/core')['templateRef']

packages/ui/client/components.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export {}
88
declare module 'vue' {
99
export interface GlobalComponents {
1010
BrowserIframe: typeof import('./components/BrowserIframe.vue')['default']
11-
CodeMirror: typeof import('./components/CodeMirror.vue')['default']
11+
CodeMirrorContainer: typeof import('./components/CodeMirrorContainer.vue')['default']
1212
ConnectionOverlay: typeof import('./components/ConnectionOverlay.vue')['default']
1313
Coverage: typeof import('./components/Coverage.vue')['default']
1414
Dashboard: typeof import('./components/Dashboard.vue')['default']

packages/ui/client/components/CodeMirror.vue renamed to packages/ui/client/components/CodeMirrorContainer.vue

+7-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import type CodeMirror from 'codemirror'
2+
import { codemirrorRef } from '~/composables/codemirror'
33
44
const { mode, readOnly } = defineProps<{
55
mode?: string
@@ -30,12 +30,9 @@ const modeMap: Record<string, any> = {
3030
3131
const el = ref<HTMLTextAreaElement>()
3232
33-
const cm = shallowRef<CodeMirror.EditorFromTextArea>()
34-
35-
defineExpose({ cm })
36-
3733
onMounted(async () => {
38-
cm.value = useCodeMirror(el, modelValue as unknown as Ref<string>, {
34+
// useCodeMirror will remove the codemirrorRef.value on onUnmounted callback
35+
const codemirror = useCodeMirror(el, modelValue as unknown as Ref<string>, {
3936
...attrs,
4037
mode: modeMap[mode || ''] || mode,
4138
readOnly: readOnly ? true : undefined,
@@ -48,9 +45,10 @@ onMounted(async () => {
4845
},
4946
},
5047
})
51-
cm.value.setSize('100%', '100%')
52-
cm.value.clearHistory()
53-
setTimeout(() => cm.value!.refresh(), 100)
48+
codemirror.setSize('100%', '100%')
49+
codemirror.clearHistory()
50+
codemirrorRef.value = codemirror
51+
setTimeout(() => codemirrorRef.value!.refresh(), 100)
5452
})
5553
</script>
5654

packages/ui/client/components/FileDetails.vue

-1
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,6 @@ debouncedWatch(
174174
Module Graph
175175
</button>
176176
<button
177-
v-if="!isReport"
178177
tab-button
179178
data-testid="btn-code"
180179
class="flex items-center gap-2"

packages/ui/client/components/ModuleTransformResultView.vue

+3-3
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,14 @@ onKeyStroke('Escape', () => {
5252
<div p="x3 y-1" bg-overlay border="base b t">
5353
Transformed
5454
</div>
55-
<CodeMirror
55+
<CodeMirrorContainer
5656
h-full
5757
:model-value="source"
5858
read-only
5959
v-bind="{ lineNumbers: true }"
6060
:mode="ext"
6161
/>
62-
<CodeMirror
62+
<CodeMirrorContainer
6363
h-full
6464
:model-value="code"
6565
read-only
@@ -71,7 +71,7 @@ onKeyStroke('Escape', () => {
7171
<div p="x3 y-1" bg-overlay border="base b t">
7272
Source map (v{{ sourceMap.version }})
7373
</div>
74-
<CodeMirror
74+
<CodeMirrorContainer
7575
:model-value="sourceMap.mappings"
7676
read-only
7777
v-bind="{ lineNumbers: true }"

packages/ui/client/components/Navigation.vue

+4-11
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
<script setup lang="ts">
22
import { Tooltip as VueTooltip } from 'floating-vue'
3-
import type { File, Task } from 'vitest'
3+
import type { File } from 'vitest'
44
import {
55
coverageConfigured,
66
coverageEnabled,
77
coverageVisible,
8-
currentModule,
98
dashboardVisible,
109
disableCoverage,
10+
navigateTo,
1111
showCoverage,
1212
showDashboard,
1313
} from '~/composables/navigation'
14-
import { client, findById, isReport, runAll, runFiles } from '~/composables/client'
14+
import { client, isReport, runAll, runFiles } from '~/composables/client'
1515
import { isDark, toggleDark } from '~/composables'
16-
import { activeFileId } from '~/composables/params'
1716
import { explorerTree } from '~/composables/explorer'
1817
import { initialized, shouldShowExpandAll } from '~/composables/explorer/state'
1918
@@ -23,12 +22,6 @@ function updateSnapshot() {
2322
2423
const toggleMode = computed(() => isDark.value ? 'light' : 'dark')
2524
26-
function onItemClick(task: Task) {
27-
activeFileId.value = task.file.id
28-
currentModule.value = findById(task.file.id)
29-
showDashboard(false)
30-
}
31-
3225
async function onRunAll(files?: File[]) {
3326
if (coverageEnabled.value) {
3427
disableCoverage.value = true
@@ -57,7 +50,7 @@ function expandTests() {
5750

5851
<template>
5952
<!-- TODO: have test tree so the folders are also nested: test -> filename -> suite -> test -->
60-
<Explorer border="r base" :on-item-click="onItemClick" :nested="true" @run="onRunAll">
53+
<Explorer border="r base" :on-item-click="navigateTo" :nested="true" @run="onRunAll">
6154
<template #header="{ filteredFiles }">
6255
<img w-6 h-6 src="/favicon.svg" alt="Vitest logo">
6356
<span font-light text-sm flex-1>Vitest</span>

packages/ui/client/components/explorer/Explorer.vue

+5-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { activeFileId } from '~/composables/params'
99
import { useSearch } from '~/composables/explorer/search'
1010
1111
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
12+
import { config } from '~/composables/client'
1213
1314
defineOptions({ inheritAttrs: false })
1415
@@ -21,6 +22,8 @@ const emit = defineEmits<{
2122
(event: 'run', files?: File[]): void
2223
}>()
2324
25+
const includeTaskLocation = computed(() => config.value.includeTaskLocation)
26+
2427
const searchBox = ref<HTMLInputElement | undefined>()
2528
2629
const {
@@ -203,6 +206,7 @@ useResizeObserver(testExplorerRef, (entries) => {
203206
>
204207
<template #default="{ item }">
205208
<ExplorerItem
209+
class="h-28px m-0 p-0"
206210
:task-id="item.id"
207211
:expandable="item.expandable"
208212
:type="item.type"
@@ -215,7 +219,7 @@ useResizeObserver(testExplorerRef, (entries) => {
215219
:state="item.state"
216220
:duration="item.duration"
217221
:opened="item.expanded"
218-
class="h-28px m-0 p-0"
222+
:disable-task-location="!includeTaskLocation"
219223
:class="activeFileId === item.id ? 'bg-active' : ''"
220224
:on-item-click="onItemClick"
221225
/>

packages/ui/client/components/explorer/ExplorerItem.vue

+64-18
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
import type { Task, TaskState } from '@vitest/runner'
33
import { nextTick } from 'vue'
44
import { hasFailedSnapshot } from '@vitest/ws-client'
5+
import { Tooltip as VueTooltip } from 'floating-vue'
56
import { client, isReport, runFiles } from '~/composables/client'
67
import { coverageEnabled } from '~/composables/navigation'
78
import type { TaskTreeNodeType } from '~/composables/explorer/types'
89
import { explorerTree } from '~/composables/explorer'
910
import { search } from '~/composables/explorer/state'
11+
import { showSource } from '~/composables/codemirror'
1012
1113
// TODO: better handling of "opened" - it means to forcefully open the tree item and set in TasksList right now
1214
const {
@@ -19,6 +21,7 @@ const {
1921
expandable,
2022
typecheck,
2123
type,
24+
disableTaskLocation,
2225
onItemClick,
2326
} = defineProps<{
2427
taskId: string
@@ -34,12 +37,21 @@ const {
3437
search?: string
3538
projectName?: string
3639
projectNameColor: string
40+
disableTaskLocation?: boolean
3741
onItemClick?: (task: Task) => void
3842
}>()
3943
4044
const task = computed(() => client.state.idMap.get(taskId))
4145
42-
const failedSnapshot = computed(() => task.value && hasFailedSnapshot(task.value))
46+
const failedSnapshot = computed(() => {
47+
// don't traverse the tree if it's a report
48+
if (isReport) {
49+
return false
50+
}
51+
52+
const t = task.value
53+
return t && hasFailedSnapshot(t)
54+
})
4355
4456
function toggleOpen() {
4557
if (!expandable) {
@@ -86,10 +98,9 @@ const gridStyles = computed(() => {
8698
}
8799
// text content
88100
gridColumns.push('minmax(0, 1fr)')
89-
// buttons
90-
if (type === 'file') {
91-
gridColumns.push('min-content')
92-
}
101+
// action buttons
102+
gridColumns.push('min-content')
103+
93104
// all the vertical lines with width 1rem and mx-2: always centered
94105
return `grid-template-columns: ${
95106
entries.map(() => '1rem').join(' ')
@@ -107,6 +118,26 @@ const highlighted = computed(() => {
107118
? name.replace(regex, match => `<span class="highlight">${match}</span>`)
108119
: name
109120
})
121+
122+
const disableShowDetails = computed(() => type !== 'file' && disableTaskLocation)
123+
const showDetailsTooltip = computed(() => {
124+
return type === 'file'
125+
? 'Open test details'
126+
: type === 'suite'
127+
? 'View Suite Source Code'
128+
: 'View Test Source Code'
129+
})
130+
const showDetailsClasses = computed(() => disableShowDetails.value ? 'color-red5 dark:color-#f43f5e' : null)
131+
132+
function showDetails() {
133+
const t = task.value!
134+
if (type === 'file') {
135+
onItemClick?.(t)
136+
}
137+
else {
138+
showSource(t)
139+
}
140+
}
110141
</script>
111142

112143
<template>
@@ -136,7 +167,6 @@ const highlighted = computed(() => {
136167
<div v-if="type === 'suite' && typecheck" class="i-logos:typescript-icon" flex-shrink-0 mr-2 />
137168
<div flex items-end gap-2 :text="state === 'fail' ? 'red-500' : ''" overflow-hidden>
138169
<span text-sm truncate font-light>
139-
<!-- only show [] in files view -->
140170
<span v-if="type === 'file' && projectName" :style="{ color: projectNameColor }">
141171
[{{ projectName }}]
142172
</span>
@@ -146,29 +176,46 @@ const highlighted = computed(() => {
146176
{{ duration > 0 ? duration : '< 1' }}ms
147177
</span>
148178
</div>
149-
<div v-if="type === 'file'" gap-1 justify-end flex-grow-1 pl-1 class="test-actions">
179+
<div gap-1 justify-end flex-grow-1 pl-1 class="test-actions">
150180
<IconAction
151181
v-if="!isReport && failedSnapshot"
152182
v-tooltip.bottom="'Fix failed snapshot(s)'"
153183
data-testid="btn-fix-snapshot"
154184
title="Fix failed snapshot(s)"
155-
icon="i-carbon-result-old"
185+
icon="i-carbon:result-old"
156186
@click.prevent.stop="updateSnapshot(task)"
157187
/>
158-
<IconAction
159-
v-tooltip.bottom="'Open test details'"
160-
data-testid="btn-open-details"
161-
title="Open test details"
162-
icon="i-carbon-intrusion-prevention"
163-
@click.prevent.stop="onItemClick?.(task)"
164-
/>
165-
<IconAction
188+
<VueTooltip
189+
placement="bottom"
190+
class="w-1.4em h-1.4em op100 rounded flex"
191+
:class="showDetailsClasses"
192+
>
193+
<IconButton
194+
data-testid="btn-open-details"
195+
icon="i-carbon:intrusion-prevention"
196+
@click.prevent.stop="showDetails"
197+
/>
198+
<template #popper>
199+
<div v-if="disableShowDetails" class="op100 gap-1 p-y-1" grid="~ items-center cols-[1.5em_1fr]">
200+
<div class="i-carbon:information-square w-1.5em h-1.5em" />
201+
<div>{{ showDetailsTooltip }}: this feature is not available, you have disabled <span class="text-[#add467]">includeTaskLocation</span> in your configuration file.</div>
202+
<div style="grid-column: 2">
203+
Clicking this button the code tab will position the cursor at first line in the source code since the UI doesn't have the information available.
204+
</div>
205+
</div>
206+
<div v-else>
207+
{{ showDetailsTooltip }}
208+
</div>
209+
</template>
210+
</VueTooltip>
211+
<IconButton
166212
v-if="!isReport"
167213
v-tooltip.bottom="'Run current test'"
168214
data-testid="btn-run-test"
169215
title="Run current test"
170216
icon="i-carbon:play-filled-alt"
171217
text-green5
218+
:disabled="type !== 'file'"
172219
@click.prevent.stop="onRun(task)"
173220
/>
174221
</div>
@@ -185,8 +232,7 @@ const highlighted = computed(() => {
185232
.test-actions {
186233
display: none;
187234
}
188-
.item-wrapper:hover .test-actions,
189-
.item-wrapper[data-current="true"] .test-actions {
235+
.item-wrapper:hover .test-actions {
190236
display: flex;
191237
}
192238
</style>

0 commit comments

Comments
 (0)