Skip to content

Commit

Permalink
feat(ui): render tests in a tree (#5807)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va authored May 31, 2024
1 parent a820e7a commit 7900f9f
Show file tree
Hide file tree
Showing 15 changed files with 200 additions and 94 deletions.
1 change: 1 addition & 0 deletions packages/ui/client/auto-imports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ declare global {
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const openInEditor: typeof import('./composables/error')['openInEditor']
const openedTreeItems: typeof import('./composables/navigation')['openedTreeItems']
const params: typeof import('./composables/params')['params']
const parseError: typeof import('./composables/error')['parseError']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
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 @@ -15,6 +15,7 @@ declare module 'vue' {
DetailsPanel: typeof import('./components/DetailsPanel.vue')['default']
ErrorEntry: typeof import('./components/dashboard/ErrorEntry.vue')['default']
FileDetails: typeof import('./components/FileDetails.vue')['default']
IconAction: typeof import('./components/IconAction.vue')['default']
IconButton: typeof import('./components/IconButton.vue')['default']
Modal: typeof import('./components/Modal.vue')['default']
ModuleTransformResultView: typeof import('./components/ModuleTransformResultView.vue')['default']
Expand All @@ -23,7 +24,6 @@ declare module 'vue' {
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
StatusIcon: typeof import('./components/StatusIcon.vue')['default']
Suites: typeof import('./components/Suites.vue')['default']
TaskItem: typeof import('./components/TaskItem.vue')['default']
TasksList: typeof import('./components/TasksList.vue')['default']
TaskTree: typeof import('./components/TaskTree.vue')['default']
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/client/components/FileDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Params } from '~/composables/params'
import { viewMode } from '~/composables/params'
import type { ModuleGraph } from '~/composables/module-graph'
import { getModuleGraph } from '~/composables/module-graph'
import { getProjectNameColor } from '~/utils/task';
const data = ref<ModuleGraphData>({ externalized: [], graph: {}, inlined: [] })
const graph = ref<ModuleGraph>({ nodes: [], links: [] })
Expand Down Expand Up @@ -49,6 +50,9 @@ function onDraft(value: boolean) {
<div>
<div p="2" h-10 flex="~ gap-2" items-center bg-header border="b base">
<StatusIcon :task="current" />
<div font-light op-50 text-sm :style="{ color: getProjectNameColor(current?.file.projectName) }">
[{{ current?.file.projectName || '' }}]
</div>
<div flex-1 font-light op-50 ws-nowrap truncate text-sm>
{{ current?.filepath }}
</div>
Expand Down
15 changes: 15 additions & 0 deletions packages/ui/client/components/IconAction.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script setup type="ts">
defineProps({
icon: String,
})
</script>

<template>
<div
bg="gray-200"
rounded-1
p-0.5
>
<div :class="icon" op50></div>
</div>
</template>
34 changes: 31 additions & 3 deletions packages/ui/client/components/Navigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { client, findById } from '../composables/client'
import { isDark, toggleDark } from '~/composables'
import { files, isReport, runAll } from '~/composables/client'
import { activeFileId } from '~/composables/params'
import { openedTreeItems } from '~/composables/navigation'
const failedSnapshot = computed(() => files.value && hasFailedSnapshot(files.value))
function updateSnapshot() {
Expand All @@ -25,8 +26,8 @@ function updateSnapshot() {
const toggleMode = computed(() => isDark.value ? 'light' : 'dark')
function onItemClick(task: Task) {
activeFileId.value = task.id
currentModule.value = findById(task.id)
activeFileId.value = task.file.id
currentModule.value = findById(task.file.id)
showDashboard(false)
}
Expand All @@ -41,14 +42,41 @@ async function onRunAll(files?: File[]) {
}
await runAll(files)
}
function collapseTests() {
openedTreeItems.value = []
}
function expandTests() {
files.value.forEach(file => {
if (!openedTreeItems.value.includes(file.id)) {
openedTreeItems.value.push(file.id)
}
})
}
</script>

<template>
<TasksList border="r base" :tasks="files" :on-item-click="onItemClick" :group-by-type="true" @run="onRunAll">
<!-- TODO: have test tree so the folders are also nested: test -> filename -> suite -> test -->
<TasksList border="r base" :tasks="files" :on-item-click="onItemClick" :group-by-type="true" @run="onRunAll" :nested="true">
<template #header="{ filteredTests }">
<img w-6 h-6 src="/favicon.svg" alt="Vitest logo">
<span font-light text-sm flex-1>Vitest</span>
<div class="flex text-lg">
<IconButton
v-show="openedTreeItems.length > 0"
v-tooltip.bottom="'Collapse tests'"
title="Collapse tests"
icon="i-carbon:collapse-all"
@click="collapseTests()"
/>
<IconButton
v-show="openedTreeItems.length === 0"
v-tooltip.bottom="'Expand tests'"
title="Expand tests"
icon="i-carbon:expand-all"
@click="expandTests()"
/>
<IconButton
v-show="(coverageConfigured && !coverageEnabled) || !dashboardVisible"
v-tooltip.bottom="'Dashboard'"
Expand Down
45 changes: 0 additions & 45 deletions packages/ui/client/components/Suites.vue

This file was deleted.

74 changes: 57 additions & 17 deletions packages/ui/client/components/TaskItem.vue
Original file line number Diff line number Diff line change
@@ -1,29 +1,25 @@
<script setup lang="ts">
import type { Task } from 'vitest'
import { getProjectNameColor } from '~/utils/task';
import { activeFileId } from '~/composables/params';
import { isReport } from '~/constants';
const props = defineProps<{
task: Task
opened: boolean
failedSnapshot: boolean
}>()
const emit = defineEmits<{
run: []
preview: []
fixSnapshot: [],
}>()
const duration = computed(() => {
const { result } = props.task
return result && Math.round(result.duration || 0)
})
function getProjectNameColor(name: string | undefined) {
if (!name)
return ''
const index = name.split('').reduce((acc, v, idx) => acc + v.charCodeAt(0) + idx, 0)
const colors = [
'blue',
'yellow',
'cyan',
'green',
'magenta',
]
return colors[index % colors.length]
}
</script>

<template>
Expand All @@ -35,13 +31,20 @@ function getProjectNameColor(name: string | undefined) {
border-rounded
cursor-pointer
hover="bg-active"
class="item-wrapper"
:aria-label="task.name"
:data-current="activeFileId === task.id"
>
<div v-if="task.type === 'suite'" pr-1>
<div v-if="opened" i-carbon-chevron-down op20 />
<div v-else i-carbon-chevron-right op20 />
</div>
<StatusIcon :task="task" mr-2 />
<div v-if="task.type === 'suite' && task.meta.typecheck" i-logos:typescript-icon flex-shrink-0 mr-2 />
<div flex items-end gap-2 :text="task?.result?.state === 'fail' ? 'red-500' : ''">
<div flex items-end gap-2 :text="task?.result?.state === 'fail' ? 'red-500' : ''" overflow-hidden>
<span text-sm truncate font-light>
<!-- only show [] in files view -->
<span v-if="task.filepath && task.file.projectName" :style="{ color: getProjectNameColor(task.file.projectName) }">
<span v-if="'filepath' in task && task.projectName" :style="{ color: getProjectNameColor(task.file.projectName) }">
[{{ task.file.projectName }}]
</span>
{{ task.name }}
Expand All @@ -50,5 +53,42 @@ function getProjectNameColor(name: string | undefined) {
{{ duration > 0 ? duration : '< 1' }}ms
</span>
</div>
<div v-if="task.type === 'suite' && 'filepath' in task" 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"
@click.prevent.stop="emit('fixSnapshot')"
/>
<IconAction
v-tooltip.bottom="'Open test details'"
data-testid="btn-open-details"
title="Open test details"
icon="i-carbon-intrusion-prevention"
@click.prevent.stop="emit('preview')"
/>
<IconAction
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="green-500"
@click.prevent.stop="emit('run')"
/>
</div>
</div>
</template>

<style scoped>
.test-actions {
display: none;
}
.item-wrapper:hover .test-actions,
.item-wrapper[data-current="true"] .test-actions {
display: flex;
}
</style>
46 changes: 41 additions & 5 deletions packages/ui/client/components/TaskTree.vue
Original file line number Diff line number Diff line change
@@ -1,32 +1,68 @@
<script setup lang="ts">
import type { Task } from 'vitest'
import { nextTick } from 'vue'
import { runFiles, client } from '~/composables/client';
import { caseInsensitiveMatch } from '~/utils/task'
import { openedTreeItems, coverageEnabled } from '~/composables/navigation';
defineOptions({ inheritAttrs: false })
const { task, indent = 0, nested = false, search, onItemClick } = defineProps<{
// TODO: better handling of "opened" - it means to forcefully open the tree item and set in TasksList right now
const { task, indent = 0, nested = false, search, onItemClick, opened = false } = defineProps<{
task: Task
failedSnapshot: boolean
indent?: number
opened?: boolean
nested?: boolean
search?: string
onItemClick?: (task: Task) => void
}>()
const isOpened = computed(() => opened || openedTreeItems.value.includes(task.id))
function toggleOpen() {
if (isOpened.value) {
const tasksIds = 'tasks' in task ? task.tasks.map(t => t.id) : []
openedTreeItems.value = openedTreeItems.value.filter(id => id !== task.id && !tasksIds.includes(id))
} else {
openedTreeItems.value = [...openedTreeItems.value, task.id]
}
}
async function onRun() {
onItemClick?.(task)
if (coverageEnabled.value) {
disableCoverage.value = true
await nextTick()
}
await runFiles([task.file])
}
function updateSnapshot() {
return client.rpc.updateSnapshot(task)
}
</script>

<template>
<!-- maybe provide a KEEP STRUCTURE mode, do not filter by search keyword -->
<!-- v-if = keepStructure || (!search || caseInsensitiveMatch(task.name, search)) -->
<TaskItem
v-if="!nested || !search || caseInsensitiveMatch(task.name, search)"
v-if="opened || !nested || !search || caseInsensitiveMatch(task.name, search)"
v-bind="$attrs"
:task="task"
:style="{ paddingLeft: `${indent * 0.75 + 1}rem` }"
@click="onItemClick && onItemClick(task)"
:style="{ paddingLeft: indent ? `${indent * 0.75 + (task.type === 'suite' ? 0.50 : 1.75)}rem` : '1rem' }"
:opened="isOpened && task.type === 'suite' && task.tasks.length"
:failed-snapshot="failedSnapshot"
@click="toggleOpen()"
@run="onRun()"
@fix-snapshot="updateSnapshot()"
@preview="onItemClick?.(task)"
/>
<div v-if="nested && task.type === 'suite' && task.tasks.length">
<div v-if="nested && task.type === 'suite' && task.tasks.length" v-show="isOpened">
<TaskTree
v-for="suite in task.tasks"
:key="suite.id"
:failed-snapshot="false"
:task="suite"
:nested="nested"
:indent="indent + 1"
Expand Down
Loading

0 comments on commit 7900f9f

Please sign in to comment.