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 action to explorer item to show the test/suite line in the source code tab #5948

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 @@ -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
6 changes: 6 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 @@ -106,6 +109,7 @@ declare global {
const refDefault: typeof import('@vueuse/core')['refDefault']
const refThrottled: typeof import('@vueuse/core')['refThrottled']
const refWithControl: typeof import('@vueuse/core')['refWithControl']
const registerInitializeListener: typeof import('./composables/codemirror')['registerInitializeListener']
userquin marked this conversation as resolved.
Show resolved Hide resolved
const registerResizingListener: typeof import("./composables/browser")["registerResizingListener"]
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
Expand All @@ -117,6 +121,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'"

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 +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
Loading