Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): allow run individual tests/suites from the UI #6641

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
24 changes: 19 additions & 5 deletions packages/ui/client/components/explorer/ExplorerItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -24,6 +24,7 @@ const {
disableTaskLocation,
onItemClick,
projectNameColor,
state,
} = defineProps<{
taskId: string
name: string
Expand Down Expand Up @@ -73,7 +74,13 @@ async function onRun(task: Task) {
disableCoverage.value = true
await nextTick()
}
await runFiles([task.file])

if (type === 'file') {
sheremet-va marked this conversation as resolved.
Show resolved Hide resolved
await runFiles([task.file])
}
else {
await runTask(task)
}
}

function updateSnapshot(task: Task) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -219,12 +234,11 @@ const projectNameTextColor = computed(() => {
</VueTooltip>
<IconButton
v-if="!isReport"
v-tooltip.bottom="'Run current test'"
v-tooltip.bottom="runButtonTitle"
data-testid="btn-run-test"
title="Run current test"
:title="runButtonTitle"
icon="i-carbon:play-filled-alt"
text-green5
:disabled="type !== 'file'"
@click.prevent.stop="onRun(task)"
/>
</div>
Expand Down
29 changes: 26 additions & 3 deletions packages/ui/client/composables/client/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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[]) {
Expand Down Expand Up @@ -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() {
Expand Down
7 changes: 5 additions & 2 deletions packages/vitest/src/api/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 2 additions & 1 deletion packages/vitest/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ export interface WebSocketHandlers {
) => Promise<TransformResultWithSource | undefined>
readTestFile: (id: string) => Promise<string | null>
saveTestFile: (id: string, content: string) => Promise<void>
rerun: (files: string[]) => Promise<void>
rerun: (files: string[], resetTestNamePattern?: boolean) => Promise<void>
rerunTask: (id: string) => Promise<void>
updateSnapshot: (file?: File) => Promise<void>
getUnhandledErrors: () => unknown[]
}
Expand Down
27 changes: 25 additions & 2 deletions packages/vitest/src/node/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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))
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion test/ui/test/ui.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading