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
29 changes: 24 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 { nextTick } from 'vue'
import { hasFailedSnapshot } from '@vitest/ws-client'
import { Tooltip as VueTooltip } from 'floating-vue'
import { client, isReport, runFiles } from '~/composables/client'
import { client, isReport, runFiles, runTestOrSuite } from '~/composables/client'
import { coverageEnabled } from '~/composables/navigation'
import type { TaskTreeNodeType } from '~/composables/explorer/types'
import { explorerTree } from '~/composables/explorer'
Expand All @@ -24,6 +24,7 @@ const {
disableTaskLocation,
onItemClick,
projectNameColor,
state,
} = defineProps<{
taskId: string
name: string
Expand Down Expand Up @@ -68,12 +69,22 @@ function toggleOpen() {
}

async function onRun(task: Task) {
if (state === 'todo') {
sheremet-va marked this conversation as resolved.
Show resolved Hide resolved
return
}

onItemClick?.(task)
if (coverageEnabled.value) {
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 runTestOrSuite(task)
}
}

function updateSnapshot(task: Task) {
Expand Down Expand Up @@ -108,6 +119,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 +238,12 @@ 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'"
:disabled="state === 'todo'"
sheremet-va marked this conversation as resolved.
Show resolved Hide resolved
@click.prevent.stop="onRun(task)"
/>
</div>
Expand Down
33 changes: 28 additions & 5 deletions packages/ui/client/composables/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { createClient, getTasks } from '@vitest/ws-client'
import type { WebSocketStatus } from '@vueuse/core'
import type { File, SerializedConfig, TaskResultPack } from 'vitest'
import type { File, SerializedConfig, Task, TaskResultPack } from 'vitest'
import { reactive as reactiveVue } from 'vue'
import { createFileTask } from '@vitest/runner/utils'
import type { BrowserRunnerState } from '../../../types'
import { ENTRY_URL, isReport } from '../../constants'
import { parseError } from '../error'
import { activeFileId } from '../params'
import { ui } from '../../composables/api'
import { createStaticClient } from './static'
import { testRunState, unhandledErrors } from './state'
import { ui } from '~/composables/api'
import { ENTRY_URL, isReport } from '~/constants'
import { explorerTree } from '~/composables/explorer'
import { isFileNode } from '~/composables/explorer/utils'
import { isSuite as isTaskSuite } from '~/utils/task'

export { ENTRY_URL, PORT, HOST, isReport } from '../../constants'
export { ENTRY_URL, PORT, HOST, isReport } from '~/constants'

export const client = (function createVitestClient() {
if (isReport) {
Expand Down Expand Up @@ -68,6 +69,20 @@ export function runAll() {
return runFiles(client.state.getFiles()/* , true */)
}

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[]) {
const map = explorerTree.nodes
useFiles.forEach((f) => {
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 runTestOrSuite(task: Task) {
clearTaskResult(task)

explorerTree.startRun()

return client.rpc.rerunTestOrSuite(task.id, task.file.filepath)
}

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 @@ -70,8 +70,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, resetTestNamePattern)
},
async rerunTestOrSuite(id, filename) {
sheremet-va marked this conversation as resolved.
Show resolved Hide resolved
await ctx.rerunTestOrSuite(id, filename)
},
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>
rerunTestOrSuite: (id: string, filename: string) => Promise<void>
updateSnapshot: (file?: File) => Promise<void>
getUnhandledErrors: () => unknown[]
}
Expand Down
30 changes: 28 additions & 2 deletions packages/vitest/src/node/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { SnapshotManager } from '@vitest/snapshot/manager'
import type { CancelReason, File, TaskResultPack } from '@vitest/runner'
import { ViteNodeServer } from 'vite-node/server'
import type { defineWorkspace } from 'vitest/config'
import type { RunnerTask, RunnerTestSuite } from '../public'
import { version } from '../../package.json' with { type: 'json' }
import { getTasks, hasFailed, noop, slash, toArray, wildcardPatternToRegExp } from '../utils'
import { getCoverageProvider } from '../integrations/coverage'
Expand Down Expand Up @@ -676,7 +677,11 @@ export class Vitest {
await Promise.all(this._onCancelListeners.splice(0).map(listener => listener(reason)))
}

async rerunFiles(files: string[] = this.state.getFilepaths(), trigger?: string) {
async rerunFiles(files: string[] = this.state.getFilepaths(), trigger?: string, 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 @@ -688,11 +693,32 @@ 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 rerunTestOrSuite(id: string, filename: string) {
const patterns: string[] = []
const task = this.state.idMap.get(id)
let trigger = 'rerun test file'
if (task) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should throw an error if task it not found?

Copy link
Member Author

@userquin userquin Oct 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I'll ask you about this... or maybe we can rerun all files or passing the filename back again and rerun all tasks in the file

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think an error is better

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just throw an error?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, something like Task ${id} was not found

trigger = this.isSuite(task) ? 'rerun suite' : 'rerun test'
patterns.push(task.name)
}
await this.changeNamePattern(
patterns.length ? patterns.join('|') : '',
[filename],
trigger,
)
}

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