From 6e87d0be1cd12927eb584cc31c12f3d73de2aa7f Mon Sep 17 00:00:00 2001 From: DerYeger Date: Fri, 20 Feb 2026 19:51:05 +0100 Subject: [PATCH 01/13] feat(ui): implement filter for slow tests Also highlight slow tests with yellow color for duration text --- .../ui/client/components/FilterStatus.vue | 5 +++-- .../client/components/explorer/Explorer.vue | 16 +++++++++++--- .../components/explorer/ExplorerItem.vue | 9 +++++++- .../client/composables/explorer/collector.ts | 1 + .../ui/client/composables/explorer/filter.ts | 14 +++++++++++-- .../ui/client/composables/explorer/search.ts | 7 ++++++- .../ui/client/composables/explorer/state.ts | 6 ++++++ .../ui/client/composables/explorer/tree.ts | 7 +++++++ .../ui/client/composables/explorer/types.ts | 2 ++ .../ui/client/composables/explorer/utils.ts | 21 ++++++++++++++++++- .../vitest/src/node/config/serializeConfig.ts | 5 +++++ packages/vitest/src/runtime/config.ts | 1 + test/config/test/pool.test.ts | 6 ++++++ 13 files changed, 90 insertions(+), 10 deletions(-) diff --git a/packages/ui/client/components/FilterStatus.vue b/packages/ui/client/components/FilterStatus.vue index da9d8a2a7540..0ff9d3f48cb3 100644 --- a/packages/ui/client/components/FilterStatus.vue +++ b/packages/ui/client/components/FilterStatus.vue @@ -16,7 +16,7 @@ function toggle() { diff --git a/packages/ui/client/components/explorer/Explorer.vue b/packages/ui/client/components/explorer/Explorer.vue index e02d84666f9d..760e978ee0be 100644 --- a/packages/ui/client/components/explorer/Explorer.vue +++ b/packages/ui/client/components/explorer/Explorer.vue @@ -62,13 +62,21 @@ const filterHeaderClass = ref('grid-col-span-2') const testExplorerRef = ref() useResizeObserver(() => testExplorerRef.value, ([{ contentRect }]) => { - if (contentRect.width < 420) { + if (contentRect.width < 220) { + filterClass.value = 'grid-cols-1' + filterHeaderClass.value = 'grid-col-span-1' + } + else if (contentRect.width < 340) { filterClass.value = 'grid-cols-2' filterHeaderClass.value = 'grid-col-span-2' } + else if (contentRect.width < 540) { + filterClass.value = 'grid-cols-3' + filterHeaderClass.value = 'grid-col-span-3' + } else { - filterClass.value = 'grid-cols-4' - filterHeaderClass.value = 'grid-col-span-4' + filterClass.value = 'grid-cols-5' + filterHeaderClass.value = 'grid-col-span-5' } }) @@ -242,6 +250,7 @@ useResizeObserver(() => testExplorerRef.value, ([{ contentRect }]) => { +
@@ -343,6 +352,7 @@ useResizeObserver(() => testExplorerRef.value, ([{ contentRect }]) => { :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' : ''" diff --git a/packages/ui/client/components/explorer/ExplorerItem.vue b/packages/ui/client/components/explorer/ExplorerItem.vue index 40a286180562..de8d7b371e2d 100644 --- a/packages/ui/client/components/explorer/ExplorerItem.vue +++ b/packages/ui/client/components/explorer/ExplorerItem.vue @@ -20,6 +20,7 @@ const { indent, name, duration, + slow, current, opened, expandable, @@ -35,6 +36,7 @@ const { indent: number typecheck?: boolean duration?: number + slow?: boolean state?: TaskState current: boolean type: TaskTreeNodeType @@ -220,7 +222,12 @@ const tagsBgGradient = computed(() => { - + {{ duration > 0 ? duration : '< 1' }}ms
diff --git a/packages/ui/client/composables/explorer/collector.ts b/packages/ui/client/composables/explorer/collector.ts index f5f1e402c7ad..619f8fda09a5 100644 --- a/packages/ui/client/composables/explorer/collector.ts +++ b/packages/ui/client/composables/explorer/collector.ts @@ -41,6 +41,7 @@ export function runLoadFiles( failed: filter.failed, success: filter.success, skipped: filter.skipped, + slow: filter.slow, onlyTests: filter.onlyTests, }) } diff --git a/packages/ui/client/composables/explorer/filter.ts b/packages/ui/client/composables/explorer/filter.ts index cc8ea4574196..f46715321c68 100644 --- a/packages/ui/client/composables/explorer/filter.ts +++ b/packages/ui/client/composables/explorer/filter.ts @@ -1,6 +1,6 @@ import type { Task } from '@vitest/runner' import type { FileTreeNode, Filter, FilterResult, ParentTreeNode, SearchMatcher, UITaskTreeNode } from '~/composables/explorer/types' -import { client, findById } from '~/composables/client' +import { client, config, findById } from '~/composables/client' import { explorerTree } from '~/composables/explorer/index' import { currentProjectName, filteredFiles, projectSort, uiEntries } from '~/composables/explorer/state' import { @@ -220,6 +220,15 @@ function* filterParents( } function matchState(task: Task, filter: Filter) { + if (filter.slow) { + if (task.type === 'test') { + const threshold = config.value.slowTestThreshold + if (typeof threshold === 'number' && typeof task.result?.duration === 'number' && task.result.duration > threshold) { + return true + } + } + } + if (filter.success || filter.failed) { if ('result' in task) { if (filter.success && task.result?.state === 'pass') { @@ -245,7 +254,8 @@ function matchTask( ) { // search and filter will apply together if (search(task)) { - if (filter.success || filter.failed || filter.skipped) { + const hasStatusFilter = filter.success || filter.failed || filter.skipped || filter.slow + if (hasStatusFilter) { if (matchState(task, filter)) { return true } diff --git a/packages/ui/client/composables/explorer/search.ts b/packages/ui/client/composables/explorer/search.ts index c46d28cba580..66ebff2fced5 100644 --- a/packages/ui/client/composables/explorer/search.ts +++ b/packages/ui/client/composables/explorer/search.ts @@ -62,6 +62,7 @@ export function useSearch( filter.failed = false filter.success = false filter.skipped = false + filter.slow = false filter.onlyTests = false if (focus) { searchBox.value?.focus() @@ -94,6 +95,7 @@ export function useSearch( failedValue: boolean, successValue: boolean, skippedValue: boolean, + slowValue: boolean, onlyTestsValue: boolean, projectValue: string, projectSortValue: SortUIType, @@ -106,6 +108,7 @@ export function useSearch( treeFilter.value.failed = failedValue treeFilter.value.success = successValue treeFilter.value.skipped = skippedValue + treeFilter.value.slow = slowValue treeFilter.value.onlyTests = onlyTestsValue treeFilter.value.project = projectValue treeFilter.value.projectSort = projectSortValue === 'default' ? undefined : projectSortValue @@ -117,16 +120,18 @@ export function useSearch( filter.failed, filter.success, filter.skipped, + filter.slow, filter.onlyTests, currentProject.value, projectSort.value, ] as const, - ([search, failed, success, skipped, onlyTests, project, projectSort]) => { + ([search, failed, success, skipped, slow, onlyTests, project, projectSort]) => { updateFilterStorage( search, failed, success, skipped, + slow, onlyTests, project, projectSort, diff --git a/packages/ui/client/composables/explorer/state.ts b/packages/ui/client/composables/explorer/state.ts index 905103d5f757..58cc3ec3daf1 100644 --- a/packages/ui/client/composables/explorer/state.ts +++ b/packages/ui/client/composables/explorer/state.ts @@ -24,6 +24,7 @@ export const treeFilter = useLocalStorage( failed: false, success: false, skipped: false, + slow: false, onlyTests: false, search: '', project: ALL_PROJECTS, @@ -92,6 +93,7 @@ export const filter = reactive({ failed: treeFilter.value.failed, success: treeFilter.value.success, skipped: treeFilter.value.skipped, + slow: treeFilter.value.slow, onlyTests: treeFilter.value.onlyTests, }) export const isFilteredByStatus = computed(() => { @@ -107,6 +109,10 @@ export const isFilteredByStatus = computed(() => { return true } + if (filter.slow) { + return true + } + return false }) export const filteredFiles = shallowRef([]) diff --git a/packages/ui/client/composables/explorer/tree.ts b/packages/ui/client/composables/explorer/tree.ts index de6f57db5134..31fd38c6f3b0 100644 --- a/packages/ui/client/composables/explorer/tree.ts +++ b/packages/ui/client/composables/explorer/tree.ts @@ -73,6 +73,7 @@ export class ExplorerTree { failed: filter.failed, success: filter.success, skipped: filter.skipped, + slow: filter.slow, onlyTests: filter.onlyTests, }, ) @@ -127,6 +128,7 @@ export class ExplorerTree { failed: filter.failed, success: filter.success, skipped: filter.skipped, + slow: filter.slow, onlyTests: filter.onlyTests, }, end ? this.executionTime : performance.now() - this.startTime, @@ -143,6 +145,7 @@ export class ExplorerTree { failed: filter.failed, success: filter.success, skipped: filter.skipped, + slow: filter.slow, onlyTests: filter.onlyTests, }, end ? this.executionTime : performance.now() - this.startTime, @@ -160,6 +163,7 @@ export class ExplorerTree { failed: filter.failed, success: filter.success, skipped: filter.skipped, + slow: filter.slow, onlyTests: filter.onlyTests, }) } @@ -176,6 +180,7 @@ export class ExplorerTree { failed: filter.failed, success: filter.success, skipped: filter.skipped, + slow: filter.slow, onlyTests: filter.onlyTests, }) }) @@ -193,6 +198,7 @@ export class ExplorerTree { failed: filter.failed, success: filter.success, skipped: filter.skipped, + slow: filter.slow, onlyTests: filter.onlyTests, }) }) @@ -204,6 +210,7 @@ export class ExplorerTree { failed: filter.failed, success: filter.success, skipped: filter.skipped, + slow: filter.slow, onlyTests: filter.onlyTests, }) }) diff --git a/packages/ui/client/composables/explorer/types.ts b/packages/ui/client/composables/explorer/types.ts index 814dfb0f9a42..d6fc386ebe3b 100644 --- a/packages/ui/client/composables/explorer/types.ts +++ b/packages/ui/client/composables/explorer/types.ts @@ -40,6 +40,7 @@ export interface UITaskTreeNode extends TaskTreeNode { indent: number state?: TaskState duration?: number + slow?: boolean } export interface TestTreeNode extends UITaskTreeNode { @@ -73,6 +74,7 @@ export interface Filter { failed: boolean success: boolean skipped: boolean + slow: boolean onlyTests: boolean } diff --git a/packages/ui/client/composables/explorer/utils.ts b/packages/ui/client/composables/explorer/utils.ts index ead22543da25..1e4cfb5a011d 100644 --- a/packages/ui/client/composables/explorer/utils.ts +++ b/packages/ui/client/composables/explorer/utils.ts @@ -8,7 +8,7 @@ import type { UITaskTreeNode, } from '~/composables/explorer/types' import { isTestCase } from '@vitest/runner/utils' -import { client } from '~/composables/client' +import { client, config } from '~/composables/client' import { explorerTree } from '~/composables/explorer/index' import { openedTreeItemsSet } from '~/composables/explorer/state' import { getBadgeNameColor, isSuite as isTaskSuite } from '~/utils/task' @@ -33,6 +33,20 @@ export function isParentNode(node: UITaskTreeNode): node is FileTreeNode | Suite return node.type === 'file' || node.type === 'suite' } +export function isSlowTestTask(task: Task) { + if (task.type !== 'test') { + return false + } + + const duration = task.result?.duration + if (typeof duration !== 'number') { + return false + } + + const treshold = config.value.slowTestThreshold + return typeof treshold === 'number' && duration > treshold +} + export function getSortedRootTasks(sort: SortUIType, tasks = explorerTree.root.tasks) { const sorted = [...tasks] @@ -73,6 +87,7 @@ export function createOrUpdateFileNode( fileNode.state = file.result?.state fileNode.mode = file.mode fileNode.duration = typeof file.result?.duration === 'number' ? Math.round(file.result.duration) : undefined + fileNode.slow = false fileNode.collectDuration = file.collectDuration fileNode.setupDuration = file.setupDuration fileNode.environmentLoad = file.environmentLoad @@ -93,6 +108,7 @@ export function createOrUpdateFileNode( typecheck: !!file.meta && 'typecheck' in file.meta, indent: 0, duration: typeof file.result?.duration === 'number' ? Math.round(file.result.duration) : undefined, + slow: false, filepath: file.filepath, projectName: file.projectName || '', projectNameColor: explorerTree.colors.get(file.projectName || '') || getBadgeNameColor(file.projectName), @@ -169,6 +185,7 @@ export function createOrUpdateNode( taskNode.name = task.name taskNode.mode = task.mode taskNode.duration = duration + taskNode.slow = isSlowTestTask(task) taskNode.state = task.result?.state } else { @@ -184,6 +201,7 @@ export function createOrUpdateNode( expanded: false, indent: node.indent + 1, duration, + slow: isSlowTestTask(task), state: task.result?.state, } as TestTreeNode } @@ -202,6 +220,7 @@ export function createOrUpdateNode( tasks: [], indent: node.indent + 1, duration, + slow: false, state: task.result?.state, } as SuiteTreeNode } diff --git a/packages/vitest/src/node/config/serializeConfig.ts b/packages/vitest/src/node/config/serializeConfig.ts index a40f289ec7db..7be23a3cb071 100644 --- a/packages/vitest/src/node/config/serializeConfig.ts +++ b/packages/vitest/src/node/config/serializeConfig.ts @@ -1,5 +1,6 @@ import type { TestProject } from '../project' import type { ApiConfig, SerializedConfig } from '../types/config' +import { configDefaults } from '../../defaults' export function serializeConfig(project: TestProject): SerializedConfig { const { config, globalConfig } = project @@ -141,5 +142,9 @@ export function serializeConfig(project: TestProject): SerializedConfig { tags: config.tags || [], tagsFilter: config.tagsFilter, strictTags: config.strictTags ?? true, + slowTestThreshold: + config.slowTestThreshold + ?? globalConfig.slowTestThreshold + ?? configDefaults.slowTestThreshold, } } diff --git a/packages/vitest/src/runtime/config.ts b/packages/vitest/src/runtime/config.ts index 521a3b2465dc..a50b5c0aa83d 100644 --- a/packages/vitest/src/runtime/config.ts +++ b/packages/vitest/src/runtime/config.ts @@ -145,6 +145,7 @@ export interface SerializedConfig { tags: TestTagDefinition[] tagsFilter: string[] | undefined strictTags: boolean + slowTestThreshold: number | undefined } export interface SerializedCoverageConfig { diff --git a/test/config/test/pool.test.ts b/test/config/test/pool.test.ts index f3f69dd04d2c..9a50dab9ceeb 100644 --- a/test/config/test/pool.test.ts +++ b/test/config/test/pool.test.ts @@ -53,6 +53,12 @@ test('project level pool options overwrites top-level', async () => { expect(config.maxWorkers).toBe(1) }) +test('serialized config includes slowTestThreshold', async () => { + const config = await getConfig({}) + + expect(config.slowTestThreshold).toBe(300) +}) + test('isolated single worker pool receives single testfile at once', async () => { const files = await getConfig({ maxWorkers: 1, From dfd2f90dbdaf6a3b8a3c1e861843a7708da89dbe Mon Sep 17 00:00:00 2001 From: DerYeger Date: Sat, 21 Feb 2026 07:25:04 +0100 Subject: [PATCH 02/13] feat(ui): show number of slow tests in dashboard --- .../client/components/dashboard/TestsEntry.vue | 18 +++++++++++++++++- .../client/composables/explorer/collector.ts | 10 ++++++++++ .../ui/client/composables/explorer/tree.ts | 1 + .../ui/client/composables/explorer/types.ts | 2 ++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/ui/client/components/dashboard/TestsEntry.vue b/packages/ui/client/components/dashboard/TestsEntry.vue index 7be0fe7bcc90..3be307ccd891 100644 --- a/packages/ui/client/components/dashboard/TestsEntry.vue +++ b/packages/ui/client/components/dashboard/TestsEntry.vue @@ -3,11 +3,12 @@ import { explorerTree } from '~/composables/explorer' import { filter } from '~/composables/explorer/state' import DashboardEntry from './DashboardEntry.vue' -function toggleFilter(type: 'success' | 'failed' | 'skipped' | 'total') { +function toggleFilter(type: 'success' | 'failed' | 'skipped' | 'slow' | 'total') { // Reset all filters first filter.success = false filter.failed = false filter.skipped = false + filter.slow = false if (type === 'total') { return @@ -74,6 +75,21 @@ function toggleFilter(type: 'success' | 'failed' | 'skipped' | 'total') { {{ explorerTree.summary.testsSkipped }} + + + + true, filter?: F ignored: 0, todo: 0, expectedFail: 0, + slow: 0, } satisfies CollectFilteredTests for (const t of testsCollector(file)) { if (!filter || testMatcher(t, search, filter)) { data.total++ + if (isSlowTestTask(t)) { + data.slow++ + } if (t.result?.state === 'fail') { data.failed++ } diff --git a/packages/ui/client/composables/explorer/tree.ts b/packages/ui/client/composables/explorer/tree.ts index 31fd38c6f3b0..e1d5336f063d 100644 --- a/packages/ui/client/composables/explorer/tree.ts +++ b/packages/ui/client/composables/explorer/tree.ts @@ -51,6 +51,7 @@ export class ExplorerTree { testsSkipped: 0, testsTodo: 0, testsExpectedFail: 0, + testsSlow: 0, totalTests: 0, failedSnapshot: false, failedSnapshotEnabled: false, diff --git a/packages/ui/client/composables/explorer/types.ts b/packages/ui/client/composables/explorer/types.ts index d6fc386ebe3b..a51056f3853e 100644 --- a/packages/ui/client/composables/explorer/types.ts +++ b/packages/ui/client/composables/explorer/types.ts @@ -18,6 +18,7 @@ export interface CollectFilteredTests extends FilteredTests { ignored: number todo: number expectedFail: number + slow: number } export interface TaskTreeNode { @@ -111,6 +112,7 @@ export interface CollectorInfo { testsSkipped: number testsTodo: number testsExpectedFail: number + testsSlow: number totalTests: number failedSnapshot: boolean failedSnapshotEnabled: boolean From 59af918051bb58d2432d6d267c82810585b3df07 Mon Sep 17 00:00:00 2001 From: userquin Date: Sat, 21 Feb 2026 14:58:06 +0100 Subject: [PATCH 03/13] chore: fix ui PW tests --- test/ui/test/html-report.spec.ts | 2 +- test/ui/test/ui.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/ui/test/html-report.spec.ts b/test/ui/test/html-report.spec.ts index f215c46963f0..4d5d34bab683 100644 --- a/test/ui/test/html-report.spec.ts +++ b/test/ui/test/html-report.spec.ts @@ -52,7 +52,7 @@ test.describe('html report', () => { await page.goto(pageUrl) // dashboard - await expect(page.locator('[aria-labelledby=tests]')).toContainText('16 Pass 2 Fail 18 Total') + await expect(page.locator('[aria-labelledby=tests]')).toContainText(/(16 Pass | 2 Fail | 18 Total)/) // unhandled errors await expect(page.getByTestId('unhandled-errors')).toContainText( diff --git a/test/ui/test/ui.spec.ts b/test/ui/test/ui.spec.ts index a741e60359a1..0fe5154bdcd2 100644 --- a/test/ui/test/ui.spec.ts +++ b/test/ui/test/ui.spec.ts @@ -71,7 +71,7 @@ test.describe('ui', () => { await page.goto(pageUrl) // dashboard - await expect(page.locator('[aria-labelledby=tests]')).toContainText('16 Pass 2 Fail 18 Total') + await expect(page.locator('[aria-labelledby=tests]')).toContainText(/(16 Pass | 2 Fail | 18 Total)/) // unhandled errors await expect(page.getByTestId('unhandled-errors')).toContainText( From e55e73295525c031194b897cb1214c21678baf1e Mon Sep 17 00:00:00 2001 From: userquin Date: Sat, 21 Feb 2026 14:58:23 +0100 Subject: [PATCH 04/13] chore: fix color contrast on light theme --- packages/ui/client/components/ProgressBar.vue | 6 +++--- packages/ui/client/components/StatusIcon.vue | 8 ++++---- .../components/dashboard/TestFilesEntry.vue | 6 +++--- .../client/components/dashboard/TestsEntry.vue | 6 +++--- .../ui/client/components/explorer/Explorer.vue | 18 +++++++++++++----- .../components/explorer/ExplorerItem.vue | 8 ++++---- .../ui/client/components/views/ViewEditor.vue | 4 ++-- .../components/views/ViewReportError.vue | 2 +- 8 files changed, 33 insertions(+), 25 deletions(-) diff --git a/packages/ui/client/components/ProgressBar.vue b/packages/ui/client/components/ProgressBar.vue index ecf2c087933a..92468c25bb98 100644 --- a/packages/ui/client/components/ProgressBar.vue +++ b/packages/ui/client/components/ProgressBar.vue @@ -50,7 +50,7 @@ const widthPending = computed(() => { absolute l-0 t-0 - bg-red5 + bg-red-700 dark:bg-red-500 h-3px :class="classes" :style="`width: ${widthFailed}px;`" @@ -61,7 +61,7 @@ const widthPending = computed(() => { absolute l-0 t-0 - bg-green5 + bg-green-700 dark:bg-green-500 h-3px :class="classes" :style="`left: ${widthFailed}px; width: ${widthPass}px;`" @@ -72,7 +72,7 @@ const widthPending = computed(() => { absolute l-0 t-0 - bg-yellow5 + bg-yellow-700 dark:bg-yellow-500 h-3px :class="classes" :style="`left: ${widthPass + widthFailed}px; width: ${widthPending}px;`" diff --git a/packages/ui/client/components/StatusIcon.vue b/packages/ui/client/components/StatusIcon.vue index 44fd3057a8b1..c7adbc621be9 100644 --- a/packages/ui/client/components/StatusIcon.vue +++ b/packages/ui/client/components/StatusIcon.vue @@ -11,20 +11,20 @@ defineProps<{ diff --git a/packages/ui/client/components/dashboard/TestFilesEntry.vue b/packages/ui/client/components/dashboard/TestFilesEntry.vue index b346f52b250a..c923349d687e 100644 --- a/packages/ui/client/components/dashboard/TestFilesEntry.vue +++ b/packages/ui/client/components/dashboard/TestFilesEntry.vue @@ -35,7 +35,7 @@ import ErrorEntry from './ErrorEntry.vue'
Fail
-
+
{{ explorerTree.summary.filesFailed }}
@@ -45,7 +45,7 @@ import ErrorEntry from './ErrorEntry.vue'
Snapshot Fail
-
+
{{ explorerTree.summary.filesSnapshotFailed }}
@@ -55,7 +55,7 @@ import ErrorEntry from './ErrorEntry.vue'
Errors
-
+
{{ unhandledErrors.length }}
diff --git a/packages/ui/client/components/dashboard/TestsEntry.vue b/packages/ui/client/components/dashboard/TestsEntry.vue index 3be307ccd891..63413afb7503 100644 --- a/packages/ui/client/components/dashboard/TestsEntry.vue +++ b/packages/ui/client/components/dashboard/TestsEntry.vue @@ -21,7 +21,7 @@ function toggleFilter(type: 'success' | 'failed' | 'skipped' | 'slow' | 'total')