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

Merged
merged 10 commits into from
Nov 18, 2024
24 changes: 19 additions & 5 deletions packages/ui/client/components/explorer/ExplorerItem.vue
Original file line number Diff line number Diff line change
@@ -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, runTask } from '~/composables/client'
import { coverageEnabled } from '~/composables/navigation'
import type { TaskTreeNodeType } from '~/composables/explorer/types'
import { explorerTree } from '~/composables/explorer'
@@ -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') {
sheremet-va marked this conversation as resolved.
Show resolved Hide resolved
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(() => {
</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>
35 changes: 29 additions & 6 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) {
@@ -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() {
7 changes: 5 additions & 2 deletions packages/vitest/src/api/setup.ts
Original file line number Diff line number Diff line change
@@ -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 rerunTask(id) {
await ctx.rerunTask(id)
},
getConfig() {
return ctx.getCoreWorkspaceProject().getSerializableConfig()
3 changes: 2 additions & 1 deletion packages/vitest/src/api/types.ts
Original file line number Diff line number Diff line change
@@ -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[]
}
26 changes: 24 additions & 2 deletions packages/vitest/src/node/core.ts
Original file line number Diff line number Diff line change
@@ -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'
@@ -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))
@@ -688,11 +693,28 @@ 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) {
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

await this.changeNamePattern(
task.name,
[task.file.filepath],
this.isSuite(task) ? 'rerun suite' : 'rerun test',
Copy link
Member

@sheremet-va sheremet-va Oct 8, 2024

Choose a reason for hiding this comment

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

task.type === 'suite' is how you check for suite, no need for extra function

)
}
}

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)
2 changes: 1 addition & 1 deletion test/ui/test/ui.spec.ts
Original file line number Diff line number Diff line change
@@ -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()