Skip to content

Commit 153ff01

Browse files
authored
fix(typecheck): run both runtime and typecheck tests if typecheck.include overlaps with include (#6256)
1 parent a68deed commit 153ff01

File tree

32 files changed

+337
-108
lines changed

32 files changed

+337
-108
lines changed

docs/guide/testing-types.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ Vitest allows you to write tests for your types, using `expectTypeOf` or `assert
1414

1515
Under the hood Vitest calls `tsc` or `vue-tsc`, depending on your config, and parses results. Vitest will also print out type errors in your source code, if it finds any. You can disable it with [`typecheck.ignoreSourceErrors`](/config/#typecheck-ignoresourceerrors) config option.
1616

17-
Keep in mind that Vitest doesn't run or compile these files, they are only statically analyzed by the compiler, and because of that you cannot use any dynamic statements. Meaning, you cannot use dynamic test names, and `test.each`, `test.runIf`, `test.skipIf`, `test.concurrent` APIs. But you can use other APIs, like `test`, `describe`, `.only`, `.skip` and `.todo`.
17+
Keep in mind that Vitest doesn't run these files, they are only statically analyzed by the compiler. Meaning, that if you use a dynamic name or `test.each` or `test.for`, the test name will not be evaluated - it will be displayed as is.
18+
19+
::: warning
20+
Before Vitest 2.1, your `typecheck.include` overrode the `include` pattern, so your runtime tests did not actually run; they were only type-checked.
21+
22+
Since Vitest 2.1, if your `include` and `typecheck.include` overlap, Vitest will report type tests and runtime tests as separate entries.
23+
:::
1824

1925
Using CLI flags, like `--allowOnly` and `-t` are also supported for type checking.
2026

packages/browser/src/node/plugin.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => {
157157
name: 'vitest:browser:tests',
158158
enforce: 'pre',
159159
async config() {
160-
const allTestFiles = await project.globTestFiles()
160+
const { testFiles: allTestFiles } = await project.globTestFiles()
161161
const browserTestFiles = allTestFiles.filter(
162162
file => getFilePoolName(project, file) === 'browser',
163163
)

packages/browser/src/node/pool.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as nodeos from 'node:os'
22
import crypto from 'node:crypto'
33
import { relative } from 'pathe'
4-
import type { BrowserProvider, ProcessPool, Vitest, WorkspaceProject } from 'vitest/node'
4+
import type { BrowserProvider, ProcessPool, Vitest, WorkspaceProject, WorkspaceSpec } from 'vitest/node'
55
import { createDebugger } from 'vitest/node'
66

77
const debug = createDebugger('vitest:browser:pool')
@@ -92,7 +92,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool {
9292
await Promise.all(promises)
9393
}
9494

95-
const runWorkspaceTests = async (method: 'run' | 'collect', specs: [WorkspaceProject, string][]) => {
95+
const runWorkspaceTests = async (method: 'run' | 'collect', specs: WorkspaceSpec[]) => {
9696
const groupedFiles = new Map<WorkspaceProject, string[]>()
9797
for (const [project, file] of specs) {
9898
const files = groupedFiles.get(project) || []

packages/browser/src/node/serverTester.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export async function resolveTester(
2222
const { contextId, testFile } = server.resolveTesterUrl(url.pathname)
2323
const project = server.project
2424
const state = server.state
25-
const testFiles = await project.globTestFiles()
25+
const { testFiles } = await project.globTestFiles()
2626
// if decoded test file is "__vitest_all__" or not in the list of known files, run all tests
2727
const tests
2828
= testFile === '__vitest_all__'

packages/ui/client/components/FileDetails.vue

+5
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ const failedSnapshot = computed(() => {
3535
return current.value && hasFailedSnapshot(current.value)
3636
})
3737
38+
const isTypecheck = computed(() => {
39+
return !!current.value?.meta?.typecheck
40+
})
41+
3842
function open() {
3943
const filePath = current.value?.filepath
4044
if (filePath) {
@@ -122,6 +126,7 @@ debouncedWatch(
122126
<div>
123127
<div p="2" h-10 flex="~ gap-2" items-center bg-header border="b base">
124128
<StatusIcon :state="current.result?.state" :mode="current.mode" :failed-snapshot="failedSnapshot" />
129+
<div v-if="isTypecheck" v-tooltip.bottom="'This is a typecheck test. It won\'t report results of the runtime tests'" class="i-logos:typescript-icon" flex-shrink-0 />
125130
<div
126131
v-if="current?.file.projectName"
127132
font-light

packages/ui/client/components/explorer/ExplorerItem.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,8 @@ const projectNameTextColor = computed(() => {
173173
<div :class="opened ? 'i-carbon:chevron-down' : 'i-carbon:chevron-right op20'" op20 />
174174
</div>
175175
<StatusIcon :state="state" :mode="task.mode" :failed-snapshot="failedSnapshot" w-4 />
176-
<div v-if="type === 'suite' && typecheck" class="i-logos:typescript-icon" flex-shrink-0 mr-2 />
177176
<div flex items-end gap-2 overflow-hidden>
177+
<div v-if="type === 'file' && typecheck" v-tooltip.bottom="'This is a typecheck test. It won\'t report results of the runtime tests'" class="i-logos:typescript-icon" flex-shrink-0 />
178178
<span text-sm truncate font-light>
179179
<span v-if="type === 'file' && projectName" class="rounded-full p-1 mr-1 text-xs" :style="{ backgroundColor: projectNameColor, color: projectNameTextColor }">
180180
{{ projectName }}

packages/ui/client/composables/explorer/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,12 @@ export interface ParentTreeNode extends UITaskTreeNode {
5555
export interface SuiteTreeNode extends ParentTreeNode {
5656
fileId: string
5757
type: 'suite'
58-
typecheck?: boolean
5958
}
6059

6160
export interface FileTreeNode extends ParentTreeNode {
6261
type: 'file'
6362
filepath: string
63+
typecheck: boolean | undefined
6464
projectName?: string
6565
projectNameColor: string
6666
collectDuration?: number

packages/ui/client/composables/explorer/utils.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export function createOrUpdateFileNode(
4646
let fileNode = explorerTree.nodes.get(file.id) as FileTreeNode | undefined
4747

4848
if (fileNode) {
49+
fileNode.typecheck = !!file.meta && 'typecheck' in file.meta
4950
fileNode.state = file.result?.state
5051
fileNode.mode = file.mode
5152
fileNode.duration = file.result?.duration
@@ -66,6 +67,7 @@ export function createOrUpdateFileNode(
6667
type: 'file',
6768
children: new Set(),
6869
tasks: [],
70+
typecheck: !!file.meta && 'typecheck' in file.meta,
6971
indent: 0,
7072
duration: file.result?.duration,
7173
filepath: file.filepath,
@@ -141,9 +143,6 @@ export function createOrUpdateNode(
141143
taskNode.mode = task.mode
142144
taskNode.duration = task.result?.duration
143145
taskNode.state = task.result?.state
144-
if (isSuiteNode(taskNode)) {
145-
taskNode.typecheck = !!task.meta && 'typecheck' in task.meta
146-
}
147146
}
148147
else {
149148
if (isAtomTest(task)) {
@@ -168,7 +167,6 @@ export function createOrUpdateNode(
168167
parentId,
169168
name: task.name,
170169
mode: task.mode,
171-
typecheck: !!task.meta && 'typecheck' in task.meta,
172170
type: 'suite',
173171
expandable: true,
174172
// When the current run finish, we will expand all nodes when required, here we expand only the opened nodes

packages/vitest/src/api/setup.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,13 @@ export function setup(ctx: Vitest, _server?: ViteDevServer) {
103103
},
104104
async getTestFiles() {
105105
const spec = await ctx.globTestFiles()
106-
return spec.map(([project, file]) => [
106+
return spec.map(([project, file, options]) => [
107107
{
108108
name: project.config.name,
109109
root: project.config.root,
110110
},
111111
file,
112+
options,
112113
])
113114
},
114115
},

packages/vitest/src/node/core.ts

+72-22
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { WebSocketReporter } from '../api/setup'
1717
import type { SerializedCoverageConfig } from '../runtime/config'
1818
import type { SerializedSpec } from '../runtime/types/utils'
1919
import type { ArgumentsType, OnServerRestartHandler, ProvidedContext, UserConsoleLog } from '../types/general'
20-
import { createPool } from './pool'
20+
import { createPool, getFilePoolName } from './pool'
2121
import type { ProcessPool, WorkspaceSpec } from './pool'
2222
import { createBenchmarkReporters, createReporters } from './reporters/utils'
2323
import { StateManager } from './state'
@@ -77,10 +77,14 @@ export class Vitest {
7777

7878
private resolvedProjects: WorkspaceProject[] = []
7979
public projects: WorkspaceProject[] = []
80-
private projectsTestFiles = new Map<string, Set<WorkspaceProject>>()
8180

8281
public distPath!: string
8382

83+
private _cachedSpecs = new Map<string, WorkspaceSpec[]>()
84+
85+
/** @deprecated use `_cachedSpecs` */
86+
projectTestFiles = this._cachedSpecs
87+
8488
constructor(
8589
public readonly mode: VitestRunMode,
8690
options: VitestOptions = {},
@@ -103,7 +107,7 @@ export class Vitest {
103107
this.coverageProvider = undefined
104108
this.runningPromise = undefined
105109
this.distPath = undefined!
106-
this.projectsTestFiles.clear()
110+
this._cachedSpecs.clear()
107111

108112
const resolved = resolveConfig(this.mode, options, server.config, this.logger)
109113

@@ -190,6 +194,13 @@ export class Vitest {
190194
this.getCoreWorkspaceProject().provide(key, value)
191195
}
192196

197+
/**
198+
* @deprecated internal, use `_createCoreProject` instead
199+
*/
200+
createCoreProject() {
201+
return this._createCoreProject()
202+
}
203+
193204
/**
194205
* @internal
195206
*/
@@ -202,6 +213,9 @@ export class Vitest {
202213
return this.coreWorkspaceProject
203214
}
204215

216+
/**
217+
* @deprecated use Reported Task API instead
218+
*/
205219
public getProjectByTaskId(taskId: string): WorkspaceProject {
206220
const task = this.state.idMap.get(taskId)
207221
const projectName = (task as File).projectName || task?.file?.projectName || ''
@@ -216,7 +230,7 @@ export class Vitest {
216230
|| this.projects[0]
217231
}
218232

219-
private async getWorkspaceConfigPath() {
233+
private async getWorkspaceConfigPath(): Promise<string | null> {
220234
if (this.config.workspace) {
221235
return this.config.workspace
222236
}
@@ -423,8 +437,8 @@ export class Vitest {
423437
}
424438
}
425439

426-
private async getTestDependencies(filepath: WorkspaceSpec, deps = new Set<string>()) {
427-
const addImports = async ([project, filepath]: WorkspaceSpec) => {
440+
private async getTestDependencies([project, filepath]: WorkspaceSpec, deps = new Set<string>()) {
441+
const addImports = async (project: WorkspaceProject, filepath: string) => {
428442
if (deps.has(filepath)) {
429443
return
430444
}
@@ -440,13 +454,13 @@ export class Vitest {
440454
const path = await project.server.pluginContainer.resolveId(dep, filepath, { ssr: true })
441455
const fsPath = path && !path.external && path.id.split('?')[0]
442456
if (fsPath && !fsPath.includes('node_modules') && !deps.has(fsPath) && existsSync(fsPath)) {
443-
await addImports([project, fsPath])
457+
await addImports(project, fsPath)
444458
}
445459
}))
446460
}
447461

448-
await addImports(filepath)
449-
deps.delete(filepath[1])
462+
await addImports(project, filepath)
463+
deps.delete(filepath)
450464

451465
return deps
452466
}
@@ -500,12 +514,31 @@ export class Vitest {
500514
return runningTests
501515
}
502516

517+
/**
518+
* @deprecated remove when vscode extension supports "getFileWorkspaceSpecs"
519+
*/
503520
getProjectsByTestFile(file: string) {
504-
const projects = this.projectsTestFiles.get(file)
505-
if (!projects) {
506-
return []
521+
return this.getFileWorkspaceSpecs(file)
522+
}
523+
524+
getFileWorkspaceSpecs(file: string) {
525+
const _cached = this._cachedSpecs.get(file)
526+
if (_cached) {
527+
return _cached
528+
}
529+
530+
const specs: WorkspaceSpec[] = []
531+
for (const project of this.projects) {
532+
if (project.isTestFile(file)) {
533+
const pool = getFilePoolName(project, file)
534+
specs.push([project, file, { pool }])
535+
}
536+
if (project.isTypecheckFile(file)) {
537+
specs.push([project, file, { pool: 'typescript' }])
538+
}
507539
}
508-
return Array.from(projects).map(project => [project, file] as WorkspaceSpec)
540+
specs.forEach(spec => this.ensureSpecCached(spec))
541+
return specs
509542
}
510543

511544
async initializeGlobalSetup(paths: WorkspaceSpec[]) {
@@ -538,8 +571,11 @@ export class Vitest {
538571

539572
await this.report('onPathsCollected', filepaths)
540573
await this.report('onSpecsCollected', specs.map(
541-
([project, file]) =>
542-
[{ name: project.config.name, root: project.config.root }, file] as SerializedSpec,
574+
([project, file, options]) =>
575+
[{
576+
name: project.config.name,
577+
root: project.config.root,
578+
}, file, options] satisfies SerializedSpec,
543579
))
544580

545581
// previous run
@@ -856,7 +892,6 @@ export class Vitest {
856892
}))
857893

858894
if (matchingProjects.length > 0) {
859-
this.projectsTestFiles.set(id, new Set(matchingProjects))
860895
this.changedTests.add(id)
861896
this.scheduleRerun([id])
862897
}
@@ -1054,17 +1089,32 @@ export class Vitest {
10541089
public async globTestFiles(filters: string[] = []) {
10551090
const files: WorkspaceSpec[] = []
10561091
await Promise.all(this.projects.map(async (project) => {
1057-
const specs = await project.globTestFiles(filters)
1058-
specs.forEach((file) => {
1059-
files.push([project, file])
1060-
const projects = this.projectsTestFiles.get(file) || new Set()
1061-
projects.add(project)
1062-
this.projectsTestFiles.set(file, projects)
1092+
const { testFiles, typecheckTestFiles } = await project.globTestFiles(filters)
1093+
testFiles.forEach((file) => {
1094+
const pool = getFilePoolName(project, file)
1095+
const spec: WorkspaceSpec = [project, file, { pool }]
1096+
this.ensureSpecCached(spec)
1097+
files.push(spec)
1098+
})
1099+
typecheckTestFiles.forEach((file) => {
1100+
const spec: WorkspaceSpec = [project, file, { pool: 'typescript' }]
1101+
this.ensureSpecCached(spec)
1102+
files.push(spec)
10631103
})
10641104
}))
10651105
return files
10661106
}
10671107

1108+
private ensureSpecCached(spec: WorkspaceSpec) {
1109+
const file = spec[1]
1110+
const specs = this._cachedSpecs.get(file) || []
1111+
const included = specs.some(_s => _s[0] === spec[0] && _s[2].pool === spec[2].pool)
1112+
if (!included) {
1113+
specs.push(spec)
1114+
this._cachedSpecs.set(file, specs)
1115+
}
1116+
}
1117+
10681118
// The server needs to be running for communication
10691119
shouldKeepServer() {
10701120
return !!this.config?.watch

packages/vitest/src/node/pool.ts

+4-11
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { WorkspaceProject } from './workspace'
1010
import { createTypecheckPool } from './pools/typecheck'
1111
import { createVmForksPool } from './pools/vmForks'
1212

13-
export type WorkspaceSpec = [project: WorkspaceProject, testFile: string]
13+
export type WorkspaceSpec = [project: WorkspaceProject, testFile: string, options: { pool: Pool }]
1414
export type RunWithFiles = (
1515
files: WorkspaceSpec[],
1616
invalidates?: string[]
@@ -39,14 +39,7 @@ export const builtinPools: BuiltinPool[] = [
3939
'typescript',
4040
]
4141

42-
function getDefaultPoolName(project: WorkspaceProject, file: string): Pool {
43-
if (project.config.typecheck.enabled) {
44-
for (const glob of project.config.typecheck.include) {
45-
if (mm.isMatch(file, glob, { cwd: project.config.root })) {
46-
return 'typescript'
47-
}
48-
}
49-
}
42+
function getDefaultPoolName(project: WorkspaceProject): Pool {
5043
if (project.config.browser.enabled) {
5144
return 'browser'
5245
}
@@ -64,7 +57,7 @@ export function getFilePoolName(project: WorkspaceProject, file: string) {
6457
return pool as Pool
6558
}
6659
}
67-
return getDefaultPoolName(project, file)
60+
return getDefaultPoolName(project)
6861
}
6962

7063
export function createPool(ctx: Vitest): ProcessPool {
@@ -172,7 +165,7 @@ export function createPool(ctx: Vitest): ProcessPool {
172165
}
173166

174167
for (const spec of files) {
175-
const pool = getFilePoolName(spec[0], spec[1])
168+
const { pool } = spec[2]
176169
filesByPool[pool] ??= []
177170
filesByPool[pool].push(spec)
178171
}

0 commit comments

Comments
 (0)