mirror of
https://github.com/vitest-dev/vitest.git
synced 2025-12-08 18:26:03 +00:00
fix(typecheck): run both runtime and typecheck tests if typecheck.include overlaps with include (#6256)
This commit is contained in:
parent
a68deed01a
commit
153ff01b10
@ -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.
|
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.
|
Using CLI flags, like `--allowOnly` and `-t` are also supported for type checking.
|
||||||
|
|
||||||
|
|||||||
@ -157,7 +157,7 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => {
|
|||||||
name: 'vitest:browser:tests',
|
name: 'vitest:browser:tests',
|
||||||
enforce: 'pre',
|
enforce: 'pre',
|
||||||
async config() {
|
async config() {
|
||||||
const allTestFiles = await project.globTestFiles()
|
const { testFiles: allTestFiles } = await project.globTestFiles()
|
||||||
const browserTestFiles = allTestFiles.filter(
|
const browserTestFiles = allTestFiles.filter(
|
||||||
file => getFilePoolName(project, file) === 'browser',
|
file => getFilePoolName(project, file) === 'browser',
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import * as nodeos from 'node:os'
|
import * as nodeos from 'node:os'
|
||||||
import crypto from 'node:crypto'
|
import crypto from 'node:crypto'
|
||||||
import { relative } from 'pathe'
|
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'
|
import { createDebugger } from 'vitest/node'
|
||||||
|
|
||||||
const debug = createDebugger('vitest:browser:pool')
|
const debug = createDebugger('vitest:browser:pool')
|
||||||
@ -92,7 +92,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool {
|
|||||||
await Promise.all(promises)
|
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[]>()
|
const groupedFiles = new Map<WorkspaceProject, string[]>()
|
||||||
for (const [project, file] of specs) {
|
for (const [project, file] of specs) {
|
||||||
const files = groupedFiles.get(project) || []
|
const files = groupedFiles.get(project) || []
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export async function resolveTester(
|
|||||||
const { contextId, testFile } = server.resolveTesterUrl(url.pathname)
|
const { contextId, testFile } = server.resolveTesterUrl(url.pathname)
|
||||||
const project = server.project
|
const project = server.project
|
||||||
const state = server.state
|
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
|
// if decoded test file is "__vitest_all__" or not in the list of known files, run all tests
|
||||||
const tests
|
const tests
|
||||||
= testFile === '__vitest_all__'
|
= testFile === '__vitest_all__'
|
||||||
|
|||||||
@ -35,6 +35,10 @@ const failedSnapshot = computed(() => {
|
|||||||
return current.value && hasFailedSnapshot(current.value)
|
return current.value && hasFailedSnapshot(current.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isTypecheck = computed(() => {
|
||||||
|
return !!current.value?.meta?.typecheck
|
||||||
|
})
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
const filePath = current.value?.filepath
|
const filePath = current.value?.filepath
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
@ -122,6 +126,7 @@ debouncedWatch(
|
|||||||
<div>
|
<div>
|
||||||
<div p="2" h-10 flex="~ gap-2" items-center bg-header border="b base">
|
<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" />
|
<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
|
<div
|
||||||
v-if="current?.file.projectName"
|
v-if="current?.file.projectName"
|
||||||
font-light
|
font-light
|
||||||
|
|||||||
@ -173,8 +173,8 @@ const projectNameTextColor = computed(() => {
|
|||||||
<div :class="opened ? 'i-carbon:chevron-down' : 'i-carbon:chevron-right op20'" op20 />
|
<div :class="opened ? 'i-carbon:chevron-down' : 'i-carbon:chevron-right op20'" op20 />
|
||||||
</div>
|
</div>
|
||||||
<StatusIcon :state="state" :mode="task.mode" :failed-snapshot="failedSnapshot" w-4 />
|
<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 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 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 }">
|
<span v-if="type === 'file' && projectName" class="rounded-full p-1 mr-1 text-xs" :style="{ backgroundColor: projectNameColor, color: projectNameTextColor }">
|
||||||
{{ projectName }}
|
{{ projectName }}
|
||||||
|
|||||||
@ -55,12 +55,12 @@ export interface ParentTreeNode extends UITaskTreeNode {
|
|||||||
export interface SuiteTreeNode extends ParentTreeNode {
|
export interface SuiteTreeNode extends ParentTreeNode {
|
||||||
fileId: string
|
fileId: string
|
||||||
type: 'suite'
|
type: 'suite'
|
||||||
typecheck?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileTreeNode extends ParentTreeNode {
|
export interface FileTreeNode extends ParentTreeNode {
|
||||||
type: 'file'
|
type: 'file'
|
||||||
filepath: string
|
filepath: string
|
||||||
|
typecheck: boolean | undefined
|
||||||
projectName?: string
|
projectName?: string
|
||||||
projectNameColor: string
|
projectNameColor: string
|
||||||
collectDuration?: number
|
collectDuration?: number
|
||||||
|
|||||||
@ -46,6 +46,7 @@ export function createOrUpdateFileNode(
|
|||||||
let fileNode = explorerTree.nodes.get(file.id) as FileTreeNode | undefined
|
let fileNode = explorerTree.nodes.get(file.id) as FileTreeNode | undefined
|
||||||
|
|
||||||
if (fileNode) {
|
if (fileNode) {
|
||||||
|
fileNode.typecheck = !!file.meta && 'typecheck' in file.meta
|
||||||
fileNode.state = file.result?.state
|
fileNode.state = file.result?.state
|
||||||
fileNode.mode = file.mode
|
fileNode.mode = file.mode
|
||||||
fileNode.duration = file.result?.duration
|
fileNode.duration = file.result?.duration
|
||||||
@ -66,6 +67,7 @@ export function createOrUpdateFileNode(
|
|||||||
type: 'file',
|
type: 'file',
|
||||||
children: new Set(),
|
children: new Set(),
|
||||||
tasks: [],
|
tasks: [],
|
||||||
|
typecheck: !!file.meta && 'typecheck' in file.meta,
|
||||||
indent: 0,
|
indent: 0,
|
||||||
duration: file.result?.duration,
|
duration: file.result?.duration,
|
||||||
filepath: file.filepath,
|
filepath: file.filepath,
|
||||||
@ -141,9 +143,6 @@ export function createOrUpdateNode(
|
|||||||
taskNode.mode = task.mode
|
taskNode.mode = task.mode
|
||||||
taskNode.duration = task.result?.duration
|
taskNode.duration = task.result?.duration
|
||||||
taskNode.state = task.result?.state
|
taskNode.state = task.result?.state
|
||||||
if (isSuiteNode(taskNode)) {
|
|
||||||
taskNode.typecheck = !!task.meta && 'typecheck' in task.meta
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (isAtomTest(task)) {
|
if (isAtomTest(task)) {
|
||||||
@ -168,7 +167,6 @@ export function createOrUpdateNode(
|
|||||||
parentId,
|
parentId,
|
||||||
name: task.name,
|
name: task.name,
|
||||||
mode: task.mode,
|
mode: task.mode,
|
||||||
typecheck: !!task.meta && 'typecheck' in task.meta,
|
|
||||||
type: 'suite',
|
type: 'suite',
|
||||||
expandable: true,
|
expandable: true,
|
||||||
// When the current run finish, we will expand all nodes when required, here we expand only the opened nodes
|
// When the current run finish, we will expand all nodes when required, here we expand only the opened nodes
|
||||||
|
|||||||
@ -103,12 +103,13 @@ export function setup(ctx: Vitest, _server?: ViteDevServer) {
|
|||||||
},
|
},
|
||||||
async getTestFiles() {
|
async getTestFiles() {
|
||||||
const spec = await ctx.globTestFiles()
|
const spec = await ctx.globTestFiles()
|
||||||
return spec.map(([project, file]) => [
|
return spec.map(([project, file, options]) => [
|
||||||
{
|
{
|
||||||
name: project.config.name,
|
name: project.config.name,
|
||||||
root: project.config.root,
|
root: project.config.root,
|
||||||
},
|
},
|
||||||
file,
|
file,
|
||||||
|
options,
|
||||||
])
|
])
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import { WebSocketReporter } from '../api/setup'
|
|||||||
import type { SerializedCoverageConfig } from '../runtime/config'
|
import type { SerializedCoverageConfig } from '../runtime/config'
|
||||||
import type { SerializedSpec } from '../runtime/types/utils'
|
import type { SerializedSpec } from '../runtime/types/utils'
|
||||||
import type { ArgumentsType, OnServerRestartHandler, ProvidedContext, UserConsoleLog } from '../types/general'
|
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 type { ProcessPool, WorkspaceSpec } from './pool'
|
||||||
import { createBenchmarkReporters, createReporters } from './reporters/utils'
|
import { createBenchmarkReporters, createReporters } from './reporters/utils'
|
||||||
import { StateManager } from './state'
|
import { StateManager } from './state'
|
||||||
@ -77,10 +77,14 @@ export class Vitest {
|
|||||||
|
|
||||||
private resolvedProjects: WorkspaceProject[] = []
|
private resolvedProjects: WorkspaceProject[] = []
|
||||||
public projects: WorkspaceProject[] = []
|
public projects: WorkspaceProject[] = []
|
||||||
private projectsTestFiles = new Map<string, Set<WorkspaceProject>>()
|
|
||||||
|
|
||||||
public distPath!: string
|
public distPath!: string
|
||||||
|
|
||||||
|
private _cachedSpecs = new Map<string, WorkspaceSpec[]>()
|
||||||
|
|
||||||
|
/** @deprecated use `_cachedSpecs` */
|
||||||
|
projectTestFiles = this._cachedSpecs
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly mode: VitestRunMode,
|
public readonly mode: VitestRunMode,
|
||||||
options: VitestOptions = {},
|
options: VitestOptions = {},
|
||||||
@ -103,7 +107,7 @@ export class Vitest {
|
|||||||
this.coverageProvider = undefined
|
this.coverageProvider = undefined
|
||||||
this.runningPromise = undefined
|
this.runningPromise = undefined
|
||||||
this.distPath = undefined!
|
this.distPath = undefined!
|
||||||
this.projectsTestFiles.clear()
|
this._cachedSpecs.clear()
|
||||||
|
|
||||||
const resolved = resolveConfig(this.mode, options, server.config, this.logger)
|
const resolved = resolveConfig(this.mode, options, server.config, this.logger)
|
||||||
|
|
||||||
@ -190,6 +194,13 @@ export class Vitest {
|
|||||||
this.getCoreWorkspaceProject().provide(key, value)
|
this.getCoreWorkspaceProject().provide(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated internal, use `_createCoreProject` instead
|
||||||
|
*/
|
||||||
|
createCoreProject() {
|
||||||
|
return this._createCoreProject()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@ -202,6 +213,9 @@ export class Vitest {
|
|||||||
return this.coreWorkspaceProject
|
return this.coreWorkspaceProject
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use Reported Task API instead
|
||||||
|
*/
|
||||||
public getProjectByTaskId(taskId: string): WorkspaceProject {
|
public getProjectByTaskId(taskId: string): WorkspaceProject {
|
||||||
const task = this.state.idMap.get(taskId)
|
const task = this.state.idMap.get(taskId)
|
||||||
const projectName = (task as File).projectName || task?.file?.projectName || ''
|
const projectName = (task as File).projectName || task?.file?.projectName || ''
|
||||||
@ -216,7 +230,7 @@ export class Vitest {
|
|||||||
|| this.projects[0]
|
|| this.projects[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getWorkspaceConfigPath() {
|
private async getWorkspaceConfigPath(): Promise<string | null> {
|
||||||
if (this.config.workspace) {
|
if (this.config.workspace) {
|
||||||
return this.config.workspace
|
return this.config.workspace
|
||||||
}
|
}
|
||||||
@ -423,8 +437,8 @@ export class Vitest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getTestDependencies(filepath: WorkspaceSpec, deps = new Set<string>()) {
|
private async getTestDependencies([project, filepath]: WorkspaceSpec, deps = new Set<string>()) {
|
||||||
const addImports = async ([project, filepath]: WorkspaceSpec) => {
|
const addImports = async (project: WorkspaceProject, filepath: string) => {
|
||||||
if (deps.has(filepath)) {
|
if (deps.has(filepath)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -440,13 +454,13 @@ export class Vitest {
|
|||||||
const path = await project.server.pluginContainer.resolveId(dep, filepath, { ssr: true })
|
const path = await project.server.pluginContainer.resolveId(dep, filepath, { ssr: true })
|
||||||
const fsPath = path && !path.external && path.id.split('?')[0]
|
const fsPath = path && !path.external && path.id.split('?')[0]
|
||||||
if (fsPath && !fsPath.includes('node_modules') && !deps.has(fsPath) && existsSync(fsPath)) {
|
if (fsPath && !fsPath.includes('node_modules') && !deps.has(fsPath) && existsSync(fsPath)) {
|
||||||
await addImports([project, fsPath])
|
await addImports(project, fsPath)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
await addImports(filepath)
|
await addImports(project, filepath)
|
||||||
deps.delete(filepath[1])
|
deps.delete(filepath)
|
||||||
|
|
||||||
return deps
|
return deps
|
||||||
}
|
}
|
||||||
@ -500,12 +514,31 @@ export class Vitest {
|
|||||||
return runningTests
|
return runningTests
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated remove when vscode extension supports "getFileWorkspaceSpecs"
|
||||||
|
*/
|
||||||
getProjectsByTestFile(file: string) {
|
getProjectsByTestFile(file: string) {
|
||||||
const projects = this.projectsTestFiles.get(file)
|
return this.getFileWorkspaceSpecs(file)
|
||||||
if (!projects) {
|
}
|
||||||
return []
|
|
||||||
|
getFileWorkspaceSpecs(file: string) {
|
||||||
|
const _cached = this._cachedSpecs.get(file)
|
||||||
|
if (_cached) {
|
||||||
|
return _cached
|
||||||
}
|
}
|
||||||
return Array.from(projects).map(project => [project, file] as WorkspaceSpec)
|
|
||||||
|
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' }])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
specs.forEach(spec => this.ensureSpecCached(spec))
|
||||||
|
return specs
|
||||||
}
|
}
|
||||||
|
|
||||||
async initializeGlobalSetup(paths: WorkspaceSpec[]) {
|
async initializeGlobalSetup(paths: WorkspaceSpec[]) {
|
||||||
@ -538,8 +571,11 @@ export class Vitest {
|
|||||||
|
|
||||||
await this.report('onPathsCollected', filepaths)
|
await this.report('onPathsCollected', filepaths)
|
||||||
await this.report('onSpecsCollected', specs.map(
|
await this.report('onSpecsCollected', specs.map(
|
||||||
([project, file]) =>
|
([project, file, options]) =>
|
||||||
[{ name: project.config.name, root: project.config.root }, file] as SerializedSpec,
|
[{
|
||||||
|
name: project.config.name,
|
||||||
|
root: project.config.root,
|
||||||
|
}, file, options] satisfies SerializedSpec,
|
||||||
))
|
))
|
||||||
|
|
||||||
// previous run
|
// previous run
|
||||||
@ -856,7 +892,6 @@ export class Vitest {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
if (matchingProjects.length > 0) {
|
if (matchingProjects.length > 0) {
|
||||||
this.projectsTestFiles.set(id, new Set(matchingProjects))
|
|
||||||
this.changedTests.add(id)
|
this.changedTests.add(id)
|
||||||
this.scheduleRerun([id])
|
this.scheduleRerun([id])
|
||||||
}
|
}
|
||||||
@ -1054,17 +1089,32 @@ export class Vitest {
|
|||||||
public async globTestFiles(filters: string[] = []) {
|
public async globTestFiles(filters: string[] = []) {
|
||||||
const files: WorkspaceSpec[] = []
|
const files: WorkspaceSpec[] = []
|
||||||
await Promise.all(this.projects.map(async (project) => {
|
await Promise.all(this.projects.map(async (project) => {
|
||||||
const specs = await project.globTestFiles(filters)
|
const { testFiles, typecheckTestFiles } = await project.globTestFiles(filters)
|
||||||
specs.forEach((file) => {
|
testFiles.forEach((file) => {
|
||||||
files.push([project, file])
|
const pool = getFilePoolName(project, file)
|
||||||
const projects = this.projectsTestFiles.get(file) || new Set()
|
const spec: WorkspaceSpec = [project, file, { pool }]
|
||||||
projects.add(project)
|
this.ensureSpecCached(spec)
|
||||||
this.projectsTestFiles.set(file, projects)
|
files.push(spec)
|
||||||
|
})
|
||||||
|
typecheckTestFiles.forEach((file) => {
|
||||||
|
const spec: WorkspaceSpec = [project, file, { pool: 'typescript' }]
|
||||||
|
this.ensureSpecCached(spec)
|
||||||
|
files.push(spec)
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
return files
|
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
|
// The server needs to be running for communication
|
||||||
shouldKeepServer() {
|
shouldKeepServer() {
|
||||||
return !!this.config?.watch
|
return !!this.config?.watch
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import type { WorkspaceProject } from './workspace'
|
|||||||
import { createTypecheckPool } from './pools/typecheck'
|
import { createTypecheckPool } from './pools/typecheck'
|
||||||
import { createVmForksPool } from './pools/vmForks'
|
import { createVmForksPool } from './pools/vmForks'
|
||||||
|
|
||||||
export type WorkspaceSpec = [project: WorkspaceProject, testFile: string]
|
export type WorkspaceSpec = [project: WorkspaceProject, testFile: string, options: { pool: Pool }]
|
||||||
export type RunWithFiles = (
|
export type RunWithFiles = (
|
||||||
files: WorkspaceSpec[],
|
files: WorkspaceSpec[],
|
||||||
invalidates?: string[]
|
invalidates?: string[]
|
||||||
@ -39,14 +39,7 @@ export const builtinPools: BuiltinPool[] = [
|
|||||||
'typescript',
|
'typescript',
|
||||||
]
|
]
|
||||||
|
|
||||||
function getDefaultPoolName(project: WorkspaceProject, file: string): Pool {
|
function getDefaultPoolName(project: WorkspaceProject): Pool {
|
||||||
if (project.config.typecheck.enabled) {
|
|
||||||
for (const glob of project.config.typecheck.include) {
|
|
||||||
if (mm.isMatch(file, glob, { cwd: project.config.root })) {
|
|
||||||
return 'typescript'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (project.config.browser.enabled) {
|
if (project.config.browser.enabled) {
|
||||||
return 'browser'
|
return 'browser'
|
||||||
}
|
}
|
||||||
@ -64,7 +57,7 @@ export function getFilePoolName(project: WorkspaceProject, file: string) {
|
|||||||
return pool as Pool
|
return pool as Pool
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return getDefaultPoolName(project, file)
|
return getDefaultPoolName(project)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPool(ctx: Vitest): ProcessPool {
|
export function createPool(ctx: Vitest): ProcessPool {
|
||||||
@ -172,7 +165,7 @@ export function createPool(ctx: Vitest): ProcessPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const spec of files) {
|
for (const spec of files) {
|
||||||
const pool = getFilePoolName(spec[0], spec[1])
|
const { pool } = spec[2]
|
||||||
filesByPool[pool] ??= []
|
filesByPool[pool] ??= []
|
||||||
filesByPool[pool].push(spec)
|
filesByPool[pool].push(spec)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import type { WorkspaceProject } from '../workspace'
|
|||||||
|
|
||||||
export function createTypecheckPool(ctx: Vitest): ProcessPool {
|
export function createTypecheckPool(ctx: Vitest): ProcessPool {
|
||||||
const promisesMap = new WeakMap<WorkspaceProject, DeferPromise<void>>()
|
const promisesMap = new WeakMap<WorkspaceProject, DeferPromise<void>>()
|
||||||
const rerunTriggered = new WeakMap<WorkspaceProject, boolean>()
|
const rerunTriggered = new WeakSet<WorkspaceProject>()
|
||||||
|
|
||||||
async function onParseEnd(
|
async function onParseEnd(
|
||||||
project: WorkspaceProject,
|
project: WorkspaceProject,
|
||||||
@ -36,7 +36,7 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool {
|
|||||||
|
|
||||||
promisesMap.get(project)?.resolve()
|
promisesMap.get(project)?.resolve()
|
||||||
|
|
||||||
rerunTriggered.set(project, false)
|
rerunTriggered.delete(project)
|
||||||
|
|
||||||
// triggered by TSC watcher, not Vitest watcher, so we need to emulate what Vitest does in this case
|
// triggered by TSC watcher, not Vitest watcher, so we need to emulate what Vitest does in this case
|
||||||
if (ctx.config.watch && !ctx.runningPromise) {
|
if (ctx.config.watch && !ctx.runningPromise) {
|
||||||
@ -68,7 +68,7 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool {
|
|||||||
checker.onParseEnd(result => onParseEnd(project, result))
|
checker.onParseEnd(result => onParseEnd(project, result))
|
||||||
|
|
||||||
checker.onWatcherRerun(async () => {
|
checker.onWatcherRerun(async () => {
|
||||||
rerunTriggered.set(project, true)
|
rerunTriggered.add(project)
|
||||||
|
|
||||||
if (!ctx.runningPromise) {
|
if (!ctx.runningPromise) {
|
||||||
ctx.state.clearErrors()
|
ctx.state.clearErrors()
|
||||||
@ -123,7 +123,7 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool {
|
|||||||
// check that watcher actually triggered rerun
|
// check that watcher actually triggered rerun
|
||||||
const _p = new Promise<boolean>((resolve) => {
|
const _p = new Promise<boolean>((resolve) => {
|
||||||
const _i = setInterval(() => {
|
const _i = setInterval(() => {
|
||||||
if (!project.typechecker || rerunTriggered.get(project)) {
|
if (!project.typechecker || rerunTriggered.has(project)) {
|
||||||
resolve(true)
|
resolve(true)
|
||||||
clearInterval(_i)
|
clearInterval(_i)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -154,6 +154,9 @@ export abstract class BaseReporter implements Reporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let title = ` ${getStateSymbol(task)} `
|
let title = ` ${getStateSymbol(task)} `
|
||||||
|
if (task.meta.typecheck) {
|
||||||
|
title += `${c.bgBlue(c.bold(' TS '))} `
|
||||||
|
}
|
||||||
if (task.projectName) {
|
if (task.projectName) {
|
||||||
title += formatProjectName(task.projectName)
|
title += formatProjectName(task.projectName)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -124,6 +124,11 @@ function renderTree(
|
|||||||
prefix += formatProjectName(task.projectName)
|
prefix += formatProjectName(task.projectName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (level === 0 && task.type === 'suite' && task.meta.typecheck) {
|
||||||
|
prefix += c.bgBlue(c.bold(' TS '))
|
||||||
|
prefix += ' '
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
task.type === 'test'
|
task.type === 'test'
|
||||||
&& task.result?.retryCount
|
&& task.result?.retryCount
|
||||||
|
|||||||
@ -78,7 +78,16 @@ export class StateManager {
|
|||||||
.flat()
|
.flat()
|
||||||
.filter(file => file && !file.local)
|
.filter(file => file && !file.local)
|
||||||
}
|
}
|
||||||
return Array.from(this.filesMap.values()).flat().filter(file => !file.local)
|
return Array.from(this.filesMap.values()).flat().filter(file => !file.local).sort((f1, f2) => {
|
||||||
|
// print typecheck files first
|
||||||
|
if (f1.meta?.typecheck && f2.meta?.typecheck) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if (f1.meta?.typecheck) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getFilepaths(): string[] {
|
getFilepaths(): string[] {
|
||||||
@ -100,8 +109,8 @@ export class StateManager {
|
|||||||
collectFiles(project: WorkspaceProject, files: File[] = []) {
|
collectFiles(project: WorkspaceProject, files: File[] = []) {
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
const existing = this.filesMap.get(file.filepath) || []
|
const existing = this.filesMap.get(file.filepath) || []
|
||||||
const otherProject = existing.filter(
|
const otherFiles = existing.filter(
|
||||||
i => i.projectName !== file.projectName,
|
i => i.projectName !== file.projectName || i.meta.typecheck !== file.meta.typecheck,
|
||||||
)
|
)
|
||||||
const currentFile = existing.find(
|
const currentFile = existing.find(
|
||||||
i => i.projectName === file.projectName,
|
i => i.projectName === file.projectName,
|
||||||
@ -111,8 +120,8 @@ export class StateManager {
|
|||||||
if (currentFile) {
|
if (currentFile) {
|
||||||
file.logs = currentFile.logs
|
file.logs = currentFile.logs
|
||||||
}
|
}
|
||||||
otherProject.push(file)
|
otherFiles.push(file)
|
||||||
this.filesMap.set(file.filepath, otherProject)
|
this.filesMap.set(file.filepath, otherFiles)
|
||||||
this.updateId(file, project)
|
this.updateId(file, project)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { AliasOptions, DepOptimizationConfig, ServerOptions } from 'vite'
|
import type { AliasOptions, ConfigEnv, DepOptimizationConfig, ServerOptions, UserConfig as ViteUserConfig } from 'vite'
|
||||||
import type { PrettyFormatOptions } from '@vitest/pretty-format'
|
import type { PrettyFormatOptions } from '@vitest/pretty-format'
|
||||||
import type { FakeTimerInstallOpts } from '@sinonjs/fake-timers'
|
import type { FakeTimerInstallOpts } from '@sinonjs/fake-timers'
|
||||||
import type { SequenceHooks, SequenceSetupFiles } from '@vitest/runner'
|
import type { SequenceHooks, SequenceSetupFiles } from '@vitest/runner'
|
||||||
@ -1094,4 +1094,22 @@ export type ResolvedProjectConfig = Omit<
|
|||||||
NonProjectOptions
|
NonProjectOptions
|
||||||
>
|
>
|
||||||
|
|
||||||
export type { UserWorkspaceConfig } from '../../public/config'
|
export interface UserWorkspaceConfig extends ViteUserConfig {
|
||||||
|
test?: ProjectConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserProjectConfigFn = (
|
||||||
|
env: ConfigEnv
|
||||||
|
) => UserWorkspaceConfig | Promise<UserWorkspaceConfig>
|
||||||
|
export type UserProjectConfigExport =
|
||||||
|
| UserWorkspaceConfig
|
||||||
|
| Promise<UserWorkspaceConfig>
|
||||||
|
| UserProjectConfigFn
|
||||||
|
|
||||||
|
export type WorkspaceProjectConfiguration = string | (UserProjectConfigExport & {
|
||||||
|
/**
|
||||||
|
* Relative path to the extendable config. All other options will be merged with this config.
|
||||||
|
* @example '../vite.config.ts'
|
||||||
|
*/
|
||||||
|
extends?: string
|
||||||
|
})
|
||||||
|
|||||||
@ -97,6 +97,7 @@ export class WorkspaceProject {
|
|||||||
closingPromise: Promise<unknown> | undefined
|
closingPromise: Promise<unknown> | undefined
|
||||||
|
|
||||||
testFilesList: string[] | null = null
|
testFilesList: string[] | null = null
|
||||||
|
typecheckFilesList: string[] | null = null
|
||||||
|
|
||||||
public testProject!: TestProject
|
public testProject!: TestProject
|
||||||
|
|
||||||
@ -225,15 +226,24 @@ export class WorkspaceProject {
|
|||||||
? []
|
? []
|
||||||
: this.globAllTestFiles(include, exclude, includeSource, dir),
|
: this.globAllTestFiles(include, exclude, includeSource, dir),
|
||||||
typecheck.enabled
|
typecheck.enabled
|
||||||
? this.globFiles(typecheck.include, typecheck.exclude, dir)
|
? (this.typecheckFilesList || this.globFiles(typecheck.include, typecheck.exclude, dir))
|
||||||
: [],
|
: [],
|
||||||
])
|
])
|
||||||
|
|
||||||
return this.filterFiles(
|
this.typecheckFilesList = typecheckTestFiles
|
||||||
[...testFiles, ...typecheckTestFiles],
|
|
||||||
filters,
|
return {
|
||||||
dir,
|
testFiles: this.filterFiles(
|
||||||
)
|
testFiles,
|
||||||
|
filters,
|
||||||
|
dir,
|
||||||
|
),
|
||||||
|
typecheckTestFiles: this.filterFiles(
|
||||||
|
typecheckTestFiles,
|
||||||
|
filters,
|
||||||
|
dir,
|
||||||
|
),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async globAllTestFiles(
|
async globAllTestFiles(
|
||||||
@ -275,6 +285,10 @@ export class WorkspaceProject {
|
|||||||
return this.testFilesList && this.testFilesList.includes(id)
|
return this.testFilesList && this.testFilesList.includes(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isTypecheckFile(id: string) {
|
||||||
|
return this.typecheckFilesList && this.typecheckFilesList.includes(id)
|
||||||
|
}
|
||||||
|
|
||||||
async globFiles(include: string[], exclude: string[], cwd: string) {
|
async globFiles(include: string[], exclude: string[], cwd: string) {
|
||||||
const globOptions: fg.Options = {
|
const globOptions: fg.Options = {
|
||||||
dot: true,
|
dot: true,
|
||||||
|
|||||||
@ -3,9 +3,8 @@ import { isMainThread } from 'node:worker_threads'
|
|||||||
import { dirname, relative, resolve } from 'pathe'
|
import { dirname, relative, resolve } from 'pathe'
|
||||||
import { mergeConfig } from 'vite'
|
import { mergeConfig } from 'vite'
|
||||||
import fg from 'fast-glob'
|
import fg from 'fast-glob'
|
||||||
import type { UserWorkspaceConfig, WorkspaceProjectConfiguration } from '../../public/config'
|
|
||||||
import type { Vitest } from '../core'
|
import type { Vitest } from '../core'
|
||||||
import type { UserConfig } from '../types/config'
|
import type { UserConfig, UserWorkspaceConfig, WorkspaceProjectConfiguration } from '../types/config'
|
||||||
import type { WorkspaceProject } from '../workspace'
|
import type { WorkspaceProject } from '../workspace'
|
||||||
import { initializeProject } from '../workspace'
|
import { initializeProject } from '../workspace'
|
||||||
import { configFiles as defaultConfigFiles } from '../../constants'
|
import { configFiles as defaultConfigFiles } from '../../constants'
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
import '../node/types/vite'
|
import '../node/types/vite'
|
||||||
|
|
||||||
import type { ConfigEnv, UserConfig as ViteUserConfig } from 'vite'
|
import type { ConfigEnv, UserConfig as ViteUserConfig } from 'vite'
|
||||||
import type { ProjectConfig } from '../node/types/config'
|
import type { UserProjectConfigExport, UserProjectConfigFn, UserWorkspaceConfig, WorkspaceProjectConfiguration } from '../node/types/config'
|
||||||
|
|
||||||
export interface UserWorkspaceConfig extends ViteUserConfig {
|
|
||||||
test?: ProjectConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// will import vitest declare test in module 'vite'
|
// will import vitest declare test in module 'vite'
|
||||||
export {
|
export {
|
||||||
@ -20,6 +16,7 @@ export { extraInlineDeps } from '../constants'
|
|||||||
export type { Plugin } from 'vite'
|
export type { Plugin } from 'vite'
|
||||||
|
|
||||||
export type { ConfigEnv, ViteUserConfig as UserConfig }
|
export type { ConfigEnv, ViteUserConfig as UserConfig }
|
||||||
|
export type { UserProjectConfigExport, UserProjectConfigFn, UserWorkspaceConfig, WorkspaceProjectConfiguration }
|
||||||
export type UserConfigFnObject = (env: ConfigEnv) => ViteUserConfig
|
export type UserConfigFnObject = (env: ConfigEnv) => ViteUserConfig
|
||||||
export type UserConfigFnPromise = (env: ConfigEnv) => Promise<ViteUserConfig>
|
export type UserConfigFnPromise = (env: ConfigEnv) => Promise<ViteUserConfig>
|
||||||
export type UserConfigFn = (
|
export type UserConfigFn = (
|
||||||
@ -32,14 +29,6 @@ export type UserConfigExport =
|
|||||||
| UserConfigFnPromise
|
| UserConfigFnPromise
|
||||||
| UserConfigFn
|
| UserConfigFn
|
||||||
|
|
||||||
export type UserProjectConfigFn = (
|
|
||||||
env: ConfigEnv
|
|
||||||
) => UserWorkspaceConfig | Promise<UserWorkspaceConfig>
|
|
||||||
export type UserProjectConfigExport =
|
|
||||||
| UserWorkspaceConfig
|
|
||||||
| Promise<UserWorkspaceConfig>
|
|
||||||
| UserProjectConfigFn
|
|
||||||
|
|
||||||
export function defineConfig(config: ViteUserConfig): ViteUserConfig
|
export function defineConfig(config: ViteUserConfig): ViteUserConfig
|
||||||
export function defineConfig(
|
export function defineConfig(
|
||||||
config: Promise<ViteUserConfig>
|
config: Promise<ViteUserConfig>
|
||||||
@ -58,14 +47,6 @@ export function defineProject(config: UserProjectConfigExport): UserProjectConfi
|
|||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkspaceProjectConfiguration = string | (UserProjectConfigExport & {
|
|
||||||
/**
|
|
||||||
* Relative path to the extendable config. All other options will be merged with this config.
|
|
||||||
* @example '../vite.config.ts'
|
|
||||||
*/
|
|
||||||
extends?: string
|
|
||||||
})
|
|
||||||
|
|
||||||
export function defineWorkspace(config: WorkspaceProjectConfiguration[]): WorkspaceProjectConfiguration[] {
|
export function defineWorkspace(config: WorkspaceProjectConfiguration[]): WorkspaceProjectConfiguration[] {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
export type SerializedSpec = [
|
export type SerializedSpec = [
|
||||||
project: { name: string | undefined; root: string },
|
project: { name: string | undefined; root: string },
|
||||||
file: string,
|
file: string,
|
||||||
|
options: { pool: string },
|
||||||
]
|
]
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { relative } from 'pathe'
|
|||||||
import { parseAstAsync } from 'vite'
|
import { parseAstAsync } from 'vite'
|
||||||
import { ancestor as walkAst } from 'acorn-walk'
|
import { ancestor as walkAst } from 'acorn-walk'
|
||||||
import type { RawSourceMap } from 'vite-node'
|
import type { RawSourceMap } from 'vite-node'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
calculateSuiteHash,
|
calculateSuiteHash,
|
||||||
generateHash,
|
generateHash,
|
||||||
@ -54,16 +53,18 @@ export async function collectTests(
|
|||||||
}
|
}
|
||||||
const ast = await parseAstAsync(request.code)
|
const ast = await parseAstAsync(request.code)
|
||||||
const testFilepath = relative(ctx.config.root, filepath)
|
const testFilepath = relative(ctx.config.root, filepath)
|
||||||
|
const projectName = ctx.getName()
|
||||||
|
const typecheckSubprojectName = projectName ? `${projectName}:__typecheck__` : '__typecheck__'
|
||||||
const file: ParsedFile = {
|
const file: ParsedFile = {
|
||||||
filepath,
|
filepath,
|
||||||
type: 'suite',
|
type: 'suite',
|
||||||
id: generateHash(`${testFilepath}${ctx.config.name || ''}`),
|
id: generateHash(`${testFilepath}${typecheckSubprojectName}`),
|
||||||
name: testFilepath,
|
name: testFilepath,
|
||||||
mode: 'run',
|
mode: 'run',
|
||||||
tasks: [],
|
tasks: [],
|
||||||
start: ast.start,
|
start: ast.start,
|
||||||
end: ast.end,
|
end: ast.end,
|
||||||
projectName: ctx.getName(),
|
projectName,
|
||||||
meta: { typecheck: true },
|
meta: { typecheck: true },
|
||||||
file: null!,
|
file: null!,
|
||||||
}
|
}
|
||||||
@ -76,6 +77,12 @@ export async function collectTests(
|
|||||||
if (callee.type === 'Identifier') {
|
if (callee.type === 'Identifier') {
|
||||||
return callee.name
|
return callee.name
|
||||||
}
|
}
|
||||||
|
if (callee.type === 'CallExpression') {
|
||||||
|
return getName(callee.callee)
|
||||||
|
}
|
||||||
|
if (callee.type === 'TaggedTemplateExpression') {
|
||||||
|
return getName(callee.tag)
|
||||||
|
}
|
||||||
if (callee.type === 'MemberExpression') {
|
if (callee.type === 'MemberExpression') {
|
||||||
// direct call as `__vite_ssr_exports_0__.test()`
|
// direct call as `__vite_ssr_exports_0__.test()`
|
||||||
if (callee.object?.name?.startsWith('__vite_ssr_')) {
|
if (callee.object?.name?.startsWith('__vite_ssr_')) {
|
||||||
@ -86,6 +93,7 @@ export async function collectTests(
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
walkAst(ast as any, {
|
walkAst(ast as any, {
|
||||||
CallExpression(node) {
|
CallExpression(node) {
|
||||||
const { callee } = node as any
|
const { callee } = node as any
|
||||||
@ -96,27 +104,45 @@ export async function collectTests(
|
|||||||
if (!['it', 'test', 'describe', 'suite'].includes(name)) {
|
if (!['it', 'test', 'describe', 'suite'].includes(name)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const {
|
|
||||||
arguments: [{ value: message }],
|
|
||||||
} = node as any
|
|
||||||
const property = callee?.property?.name
|
const property = callee?.property?.name
|
||||||
let mode = !property || property === name ? 'run' : property
|
const mode = !property || property === name ? 'run' : property
|
||||||
if (!['run', 'skip', 'todo', 'only', 'skipIf', 'runIf'].includes(mode)) {
|
// the test node for skipIf and runIf will be the next CallExpression
|
||||||
throw new Error(
|
if (mode === 'each' || mode === 'skipIf' || mode === 'runIf' || mode === 'for') {
|
||||||
`${name}.${mode} syntax is not supported when testing types`,
|
return
|
||||||
)
|
|
||||||
}
|
}
|
||||||
// cannot statically analyze, so we always skip it
|
|
||||||
if (mode === 'skipIf' || mode === 'runIf') {
|
let start: number
|
||||||
mode = 'skip'
|
const end = node.end
|
||||||
|
|
||||||
|
if (callee.type === 'CallExpression') {
|
||||||
|
start = callee.end
|
||||||
}
|
}
|
||||||
|
else if (callee.type === 'TaggedTemplateExpression') {
|
||||||
|
start = callee.end + 1
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
start = node.start
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
arguments: [messageNode],
|
||||||
|
} = node
|
||||||
|
|
||||||
|
if (!messageNode) {
|
||||||
|
// called as "test()"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = getNodeAsString(messageNode, request.code)
|
||||||
|
|
||||||
definitions.push({
|
definitions.push({
|
||||||
start: node.start,
|
start,
|
||||||
end: node.end,
|
end,
|
||||||
name: message,
|
name: message,
|
||||||
type: name === 'it' || name === 'test' ? 'test' : 'suite',
|
type: name === 'it' || name === 'test' ? 'test' : 'suite',
|
||||||
mode,
|
mode,
|
||||||
} as LocalCallDefinition)
|
task: null as any,
|
||||||
|
} satisfies LocalCallDefinition)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
let lastSuite: ParsedSuite = file
|
let lastSuite: ParsedSuite = file
|
||||||
@ -189,3 +215,39 @@ export async function collectTests(
|
|||||||
definitions,
|
definitions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNodeAsString(node: any, code: string): string {
|
||||||
|
if (node.type === 'Literal') {
|
||||||
|
return String(node.value)
|
||||||
|
}
|
||||||
|
else if (node.type === 'Identifier') {
|
||||||
|
return node.name
|
||||||
|
}
|
||||||
|
else if (node.type === 'TemplateLiteral') {
|
||||||
|
return mergeTemplateLiteral(node, code)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return code.slice(node.start, node.end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeTemplateLiteral(node: any, code: string): string {
|
||||||
|
let result = ''
|
||||||
|
let expressionsIndex = 0
|
||||||
|
|
||||||
|
for (let quasisIndex = 0; quasisIndex < node.quasis.length; quasisIndex++) {
|
||||||
|
result += node.quasis[quasisIndex].value.raw
|
||||||
|
if (expressionsIndex in node.expressions) {
|
||||||
|
const expression = node.expressions[expressionsIndex]
|
||||||
|
const string = expression.type === 'Literal' ? expression.raw : getNodeAsString(expression, code)
|
||||||
|
if (expression.type === 'TemplateLiteral') {
|
||||||
|
result += `\${\`${string}\`}`
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result += `\${${string}}`
|
||||||
|
}
|
||||||
|
expressionsIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@ -145,7 +145,7 @@ export class Typechecker {
|
|||||||
...definitions.sort((a, b) => b.start - a.start),
|
...definitions.sort((a, b) => b.start - a.start),
|
||||||
]
|
]
|
||||||
// has no map for ".js" files that use // @ts-check
|
// has no map for ".js" files that use // @ts-check
|
||||||
const traceMap = map && new TraceMap(map as unknown as RawSourceMap)
|
const traceMap = (map && new TraceMap(map as unknown as RawSourceMap))
|
||||||
const indexMap = createIndexMap(parsed)
|
const indexMap = createIndexMap(parsed)
|
||||||
const markState = (task: Task, state: TaskState) => {
|
const markState = (task: Task, state: TaskState) => {
|
||||||
task.result = {
|
task.result = {
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { promises as fs } from 'node:fs'
|
import { promises as fs } from 'node:fs'
|
||||||
import mm from 'micromatch'
|
import mm from 'micromatch'
|
||||||
import type { WorkspaceProject } from '../node/workspace'
|
|
||||||
import type { EnvironmentOptions, TransformModePatterns, VitestEnvironment } from '../node/types/config'
|
import type { EnvironmentOptions, TransformModePatterns, VitestEnvironment } from '../node/types/config'
|
||||||
import type { ContextTestEnvironment } from '../types/worker'
|
import type { ContextTestEnvironment } from '../types/worker'
|
||||||
|
import type { WorkspaceSpec } from '../node/pool'
|
||||||
import { groupBy } from './base'
|
import { groupBy } from './base'
|
||||||
|
|
||||||
export const envsOrder = ['node', 'jsdom', 'happy-dom', 'edge-runtime']
|
export const envsOrder = ['node', 'jsdom', 'happy-dom', 'edge-runtime']
|
||||||
@ -27,7 +27,7 @@ function getTransformMode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function groupFilesByEnv(
|
export async function groupFilesByEnv(
|
||||||
files: (readonly [WorkspaceProject, string])[],
|
files: Array<WorkspaceSpec>,
|
||||||
) {
|
) {
|
||||||
const filesWithEnv = await Promise.all(
|
const filesWithEnv = await Promise.all(
|
||||||
files.map(async ([project, file]) => {
|
files.map(async ([project, file]) => {
|
||||||
|
|||||||
@ -48,7 +48,7 @@ export class StateManager {
|
|||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
const existing = this.filesMap.get(file.filepath) || []
|
const existing = this.filesMap.get(file.filepath) || []
|
||||||
const otherProject = existing.filter(
|
const otherProject = existing.filter(
|
||||||
i => i.projectName !== file.projectName,
|
i => i.projectName !== file.projectName || i.meta.typecheck !== file.meta.typecheck,
|
||||||
)
|
)
|
||||||
const currentFile = existing.find(
|
const currentFile = existing.find(
|
||||||
i => i.projectName === file.projectName,
|
i => i.projectName === file.projectName,
|
||||||
|
|||||||
@ -87,5 +87,7 @@ function createWrapper() {
|
|||||||
const wrapper = document.createElement('div')
|
const wrapper = document.createElement('div')
|
||||||
wrapper.className = 'wrapper'
|
wrapper.className = 'wrapper'
|
||||||
document.body.appendChild(wrapper)
|
document.body.appendChild(wrapper)
|
||||||
|
wrapper.style.height = '200px'
|
||||||
|
wrapper.style.width = '200px'
|
||||||
return wrapper
|
return wrapper
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ function buildWorkspace() {
|
|||||||
const workspace = buildWorkspace()
|
const workspace = buildWorkspace()
|
||||||
|
|
||||||
function workspaced(files: string[]) {
|
function workspaced(files: string[]) {
|
||||||
return files.map(file => [workspace, file] as WorkspaceSpec)
|
return files.map(file => [workspace, file, { pool: 'forks' }] satisfies WorkspaceSpec)
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('base sequencer', () => {
|
describe('base sequencer', () => {
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json"
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"include": ["./tests/**/*.test-d.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,34 @@
|
|||||||
|
import { expectTypeOf, test } from 'vitest'
|
||||||
|
|
||||||
|
test.each(['some-value'])('each: %s', () => {
|
||||||
|
expectTypeOf(1).toEqualTypeOf(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test.for(['some-value'])('for: %s', () => {
|
||||||
|
expectTypeOf(1).toEqualTypeOf(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test.skipIf(false)('dynamic skip', () => {
|
||||||
|
expectTypeOf(1).toEqualTypeOf(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test(`template string`, () => {
|
||||||
|
expectTypeOf(1).toEqualTypeOf(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test(`template ${'some value'} string`, () => {
|
||||||
|
expectTypeOf(1).toEqualTypeOf(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test(`template ${`literal`} string`, () => {
|
||||||
|
expectTypeOf(1).toEqualTypeOf(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
const name = 'some value'
|
||||||
|
test(name, () => {
|
||||||
|
expectTypeOf(1).toEqualTypeOf(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test((() => 'some name')(), () => {
|
||||||
|
expectTypeOf(1).toEqualTypeOf(2)
|
||||||
|
})
|
||||||
12
test/typescript/fixtures/dynamic-title/tsconfig.json
Normal file
12
test/typescript/fixtures/dynamic-title/tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true,
|
||||||
|
"target": "es2020",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"strict": true,
|
||||||
|
"verbatimModuleSyntax": true
|
||||||
|
},
|
||||||
|
"include": ["test"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
10
test/typescript/fixtures/dynamic-title/vitest.config.ts
Normal file
10
test/typescript/fixtures/dynamic-title/vitest.config.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
typecheck: {
|
||||||
|
enabled: true,
|
||||||
|
tsconfig: './tsconfig.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@ -112,3 +112,24 @@ describe('ignoreSourceErrors', () => {
|
|||||||
expect(vitest.stderr).not.toContain('TypeCheckError: Cannot find name \'thisIsSourceError\'')
|
expect(vitest.stderr).not.toContain('TypeCheckError: Cannot find name \'thisIsSourceError\'')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('when the title is dynamic', () => {
|
||||||
|
it('works correctly', async () => {
|
||||||
|
const vitest = await runVitest({
|
||||||
|
root: resolve(__dirname, '../fixtures/dynamic-title'),
|
||||||
|
reporters: [['default', { isTTY: true }]],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(vitest.stdout).toContain('✓ for: %s')
|
||||||
|
expect(vitest.stdout).toContain('✓ each: %s')
|
||||||
|
expect(vitest.stdout).toContain('✓ dynamic skip')
|
||||||
|
expect(vitest.stdout).not.toContain('✓ false') // .skipIf is not reported as a separate test
|
||||||
|
expect(vitest.stdout).toContain('✓ template string')
|
||||||
|
// eslint-disable-next-line no-template-curly-in-string
|
||||||
|
expect(vitest.stdout).toContain('✓ template ${"some value"} string')
|
||||||
|
// eslint-disable-next-line no-template-curly-in-string
|
||||||
|
expect(vitest.stdout).toContain('✓ template ${`literal`} string')
|
||||||
|
expect(vitest.stdout).toContain('✓ name')
|
||||||
|
expect(vitest.stdout).toContain('✓ (() => "some name")()')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"include": [
|
||||||
|
"./**/*.ts",
|
||||||
|
"./**/*.js"
|
||||||
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"**/dist/**",
|
"**/dist/**",
|
||||||
"**/fixtures/**"
|
"**/fixtures/**"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user