Skip to content

Commit

Permalink
feat(ui): add action to explorer item to show the test/suite line in …
Browse files Browse the repository at this point in the history
…the source code tab (#5948)

Co-authored-by: Anjorin Damilare <[email protected]>
  • Loading branch information
userquin and dammy001 committed Jun 27, 2024
1 parent caef40a commit 7ec298e
Show file tree
Hide file tree
Showing 17 changed files with 248 additions and 68 deletions.
4 changes: 4 additions & 0 deletions docs/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2297,6 +2297,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
5 changes: 5 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 @@ -61,8 +62,10 @@ declare global {
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const lineNumber: typeof import('./composables/params')['lineNumber']
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 +120,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
2 changes: 1 addition & 1 deletion packages/ui/client/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
BrowserIframe: typeof import('./components/BrowserIframe.vue')['default']
CodeMirror: typeof import('./components/CodeMirror.vue')['default']
CodeMirrorContainer: typeof import('./components/CodeMirrorContainer.vue')['default']
ConnectionOverlay: typeof import('./components/ConnectionOverlay.vue')['default']
Coverage: typeof import('./components/Coverage.vue')['default']
Dashboard: typeof import('./components/Dashboard.vue')['default']
Expand Down
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
1 change: 0 additions & 1 deletion packages/ui/client/components/FileDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,6 @@ debouncedWatch(
Module Graph
</button>
<button
v-if="!isReport"
tab-button
data-testid="btn-code"
class="flex items-center gap-2"
Expand Down
6 changes: 3 additions & 3 deletions packages/ui/client/components/ModuleTransformResultView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ onKeyStroke('Escape', () => {
<div p="x3 y-1" bg-overlay border="base b t">
Transformed
</div>
<CodeMirror
<CodeMirrorContainer
h-full
:model-value="source"
read-only
v-bind="{ lineNumbers: true }"
:mode="ext"
/>
<CodeMirror
<CodeMirrorContainer
h-full
:model-value="code"
read-only
Expand All @@ -71,7 +71,7 @@ onKeyStroke('Escape', () => {
<div p="x3 y-1" bg-overlay border="base b t">
Source map (v{{ sourceMap.version }})
</div>
<CodeMirror
<CodeMirrorContainer
:model-value="sourceMap.mappings"
read-only
v-bind="{ lineNumbers: true }"
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 @@ -203,6 +206,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 @@ -215,7 +219,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
82 changes: 64 additions & 18 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,12 +37,21 @@ const {
search?: string
projectName?: string
projectNameColor: string
disableTaskLocation?: boolean
onItemClick?: (task: Task) => void
}>()
const task = computed(() => client.state.idMap.get(taskId))
const failedSnapshot = computed(() => task.value && hasFailedSnapshot(task.value))
const failedSnapshot = computed(() => {
// don't traverse the tree if it's a report
if (isReport) {
return false
}
const t = task.value
return t && hasFailedSnapshot(t)
})
function toggleOpen() {
if (!expandable) {
Expand Down Expand Up @@ -86,10 +98,9 @@ const gridStyles = computed(() => {
}
// text content
gridColumns.push('minmax(0, 1fr)')
// buttons
if (type === 'file') {
gridColumns.push('min-content')
}
// action buttons
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 +118,26 @@ 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'
})
const showDetailsClasses = computed(() => disableShowDetails.value ? 'color-red5 dark:color-#f43f5e' : null)
function showDetails() {
const t = task.value!
if (type === 'file') {
onItemClick?.(t)
}
else {
showSource(t)
}
}
</script>

<template>
Expand Down Expand Up @@ -136,7 +167,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 +176,46 @@ 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 gap-1 justify-end flex-grow-1 pl-1 class="test-actions">
<IconAction
v-if="!isReport && 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'"
data-testid="btn-open-details"
title="Open test details"
icon="i-carbon-intrusion-prevention"
@click.prevent.stop="onItemClick?.(task)"
/>
<IconAction
<VueTooltip
placement="bottom"
class="w-1.4em h-1.4em op100 rounded flex"
:class="showDetailsClasses"
>
<IconButton
data-testid="btn-open-details"
icon="i-carbon:intrusion-prevention"
@click.prevent.stop="showDetails"
/>
<template #popper>
<div v-if="disableShowDetails" 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>{{ showDetailsTooltip }}: this feature is not available, you have disabled <span class="text-[#add467]">includeTaskLocation</span> in your configuration file.</div>
<div style="grid-column: 2">
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.
</div>
</div>
<div v-else>
{{ showDetailsTooltip }}
</div>
</template>
</VueTooltip>
<IconButton
v-if="!isReport"
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'"
@click.prevent.stop="onRun(task)"
/>
</div>
Expand All @@ -185,8 +232,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>
Loading

0 comments on commit 7ec298e

Please sign in to comment.