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()