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): render tests in a tree #5807

Merged
merged 5 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading