Skip to content

Commit 7900f9f

Browse files
authored
feat(ui): render tests in a tree (#5807)
1 parent a820e7a commit 7900f9f

15 files changed

+200
-94
lines changed

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

+1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ declare global {
8989
const onUnmounted: typeof import('vue')['onUnmounted']
9090
const onUpdated: typeof import('vue')['onUpdated']
9191
const openInEditor: typeof import('./composables/error')['openInEditor']
92+
const openedTreeItems: typeof import('./composables/navigation')['openedTreeItems']
9293
const params: typeof import('./composables/params')['params']
9394
const parseError: typeof import('./composables/error')['parseError']
9495
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']

packages/ui/client/components.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ declare module 'vue' {
1515
DetailsPanel: typeof import('./components/DetailsPanel.vue')['default']
1616
ErrorEntry: typeof import('./components/dashboard/ErrorEntry.vue')['default']
1717
FileDetails: typeof import('./components/FileDetails.vue')['default']
18+
IconAction: typeof import('./components/IconAction.vue')['default']
1819
IconButton: typeof import('./components/IconButton.vue')['default']
1920
Modal: typeof import('./components/Modal.vue')['default']
2021
ModuleTransformResultView: typeof import('./components/ModuleTransformResultView.vue')['default']
@@ -23,7 +24,6 @@ declare module 'vue' {
2324
RouterLink: typeof import('vue-router')['RouterLink']
2425
RouterView: typeof import('vue-router')['RouterView']
2526
StatusIcon: typeof import('./components/StatusIcon.vue')['default']
26-
Suites: typeof import('./components/Suites.vue')['default']
2727
TaskItem: typeof import('./components/TaskItem.vue')['default']
2828
TasksList: typeof import('./components/TasksList.vue')['default']
2929
TaskTree: typeof import('./components/TaskTree.vue')['default']

packages/ui/client/components/FileDetails.vue

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { Params } from '~/composables/params'
55
import { viewMode } from '~/composables/params'
66
import type { ModuleGraph } from '~/composables/module-graph'
77
import { getModuleGraph } from '~/composables/module-graph'
8+
import { getProjectNameColor } from '~/utils/task';
89
910
const data = ref<ModuleGraphData>({ externalized: [], graph: {}, inlined: [] })
1011
const graph = ref<ModuleGraph>({ nodes: [], links: [] })
@@ -49,6 +50,9 @@ function onDraft(value: boolean) {
4950
<div>
5051
<div p="2" h-10 flex="~ gap-2" items-center bg-header border="b base">
5152
<StatusIcon :task="current" />
53+
<div font-light op-50 text-sm :style="{ color: getProjectNameColor(current?.file.projectName) }">
54+
[{{ current?.file.projectName || '' }}]
55+
</div>
5256
<div flex-1 font-light op-50 ws-nowrap truncate text-sm>
5357
{{ current?.filepath }}
5458
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script setup type="ts">
2+
defineProps({
3+
icon: String,
4+
})
5+
</script>
6+
7+
<template>
8+
<div
9+
bg="gray-200"
10+
rounded-1
11+
p-0.5
12+
>
13+
<div :class="icon" op50></div>
14+
</div>
15+
</template>

packages/ui/client/components/Navigation.vue

+31-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { client, findById } from '../composables/client'
1616
import { isDark, toggleDark } from '~/composables'
1717
import { files, isReport, runAll } from '~/composables/client'
1818
import { activeFileId } from '~/composables/params'
19+
import { openedTreeItems } from '~/composables/navigation'
1920
2021
const failedSnapshot = computed(() => files.value && hasFailedSnapshot(files.value))
2122
function updateSnapshot() {
@@ -25,8 +26,8 @@ function updateSnapshot() {
2526
const toggleMode = computed(() => isDark.value ? 'light' : 'dark')
2627
2728
function onItemClick(task: Task) {
28-
activeFileId.value = task.id
29-
currentModule.value = findById(task.id)
29+
activeFileId.value = task.file.id
30+
currentModule.value = findById(task.file.id)
3031
showDashboard(false)
3132
}
3233
@@ -41,14 +42,41 @@ async function onRunAll(files?: File[]) {
4142
}
4243
await runAll(files)
4344
}
45+
46+
function collapseTests() {
47+
openedTreeItems.value = []
48+
}
49+
50+
function expandTests() {
51+
files.value.forEach(file => {
52+
if (!openedTreeItems.value.includes(file.id)) {
53+
openedTreeItems.value.push(file.id)
54+
}
55+
})
56+
}
4457
</script>
4558

4659
<template>
47-
<TasksList border="r base" :tasks="files" :on-item-click="onItemClick" :group-by-type="true" @run="onRunAll">
60+
<!-- TODO: have test tree so the folders are also nested: test -> filename -> suite -> test -->
61+
<TasksList border="r base" :tasks="files" :on-item-click="onItemClick" :group-by-type="true" @run="onRunAll" :nested="true">
4862
<template #header="{ filteredTests }">
4963
<img w-6 h-6 src="/favicon.svg" alt="Vitest logo">
5064
<span font-light text-sm flex-1>Vitest</span>
5165
<div class="flex text-lg">
66+
<IconButton
67+
v-show="openedTreeItems.length > 0"
68+
v-tooltip.bottom="'Collapse tests'"
69+
title="Collapse tests"
70+
icon="i-carbon:collapse-all"
71+
@click="collapseTests()"
72+
/>
73+
<IconButton
74+
v-show="openedTreeItems.length === 0"
75+
v-tooltip.bottom="'Expand tests'"
76+
title="Expand tests"
77+
icon="i-carbon:expand-all"
78+
@click="expandTests()"
79+
/>
5280
<IconButton
5381
v-show="(coverageConfigured && !coverageEnabled) || !dashboardVisible"
5482
v-tooltip.bottom="'Dashboard'"

packages/ui/client/components/Suites.vue

-45
This file was deleted.
+57-17
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,25 @@
11
<script setup lang="ts">
22
import type { Task } from 'vitest'
3+
import { getProjectNameColor } from '~/utils/task';
4+
import { activeFileId } from '~/composables/params';
5+
import { isReport } from '~/constants';
36
47
const props = defineProps<{
58
task: Task
9+
opened: boolean
10+
failedSnapshot: boolean
11+
}>()
12+
13+
const emit = defineEmits<{
14+
run: []
15+
preview: []
16+
fixSnapshot: [],
617
}>()
718
819
const duration = computed(() => {
920
const { result } = props.task
1021
return result && Math.round(result.duration || 0)
1122
})
12-
13-
function getProjectNameColor(name: string | undefined) {
14-
if (!name)
15-
return ''
16-
const index = name.split('').reduce((acc, v, idx) => acc + v.charCodeAt(0) + idx, 0)
17-
const colors = [
18-
'blue',
19-
'yellow',
20-
'cyan',
21-
'green',
22-
'magenta',
23-
]
24-
return colors[index % colors.length]
25-
}
26-
2723
</script>
2824

2925
<template>
@@ -35,13 +31,20 @@ function getProjectNameColor(name: string | undefined) {
3531
border-rounded
3632
cursor-pointer
3733
hover="bg-active"
34+
class="item-wrapper"
35+
:aria-label="task.name"
36+
:data-current="activeFileId === task.id"
3837
>
38+
<div v-if="task.type === 'suite'" pr-1>
39+
<div v-if="opened" i-carbon-chevron-down op20 />
40+
<div v-else i-carbon-chevron-right op20 />
41+
</div>
3942
<StatusIcon :task="task" mr-2 />
4043
<div v-if="task.type === 'suite' && task.meta.typecheck" i-logos:typescript-icon flex-shrink-0 mr-2 />
41-
<div flex items-end gap-2 :text="task?.result?.state === 'fail' ? 'red-500' : ''">
44+
<div flex items-end gap-2 :text="task?.result?.state === 'fail' ? 'red-500' : ''" overflow-hidden>
4245
<span text-sm truncate font-light>
4346
<!-- only show [] in files view -->
44-
<span v-if="task.filepath && task.file.projectName" :style="{ color: getProjectNameColor(task.file.projectName) }">
47+
<span v-if="'filepath' in task && task.projectName" :style="{ color: getProjectNameColor(task.file.projectName) }">
4548
[{{ task.file.projectName }}]
4649
</span>
4750
{{ task.name }}
@@ -50,5 +53,42 @@ function getProjectNameColor(name: string | undefined) {
5053
{{ duration > 0 ? duration : '< 1' }}ms
5154
</span>
5255
</div>
56+
<div v-if="task.type === 'suite' && 'filepath' in task" gap-1 justify-end flex-grow-1 pl-1 class="test-actions">
57+
<IconAction
58+
v-if="!isReport && failedSnapshot"
59+
v-tooltip.bottom="'Fix failed snapshot(s)'"
60+
data-testid="btn-fix-snapshot"
61+
title="Fix failed snapshot(s)"
62+
icon="i-carbon-result-old"
63+
@click.prevent.stop="emit('fixSnapshot')"
64+
/>
65+
<IconAction
66+
v-tooltip.bottom="'Open test details'"
67+
data-testid="btn-open-details"
68+
title="Open test details"
69+
icon="i-carbon-intrusion-prevention"
70+
@click.prevent.stop="emit('preview')"
71+
/>
72+
<IconAction
73+
v-if="!isReport"
74+
v-tooltip.bottom="'Run current test'"
75+
data-testid="btn-run-test"
76+
title="Run current test"
77+
icon="i-carbon-play-filled-alt"
78+
text="green-500"
79+
@click.prevent.stop="emit('run')"
80+
/>
81+
</div>
5382
</div>
5483
</template>
84+
85+
<style scoped>
86+
.test-actions {
87+
display: none;
88+
}
89+
90+
.item-wrapper:hover .test-actions,
91+
.item-wrapper[data-current="true"] .test-actions {
92+
display: flex;
93+
}
94+
</style>

packages/ui/client/components/TaskTree.vue

+41-5
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,68 @@
11
<script setup lang="ts">
22
import type { Task } from 'vitest'
3+
import { nextTick } from 'vue'
4+
import { runFiles, client } from '~/composables/client';
35
import { caseInsensitiveMatch } from '~/utils/task'
6+
import { openedTreeItems, coverageEnabled } from '~/composables/navigation';
47
58
defineOptions({ inheritAttrs: false })
69
7-
const { task, indent = 0, nested = false, search, onItemClick } = defineProps<{
10+
// TODO: better handling of "opened" - it means to forcefully open the tree item and set in TasksList right now
11+
const { task, indent = 0, nested = false, search, onItemClick, opened = false } = defineProps<{
812
task: Task
13+
failedSnapshot: boolean
914
indent?: number
15+
opened?: boolean
1016
nested?: boolean
1117
search?: string
1218
onItemClick?: (task: Task) => void
1319
}>()
20+
21+
const isOpened = computed(() => opened || openedTreeItems.value.includes(task.id))
22+
23+
function toggleOpen() {
24+
if (isOpened.value) {
25+
const tasksIds = 'tasks' in task ? task.tasks.map(t => t.id) : []
26+
openedTreeItems.value = openedTreeItems.value.filter(id => id !== task.id && !tasksIds.includes(id))
27+
} else {
28+
openedTreeItems.value = [...openedTreeItems.value, task.id]
29+
}
30+
}
31+
32+
async function onRun() {
33+
onItemClick?.(task)
34+
if (coverageEnabled.value) {
35+
disableCoverage.value = true
36+
await nextTick()
37+
}
38+
await runFiles([task.file])
39+
}
40+
41+
function updateSnapshot() {
42+
return client.rpc.updateSnapshot(task)
43+
}
1444
</script>
1545

1646
<template>
1747
<!-- maybe provide a KEEP STRUCTURE mode, do not filter by search keyword -->
1848
<!-- v-if = keepStructure || (!search || caseInsensitiveMatch(task.name, search)) -->
1949
<TaskItem
20-
v-if="!nested || !search || caseInsensitiveMatch(task.name, search)"
50+
v-if="opened || !nested || !search || caseInsensitiveMatch(task.name, search)"
2151
v-bind="$attrs"
2252
:task="task"
23-
:style="{ paddingLeft: `${indent * 0.75 + 1}rem` }"
24-
@click="onItemClick && onItemClick(task)"
53+
:style="{ paddingLeft: indent ? `${indent * 0.75 + (task.type === 'suite' ? 0.50 : 1.75)}rem` : '1rem' }"
54+
:opened="isOpened && task.type === 'suite' && task.tasks.length"
55+
:failed-snapshot="failedSnapshot"
56+
@click="toggleOpen()"
57+
@run="onRun()"
58+
@fix-snapshot="updateSnapshot()"
59+
@preview="onItemClick?.(task)"
2560
/>
26-
<div v-if="nested && task.type === 'suite' && task.tasks.length">
61+
<div v-if="nested && task.type === 'suite' && task.tasks.length" v-show="isOpened">
2762
<TaskTree
2863
v-for="suite in task.tasks"
2964
:key="suite.id"
65+
:failed-snapshot="false"
3066
:task="suite"
3167
:nested="nested"
3268
:indent="indent + 1"

0 commit comments

Comments
 (0)