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

fix(typecheck): run both runtime and typecheck tests if typecheck.include overlaps with include #6256

Merged
merged 19 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/guide/testing-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ Vitest allows you to write tests for your types, using `expectTypeOf` or `assert

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.

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`.
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.

::: warning
Before Vitest 2.1, your `typecheck.include` overrode the `include` pattern, so your runtime tests did not actually run; they were only type-checked.

Since Vitest 2.1, if your `include` and `typecheck.include` overlap, Vitest will report type tests and runtime tests as separate entries.
:::

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

Expand Down
4 changes: 2 additions & 2 deletions packages/browser/src/client/tester/unhandled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ function catchWindowErrors(cb: (e: ErrorEvent) => void) {
const removeEventListener = window.removeEventListener.bind(window)
window.addEventListener('error', throwUnhandlerError)
window.addEventListener = function (
...args: Parameters<typeof addEventListener>
...args: [any, any, any]
) {
if (args[0] === 'error') {
userErrorListenerCount++
}
return addEventListener.apply(this, args)
}
window.removeEventListener = function (
...args: Parameters<typeof removeEventListener>
...args: [any, any, any]
) {
if (args[0] === 'error' && userErrorListenerCount) {
userErrorListenerCount--
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/src/node/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => {
name: 'vitest:browser:tests',
enforce: 'pre',
async config() {
const allTestFiles = await project.globTestFiles()
const { testFiles: allTestFiles } = await project.globTestFiles()
const browserTestFiles = allTestFiles.filter(
file => getFilePoolName(project, file) === 'browser',
)
Expand Down
4 changes: 2 additions & 2 deletions packages/browser/src/node/pool.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as nodeos from 'node:os'
import crypto from 'node:crypto'
import { relative } from 'pathe'
import type { BrowserProvider, ProcessPool, Vitest, WorkspaceProject } from 'vitest/node'
import type { BrowserProvider, ProcessPool, Vitest, WorkspaceProject, WorkspaceSpec } from 'vitest/node'
import { createDebugger } from 'vitest/node'

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

const runWorkspaceTests = async (method: 'run' | 'collect', specs: [WorkspaceProject, string][]) => {
const runWorkspaceTests = async (method: 'run' | 'collect', specs: WorkspaceSpec[]) => {
const groupedFiles = new Map<WorkspaceProject, string[]>()
for (const [project, file] of specs) {
const files = groupedFiles.get(project) || []
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/src/node/serverTester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export async function resolveTester(
const { contextId, testFile } = server.resolveTesterUrl(url.pathname)
const project = server.project
const state = server.state
const testFiles = await project.globTestFiles()
const { testFiles } = await project.globTestFiles()
// if decoded test file is "__vitest_all__" or not in the list of known files, run all tests
const tests
= testFile === '__vitest_all__'
Expand Down
5 changes: 5 additions & 0 deletions packages/ui/client/components/FileDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ const failedSnapshot = computed(() => {
return current.value && hasFailedSnapshot(current.value)
})

const isTypecheck = computed(() => {
return !!current.value?.meta?.typecheck
})

function open() {
const filePath = current.value?.filepath
if (filePath) {
Expand Down Expand Up @@ -122,6 +126,7 @@ debouncedWatch(
<div>
<div p="2" h-10 flex="~ gap-2" items-center bg-header border="b base">
<StatusIcon :state="current.result?.state" :mode="current.mode" :failed-snapshot="failedSnapshot" />
<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 />
<div
v-if="current?.file.projectName"
font-light
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/client/components/explorer/ExplorerItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,8 @@ const projectNameTextColor = computed(() => {
<div :class="opened ? 'i-carbon:chevron-down' : 'i-carbon:chevron-right op20'" op20 />
</div>
<StatusIcon :state="state" :mode="task.mode" :failed-snapshot="failedSnapshot" w-4 />
<div v-if="type === 'suite' && typecheck" class="i-logos:typescript-icon" flex-shrink-0 mr-2 />
<div flex items-end gap-2 overflow-hidden>
<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 />
<span text-sm truncate font-light>
<span v-if="type === 'file' && projectName" class="rounded-full p-1 mr-1 text-xs" :style="{ backgroundColor: projectNameColor, color: projectNameTextColor }">
{{ projectName }}
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/client/composables/explorer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ export interface ParentTreeNode extends UITaskTreeNode {
export interface SuiteTreeNode extends ParentTreeNode {
fileId: string
type: 'suite'
typecheck?: boolean
}

export interface FileTreeNode extends ParentTreeNode {
type: 'file'
filepath: string
typecheck: boolean | undefined
projectName?: string
projectNameColor: string
collectDuration?: number
Expand Down
6 changes: 2 additions & 4 deletions packages/ui/client/composables/explorer/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export function createOrUpdateFileNode(
let fileNode = explorerTree.nodes.get(file.id) as FileTreeNode | undefined

if (fileNode) {
fileNode.typecheck = !!file.meta && 'typecheck' in file.meta
fileNode.state = file.result?.state
fileNode.mode = file.mode
fileNode.duration = file.result?.duration
Expand All @@ -66,6 +67,7 @@ export function createOrUpdateFileNode(
type: 'file',
children: new Set(),
tasks: [],
typecheck: !!file.meta && 'typecheck' in file.meta,
indent: 0,
duration: file.result?.duration,
filepath: file.filepath,
Expand Down Expand Up @@ -141,9 +143,6 @@ export function createOrUpdateNode(
taskNode.mode = task.mode
taskNode.duration = task.result?.duration
taskNode.state = task.result?.state
if (isSuiteNode(taskNode)) {
taskNode.typecheck = !!task.meta && 'typecheck' in task.meta
}
}
else {
if (isAtomTest(task)) {
Expand All @@ -168,7 +167,6 @@ export function createOrUpdateNode(
parentId,
name: task.name,
mode: task.mode,
typecheck: !!task.meta && 'typecheck' in task.meta,
type: 'suite',
expandable: true,
// When the current run finish, we will expand all nodes when required, here we expand only the opened nodes
Expand Down
3 changes: 2 additions & 1 deletion packages/vitest/src/api/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,13 @@ export function setup(ctx: Vitest, _server?: ViteDevServer) {
},
async getTestFiles() {
const spec = await ctx.globTestFiles()
return spec.map(([project, file]) => [
return spec.map(([project, file, options]) => [
{
name: project.config.name,
root: project.config.root,
},
file,
options,
])
},
},
Expand Down
4 changes: 2 additions & 2 deletions packages/vitest/src/integrations/env/jsdom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ function catchWindowErrors(window: Window) {
const removeEventListener = window.removeEventListener.bind(window)
window.addEventListener('error', throwUnhandlerError)
window.addEventListener = function (
...args: Parameters<typeof addEventListener>
...args: [any, any, any]
) {
if (args[0] === 'error') {
userErrorListenerCount++
}
return addEventListener.apply(this, args)
}
window.removeEventListener = function (
...args: Parameters<typeof removeEventListener>
...args: [any, any, any]
) {
if (args[0] === 'error' && userErrorListenerCount) {
userErrorListenerCount--
Expand Down
94 changes: 72 additions & 22 deletions packages/vitest/src/node/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { WebSocketReporter } from '../api/setup'
import type { SerializedCoverageConfig } from '../runtime/config'
import type { SerializedSpec } from '../runtime/types/utils'
import type { ArgumentsType, OnServerRestartHandler, ProvidedContext, UserConsoleLog } from '../types/general'
import { createPool } from './pool'
import { createPool, getFilePoolName } from './pool'
import type { ProcessPool, WorkspaceSpec } from './pool'
import { createBenchmarkReporters, createReporters } from './reporters/utils'
import { StateManager } from './state'
Expand Down Expand Up @@ -77,10 +77,14 @@ export class Vitest {

private resolvedProjects: WorkspaceProject[] = []
public projects: WorkspaceProject[] = []
private projectsTestFiles = new Map<string, Set<WorkspaceProject>>()

public distPath!: string

private _cachedSpecs = new Map<string, WorkspaceSpec[]>()

/** @deprecated use `_cachedSpecs` */
projectTestFiles = this._cachedSpecs

constructor(
public readonly mode: VitestRunMode,
options: VitestOptions = {},
Expand All @@ -103,7 +107,7 @@ export class Vitest {
this.coverageProvider = undefined
this.runningPromise = undefined
this.distPath = undefined!
this.projectsTestFiles.clear()
this._cachedSpecs.clear()

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

Expand Down Expand Up @@ -190,6 +194,13 @@ export class Vitest {
this.getCoreWorkspaceProject().provide(key, value)
}

/**
* @deprecated internal, use `_createCoreProject` instead
*/
createCoreProject() {
return this._createCoreProject()
}

/**
* @internal
*/
Expand All @@ -202,6 +213,9 @@ export class Vitest {
return this.coreWorkspaceProject
}

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

private async getWorkspaceConfigPath() {
private async getWorkspaceConfigPath(): Promise<string | null> {
if (this.config.workspace) {
return this.config.workspace
}
Expand Down Expand Up @@ -423,8 +437,8 @@ export class Vitest {
}
}

private async getTestDependencies(filepath: WorkspaceSpec, deps = new Set<string>()) {
const addImports = async ([project, filepath]: WorkspaceSpec) => {
private async getTestDependencies([project, filepath]: WorkspaceSpec, deps = new Set<string>()) {
const addImports = async (project: WorkspaceProject, filepath: string) => {
if (deps.has(filepath)) {
return
}
Expand All @@ -440,13 +454,13 @@ export class Vitest {
const path = await project.server.pluginContainer.resolveId(dep, filepath, { ssr: true })
const fsPath = path && !path.external && path.id.split('?')[0]
if (fsPath && !fsPath.includes('node_modules') && !deps.has(fsPath) && existsSync(fsPath)) {
await addImports([project, fsPath])
await addImports(project, fsPath)
}
}))
}

await addImports(filepath)
deps.delete(filepath[1])
await addImports(project, filepath)
deps.delete(filepath)

return deps
}
Expand Down Expand Up @@ -500,12 +514,31 @@ export class Vitest {
return runningTests
}

/**
* @deprecated remove when vscode extension supports "getFileWorkspaceSpecs"
*/
getProjectsByTestFile(file: string) {
const projects = this.projectsTestFiles.get(file)
if (!projects) {
return []
return this.getFileWorkspaceSpecs(file)
}

getFileWorkspaceSpecs(file: string) {
const _cached = this._cachedSpecs.get(file)
if (_cached) {
return _cached
}

const specs: WorkspaceSpec[] = []
for (const project of this.projects) {
if (project.isTestFile(file)) {
const pool = getFilePoolName(project, file)
specs.push([project, file, { pool }])
}
if (project.isTypecheckFile(file)) {
specs.push([project, file, { pool: 'typescript' }])
}
}
return Array.from(projects).map(project => [project, file] as WorkspaceSpec)
specs.forEach(spec => this.ensureSpecCached(spec))
return specs
}

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

await this.report('onPathsCollected', filepaths)
await this.report('onSpecsCollected', specs.map(
([project, file]) =>
[{ name: project.config.name, root: project.config.root }, file] as SerializedSpec,
([project, file, options]) =>
[{
name: project.config.name,
root: project.config.root,
}, file, options] satisfies SerializedSpec,
))

// previous run
Expand Down Expand Up @@ -856,7 +892,6 @@ export class Vitest {
}))

if (matchingProjects.length > 0) {
this.projectsTestFiles.set(id, new Set(matchingProjects))
this.changedTests.add(id)
this.scheduleRerun([id])
}
Expand Down Expand Up @@ -1054,17 +1089,32 @@ export class Vitest {
public async globTestFiles(filters: string[] = []) {
const files: WorkspaceSpec[] = []
await Promise.all(this.projects.map(async (project) => {
const specs = await project.globTestFiles(filters)
specs.forEach((file) => {
files.push([project, file])
const projects = this.projectsTestFiles.get(file) || new Set()
projects.add(project)
this.projectsTestFiles.set(file, projects)
const { testFiles, typecheckTestFiles } = await project.globTestFiles(filters)
testFiles.forEach((file) => {
const pool = getFilePoolName(project, file)
const spec: WorkspaceSpec = [project, file, { pool }]
this.ensureSpecCached(spec)
files.push(spec)
})
typecheckTestFiles.forEach((file) => {
const spec: WorkspaceSpec = [project, file, { pool: 'typescript' }]
this.ensureSpecCached(spec)
files.push(spec)
})
}))
return files
}

private ensureSpecCached(spec: WorkspaceSpec) {
const file = spec[1]
const specs = this._cachedSpecs.get(file) || []
const included = specs.some(_s => _s[0] === spec[0] && _s[2].pool === spec[2].pool)
if (!included) {
specs.push(spec)
this._cachedSpecs.set(file, specs)
}
}

// The server needs to be running for communication
shouldKeepServer() {
return !!this.config?.watch
Expand Down
Loading