mirror of
https://github.com/hanzoai/vitest.git
synced 2026-09-24 17:04:25 +00:00
352 lines
11 KiB
Vue
352 lines
11 KiB
Vue
<script setup lang="ts">
|
|
import type { RunnerTestFile as File, RunnerTask as Task } from 'vitest'
|
|
import { hideAllPoppers } from 'floating-vue'
|
|
import { computed, ref } from 'vue'
|
|
|
|
// @ts-expect-error missing types
|
|
import { RecycleScroller } from 'vue-virtual-scroller'
|
|
import { availableProjects, config } from '~/composables/client'
|
|
import { useSearch } from '~/composables/explorer/search'
|
|
import { ALL_PROJECTS, projectSort } from '~/composables/explorer/state'
|
|
import { activeFileId, selectedTest } from '~/composables/params'
|
|
import FilterStatus from '../FilterStatus.vue'
|
|
import IconButton from '../IconButton.vue'
|
|
import ResultsPanel from '../ResultsPanel.vue'
|
|
import ExplorerItem from './ExplorerItem.vue'
|
|
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
|
|
|
defineOptions({ inheritAttrs: false })
|
|
|
|
const { onItemClick } = defineProps<{
|
|
onItemClick?: (task: Task) => void
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
(event: 'item-click', files?: File[]): void
|
|
(event: 'run', files?: File[]): void
|
|
}>()
|
|
|
|
const includeTaskLocation = computed(() => config.value.includeTaskLocation)
|
|
const slowTime = computed(() => {
|
|
const threshold = config.value.slowTestThreshold
|
|
if (typeof threshold === 'number') {
|
|
return ` (>${threshold}ms)`
|
|
}
|
|
|
|
return ''
|
|
})
|
|
|
|
const searchBox = ref<HTMLInputElement | undefined>()
|
|
const selectProjectRef = ref<HTMLSelectElement | undefined>()
|
|
const sortProjectRef = ref<HTMLSelectElement | undefined>()
|
|
|
|
const {
|
|
initialized,
|
|
filter,
|
|
search,
|
|
disableFilter,
|
|
isFiltered,
|
|
isFilteredByStatus,
|
|
disableClearSearch,
|
|
clearAll,
|
|
clearSearch,
|
|
clearFilter,
|
|
filteredFiles,
|
|
testsTotal,
|
|
uiEntries,
|
|
enableProjects,
|
|
disableClearProjects,
|
|
currentProject,
|
|
currentProjectName,
|
|
clearProject,
|
|
clearProjectSort,
|
|
disableClearProjectSort,
|
|
searchMatcher,
|
|
} = useSearch(searchBox, selectProjectRef, sortProjectRef)
|
|
</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-files="isFiltered || isFilteredByStatus ? filteredFiles : undefined" />
|
|
</div>
|
|
<div
|
|
v-if="enableProjects"
|
|
p="l3 y2 r2"
|
|
bg-header
|
|
border="b-2 base"
|
|
grid="~ cols-[auto_auto_minmax(0,1fr)_auto] gap-x-2 gap-y-1"
|
|
items-center
|
|
>
|
|
<div class="i-carbon:workspace" flex-shrink-0 />
|
|
<label for="project-select" text-sm>
|
|
Projects
|
|
</label>
|
|
<div class="relative flex-1">
|
|
<select
|
|
id="project-select"
|
|
ref="selectProjectRef"
|
|
v-model="currentProject"
|
|
w-full
|
|
appearance-none
|
|
bg-base
|
|
text-base
|
|
border="~ base rounded"
|
|
pl-2
|
|
pr-8
|
|
py-1
|
|
text-sm
|
|
cursor-pointer
|
|
hover:bg-active
|
|
class="outline-none"
|
|
>
|
|
<option :value="ALL_PROJECTS" class="text-base bg-base">
|
|
All Projects
|
|
</option>
|
|
<option
|
|
v-for="project in availableProjects"
|
|
:key="project"
|
|
:value="project"
|
|
class="text-base bg-base"
|
|
>
|
|
{{ project }}
|
|
</option>
|
|
</select>
|
|
<div class="i-carbon:chevron-down absolute right-2 top-1/2 op50 -translate-y-1/2 pointer-events-none" />
|
|
</div>
|
|
|
|
<IconButton
|
|
v-tooltip.bottom="'Clear project filter'"
|
|
:disabled="disableClearProjects"
|
|
title="Clear project filter"
|
|
icon="i-carbon:filter-remove"
|
|
@click.passive="clearProject(true)"
|
|
/>
|
|
</div>
|
|
<div
|
|
p="l3 y2 r2"
|
|
bg-header
|
|
border="b-2 base"
|
|
grid="~ cols-[auto_auto_minmax(0,1fr)_auto] gap-x-2"
|
|
items-center
|
|
>
|
|
<div class="i-carbon:arrows-vertical" flex-shrink-0 />
|
|
<label for="project-sort" text-sm>
|
|
Sort by
|
|
</label>
|
|
<div class="relative flex-1">
|
|
<select
|
|
id="project-sort"
|
|
ref="sortProjectRef"
|
|
v-model="projectSort"
|
|
w-full
|
|
appearance-none
|
|
bg-base
|
|
text-base
|
|
border="~ base rounded"
|
|
pl-2
|
|
pr-8
|
|
py-1
|
|
text-sm
|
|
cursor-pointer
|
|
hover:bg-active
|
|
class="outline-none"
|
|
>
|
|
<option value="default" class="text-base bg-base">
|
|
Default
|
|
</option>
|
|
<option value="duration-desc" class="text-base bg-base">
|
|
Slowest first
|
|
</option>
|
|
<option value="duration-asc" class="text-base bg-base">
|
|
Fastest first
|
|
</option>
|
|
<option v-if="enableProjects" value="asc" class="text-base bg-base">
|
|
Project A-Z
|
|
</option>
|
|
<option v-if="enableProjects" value="desc" class="text-base bg-base">
|
|
Project Z-A
|
|
</option>
|
|
</select>
|
|
<div class="i-carbon:chevron-down absolute right-2 top-1/2 op50 -translate-y-1/2 pointer-events-none" />
|
|
</div>
|
|
<IconButton
|
|
v-tooltip.bottom="'Reset sort'"
|
|
:disabled="disableClearProjectSort"
|
|
title="Reset sort"
|
|
icon="i-carbon:filter-reset"
|
|
@click.passive="clearProjectSort(true)"
|
|
/>
|
|
</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... (e.g. test name, tag:expression)"
|
|
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 || isFilteredByStatus ? filteredFiles : undefined)"
|
|
>
|
|
<IconButton
|
|
v-tooltip.bottom="'Clear search'"
|
|
:disabled="disableClearSearch"
|
|
title="Clear search"
|
|
icon="i-carbon:filter-remove"
|
|
@click.passive="clearSearch(true)"
|
|
/>
|
|
</div>
|
|
<div
|
|
p="l3 y2 r2"
|
|
items-center
|
|
bg-header
|
|
border="b-2 base"
|
|
flex="~ wrap gap-x-4 justify-between"
|
|
>
|
|
<div min-w-full flex="~ gap-2 items-center">
|
|
<div aria-hidden="true" class="i-carbon:filter" flex-shrink-0 />
|
|
<div flex-grow-1 text-sm>
|
|
Filter
|
|
</div>
|
|
<IconButton
|
|
v-tooltip.bottom="'Clear Filter'"
|
|
:disabled="disableFilter"
|
|
title="Clear filter"
|
|
icon="i-carbon:filter-remove"
|
|
@click.passive="clearFilter(false)"
|
|
/>
|
|
</div>
|
|
<FilterStatus v-model="filter.failed" label="Fail" />
|
|
<FilterStatus v-model="filter.success" label="Pass" />
|
|
<FilterStatus v-model="filter.skipped" label="Skip" />
|
|
<FilterStatus v-model="filter.onlyTests" label="Only Tests" />
|
|
<FilterStatus v-model="filter.slow" :label="`Slow${slowTime}`" />
|
|
</div>
|
|
</div>
|
|
<div class="scrolls" flex-auto py-1 @scroll.passive="hideAllPoppers">
|
|
<ResultsPanel>
|
|
<template v-if="initialized" #summary>
|
|
<div grid="~ items-center gap-x-1 cols-[auto_min-content_auto] rows-[min-content_min-content]">
|
|
<span text-red-700 dark:text-red-500>
|
|
FAIL ({{ testsTotal.failed }})
|
|
</span>
|
|
<span>/</span>
|
|
<span text-yellow-700 dark:text-yellow-500>
|
|
RUNNING ({{ testsTotal.running }})
|
|
</span>
|
|
<span text-green-700 dark:text-green-500>
|
|
PASS ({{ testsTotal.success }})
|
|
</span>
|
|
<span>/</span>
|
|
<span class="text-purple-700 dark:text-purple-400">
|
|
SKIP ({{ filter.onlyTests ? testsTotal.skipped : '--' }})
|
|
</span>
|
|
</div>
|
|
</template>
|
|
<!-- empty-state -->
|
|
<template v-if="(isFiltered || isFilteredByStatus || !!currentProjectName) && uiEntries.length === 0">
|
|
<div v-if="initialized" flex="~ col" items-center p="x4 y4" font-light>
|
|
<div v-if="searchMatcher.error" text-red text-center>
|
|
{{ searchMatcher.error }}
|
|
</div>
|
|
<div v-else op30>
|
|
No matched test
|
|
</div>
|
|
<button
|
|
type="button"
|
|
font-light
|
|
text-sm
|
|
border="~ gray-400/50 rounded"
|
|
p="x2 y0.5"
|
|
m="t2"
|
|
op="50"
|
|
:class="disableClearSearch ? null : 'hover:op100'"
|
|
:disabled="disableClearSearch"
|
|
@click.passive="clearSearch(true)"
|
|
>
|
|
Clear Search
|
|
</button>
|
|
<button
|
|
type="button"
|
|
font-light
|
|
text-sm
|
|
border="~ gray-400/50 rounded"
|
|
p="x2 y0.5"
|
|
m="t2"
|
|
op="50"
|
|
:class="disableFilter ? null : 'hover:op100'"
|
|
:disabled="disableFilter"
|
|
@click.passive="clearFilter(true)"
|
|
>
|
|
Clear Filter
|
|
</button>
|
|
<button
|
|
type="button"
|
|
font-light
|
|
op="50 hover:100"
|
|
text-sm
|
|
border="~ gray-400/50 rounded"
|
|
p="x2 y0.5"
|
|
m="t2"
|
|
@click.passive="clearAll"
|
|
>
|
|
Clear All
|
|
</button>
|
|
</div>
|
|
<div v-else flex="~ col" items-center p="x4 y4" font-light>
|
|
<div class="i-carbon:circle-dash animate-spin" />
|
|
<div op30>
|
|
Loading...
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<RecycleScroller
|
|
page-mode
|
|
key-field="id"
|
|
:item-size="28"
|
|
:items="uiEntries"
|
|
:buffer="100"
|
|
>
|
|
<template #default="{ item }">
|
|
<ExplorerItem
|
|
class="h-28px m-0 p-0"
|
|
:task-id="item.id"
|
|
:expandable="item.expandable"
|
|
:type="item.type"
|
|
:current="activeFileId === item.id"
|
|
:indent="item.indent"
|
|
:name="item.name"
|
|
:typecheck="item.typecheck === true"
|
|
:label="item.label"
|
|
:project-name="item.projectName ?? ''"
|
|
:project-name-color="item.projectNameColor ?? ''"
|
|
:state="item.state"
|
|
:duration="item.duration"
|
|
:slow="item.slow === true"
|
|
:opened="item.expanded"
|
|
:disable-task-location="!includeTaskLocation"
|
|
:class="selectedTest === item.id || (!selectedTest && activeFileId === item.id) ? 'bg-active' : ''"
|
|
:on-item-click="onItemClick"
|
|
/>
|
|
</template>
|
|
</RecycleScroller>
|
|
</template>
|
|
</ResultsPanel>
|
|
</div>
|
|
</div>
|
|
</template>
|