[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat(ui): add summary (#493)

Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>

authored by

Joaquín Sánchez
Anthony Fu
and committed by
GitHub
(Jan 18, 2022, 1:53 PM +0800) bdedbc2c 1b1807b7

+473 -67
+1 -1
packages/ui/vite.config.ts
··· 26 26 ], 27 27 shortcuts: { 28 28 'bg-base': 'bg-white dark:bg-[#111]', 29 - 'bg-overlay': 'bg-white:2 dark:bg-[#222]:2', 29 + 'bg-overlay': 'bg-[#eee]:2 dark:bg-[#222]:2', 30 30 'bg-header': 'bg-gray-500:5', 31 31 'bg-active': 'bg-gray-500:8', 32 32 'bg-hover': 'bg-gray-500:20',
+6
packages/ui/client/components.d.ts
··· 6 6 export interface GlobalComponents { 7 7 CodeMirror: typeof import('./components/CodeMirror.vue')['default'] 8 8 ConnectionOverlay: typeof import('./components/ConnectionOverlay.vue')['default'] 9 + Dashboard: typeof import('./components/Dashboard.vue')['default'] 10 + DashboardEntry: typeof import('./components/dashboard/DashboardEntry.vue')['default'] 9 11 DetailsPanel: typeof import('./components/DetailsPanel.vue')['default'] 10 12 FileDetails: typeof import('./components/FileDetails.vue')['default'] 11 13 IconButton: typeof import('./components/IconButton.vue')['default'] 12 14 Modal: typeof import('./components/Modal.vue')['default'] 13 15 ModuleTransformResultView: typeof import('./components/ModuleTransformResultView.vue')['default'] 14 16 Navigation: typeof import('./components/Navigation.vue')['default'] 17 + ProgressBar: typeof import('./components/ProgressBar.vue')['default'] 15 18 StatusIcon: typeof import('./components/StatusIcon.vue')['default'] 16 19 Suites: typeof import('./components/Suites.vue')['default'] 17 20 TaskItem: typeof import('./components/TaskItem.vue')['default'] 18 21 TasksList: typeof import('./components/TasksList.vue')['default'] 19 22 TaskTree: typeof import('./components/TaskTree.vue')['default'] 23 + TestFilesEntry: typeof import('./components/dashboard/TestFilesEntry.vue')['default'] 24 + TestsEntry: typeof import('./components/dashboard/TestsEntry.vue')['default'] 25 + TestsFilesContainer: typeof import('./components/dashboard/TestsFilesContainer.vue')['default'] 20 26 ViewConsoleOutput: typeof import('./components/views/ViewConsoleOutput.vue')['default'] 21 27 ViewEditor: typeof import('./components/views/ViewEditor.vue')['default'] 22 28 ViewModuleGraph: typeof import('./components/views/ViewModuleGraph.vue')['default']
+7 -7
test/core/test/snapshot.test.ts
··· 15 15 test('inline snapshot', () => { 16 16 expect('inline string').toMatchInlineSnapshot('"inline string"') 17 17 expect({ foo: { type: 'object', map: new Map() } }).toMatchInlineSnapshot(` 18 - { 19 - "foo": { 20 - "map": Map {}, 21 - "type": "object", 22 - }, 23 - } 24 - `) 18 + { 19 + "foo": { 20 + "map": Map {}, 21 + "type": "object", 22 + }, 23 + } 24 + `) 25 25 const indent = ` 26 26 ()=> 27 27 array
+25
packages/ui/client/components/Dashboard.vue
··· 1 + <template> 2 + <div h="full" flex="~ col"> 3 + <div 4 + p="2" 5 + h-10 6 + flex="~ gap-2" 7 + items-center 8 + bg-header 9 + border="b base" 10 + > 11 + <div class="i-carbon-dashboard" /> 12 + <span 13 + font-light 14 + text-sm 15 + flex-auto 16 + ws-nowrap 17 + overflow-hidden 18 + truncate 19 + >Dashboard</span> 20 + </div> 21 + <div class="scrolls" flex-auto py-1> 22 + <TestsFilesContainer /> 23 + </div> 24 + </div> 25 + </template>
+12 -19
packages/ui/client/components/FileDetails.vue
··· 6 6 import { getModuleGraph } from '~/composables/module-graph' 7 7 import type { ModuleGraphData } from '#types' 8 8 9 - const sizes = reactive([95, 5]) 10 - 11 - onMounted(() => { 12 - const bottomPanelPercent = 42 / window.innerHeight * 100 13 - 14 - sizes[0] = 100 - bottomPanelPercent 15 - sizes[1] = bottomPanelPercent 16 - }) 17 - 18 - function open() { 19 - const filePath = current.value?.filepath 20 - if (filePath) 21 - fetch(`/__open-in-editor?file=${encodeURIComponent(filePath)}`) 22 - } 23 - 24 9 const data = ref<ModuleGraphData>({ externalized: [], graph: {}, inlined: [] }) 25 10 const graph = ref<ModuleGraph>({ nodes: [], links: [] }) 26 11 ··· 34 19 }, 35 20 { debounce: 100 }, 36 21 ) 22 + 23 + const open = () => { 24 + const filePath = current.value?.filepath 25 + if (filePath) 26 + fetch(`/__open-in-editor?file=${encodeURIComponent(filePath)}`) 27 + } 37 28 38 29 const changeViewMode = (view: Params['view']) => { 39 30 viewMode.value = view ··· 68 59 </div> 69 60 </div> 70 61 71 - <ViewModuleGraph v-show="viewMode === 'graph'" :graph="graph" /> 72 - <ViewEditor v-if="viewMode === 'editor'" :file="current" /> 73 - <ViewConsoleOutput v-else-if="viewMode === 'console'" :file="current" /> 74 - <ViewReport v-else-if="!viewMode" :file="current" /> 62 + <div flex flex-col flex-1 overflow="hidden"> 63 + <ViewModuleGraph v-show="viewMode === 'graph'" :graph="graph" /> 64 + <ViewEditor v-if="viewMode === 'editor'" :file="current" /> 65 + <ViewConsoleOutput v-else-if="viewMode === 'console'" :file="current" /> 66 + <ViewReport v-else-if="!viewMode" :file="current" /> 67 + </div> 75 68 </div> 76 69 </template>
+2 -2
packages/ui/client/components/IconButton.vue
··· 1 1 <script setup lang="ts"> 2 - defineProps<{ icon?: string }>() 2 + defineProps<{ icon?: string; title?: string }>() 3 3 </script> 4 4 5 5 <template> 6 - <button op70 rounded hover="bg-active op100" class="w-1.4em h-1.4em flex"> 6 + <button :aria-label="title" role="button" op70 rounded hover="bg-active op100" class="w-1.4em h-1.4em flex"> 7 7 <slot> 8 8 <div :class="icon" ma /> 9 9 </slot>
+13
packages/ui/client/components/Navigation.vue
··· 1 1 <script setup lang="ts"> 2 + import { currentModule, dashboardVisible, showDashboard } from '../composables/navigation' 3 + import { findById } from '../composables/client' 2 4 import type { Task } from '#types' 3 5 import { isDark, toggleDark } from '~/composables' 4 6 import { files, runAll } from '~/composables/client' ··· 6 8 7 9 function onItemClick(task: Task) { 8 10 activeFileId.value = task.id 11 + currentModule.value = findById(task.id) 12 + showDashboard(false) 9 13 } 10 14 const toggleMode = computed(() => isDark.value ? 'light' : 'dark') 11 15 </script> ··· 16 20 <img w-6 h-6 mx-2 src="/favicon.svg"> 17 21 <span font-light text-sm flex-1>Vitest</span> 18 22 <div class="flex text-lg"> 23 + <IconButton 24 + v-show="!dashboardVisible" 25 + v-tooltip.bottom="'Dashboard'" 26 + title="Show dashboard" 27 + class="!animate-100ms" 28 + animate-count-1 29 + icon="i-carbon-dashboard" 30 + @click="showDashboard(true)" 31 + /> 19 32 <IconButton v-tooltip.bottom="'Rerun all'" icon="i-carbon-play" @click="runAll" /> 20 33 <IconButton 21 34 v-tooltip.bottom="`Toggle to ${toggleMode} mode`"
+104
packages/ui/client/components/ProgressBar.vue
··· 1 + <script setup lang="ts"> 2 + import { files } from '../composables/client' 3 + import { filesFailed, filesSuccess, finished } from '../composables/summary' 4 + 5 + const { width } = useWindowSize() 6 + const classes = computed(() => { 7 + // if there is no files, then in progress and gray 8 + if (files.value.length === 0) 9 + return '!bg-gray-4 !dark:bg-gray-7 in-progress' 10 + else if (!finished.value) 11 + return 'in-progress' 12 + 13 + return null 14 + }) 15 + const total = computed(() => files.value.length) 16 + const pass = computed(() => filesSuccess.value.length) 17 + const failed = computed(() => filesFailed.value.length) 18 + 19 + const widthPass = computed(() => { 20 + const t = unref(total) 21 + return t > 0 ? (width.value * pass.value / t) : 0 22 + }) 23 + const widthFailed = computed(() => { 24 + const t = unref(total) 25 + return t > 0 ? (width.value * failed.value / t) : 0 26 + }) 27 + const pending = computed(() => { 28 + const t = unref(total) 29 + return t - failed.value - pass.value 30 + }) 31 + const widthPending = computed(() => { 32 + const t = unref(total) 33 + return t > 0 ? (width.value * pending.value / t) : 0 34 + }) 35 + </script> 36 + 37 + <template> 38 + <div 39 + absolute 40 + t-0 41 + l-0 42 + r-0 43 + z-index-1031 44 + pointer-events-none 45 + p-0 46 + h-3px 47 + grid="~ auto-cols-max" 48 + justify-items-center 49 + w-screen 50 + :class="classes" 51 + > 52 + <div h-3px relative overflow-hidden class="px-0" w-screen> 53 + <div 54 + absolute 55 + l-0 56 + t-0 57 + bg-red5 58 + h-3px 59 + :class="classes" 60 + :style="`width: ${widthFailed}px;`" 61 + > 62 + &#160; 63 + </div> 64 + <div 65 + absolute 66 + l-0 67 + t-0 68 + bg-green5 69 + h-3px 70 + :class="classes" 71 + :style="`left: ${widthFailed}px; width: ${widthPass}px;`" 72 + > 73 + &#160; 74 + </div> 75 + <div 76 + absolute 77 + l-0 78 + t-0 79 + bg-yellow5 80 + h-3px 81 + :class="classes" 82 + :style="`left: ${widthPass + widthFailed}px; width: ${widthPending}px;`" 83 + > 84 + &#160; 85 + </div> 86 + </div> 87 + </div> 88 + </template> 89 + 90 + <style scoped> 91 + .in-progress { 92 + 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); 93 + background-size: 40px 40px; 94 + animation: in-progress-stripes 2s linear infinite; 95 + } 96 + @keyframes in-progress-stripes { 97 + from { 98 + background-position: 40px 0; 99 + } 100 + to { 101 + background-position: 0 0; 102 + } 103 + } 104 + </style>
+7 -3
packages/ui/client/components/Suites.vue
··· 9 9 </script> 10 10 11 11 <template> 12 - <div v-if="current" border="r base"> 12 + <div v-if="current" h-full border="r base"> 13 13 <TasksList :tasks="current.tasks" :nested="true"> 14 14 <template #header> 15 15 <StatusIcon :task="current" /> ··· 19 19 v-if="failedSnapshot" 20 20 v-tooltip.bottom="'Update failed snapshots'" 21 21 icon="i-carbon-result-old" 22 - @click="updateSnapshot" 22 + @click="updateSnapshot()" 23 23 /> 24 - <IconButton v-tooltip.bottom="'Rerun file'" icon="i-carbon-play" @click="runCurrent" /> 24 + <IconButton 25 + v-tooltip.bottom="'Rerun file'" 26 + icon="i-carbon-play" 27 + @click="runCurrent()" 28 + /> 25 29 </div> 26 30 </template> 27 31 </TasksList>
+1
packages/ui/client/components/TasksList.vue
··· 65 65 bg="transparent" 66 66 font="light" 67 67 text="sm" 68 + flex-1 68 69 :op="search.length ? '100' : '50'" 69 70 > 70 71 </div>
+21 -17
packages/ui/client/composables/client.ts
··· 18 18 export const current = computed(() => files.value.find(file => file.id === activeFileId.value)) 19 19 export const currentLogs = computed(() => getTasks(current.value).map(i => i?.logs || []).flat() || []) 20 20 21 + export const findById = (id: string) => { 22 + return files.value.find(file => file.id === id) 23 + } 24 + 21 25 export const isConnected = computed(() => status.value === 'OPEN') 22 26 export const isConnecting = computed(() => status.value === 'CONNECTING') 23 27 export const isDisconnected = computed(() => status.value === 'CLOSED') ··· 62 66 ) 63 67 64 68 // display the first file on init 65 - if (!activeFileId.value) { 66 - const stop = watch( 67 - () => client.state.getFiles(), 68 - (files) => { 69 - if (activeFileId.value) { 70 - stop() 71 - return 72 - } 73 - 74 - if (files.length && files[0].id) { 75 - activeFileId.value = files[0].id 76 - stop() 77 - } 78 - }, 79 - { immediate: true }, 80 - ) 81 - } 69 + // if (!activeFileId.value) { 70 + // const stop = watch( 71 + // () => client.state.getFiles(), 72 + // (files) => { 73 + // if (activeFileId.value) { 74 + // stop() 75 + // return 76 + // } 77 + // 78 + // if (files.length && files[0].id) { 79 + // activeFileId.value = files[0].id 80 + // stop() 81 + // } 82 + // }, 83 + // { immediate: true }, 84 + // ) 85 + // }
+36
packages/ui/client/composables/navigation.ts
··· 1 + import { client, findById } from './client' 2 + import { activeFileId } from './params' 3 + import type { File } from '#types' 4 + 5 + export const currentModule = ref<File | undefined>(undefined) 6 + export const dashboardVisible = ref(true) 7 + 8 + export function initializeNavigation() { 9 + const file = activeFileId.value 10 + if (file && file.length > 0) { 11 + const current = findById(file) 12 + if (current) { 13 + currentModule.value = current 14 + dashboardVisible.value = false 15 + } 16 + else { 17 + watchOnce( 18 + () => client.state.getFiles(), 19 + () => { 20 + currentModule.value = findById(file) 21 + dashboardVisible.value = false 22 + }, 23 + ) 24 + } 25 + } 26 + 27 + return dashboardVisible 28 + } 29 + 30 + export function showDashboard(show: boolean) { 31 + dashboardVisible.value = show 32 + if (show) { 33 + currentModule.value = undefined 34 + activeFileId.value = '' 35 + } 36 + }
+57
packages/ui/client/composables/summary.ts
··· 1 + import { hasFailedSnapshot } from '@vitest/ws-client' 2 + import type { Task, Test } from 'vitest/src' 3 + import { files } from '~/composables/client' 4 + 5 + type Nullable<T> = T | null | undefined 6 + type Arrayable<T> = T | Array<T> 7 + 8 + // files 9 + export const filesFailed = computed(() => files.value.filter(f => f.result?.state === 'fail')) 10 + export const filesSuccess = computed(() => files.value.filter(f => f.result?.state === 'pass')) 11 + export const filesIgnore = computed(() => files.value.filter(f => f.mode === 'skip' || f.mode === 'todo')) 12 + export const filesRunning = computed(() => files.value.filter(f => 13 + !filesFailed.value.includes(f) 14 + && !filesSuccess.value.includes(f) 15 + && !filesIgnore.value.includes(f), 16 + )) 17 + export const filesSkipped = computed(() => filesIgnore.value.filter(f => f.mode === 'skip')) 18 + export const filesSnapshotFailed = computed(() => files.value.filter(hasFailedSnapshot)) 19 + export const filesTodo = computed(() => filesIgnore.value.filter(f => f.mode === 'todo')) 20 + export const finished = computed(() => filesRunning.value.length === 0) 21 + // tests 22 + export const tests = computed(() => { 23 + return getTests(files.value) 24 + }) 25 + export const testsFailed = computed(() => { 26 + return tests.value.filter(f => f.result?.state === 'fail') 27 + }) 28 + export const testsSuccess = computed(() => { 29 + return tests.value.filter(f => f.result?.state === 'pass') 30 + }) 31 + export const testsIgnore = computed(() => tests.value.filter(f => f.mode === 'skip' || f.mode === 'todo')) 32 + export const testsSkipped = computed(() => testsIgnore.value.filter(f => f.mode === 'skip')) 33 + export const testsTodo = computed(() => testsIgnore.value.filter(f => f.mode === 'todo')) 34 + export const totalTests = computed(() => testsFailed.value.length + testsSuccess.value.length) 35 + export const time = computed(() => { 36 + const t = getTests(tests.value).reduce((acc, t) => { 37 + if (t.result?.duration) 38 + acc += t.result.duration 39 + 40 + return acc 41 + }, 0) 42 + 43 + if (t > 1000) 44 + return `${(t / 1000).toFixed(2)}s` 45 + 46 + return `${Math.round(t)}ms` 47 + }) 48 + 49 + function toArray<T>(array?: Nullable<Arrayable<T>>): Array<T> { 50 + array = array || [] 51 + if (Array.isArray(array)) 52 + return array 53 + return [array] 54 + } 55 + function getTests(suite: Arrayable<Task>): Test[] { 56 + return toArray(suite).flatMap(s => s.type === 'test' ? [s] : s.tasks.flatMap(c => c.type === 'test' ? [c] : getTests(c))) 57 + }
+34 -17
packages/ui/client/pages/index.vue
··· 1 1 <script setup lang="ts"> 2 2 // @ts-expect-error missing types 3 3 import { Pane, Splitpanes } from 'splitpanes' 4 + import { initializeNavigation } from '../composables/navigation' 4 5 5 - const sizes = reactive([33, 33, 34]) 6 + const dashboardVisible = initializeNavigation() 7 + const mainSizes = reactive([33, 67]) 8 + const detailSizes = reactive([33, 67]) 6 9 7 - function onResize(event: { size: number }[]) { 10 + const onMainResized = useDebounceFn((event: { size: number }[]) => { 8 11 event.forEach((e, i) => { 9 - sizes[i] = e.size 12 + mainSizes[i] = e.size 10 13 }) 11 - } 14 + }, 0) 15 + const onModuleResized = useDebounceFn((event: { size: number }[]) => { 16 + event.forEach((e, i) => { 17 + detailSizes[i] = e.size 18 + }) 19 + }, 0) 12 20 13 - onMounted(() => { 21 + const resizeMain = () => { 14 22 const width = window.innerWidth 15 23 const panelWidth = Math.min(width / 3, 300) 16 - const panelPercent = panelWidth / width * 100 17 - sizes[0] = panelPercent 18 - sizes[1] = panelPercent 19 - sizes[2] = 100 - panelPercent * 2 20 - }) 24 + mainSizes[0] = (100 * panelWidth) / width 25 + mainSizes[1] = 100 - mainSizes[0] 26 + // initialize suite width with the same navigation panel width in pixels (adjust its % inside detail's split pane) 27 + detailSizes[0] = (100 * panelWidth) / (width - panelWidth) 28 + detailSizes[1] = 100 - detailSizes[0] 29 + } 21 30 </script> 22 31 23 32 <template> 33 + <ProgressBar /> 24 34 <div h-screen w-screen overflow="hidden"> 25 - <Splitpanes @resize="onResize"> 26 - <Pane :size="sizes[0]"> 35 + <Splitpanes class="pt-4px" @resized="onMainResized" @ready="resizeMain"> 36 + <Pane :size="mainSizes[0]"> 27 37 <Navigation /> 28 38 </Pane> 29 - <Pane :size="sizes[1]"> 30 - <Suites /> 31 - </Pane> 32 - <Pane :size="sizes[2]"> 33 - <FileDetails /> 39 + <Pane :size="mainSizes[1]"> 40 + <transition> 41 + <Dashboard v-if="dashboardVisible" key="summary" /> 42 + <Splitpanes v-else key="detail" @resized="onModuleResized"> 43 + <Pane :size="detailSizes[0]"> 44 + <Suites /> 45 + </Pane> 46 + <Pane :size="detailSizes[1]"> 47 + <FileDetails /> 48 + </Pane> 49 + </Splitpanes> 50 + </transition> 34 51 </Pane> 35 52 </Splitpanes> 36 53 </div>
+3 -1
packages/ui/client/styles/main.css
··· 110 110 } 111 111 112 112 .splitpanes--vertical > .splitpanes__splitter:before { 113 - left: -10px; 113 + /* make vertical scroll usable */ 114 + /*left: -10px;*/ 114 115 right: -10px; 115 116 height: 100%; 116 117 } ··· 162 163 margin-right: unset !important; 163 164 padding-bottom: unset !important; 164 165 } 166 + 165 167
+17
packages/ui/client/components/dashboard/DashboardEntry.vue
··· 1 + <script setup lang="ts"> 2 + withDefaults(defineProps<{ tail?: boolean }>(), { tail: false }) 3 + </script> 4 + 5 + <template> 6 + <div p-2 text-center flex> 7 + <div> 8 + <div text-4xl> 9 + <slot name="body" /> 10 + </div> 11 + <div text-md> 12 + <slot name="header" /> 13 + </div> 14 + </div> 15 + <div v-if="!tail" my-2 op50 w-1px bg-current origin-center rotate-15 translate-x-3 /> 16 + </div> 17 + </template>
+59
packages/ui/client/components/dashboard/TestFilesEntry.vue
··· 1 + <script setup lang="ts"> 2 + import { files } from '../../composables/client' 3 + import { filesFailed, filesSnapshotFailed, filesSuccess, time } from '../../composables/summary' 4 + </script> 5 + 6 + <template> 7 + <div 8 + grid="~ cols-[min-content_1fr_min-content]" 9 + items-center gap="x-2 y-3" p="x4" relative font-light w-80 10 + op80 11 + > 12 + <div i-carbon-document /> 13 + <div>Files</div> 14 + <div class="number"> 15 + {{ files.length }} 16 + </div> 17 + 18 + <template v-if="filesSuccess.length"> 19 + <div i-carbon-checkmark /> 20 + <div>Pass</div> 21 + <div class="number"> 22 + {{ filesSuccess.length }} 23 + </div> 24 + </template> 25 + 26 + <template v-if="filesFailed.length"> 27 + <div i-carbon-close /> 28 + <div> 29 + Fail 30 + </div> 31 + <div class="number" text-red5> 32 + {{ filesFailed.length }} 33 + </div> 34 + </template> 35 + 36 + <template v-if="filesSnapshotFailed.length"> 37 + <div i-carbon-compare /> 38 + <div> 39 + Snapshot Fail 40 + </div> 41 + <div class="number" text-red5> 42 + {{ filesSnapshotFailed.length }} 43 + </div> 44 + </template> 45 + 46 + <div i-carbon-timer /> 47 + <div>Time</div> 48 + <div class="number"> 49 + {{ time }} 50 + </div> 51 + </div> 52 + </template> 53 + 54 + <style scoped> 55 + .number { 56 + font-weight: 400; 57 + text-align: right; 58 + } 59 + </style>
+58
packages/ui/client/components/dashboard/TestsEntry.vue
··· 1 + <script setup lang="ts"> 2 + import { tests, testsFailed, testsSkipped, testsSuccess, testsTodo } from '../../composables/summary' 3 + 4 + const total = computed(() => tests.value.length) 5 + const pass = computed(() => testsSuccess.value.length) 6 + const failed = computed(() => testsFailed.value.length) 7 + const skipped = computed(() => testsSkipped.value.length) 8 + const todo = computed(() => testsTodo.value.length) 9 + const pending = computed(() => { 10 + const t = unref(total) 11 + return t - failed.value - pass.value 12 + }) 13 + </script> 14 + 15 + <template> 16 + <div flex="~ wrap" justify-evenly gap-2 p="x-4" relative> 17 + <DashboardEntry text-green5> 18 + <template #header> 19 + Pass 20 + </template> 21 + <template #body> 22 + {{ pass }} 23 + </template> 24 + </DashboardEntry> 25 + <DashboardEntry :class="{ 'text-red5': failed, 'op50': !failed }"> 26 + <template #header> 27 + Fail 28 + </template> 29 + <template #body> 30 + {{ failed }} 31 + </template> 32 + </DashboardEntry> 33 + <DashboardEntry v-if="skipped" op50> 34 + <template #header> 35 + Skip 36 + </template> 37 + <template #body> 38 + {{ skipped }} 39 + </template> 40 + </DashboardEntry> 41 + <DashboardEntry v-if="todo" op50> 42 + <template #header> 43 + Todo 44 + </template> 45 + <template #body> 46 + {{ todo }} 47 + </template> 48 + </DashboardEntry> 49 + <DashboardEntry :tail="true"> 50 + <template #header> 51 + Total 52 + </template> 53 + <template #body> 54 + {{ total }} 55 + </template> 56 + </DashboardEntry> 57 + </div> 58 + </template>
+10
packages/ui/client/components/dashboard/TestsFilesContainer.vue
··· 1 + <template> 2 + <div gap-0 flex="~ col gap-4" h-full justify-center items-center> 3 + <div bg-header rounded-lg p="y4 x2"> 4 + <!-- <section aria-labelledby="tests" m="y-4 x-2"> 5 + <TestsEntry /> 6 + </section> --> 7 + <TestFilesEntry /> 8 + </div> 9 + </div> 10 + </template>