mirror of
https://github.com/vitest-dev/vitest.git
synced 2026-02-01 17:36:51 +00:00
223 lines
6.2 KiB
Vue
223 lines
6.2 KiB
Vue
<script setup lang="ts">
|
|
import type { ComputedRef } from 'vue'
|
|
import type { File, Task } from '#types'
|
|
import { findById, testRunState } from '~/composables/client'
|
|
import { activeFileId } from '~/composables/params'
|
|
import { caseInsensitiveMatch, isSuite } from '~/utils/task'
|
|
|
|
const props = withDefaults(defineProps<{
|
|
tasks: Task[]
|
|
indent?: number
|
|
nested?: boolean
|
|
groupByType?: boolean
|
|
onItemClick?: (task: Task) => void
|
|
}>(), {
|
|
indent: 0,
|
|
groupByType: false,
|
|
nested: false,
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
(event: 'run', files?: File[]): void
|
|
}>()
|
|
|
|
const search = ref('')
|
|
const searchBox = ref<HTMLInputElement | undefined>()
|
|
const isFiltered = computed(() => search.value.trim() !== '')
|
|
|
|
const matchTasks = (tasks: Task[], search: string): boolean => {
|
|
let result = false
|
|
|
|
for (let i = 0; i < tasks.length; i++) {
|
|
const task = tasks[i]
|
|
|
|
if (caseInsensitiveMatch(task.name, search)) {
|
|
result = true
|
|
break
|
|
}
|
|
|
|
// walk whole task tree
|
|
if (isSuite(task) && task.tasks) {
|
|
result = matchTasks(task.tasks, search)
|
|
if (result)
|
|
break
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
const filtered = computed(() => {
|
|
if (!search.value.trim())
|
|
return props.tasks
|
|
|
|
return props.tasks.filter(task => matchTasks([task], search.value))
|
|
})
|
|
const filteredTests: ComputedRef<File[]> = computed(() => isFiltered.value ? filtered.value.map(task => findById(task.id)!).filter(Boolean) : [])
|
|
|
|
const failed = computed(() => filtered.value.filter(task => task.result?.state === 'fail'))
|
|
const success = computed(() => filtered.value.filter(task => task.result?.state === 'pass'))
|
|
const skipped = computed(() => filtered.value.filter(task => task.mode === 'skip' || task.mode === 'todo'))
|
|
const running = computed(() => filtered.value.filter(task =>
|
|
!failed.value.includes(task)
|
|
&& !success.value.includes(task)
|
|
&& !skipped.value.includes(task),
|
|
))
|
|
const throttledRunning = useThrottle(running, 250)
|
|
|
|
const clearSearch = (focus: boolean) => {
|
|
search.value = ''
|
|
focus && searchBox.value?.focus()
|
|
}
|
|
const disableClearSearch = computed(() => {
|
|
return search.value === ''
|
|
})
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
export default {
|
|
inheritAttrs: false,
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div h="full" flex="~ col">
|
|
<div>
|
|
<div p="2" h-10 flex="~ gap-2" items-center bg-header border="b base">
|
|
<slot name="header" :filtered-tests="isFiltered ? filteredTests : undefined" />
|
|
</div>
|
|
<div
|
|
p="l3 y2 r2"
|
|
flex="~ gap-2"
|
|
items-center
|
|
bg-header
|
|
border="b-2 base"
|
|
>
|
|
<div class="i-carbon:search" flex-shrink-0 />
|
|
<input
|
|
ref="searchBox"
|
|
v-model="search"
|
|
placeholder="Search..."
|
|
outline="none"
|
|
bg="transparent"
|
|
font="light"
|
|
text="sm"
|
|
flex-1
|
|
pl-1
|
|
:op="search.length ? '100' : '50'"
|
|
@keydown.esc="clearSearch(false)"
|
|
@keydown.enter="emit('run', isFiltered ? filteredTests : undefined)"
|
|
>
|
|
<IconButton
|
|
v-tooltip.bottom="'Clear search'"
|
|
:disabled="disableClearSearch"
|
|
title="Clear search"
|
|
icon="i-carbon:filter-remove"
|
|
@click.passive="clearSearch(true)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="scrolls" flex-auto py-1>
|
|
<template v-if="groupByType">
|
|
<DetailsPanel v-if="failed.length">
|
|
<template #summary>
|
|
<div text-red5>
|
|
FAIL ({{ failed.length }})
|
|
</div>
|
|
</template>
|
|
<TaskTree
|
|
v-for="task in failed"
|
|
:key="task.id"
|
|
:task="task"
|
|
:nested="nested"
|
|
:search="search"
|
|
:class="activeFileId === task.id ? 'bg-active' : ''"
|
|
:on-item-click="onItemClick"
|
|
/>
|
|
</DetailsPanel>
|
|
<DetailsPanel v-if="running.length || testRunState === 'running'">
|
|
<template #summary>
|
|
<div text-yellow5>
|
|
RUNNING ({{ throttledRunning.length }})
|
|
</div>
|
|
</template>
|
|
<TaskTree
|
|
v-for="task in throttledRunning"
|
|
:key="task.id"
|
|
:task="task"
|
|
:nested="nested"
|
|
:search="search"
|
|
:class="activeFileId === task.id ? 'bg-active' : ''"
|
|
:on-item-click="onItemClick"
|
|
/>
|
|
</DetailsPanel>
|
|
<DetailsPanel v-if="success.length">
|
|
<template #summary>
|
|
<div text-green5>
|
|
PASS ({{ success.length }})
|
|
</div>
|
|
</template>
|
|
<TaskTree
|
|
v-for="task in success"
|
|
:key="task.id"
|
|
:task="task"
|
|
:nested="nested"
|
|
:search="search"
|
|
:class="activeFileId === task.id ? 'bg-active' : ''"
|
|
:on-item-click="onItemClick"
|
|
/>
|
|
</DetailsPanel>
|
|
<DetailsPanel v-if="skipped.length">
|
|
<template #summary>
|
|
<div class="text-purple5:50">
|
|
SKIP ({{ skipped.length }})
|
|
</div>
|
|
</template>
|
|
<TaskTree
|
|
v-for="task in skipped"
|
|
:key="task.id"
|
|
:task="task"
|
|
:nested="nested"
|
|
:search="search"
|
|
:class="activeFileId === task.id ? 'bg-active' : ''"
|
|
:on-item-click="onItemClick"
|
|
/>
|
|
</DetailsPanel>
|
|
</template>
|
|
|
|
<!--flat-->
|
|
<template v-else>
|
|
<TaskTree
|
|
v-for="task in filtered"
|
|
:key="task.id"
|
|
:task="task"
|
|
:nested="nested"
|
|
:search="search"
|
|
:class="activeFileId === task.id ? 'bg-active' : ''"
|
|
:on-item-click="onItemClick"
|
|
/>
|
|
</template>
|
|
<!--empty-state-->
|
|
<template v-if="isFiltered && filtered.length === 0">
|
|
<div flex="~ col" items-center p="x4 y4" font-light>
|
|
<div op30>
|
|
No matched test
|
|
</div>
|
|
<button
|
|
font-light
|
|
op="50 hover:100"
|
|
text-sm
|
|
border="~ gray-400/50 rounded"
|
|
p="x2 y0.5"
|
|
m="t2"
|
|
@click.passive="clearSearch(true)"
|
|
>
|
|
Clear
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|