@@ -222,18 +214,17 @@ useResizeObserver(() => testExplorerRef.value, ([{ contentRect }]) => {
items-center
bg-header
border="b-2 base"
- grid="~ items-center gap-x-2 rows-[auto_auto]"
- :class="filterClass"
+ flex="~ wrap gap-x-4 justify-between"
>
-
+
{{ projectName }}
-
+
-
+
{{ duration > 0 ? duration : '< 1' }}ms
@@ -276,7 +283,7 @@ const tagsBgGradient = computed(() => {
data-testid="btn-run-test"
:title="runButtonTitle"
icon="i-carbon:play-filled-alt"
- text-green5
+ text-green-700 dark:text-green-500
:disabled="config.api?.allowExec === false"
@click.prevent.stop="onRun(task)"
/>
diff --git a/packages/ui/client/components/views/ViewEditor.vue b/packages/ui/client/components/views/ViewEditor.vue
index 022d1226edbe..2d8c0fe1ee71 100644
--- a/packages/ui/client/components/views/ViewEditor.vue
+++ b/packages/ui/client/components/views/ViewEditor.vue
@@ -158,14 +158,14 @@ function createErrorElement(e: TestError) {
const div = document.createElement('div')
div.className = 'op80 flex gap-x-2 items-center'
const pre = document.createElement('pre')
- pre.className = 'c-red-600 dark:c-red-400'
+ pre.className = 'c-red-700 dark:c-red-400'
pre.textContent = `${' '.repeat(stack.column)}^ ${e.name}: ${
e?.message || ''
}`
div.appendChild(pre)
const span = document.createElement('span')
span.className
- = 'i-carbon-launch c-red-600 dark:c-red-400 hover:cursor-pointer min-w-1em min-h-1em'
+ = 'i-carbon-launch c-red-700 dark:c-red-400 hover:cursor-pointer min-w-1em min-h-1em'
span.tabIndex = 0
span.ariaLabel = 'Open in Editor'
createTooltip(
diff --git a/packages/ui/client/components/views/ViewReportError.vue b/packages/ui/client/components/views/ViewReportError.vue
index c6e1d1004361..7c23d5287f8e 100644
--- a/packages/ui/client/components/views/ViewReportError.vue
+++ b/packages/ui/client/components/views/ViewReportError.vue
@@ -53,7 +53,7 @@ function showCode(stack: ParsedStack) {
- {{ relative(stack.file) }}:{{ stack.line }}:{{ stack.column }}
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/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..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,
@@ -73,6 +74,7 @@ export class ExplorerTree {
failed: filter.failed,
success: filter.success,
skipped: filter.skipped,
+ slow: filter.slow,
onlyTests: filter.onlyTests,
},
)
@@ -127,6 +129,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 +146,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 +164,7 @@ export class ExplorerTree {
failed: filter.failed,
success: filter.success,
skipped: filter.skipped,
+ slow: filter.slow,
onlyTests: filter.onlyTests,
})
}
@@ -176,6 +181,7 @@ export class ExplorerTree {
failed: filter.failed,
success: filter.success,
skipped: filter.skipped,
+ slow: filter.slow,
onlyTests: filter.onlyTests,
})
})
@@ -193,6 +199,7 @@ export class ExplorerTree {
failed: filter.failed,
success: filter.success,
skipped: filter.skipped,
+ slow: filter.slow,
onlyTests: filter.onlyTests,
})
})
@@ -204,6 +211,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..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 {
@@ -40,6 +41,7 @@ export interface UITaskTreeNode extends TaskTreeNode {
indent: number
state?: TaskState
duration?: number
+ slow?: boolean
}
export interface TestTreeNode extends UITaskTreeNode {
@@ -73,6 +75,7 @@ export interface Filter {
failed: boolean
success: boolean
skipped: boolean
+ slow: boolean
onlyTests: boolean
}
@@ -109,6 +112,7 @@ export interface CollectorInfo {
testsSkipped: number
testsTodo: number
testsExpectedFail: number
+ testsSlow: number
totalTests: number
failedSnapshot: boolean
failedSnapshotEnabled: 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,
diff --git a/test/ui/test/html-report.spec.ts b/test/ui/test/html-report.spec.ts
index f215c46963f0..decfb3c20d3e 100644
--- a/test/ui/test/html-report.spec.ts
+++ b/test/ui/test/html-report.spec.ts
@@ -52,7 +52,9 @@ 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.getByTestId('pass-entry')).toContainText('16 Pass')
+ await expect(page.getByTestId('fail-entry')).toContainText('2 Fail')
+ await expect(page.getByTestId('total-entry')).toContainText('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..6c658945d0d0 100644
--- a/test/ui/test/ui.spec.ts
+++ b/test/ui/test/ui.spec.ts
@@ -71,7 +71,9 @@ 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.getByTestId('pass-entry')).toContainText('16 Pass')
+ await expect(page.getByTestId('fail-entry')).toContainText('2 Fail')
+ await expect(page.getByTestId('total-entry')).toContainText('18 Total')
// unhandled errors
await expect(page.getByTestId('unhandled-errors')).toContainText(