import type { Arrayable } from '@vitest/utils' import type { RunnerTestFile as File, RunnerTask as Task, RunnerTaskResultPack as TaskResultPack, RunnerTestCase as Test, TestArtifact, } from 'vitest' import type { CollectFilteredTests, CollectorInfo, Filter, FilteredTests, SearchMatcher, } from '~/composables/explorer/types' import { toArray } from '@vitest/utils/helpers' import { client, findById } from '~/composables/client' import { testRunState } from '~/composables/client/state' import { expandNodesOnEndRun } from '~/composables/explorer/expand' import { runFilter, testMatcher } from '~/composables/explorer/filter' import { explorerTree } from '~/composables/explorer/index' import { initialized, openedTreeItems, treeFilter, uiEntries, uiFiles, } from '~/composables/explorer/state' import { createOrUpdateFileNode, createOrUpdateNodeTask, createOrUpdateSuiteTask, isRunningTestNode, isSlowTestTask, } from '~/composables/explorer/utils' import { isSuite } from '~/utils/task' import { hasFailedSnapshot } from '../../../../vitest/src/utils/tasks' export { hasFailedSnapshot } export function runLoadFiles( remoteFiles: File[], collect: boolean, search: SearchMatcher, filter: Filter, ) { remoteFiles .map((f) => [`${f.filepath}:${f.projectName || ''}`, f] as const) .sort(([a], [b]) => a.localeCompare(b)) .map(([, f]) => createOrUpdateFileNode(f, collect)) uiFiles.value = [...explorerTree.root.tasks] runFilter(search, { failed: filter.failed, success: filter.success, skipped: filter.skipped, slow: filter.slow, onlyTests: filter.onlyTests, }) } export function preparePendingTasks(packs: TaskResultPack[]) { const pending = explorerTree.pendingTasks const idMap = client.state.idMap for (const pack of packs) { const result = pack[1] if (result) { const task = idMap.get(pack[0]) if (task) { let file = pending.get(task.file.id) if (!file) { file = new Set() pending.set(task.file.id, file) } file.add(task.id) } } } } export function recordTestArtifact(id: string, artifact: TestArtifact) { const pending = explorerTree.pendingTasks const idMap = client.state.idMap const test = idMap.get(id) if (test?.type === 'test') { let file = pending.get(test.file.id) if (!file) { file = new Set() pending.set(test.file.id, file) } file.add(test.id) if (artifact.type === 'internal:annotation') { test.annotations.push(artifact.annotation) } else { test.artifacts.push(artifact) } } } export function runCollect( start: boolean, end: boolean, summary: CollectorInfo, search: SearchMatcher, filter: Filter, executionTime: number, ) { if (start) { resetCollectorInfo(summary) } const collect = !start if (end) { traverseFiles(collect) } else { traverseReceivedFiles(collect) } collectData(summary, executionTime) if (end) { summary.failedSnapshot = uiFiles.value && hasFailedSnapshot(uiFiles.value.map((f) => findById(f.id)!)) summary.failedSnapshotEnabled = true } doRunFilter(search, filter, end) } function* collectRunningTodoTests() { yield* uiEntries.value.filter(isRunningTestNode) } function updateRunningTodoTests() { const idMap = client.state.idMap let task: Task | undefined for (const test of collectRunningTodoTests()) { // lookup the parent task = idMap.get(test.parentId) if (task && isSuite(task) && task.mode === 'todo') { task = idMap.get(test.id) if (task) { task.mode = 'todo' } } } } function traverseFiles(collect: boolean) { // add missing files: now we have only files with running tests on the initial ws open event const files = client.state.getFiles() const currentFiles = explorerTree.nodes const missingFiles = files.filter((f) => !currentFiles.has(f.id)) for (let i = 0; i < missingFiles.length; i++) { createOrUpdateFileNode(missingFiles[i], collect) createOrUpdateEntry(missingFiles[i].tasks) } // update pending tasks const rootTasks = explorerTree.root.tasks // collect remote children for (let i = 0; i < rootTasks.length; i++) { const fileNode = rootTasks[i] const file = findById(fileNode.id) if (!file) { continue } createOrUpdateFileNode(file, collect) const tasks = file.tasks if (!tasks?.length) { continue } createOrUpdateEntry(file.tasks) } } function traverseReceivedFiles(collect: boolean) { const updatedFiles = new Map(explorerTree.pendingTasks.entries()) explorerTree.pendingTasks.clear() // add missing files: now we have only files with running tests on the initial ws open event const currentFiles = explorerTree.nodes const missingFiles = Array.from(updatedFiles.keys()) .filter((id) => !currentFiles.has(id)) .map((id) => findById(id)) .filter(Boolean) as File[] let newFile: File for (let i = 0; i < missingFiles.length; i++) { newFile = missingFiles[i] createOrUpdateFileNode(newFile, false) createOrUpdateEntry(newFile.tasks) // remove the file from the updated files updatedFiles.delete(newFile.id) } // collect remote children const idMap = client.state.idMap const rootTasks = explorerTree.root.tasks for (let i = 0; i < rootTasks.length; i++) { const fileNode = rootTasks[i] const file = findById(fileNode.id) if (!file) { continue } const entries = updatedFiles.get(file.id) if (!entries) { continue } createOrUpdateFileNode(file, collect) createOrUpdateEntry(Array.from(entries, (id) => idMap.get(id)).filter(Boolean) as Task[]) } } function doRunFilter(search: SearchMatcher, filter: Filter, end = false) { const expandAll = treeFilter.value.expandAll const resetExpandAll = expandAll !== true const ids = new Set(openedTreeItems.value) const applyExpandNodes = (ids.size > 0 && expandAll === false) || resetExpandAll refreshExplorer(search, filter, end) // initialize the explorer if (!initialized.value) { if (uiEntries.value.length || end) { initialized.value = true } } if (applyExpandNodes) { // expand all nodes expandNodesOnEndRun(ids, end) if (resetExpandAll) { treeFilter.value.expandAll = false } // refresh explorer refreshExplorer(search, filter, end) } } function refreshExplorer(search: SearchMatcher, filter: Filter, end: boolean) { runFilter(search, filter) // update only at the end if (end) { updateRunningTodoTests() testRunState.value = 'idle' } } function createOrUpdateEntry(tasks: Task[]) { let task: Task for (let i = 0; i < tasks.length; i++) { task = tasks[i] if (isSuite(task)) { createOrUpdateSuiteTask(task.id, true) } else { createOrUpdateNodeTask(task.id) } } } function resetCollectorInfo(summary: CollectorInfo) { summary.files = 0 summary.time = '' summary.filesFailed = 0 summary.filesSuccess = 0 summary.filesIgnore = 0 summary.filesRunning = 0 summary.filesSkipped = 0 summary.filesTodo = 0 summary.testsFailed = 0 summary.testsSuccess = 0 summary.testsIgnore = 0 summary.testsSkipped = 0 summary.testsTodo = 0 summary.testsExpectedFail = 0 summary.testsSlow = 0 summary.totalTests = 0 summary.failedSnapshotEnabled = false } function collectData(summary: CollectorInfo, time: number) { const idMap = client.state.idMap const filesMap = new Map( explorerTree.root.tasks.filter((f) => idMap.has(f.id)).map((f) => [f.id, f]), ) const useFiles = Array.from(filesMap.values(), (file) => [file.id, findById(file.id)] as const) const data = { files: filesMap.size, time: time > 1000 ? `${(time / 1000).toFixed(2)}s` : `${Math.round(time)}ms`, filesFailed: 0, filesSuccess: 0, filesIgnore: 0, filesRunning: 0, filesSkipped: 0, filesTodo: 0, filesSnapshotFailed: 0, testsFailed: 0, testsSuccess: 0, testsIgnore: 0, testsSkipped: 0, testsTodo: 0, testsExpectedFail: 0, testsSlow: 0, totalTests: 0, failedSnapshot: false, failedSnapshotEnabled: false, } satisfies CollectorInfo for (const [_, f] of useFiles) { if (!f) { continue } if (f.result?.state === 'fail') { data.filesFailed++ } else if (f.result?.state === 'pass') { data.filesSuccess++ } else if (f.mode === 'skip') { data.filesIgnore++ data.filesSkipped++ } else if (f.mode === 'todo') { data.filesIgnore++ data.filesTodo++ } else { data.filesRunning++ } const { failed, success, skipped, total, ignored, todo, expectedFail, slow } = collectTests(f) data.totalTests += total data.testsFailed += failed data.testsSuccess += success data.testsSkipped += skipped data.testsTodo += todo data.testsExpectedFail += expectedFail data.testsSlow += slow data.testsIgnore += ignored } summary.files = data.files summary.time = data.time summary.filesFailed = data.filesFailed summary.filesSuccess = data.filesSuccess summary.filesIgnore = data.filesIgnore summary.filesRunning = data.filesRunning summary.filesSkipped = data.filesSkipped summary.filesTodo = data.filesTodo summary.testsFailed = data.testsFailed summary.testsSuccess = data.testsSuccess summary.testsTodo = data.testsTodo summary.testsExpectedFail = data.testsExpectedFail summary.testsSlow = data.testsSlow summary.testsIgnore = data.testsIgnore summary.testsSkipped = data.testsSkipped summary.totalTests = data.totalTests } function collectTests(file: File, search: SearchMatcher = () => true, filter?: Filter) { const data = { failed: 0, success: 0, skipped: 0, running: 0, total: 0, ignored: 0, todo: 0, expectedFail: 0, slow: 0, } satisfies CollectFilteredTests for (const t of testsCollector(file)) { if (!filter || testMatcher(t, search, filter)) { data.total++ if (isSlowTestTask(t)) { data.slow++ } if (t.result?.state === 'fail') { data.failed++ } else if (t.result?.state === 'pass') { // Check if this is an expected failure if (t.fails) { data.expectedFail++ } else { data.success++ } } else if (t.mode === 'skip') { data.ignored++ data.skipped++ } else if (t.mode === 'todo') { data.ignored++ data.todo++ } } } data.running = data.total - data.failed - data.success - data.ignored - data.expectedFail return data } export function collectTestsTotalData( filtered: boolean, onlyTests: boolean, tests: File[], filesSummary: FilteredTests, search: SearchMatcher, filter: Filter, ) { if (onlyTests) { // todo: apply similar logic when filtered return tests .map((file) => collectTests(file, search, filter)) .reduce( (acc, { failed, success, ignored, running }) => { acc.failed += failed acc.success += success acc.skipped += ignored acc.running += running return acc }, { failed: 0, success: 0, skipped: 0, running: 0 }, ) } else if (filtered) { const data = { failed: 0, success: 0, skipped: 0, running: 0, } satisfies FilteredTests for (const f of tests) { if (f.result?.state === 'fail') { data.failed++ } else if (f.result?.state === 'pass') { data.success++ } else if (f.mode === 'skip' || f.mode === 'todo') { data.skipped++ } else { data.running++ } } return data } return filesSummary } function* testsCollector(suite: Arrayable): Generator { const arraySuites = toArray(suite) let s: Task for (let i = 0; i < arraySuites.length; i++) { s = arraySuites[i] if (s.type === 'test') { yield s } else { yield* testsCollector(s.tasks) } } }