diff --git a/packages/ui/client/components/explorer/ExplorerItem.vue b/packages/ui/client/components/explorer/ExplorerItem.vue
index a46162a69e8b..468a81ddb1c8 100644
--- a/packages/ui/client/components/explorer/ExplorerItem.vue
+++ b/packages/ui/client/components/explorer/ExplorerItem.vue
@@ -3,7 +3,7 @@ import type { Task, TaskState } from '@vitest/runner'
import { hasFailedSnapshot } from '@vitest/ws-client'
import { Tooltip as VueTooltip } from 'floating-vue'
import { nextTick } from 'vue'
-import { client, isReport, runFiles } from '~/composables/client'
+import { client, isReport, runFiles, runTask } from '~/composables/client'
import { showSource } from '~/composables/codemirror'
import { explorerTree } from '~/composables/explorer'
import { escapeHtml, highlightRegex } from '~/composables/explorer/state'
@@ -24,6 +24,7 @@ const {
disableTaskLocation,
onItemClick,
projectNameColor,
+ state,
} = defineProps<{
taskId: string
name: string
@@ -73,7 +74,13 @@ async function onRun(task: Task) {
disableCoverage.value = true
await nextTick()
}
- await runFiles([task.file])
+
+ if (type === 'file') {
+ await runFiles([task.file])
+ }
+ else {
+ await runTask(task)
+ }
}
function updateSnapshot(task: Task) {
@@ -108,6 +115,14 @@ const gridStyles = computed(() => {
} ${gridColumns.join(' ')};`
})
+const runButtonTitle = computed(() => {
+ return type === 'file'
+ ? 'Run current file'
+ : type === 'suite'
+ ? 'Run all tests in this suite'
+ : 'Run current test'
+})
+
const escapedName = computed(() => escapeHtml(name))
const highlighted = computed(() => {
const regex = highlightRegex.value
@@ -219,12 +234,11 @@ const projectNameTextColor = computed(() => {
diff --git a/packages/ui/client/composables/client/index.ts b/packages/ui/client/composables/client/index.ts
index 73af2c07bd4c..d633533dae26 100644
--- a/packages/ui/client/composables/client/index.ts
+++ b/packages/ui/client/composables/client/index.ts
@@ -1,11 +1,12 @@
import type { WebSocketStatus } from '@vueuse/core'
-import type { File, SerializedConfig, TaskResultPack } from 'vitest'
+import type { File, SerializedConfig, Task, TaskResultPack } from 'vitest'
import type { BrowserRunnerState } from '../../../types'
import { createFileTask } from '@vitest/runner/utils'
import { createClient, getTasks } from '@vitest/ws-client'
import { reactive as reactiveVue } from 'vue'
import { explorerTree } from '~/composables/explorer'
import { isFileNode } from '~/composables/explorer/utils'
+import { isSuite as isTaskSuite } from '~/utils/task'
import { ui } from '../../composables/api'
import { ENTRY_URL, isReport } from '../../constants'
import { parseError } from '../error'
@@ -65,7 +66,21 @@ export const isConnecting = computed(() => status.value === 'CONNECTING')
export const isDisconnected = computed(() => status.value === 'CLOSED')
export function runAll() {
- return runFiles(client.state.getFiles()/* , true */)
+ return runFiles(client.state.getFiles())
+}
+
+function clearTaskResult(task: Task) {
+ delete task.result
+ const node = explorerTree.nodes.get(task.id)
+ if (node) {
+ node.state = undefined
+ node.duration = undefined
+ if (isTaskSuite(task)) {
+ for (const t of task.tasks) {
+ clearTaskResult(t)
+ }
+ }
+ }
}
function clearResults(useFiles: File[]) {
@@ -98,7 +113,15 @@ export function runFiles(useFiles: File[]) {
explorerTree.startRun()
- return client.rpc.rerun(useFiles.map(i => i.filepath))
+ return client.rpc.rerun(useFiles.map(i => i.filepath), true)
+}
+
+export function runTask(task: Task) {
+ clearTaskResult(task)
+
+ explorerTree.startRun()
+
+ return client.rpc.rerunTask(task.id)
}
export function runCurrent() {
diff --git a/packages/vitest/src/api/setup.ts b/packages/vitest/src/api/setup.ts
index f0554d62e2b4..bccf237a2d32 100644
--- a/packages/vitest/src/api/setup.ts
+++ b/packages/vitest/src/api/setup.ts
@@ -72,8 +72,11 @@ export function setup(ctx: Vitest, _server?: ViteDevServer) {
}
return fs.writeFile(id, content, 'utf-8')
},
- async rerun(files) {
- await ctx.rerunFiles(files)
+ async rerun(files, resetTestNamePattern) {
+ await ctx.rerunFiles(files, undefined, true, resetTestNamePattern)
+ },
+ async rerunTask(id) {
+ await ctx.rerunTask(id)
},
getConfig() {
return ctx.getCoreWorkspaceProject().getSerializableConfig()
diff --git a/packages/vitest/src/api/types.ts b/packages/vitest/src/api/types.ts
index 78b6314fe59f..d9c75773f699 100644
--- a/packages/vitest/src/api/types.ts
+++ b/packages/vitest/src/api/types.ts
@@ -44,7 +44,8 @@ export interface WebSocketHandlers {
) => Promise
readTestFile: (id: string) => Promise
saveTestFile: (id: string, content: string) => Promise
- rerun: (files: string[]) => Promise
+ rerun: (files: string[], resetTestNamePattern?: boolean) => Promise
+ rerunTask: (id: string) => Promise
updateSnapshot: (file?: File) => Promise
getUnhandledErrors: () => unknown[]
}
diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts
index f10eb087b8a0..ecb88fd14623 100644
--- a/packages/vitest/src/node/core.ts
+++ b/packages/vitest/src/node/core.ts
@@ -2,6 +2,7 @@ import type { CancelReason, File, TaskResultPack } from '@vitest/runner'
import type { Writable } from 'node:stream'
import type { ViteDevServer } from 'vite'
import type { defineWorkspace } from 'vitest/config'
+import type { RunnerTask, RunnerTestSuite } from '../public'
import type { SerializedCoverageConfig } from '../runtime/config'
import type { ArgumentsType, OnServerRestartHandler, OnTestsRerunHandler, ProvidedContext, UserConsoleLog } from '../types/general'
import type { ProcessPool, WorkspaceSpec } from './pool'
@@ -691,7 +692,11 @@ export class Vitest {
await Promise.all(this.projects.map(p => p.initBrowserServer()))
}
- async rerunFiles(files: string[] = this.state.getFilepaths(), trigger?: string, allTestsRun = true) {
+ async rerunFiles(files: string[] = this.state.getFilepaths(), trigger?: string, allTestsRun = true, resetTestNamePattern = false) {
+ if (resetTestNamePattern) {
+ this.configOverride.testNamePattern = undefined
+ }
+
if (this.filenamePattern) {
const filteredFiles = await this.globTestFiles([this.filenamePattern])
files = files.filter(file => filteredFiles.some(f => f[1] === file))
@@ -706,11 +711,29 @@ export class Vitest {
await this.report('onWatcherStart', this.state.getFiles(files))
}
+ private isSuite(task: RunnerTask): task is RunnerTestSuite {
+ return Object.hasOwnProperty.call(task, 'tasks')
+ }
+
+ async rerunTask(id: string) {
+ const task = this.state.idMap.get(id)
+ if (!task) {
+ throw new Error(`Task ${id} was not found`)
+ }
+ await this.changeNamePattern(
+ task.name,
+ [task.file.filepath],
+ this.isSuite(task) ? 'rerun suite' : 'rerun test',
+ )
+ }
+
async changeProjectName(pattern: string) {
if (pattern === '') {
delete this.configOverride.project
}
- else { this.configOverride.project = pattern }
+ else {
+ this.configOverride.project = pattern
+ }
this.projects = this.resolvedProjects.filter(p => p.getName() === pattern)
const files = (await this.globTestSpecs()).map(spec => spec.moduleId)
diff --git a/test/ui/test/ui.spec.ts b/test/ui/test/ui.spec.ts
index 24e461b8e577..92267f1d90f6 100644
--- a/test/ui/test/ui.spec.ts
+++ b/test/ui/test/ui.spec.ts
@@ -172,7 +172,7 @@ test.describe('standalone', () => {
// run single file
await page.getByText('fixtures/sample.test.ts').hover()
- await page.getByRole('button', { name: 'Run current test' }).click()
+ await page.getByRole('button', { name: 'Run current file' }).click()
// check results
await page.getByText('PASS (1)').click()