mirror of
https://github.com/vitest-dev/vitest.git
synced 2026-02-01 17:36:51 +00:00
feat(ui): add summary (#493)
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
This commit is contained in:
parent
1b1807b7da
commit
bdedbc2c19
6
packages/ui/client/components.d.ts
vendored
6
packages/ui/client/components.d.ts
vendored
@ -6,17 +6,23 @@ declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
CodeMirror: typeof import('./components/CodeMirror.vue')['default']
|
||||
ConnectionOverlay: typeof import('./components/ConnectionOverlay.vue')['default']
|
||||
Dashboard: typeof import('./components/Dashboard.vue')['default']
|
||||
DashboardEntry: typeof import('./components/dashboard/DashboardEntry.vue')['default']
|
||||
DetailsPanel: typeof import('./components/DetailsPanel.vue')['default']
|
||||
FileDetails: typeof import('./components/FileDetails.vue')['default']
|
||||
IconButton: typeof import('./components/IconButton.vue')['default']
|
||||
Modal: typeof import('./components/Modal.vue')['default']
|
||||
ModuleTransformResultView: typeof import('./components/ModuleTransformResultView.vue')['default']
|
||||
Navigation: typeof import('./components/Navigation.vue')['default']
|
||||
ProgressBar: typeof import('./components/ProgressBar.vue')['default']
|
||||
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']
|
||||
TestFilesEntry: typeof import('./components/dashboard/TestFilesEntry.vue')['default']
|
||||
TestsEntry: typeof import('./components/dashboard/TestsEntry.vue')['default']
|
||||
TestsFilesContainer: typeof import('./components/dashboard/TestsFilesContainer.vue')['default']
|
||||
ViewConsoleOutput: typeof import('./components/views/ViewConsoleOutput.vue')['default']
|
||||
ViewEditor: typeof import('./components/views/ViewEditor.vue')['default']
|
||||
ViewModuleGraph: typeof import('./components/views/ViewModuleGraph.vue')['default']
|
||||
|
||||
25
packages/ui/client/components/Dashboard.vue
Normal file
25
packages/ui/client/components/Dashboard.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div h="full" flex="~ col">
|
||||
<div
|
||||
p="2"
|
||||
h-10
|
||||
flex="~ gap-2"
|
||||
items-center
|
||||
bg-header
|
||||
border="b base"
|
||||
>
|
||||
<div class="i-carbon-dashboard" />
|
||||
<span
|
||||
font-light
|
||||
text-sm
|
||||
flex-auto
|
||||
ws-nowrap
|
||||
overflow-hidden
|
||||
truncate
|
||||
>Dashboard</span>
|
||||
</div>
|
||||
<div class="scrolls" flex-auto py-1>
|
||||
<TestsFilesContainer />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -6,21 +6,6 @@ import type { ModuleGraph } from '~/composables/module-graph'
|
||||
import { getModuleGraph } from '~/composables/module-graph'
|
||||
import type { ModuleGraphData } from '#types'
|
||||
|
||||
const sizes = reactive([95, 5])
|
||||
|
||||
onMounted(() => {
|
||||
const bottomPanelPercent = 42 / window.innerHeight * 100
|
||||
|
||||
sizes[0] = 100 - bottomPanelPercent
|
||||
sizes[1] = bottomPanelPercent
|
||||
})
|
||||
|
||||
function open() {
|
||||
const filePath = current.value?.filepath
|
||||
if (filePath)
|
||||
fetch(`/__open-in-editor?file=${encodeURIComponent(filePath)}`)
|
||||
}
|
||||
|
||||
const data = ref<ModuleGraphData>({ externalized: [], graph: {}, inlined: [] })
|
||||
const graph = ref<ModuleGraph>({ nodes: [], links: [] })
|
||||
|
||||
@ -35,6 +20,12 @@ debouncedWatch(
|
||||
{ debounce: 100 },
|
||||
)
|
||||
|
||||
const open = () => {
|
||||
const filePath = current.value?.filepath
|
||||
if (filePath)
|
||||
fetch(`/__open-in-editor?file=${encodeURIComponent(filePath)}`)
|
||||
}
|
||||
|
||||
const changeViewMode = (view: Params['view']) => {
|
||||
viewMode.value = view
|
||||
}
|
||||
@ -68,9 +59,11 @@ const changeViewMode = (view: Params['view']) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ViewModuleGraph v-show="viewMode === 'graph'" :graph="graph" />
|
||||
<ViewEditor v-if="viewMode === 'editor'" :file="current" />
|
||||
<ViewConsoleOutput v-else-if="viewMode === 'console'" :file="current" />
|
||||
<ViewReport v-else-if="!viewMode" :file="current" />
|
||||
<div flex flex-col flex-1 overflow="hidden">
|
||||
<ViewModuleGraph v-show="viewMode === 'graph'" :graph="graph" />
|
||||
<ViewEditor v-if="viewMode === 'editor'" :file="current" />
|
||||
<ViewConsoleOutput v-else-if="viewMode === 'console'" :file="current" />
|
||||
<ViewReport v-else-if="!viewMode" :file="current" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{ icon?: string }>()
|
||||
defineProps<{ icon?: string; title?: string }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button op70 rounded hover="bg-active op100" class="w-1.4em h-1.4em flex">
|
||||
<button :aria-label="title" role="button" op70 rounded hover="bg-active op100" class="w-1.4em h-1.4em flex">
|
||||
<slot>
|
||||
<div :class="icon" ma />
|
||||
</slot>
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { currentModule, dashboardVisible, showDashboard } from '../composables/navigation'
|
||||
import { findById } from '../composables/client'
|
||||
import type { Task } from '#types'
|
||||
import { isDark, toggleDark } from '~/composables'
|
||||
import { files, runAll } from '~/composables/client'
|
||||
@ -6,6 +8,8 @@ import { activeFileId } from '~/composables/params'
|
||||
|
||||
function onItemClick(task: Task) {
|
||||
activeFileId.value = task.id
|
||||
currentModule.value = findById(task.id)
|
||||
showDashboard(false)
|
||||
}
|
||||
const toggleMode = computed(() => isDark.value ? 'light' : 'dark')
|
||||
</script>
|
||||
@ -16,6 +20,15 @@ const toggleMode = computed(() => isDark.value ? 'light' : 'dark')
|
||||
<img w-6 h-6 mx-2 src="/favicon.svg">
|
||||
<span font-light text-sm flex-1>Vitest</span>
|
||||
<div class="flex text-lg">
|
||||
<IconButton
|
||||
v-show="!dashboardVisible"
|
||||
v-tooltip.bottom="'Dashboard'"
|
||||
title="Show dashboard"
|
||||
class="!animate-100ms"
|
||||
animate-count-1
|
||||
icon="i-carbon-dashboard"
|
||||
@click="showDashboard(true)"
|
||||
/>
|
||||
<IconButton v-tooltip.bottom="'Rerun all'" icon="i-carbon-play" @click="runAll" />
|
||||
<IconButton
|
||||
v-tooltip.bottom="`Toggle to ${toggleMode} mode`"
|
||||
|
||||
104
packages/ui/client/components/ProgressBar.vue
Normal file
104
packages/ui/client/components/ProgressBar.vue
Normal file
@ -0,0 +1,104 @@
|
||||
<script setup lang="ts">
|
||||
import { files } from '../composables/client'
|
||||
import { filesFailed, filesSuccess, finished } from '../composables/summary'
|
||||
|
||||
const { width } = useWindowSize()
|
||||
const classes = computed(() => {
|
||||
// if there is no files, then in progress and gray
|
||||
if (files.value.length === 0)
|
||||
return '!bg-gray-4 !dark:bg-gray-7 in-progress'
|
||||
else if (!finished.value)
|
||||
return 'in-progress'
|
||||
|
||||
return null
|
||||
})
|
||||
const total = computed(() => files.value.length)
|
||||
const pass = computed(() => filesSuccess.value.length)
|
||||
const failed = computed(() => filesFailed.value.length)
|
||||
|
||||
const widthPass = computed(() => {
|
||||
const t = unref(total)
|
||||
return t > 0 ? (width.value * pass.value / t) : 0
|
||||
})
|
||||
const widthFailed = computed(() => {
|
||||
const t = unref(total)
|
||||
return t > 0 ? (width.value * failed.value / t) : 0
|
||||
})
|
||||
const pending = computed(() => {
|
||||
const t = unref(total)
|
||||
return t - failed.value - pass.value
|
||||
})
|
||||
const widthPending = computed(() => {
|
||||
const t = unref(total)
|
||||
return t > 0 ? (width.value * pending.value / t) : 0
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
absolute
|
||||
t-0
|
||||
l-0
|
||||
r-0
|
||||
z-index-1031
|
||||
pointer-events-none
|
||||
p-0
|
||||
h-3px
|
||||
grid="~ auto-cols-max"
|
||||
justify-items-center
|
||||
w-screen
|
||||
:class="classes"
|
||||
>
|
||||
<div h-3px relative overflow-hidden class="px-0" w-screen>
|
||||
<div
|
||||
absolute
|
||||
l-0
|
||||
t-0
|
||||
bg-red5
|
||||
h-3px
|
||||
:class="classes"
|
||||
:style="`width: ${widthFailed}px;`"
|
||||
>
|
||||
 
|
||||
</div>
|
||||
<div
|
||||
absolute
|
||||
l-0
|
||||
t-0
|
||||
bg-green5
|
||||
h-3px
|
||||
:class="classes"
|
||||
:style="`left: ${widthFailed}px; width: ${widthPass}px;`"
|
||||
>
|
||||
 
|
||||
</div>
|
||||
<div
|
||||
absolute
|
||||
l-0
|
||||
t-0
|
||||
bg-yellow5
|
||||
h-3px
|
||||
:class="classes"
|
||||
:style="`left: ${widthPass + widthFailed}px; width: ${widthPending}px;`"
|
||||
>
|
||||
 
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.in-progress {
|
||||
background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
|
||||
background-size: 40px 40px;
|
||||
animation: in-progress-stripes 2s linear infinite;
|
||||
}
|
||||
@keyframes in-progress-stripes {
|
||||
from {
|
||||
background-position: 40px 0;
|
||||
}
|
||||
to {
|
||||
background-position: 0 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -9,7 +9,7 @@ const updateSnapshot = () => current.value && client.rpc.updateSnapshot(current.
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="current" border="r base">
|
||||
<div v-if="current" h-full border="r base">
|
||||
<TasksList :tasks="current.tasks" :nested="true">
|
||||
<template #header>
|
||||
<StatusIcon :task="current" />
|
||||
@ -19,9 +19,13 @@ const updateSnapshot = () => current.value && client.rpc.updateSnapshot(current.
|
||||
v-if="failedSnapshot"
|
||||
v-tooltip.bottom="'Update failed snapshots'"
|
||||
icon="i-carbon-result-old"
|
||||
@click="updateSnapshot"
|
||||
@click="updateSnapshot()"
|
||||
/>
|
||||
<IconButton
|
||||
v-tooltip.bottom="'Rerun file'"
|
||||
icon="i-carbon-play"
|
||||
@click="runCurrent()"
|
||||
/>
|
||||
<IconButton v-tooltip.bottom="'Rerun file'" icon="i-carbon-play" @click="runCurrent" />
|
||||
</div>
|
||||
</template>
|
||||
</TasksList>
|
||||
|
||||
@ -65,6 +65,7 @@ export default {
|
||||
bg="transparent"
|
||||
font="light"
|
||||
text="sm"
|
||||
flex-1
|
||||
:op="search.length ? '100' : '50'"
|
||||
>
|
||||
</div>
|
||||
|
||||
17
packages/ui/client/components/dashboard/DashboardEntry.vue
Normal file
17
packages/ui/client/components/dashboard/DashboardEntry.vue
Normal file
@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(defineProps<{ tail?: boolean }>(), { tail: false })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div p-2 text-center flex>
|
||||
<div>
|
||||
<div text-4xl>
|
||||
<slot name="body" />
|
||||
</div>
|
||||
<div text-md>
|
||||
<slot name="header" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!tail" my-2 op50 w-1px bg-current origin-center rotate-15 translate-x-3 />
|
||||
</div>
|
||||
</template>
|
||||
59
packages/ui/client/components/dashboard/TestFilesEntry.vue
Normal file
59
packages/ui/client/components/dashboard/TestFilesEntry.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { files } from '../../composables/client'
|
||||
import { filesFailed, filesSnapshotFailed, filesSuccess, time } from '../../composables/summary'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
grid="~ cols-[min-content_1fr_min-content]"
|
||||
items-center gap="x-2 y-3" p="x4" relative font-light w-80
|
||||
op80
|
||||
>
|
||||
<div i-carbon-document />
|
||||
<div>Files</div>
|
||||
<div class="number">
|
||||
{{ files.length }}
|
||||
</div>
|
||||
|
||||
<template v-if="filesSuccess.length">
|
||||
<div i-carbon-checkmark />
|
||||
<div>Pass</div>
|
||||
<div class="number">
|
||||
{{ filesSuccess.length }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="filesFailed.length">
|
||||
<div i-carbon-close />
|
||||
<div>
|
||||
Fail
|
||||
</div>
|
||||
<div class="number" text-red5>
|
||||
{{ filesFailed.length }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="filesSnapshotFailed.length">
|
||||
<div i-carbon-compare />
|
||||
<div>
|
||||
Snapshot Fail
|
||||
</div>
|
||||
<div class="number" text-red5>
|
||||
{{ filesSnapshotFailed.length }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div i-carbon-timer />
|
||||
<div>Time</div>
|
||||
<div class="number">
|
||||
{{ time }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.number {
|
||||
font-weight: 400;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
58
packages/ui/client/components/dashboard/TestsEntry.vue
Normal file
58
packages/ui/client/components/dashboard/TestsEntry.vue
Normal file
@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import { tests, testsFailed, testsSkipped, testsSuccess, testsTodo } from '../../composables/summary'
|
||||
|
||||
const total = computed(() => tests.value.length)
|
||||
const pass = computed(() => testsSuccess.value.length)
|
||||
const failed = computed(() => testsFailed.value.length)
|
||||
const skipped = computed(() => testsSkipped.value.length)
|
||||
const todo = computed(() => testsTodo.value.length)
|
||||
const pending = computed(() => {
|
||||
const t = unref(total)
|
||||
return t - failed.value - pass.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex="~ wrap" justify-evenly gap-2 p="x-4" relative>
|
||||
<DashboardEntry text-green5>
|
||||
<template #header>
|
||||
Pass
|
||||
</template>
|
||||
<template #body>
|
||||
{{ pass }}
|
||||
</template>
|
||||
</DashboardEntry>
|
||||
<DashboardEntry :class="{ 'text-red5': failed, 'op50': !failed }">
|
||||
<template #header>
|
||||
Fail
|
||||
</template>
|
||||
<template #body>
|
||||
{{ failed }}
|
||||
</template>
|
||||
</DashboardEntry>
|
||||
<DashboardEntry v-if="skipped" op50>
|
||||
<template #header>
|
||||
Skip
|
||||
</template>
|
||||
<template #body>
|
||||
{{ skipped }}
|
||||
</template>
|
||||
</DashboardEntry>
|
||||
<DashboardEntry v-if="todo" op50>
|
||||
<template #header>
|
||||
Todo
|
||||
</template>
|
||||
<template #body>
|
||||
{{ todo }}
|
||||
</template>
|
||||
</DashboardEntry>
|
||||
<DashboardEntry :tail="true">
|
||||
<template #header>
|
||||
Total
|
||||
</template>
|
||||
<template #body>
|
||||
{{ total }}
|
||||
</template>
|
||||
</DashboardEntry>
|
||||
</div>
|
||||
</template>
|
||||
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div gap-0 flex="~ col gap-4" h-full justify-center items-center>
|
||||
<div bg-header rounded-lg p="y4 x2">
|
||||
<!-- <section aria-labelledby="tests" m="y-4 x-2">
|
||||
<TestsEntry />
|
||||
</section> -->
|
||||
<TestFilesEntry />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -18,6 +18,10 @@ export const files = computed(() => client.state.getFiles())
|
||||
export const current = computed(() => files.value.find(file => file.id === activeFileId.value))
|
||||
export const currentLogs = computed(() => getTasks(current.value).map(i => i?.logs || []).flat() || [])
|
||||
|
||||
export const findById = (id: string) => {
|
||||
return files.value.find(file => file.id === id)
|
||||
}
|
||||
|
||||
export const isConnected = computed(() => status.value === 'OPEN')
|
||||
export const isConnecting = computed(() => status.value === 'CONNECTING')
|
||||
export const isDisconnected = computed(() => status.value === 'CLOSED')
|
||||
@ -62,20 +66,20 @@ watch(
|
||||
)
|
||||
|
||||
// display the first file on init
|
||||
if (!activeFileId.value) {
|
||||
const stop = watch(
|
||||
() => client.state.getFiles(),
|
||||
(files) => {
|
||||
if (activeFileId.value) {
|
||||
stop()
|
||||
return
|
||||
}
|
||||
|
||||
if (files.length && files[0].id) {
|
||||
activeFileId.value = files[0].id
|
||||
stop()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
}
|
||||
// if (!activeFileId.value) {
|
||||
// const stop = watch(
|
||||
// () => client.state.getFiles(),
|
||||
// (files) => {
|
||||
// if (activeFileId.value) {
|
||||
// stop()
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// if (files.length && files[0].id) {
|
||||
// activeFileId.value = files[0].id
|
||||
// stop()
|
||||
// }
|
||||
// },
|
||||
// { immediate: true },
|
||||
// )
|
||||
// }
|
||||
|
||||
36
packages/ui/client/composables/navigation.ts
Normal file
36
packages/ui/client/composables/navigation.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { client, findById } from './client'
|
||||
import { activeFileId } from './params'
|
||||
import type { File } from '#types'
|
||||
|
||||
export const currentModule = ref<File | undefined>(undefined)
|
||||
export const dashboardVisible = ref(true)
|
||||
|
||||
export function initializeNavigation() {
|
||||
const file = activeFileId.value
|
||||
if (file && file.length > 0) {
|
||||
const current = findById(file)
|
||||
if (current) {
|
||||
currentModule.value = current
|
||||
dashboardVisible.value = false
|
||||
}
|
||||
else {
|
||||
watchOnce(
|
||||
() => client.state.getFiles(),
|
||||
() => {
|
||||
currentModule.value = findById(file)
|
||||
dashboardVisible.value = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return dashboardVisible
|
||||
}
|
||||
|
||||
export function showDashboard(show: boolean) {
|
||||
dashboardVisible.value = show
|
||||
if (show) {
|
||||
currentModule.value = undefined
|
||||
activeFileId.value = ''
|
||||
}
|
||||
}
|
||||
57
packages/ui/client/composables/summary.ts
Normal file
57
packages/ui/client/composables/summary.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { hasFailedSnapshot } from '@vitest/ws-client'
|
||||
import type { Task, Test } from 'vitest/src'
|
||||
import { files } from '~/composables/client'
|
||||
|
||||
type Nullable<T> = T | null | undefined
|
||||
type Arrayable<T> = T | Array<T>
|
||||
|
||||
// files
|
||||
export const filesFailed = computed(() => files.value.filter(f => f.result?.state === 'fail'))
|
||||
export const filesSuccess = computed(() => files.value.filter(f => f.result?.state === 'pass'))
|
||||
export const filesIgnore = computed(() => files.value.filter(f => f.mode === 'skip' || f.mode === 'todo'))
|
||||
export const filesRunning = computed(() => files.value.filter(f =>
|
||||
!filesFailed.value.includes(f)
|
||||
&& !filesSuccess.value.includes(f)
|
||||
&& !filesIgnore.value.includes(f),
|
||||
))
|
||||
export const filesSkipped = computed(() => filesIgnore.value.filter(f => f.mode === 'skip'))
|
||||
export const filesSnapshotFailed = computed(() => files.value.filter(hasFailedSnapshot))
|
||||
export const filesTodo = computed(() => filesIgnore.value.filter(f => f.mode === 'todo'))
|
||||
export const finished = computed(() => filesRunning.value.length === 0)
|
||||
// tests
|
||||
export const tests = computed(() => {
|
||||
return getTests(files.value)
|
||||
})
|
||||
export const testsFailed = computed(() => {
|
||||
return tests.value.filter(f => f.result?.state === 'fail')
|
||||
})
|
||||
export const testsSuccess = computed(() => {
|
||||
return tests.value.filter(f => f.result?.state === 'pass')
|
||||
})
|
||||
export const testsIgnore = computed(() => tests.value.filter(f => f.mode === 'skip' || f.mode === 'todo'))
|
||||
export const testsSkipped = computed(() => testsIgnore.value.filter(f => f.mode === 'skip'))
|
||||
export const testsTodo = computed(() => testsIgnore.value.filter(f => f.mode === 'todo'))
|
||||
export const totalTests = computed(() => testsFailed.value.length + testsSuccess.value.length)
|
||||
export const time = computed(() => {
|
||||
const t = getTests(tests.value).reduce((acc, t) => {
|
||||
if (t.result?.duration)
|
||||
acc += t.result.duration
|
||||
|
||||
return acc
|
||||
}, 0)
|
||||
|
||||
if (t > 1000)
|
||||
return `${(t / 1000).toFixed(2)}s`
|
||||
|
||||
return `${Math.round(t)}ms`
|
||||
})
|
||||
|
||||
function toArray<T>(array?: Nullable<Arrayable<T>>): Array<T> {
|
||||
array = array || []
|
||||
if (Array.isArray(array))
|
||||
return array
|
||||
return [array]
|
||||
}
|
||||
function getTests(suite: Arrayable<Task>): Test[] {
|
||||
return toArray(suite).flatMap(s => s.type === 'test' ? [s] : s.tasks.flatMap(c => c.type === 'test' ? [c] : getTests(c)))
|
||||
}
|
||||
@ -1,36 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
// @ts-expect-error missing types
|
||||
import { Pane, Splitpanes } from 'splitpanes'
|
||||
import { initializeNavigation } from '../composables/navigation'
|
||||
|
||||
const sizes = reactive([33, 33, 34])
|
||||
const dashboardVisible = initializeNavigation()
|
||||
const mainSizes = reactive([33, 67])
|
||||
const detailSizes = reactive([33, 67])
|
||||
|
||||
function onResize(event: { size: number }[]) {
|
||||
const onMainResized = useDebounceFn((event: { size: number }[]) => {
|
||||
event.forEach((e, i) => {
|
||||
sizes[i] = e.size
|
||||
mainSizes[i] = e.size
|
||||
})
|
||||
}
|
||||
}, 0)
|
||||
const onModuleResized = useDebounceFn((event: { size: number }[]) => {
|
||||
event.forEach((e, i) => {
|
||||
detailSizes[i] = e.size
|
||||
})
|
||||
}, 0)
|
||||
|
||||
onMounted(() => {
|
||||
const resizeMain = () => {
|
||||
const width = window.innerWidth
|
||||
const panelWidth = Math.min(width / 3, 300)
|
||||
const panelPercent = panelWidth / width * 100
|
||||
sizes[0] = panelPercent
|
||||
sizes[1] = panelPercent
|
||||
sizes[2] = 100 - panelPercent * 2
|
||||
})
|
||||
mainSizes[0] = (100 * panelWidth) / width
|
||||
mainSizes[1] = 100 - mainSizes[0]
|
||||
// initialize suite width with the same navigation panel width in pixels (adjust its % inside detail's split pane)
|
||||
detailSizes[0] = (100 * panelWidth) / (width - panelWidth)
|
||||
detailSizes[1] = 100 - detailSizes[0]
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProgressBar />
|
||||
<div h-screen w-screen overflow="hidden">
|
||||
<Splitpanes @resize="onResize">
|
||||
<Pane :size="sizes[0]">
|
||||
<Splitpanes class="pt-4px" @resized="onMainResized" @ready="resizeMain">
|
||||
<Pane :size="mainSizes[0]">
|
||||
<Navigation />
|
||||
</Pane>
|
||||
<Pane :size="sizes[1]">
|
||||
<Suites />
|
||||
</Pane>
|
||||
<Pane :size="sizes[2]">
|
||||
<FileDetails />
|
||||
<Pane :size="mainSizes[1]">
|
||||
<transition>
|
||||
<Dashboard v-if="dashboardVisible" key="summary" />
|
||||
<Splitpanes v-else key="detail" @resized="onModuleResized">
|
||||
<Pane :size="detailSizes[0]">
|
||||
<Suites />
|
||||
</Pane>
|
||||
<Pane :size="detailSizes[1]">
|
||||
<FileDetails />
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</transition>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</div>
|
||||
|
||||
@ -110,7 +110,8 @@ html.dark {
|
||||
}
|
||||
|
||||
.splitpanes--vertical > .splitpanes__splitter:before {
|
||||
left: -10px;
|
||||
/* make vertical scroll usable */
|
||||
/*left: -10px;*/
|
||||
right: -10px;
|
||||
height: 100%;
|
||||
}
|
||||
@ -163,3 +164,4 @@ html.dark {
|
||||
padding-bottom: unset !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -26,7 +26,7 @@ export default defineConfig({
|
||||
],
|
||||
shortcuts: {
|
||||
'bg-base': 'bg-white dark:bg-[#111]',
|
||||
'bg-overlay': 'bg-white:2 dark:bg-[#222]:2',
|
||||
'bg-overlay': 'bg-[#eee]:2 dark:bg-[#222]:2',
|
||||
'bg-header': 'bg-gray-500:5',
|
||||
'bg-active': 'bg-gray-500:8',
|
||||
'bg-hover': 'bg-gray-500:20',
|
||||
|
||||
@ -15,13 +15,13 @@ test('outside snapshot', () => {
|
||||
test('inline snapshot', () => {
|
||||
expect('inline string').toMatchInlineSnapshot('"inline string"')
|
||||
expect({ foo: { type: 'object', map: new Map() } }).toMatchInlineSnapshot(`
|
||||
{
|
||||
"foo": {
|
||||
"map": Map {},
|
||||
"type": "object",
|
||||
},
|
||||
}
|
||||
`)
|
||||
{
|
||||
"foo": {
|
||||
"map": Map {},
|
||||
"type": "object",
|
||||
},
|
||||
}
|
||||
`)
|
||||
const indent = `
|
||||
()=>
|
||||
array
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user