mirror of
https://github.com/vitest-dev/vitest.git
synced 2025-12-08 18:26:03 +00:00
239 lines
7.3 KiB
Vue
239 lines
7.3 KiB
Vue
<script setup lang="ts">
|
|
import type { File, Task } from '@vitest/runner'
|
|
import { useResizeObserver } from '@vueuse/core'
|
|
|
|
import { hideAllPoppers } from 'floating-vue'
|
|
|
|
import { computed, ref } from 'vue'
|
|
// @ts-expect-error missing types
|
|
import { RecycleScroller } from 'vue-virtual-scroller'
|
|
|
|
import { config } from '~/composables/client'
|
|
|
|
import { useSearch } from '~/composables/explorer/search'
|
|
import { activeFileId } from '~/composables/params'
|
|
import DetailsPanel from '../DetailsPanel.vue'
|
|
import FilterStatus from '../FilterStatus.vue'
|
|
import IconButton from '../IconButton.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 searchBox = ref<HTMLInputElement | undefined>()
|
|
|
|
const {
|
|
initialized,
|
|
filter,
|
|
search,
|
|
disableFilter,
|
|
isFiltered,
|
|
isFilteredByStatus,
|
|
disableClearSearch,
|
|
clearAll,
|
|
clearSearch,
|
|
clearFilter,
|
|
filteredFiles,
|
|
testsTotal,
|
|
uiEntries,
|
|
} = useSearch(searchBox)
|
|
|
|
const filterClass = ref<string>('grid-cols-2')
|
|
const filterHeaderClass = ref<string>('grid-col-span-2')
|
|
|
|
const testExplorerRef = ref<HTMLElement | undefined>()
|
|
useResizeObserver(() => testExplorerRef.value, ([{ contentRect }]) => {
|
|
if (contentRect.width < 420) {
|
|
filterClass.value = 'grid-cols-2'
|
|
filterHeaderClass.value = 'grid-col-span-2'
|
|
}
|
|
else {
|
|
filterClass.value = 'grid-cols-4'
|
|
filterHeaderClass.value = 'grid-col-span-4'
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div ref="testExplorerRef" 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
|
|
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 || 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"
|
|
grid="~ items-center gap-x-2 rows-[auto_auto]"
|
|
:class="filterClass"
|
|
>
|
|
<div :class="filterHeaderClass" flex="~ gap-2 items-center">
|
|
<div aria-hidden="true" class="i-carbon:filter" />
|
|
<div flex-grow-1 text-sm>
|
|
Filter
|
|
</div>
|
|
<IconButton
|
|
v-tooltip.bottom="'Clear Filter'"
|
|
:disabled="disableFilter"
|
|
title="Clear search"
|
|
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" />
|
|
</div>
|
|
</div>
|
|
<div class="scrolls" flex-auto py-1 @scroll.passive="hideAllPoppers">
|
|
<DetailsPanel>
|
|
<template v-if="initialized" #summary>
|
|
<div grid="~ items-center gap-x-1 cols-[auto_min-content_auto] rows-[min-content_min-content]">
|
|
<span text-red5>
|
|
FAIL ({{ testsTotal.failed }})
|
|
</span>
|
|
<span>/</span>
|
|
<span text-yellow5>
|
|
RUNNING ({{ testsTotal.running }})
|
|
</span>
|
|
<span text-green5>
|
|
PASS ({{ testsTotal.success }})
|
|
</span>
|
|
<span>/</span>
|
|
<span class="text-purple5:50">
|
|
SKIP ({{ filter.onlyTests ? testsTotal.skipped : '--' }})
|
|
</span>
|
|
</div>
|
|
</template>
|
|
<!-- empty-state -->
|
|
<template v-if="(isFiltered || isFilteredByStatus) && uiEntries.length === 0">
|
|
<div v-if="initialized" flex="~ col" items-center p="x4 y4" font-light>
|
|
<div 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"
|
|
:project-name="item.projectName ?? ''"
|
|
:project-name-color="item.projectNameColor ?? ''"
|
|
:state="item.state"
|
|
:duration="item.duration"
|
|
:opened="item.expanded"
|
|
:disable-task-location="!includeTaskLocation"
|
|
:class="activeFileId === item.id ? 'bg-active' : ''"
|
|
:on-item-click="onItemClick"
|
|
/>
|
|
</template>
|
|
</RecycleScroller>
|
|
</template>
|
|
</DetailsPanel>
|
|
</div>
|
|
</div>
|
|
</template>
|