[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

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

authored by

Vladimir and committed by
GitHub
(Aug 13, 2024, 12:42 PM +0200) 153ff01b a68deed0

+337 -108
+7 -1
docs/guide/testing-types.md
··· 14 14 15 15 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. 16 16 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 + ::: 18 24 19 25 Using CLI flags, like `--allowOnly` and `-t` are also supported for type checking. 20 26
+2 -1
test/reporters/tsconfig.json
··· 1 1 { 2 - "extends": "../../tsconfig.base.json" 2 + "extends": "../../tsconfig.base.json", 3 + "include": ["./tests/**/*.test-d.ts"] 3 4 }
+4
test/typescript/tsconfig.json
··· 1 1 { 2 2 "extends": "../../tsconfig.base.json", 3 + "include": [ 4 + "./**/*.ts", 5 + "./**/*.js" 6 + ], 3 7 "exclude": [ 4 8 "**/dist/**", 5 9 "**/fixtures/**"
+1 -1
packages/ws-client/src/state.ts
··· 48 48 files.forEach((file) => { 49 49 const existing = this.filesMap.get(file.filepath) || [] 50 50 const otherProject = existing.filter( 51 - i => i.projectName !== file.projectName, 51 + i => i.projectName !== file.projectName || i.meta.typecheck !== file.meta.typecheck, 52 52 ) 53 53 const currentFile = existing.find( 54 54 i => i.projectName === file.projectName,
+2
test/browser/test/dom.test.ts
··· 87 87 const wrapper = document.createElement('div') 88 88 wrapper.className = 'wrapper' 89 89 document.body.appendChild(wrapper) 90 + wrapper.style.height = '200px' 91 + wrapper.style.width = '200px' 90 92 return wrapper 91 93 }
+1 -1
test/core/test/sequencers.test.ts
··· 26 26 const workspace = buildWorkspace() 27 27 28 28 function workspaced(files: string[]) { 29 - return files.map(file => [workspace, file] as WorkspaceSpec) 29 + return files.map(file => [workspace, file, { pool: 'forks' }] satisfies WorkspaceSpec) 30 30 } 31 31 32 32 describe('base sequencer', () => {
+21
test/typescript/test/runner.test.ts
··· 112 112 expect(vitest.stderr).not.toContain('TypeCheckError: Cannot find name \'thisIsSourceError\'') 113 113 }) 114 114 }) 115 + 116 + describe('when the title is dynamic', () => { 117 + it('works correctly', async () => { 118 + const vitest = await runVitest({ 119 + root: resolve(__dirname, '../fixtures/dynamic-title'), 120 + reporters: [['default', { isTTY: true }]], 121 + }) 122 + 123 + expect(vitest.stdout).toContain('✓ for: %s') 124 + expect(vitest.stdout).toContain('✓ each: %s') 125 + expect(vitest.stdout).toContain('✓ dynamic skip') 126 + expect(vitest.stdout).not.toContain('✓ false') // .skipIf is not reported as a separate test 127 + expect(vitest.stdout).toContain('✓ template string') 128 + // eslint-disable-next-line no-template-curly-in-string 129 + expect(vitest.stdout).toContain('✓ template ${"some value"} string') 130 + // eslint-disable-next-line no-template-curly-in-string 131 + expect(vitest.stdout).toContain('✓ template ${`literal`} string') 132 + expect(vitest.stdout).toContain('✓ name') 133 + expect(vitest.stdout).toContain('✓ (() => "some name")()') 134 + }) 135 + })
+1 -1
packages/browser/src/node/plugin.ts
··· 157 157 name: 'vitest:browser:tests', 158 158 enforce: 'pre', 159 159 async config() { 160 - const allTestFiles = await project.globTestFiles() 160 + const { testFiles: allTestFiles } = await project.globTestFiles() 161 161 const browserTestFiles = allTestFiles.filter( 162 162 file => getFilePoolName(project, file) === 'browser', 163 163 )
+2 -2
packages/browser/src/node/pool.ts
··· 1 1 import * as nodeos from 'node:os' 2 2 import crypto from 'node:crypto' 3 3 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' 5 5 import { createDebugger } from 'vitest/node' 6 6 7 7 const debug = createDebugger('vitest:browser:pool') ··· 92 92 await Promise.all(promises) 93 93 } 94 94 95 - const runWorkspaceTests = async (method: 'run' | 'collect', specs: [WorkspaceProject, string][]) => { 95 + const runWorkspaceTests = async (method: 'run' | 'collect', specs: WorkspaceSpec[]) => { 96 96 const groupedFiles = new Map<WorkspaceProject, string[]>() 97 97 for (const [project, file] of specs) { 98 98 const files = groupedFiles.get(project) || []
+1 -1
packages/browser/src/node/serverTester.ts
··· 22 22 const { contextId, testFile } = server.resolveTesterUrl(url.pathname) 23 23 const project = server.project 24 24 const state = server.state 25 - const testFiles = await project.globTestFiles() 25 + const { testFiles } = await project.globTestFiles() 26 26 // if decoded test file is "__vitest_all__" or not in the list of known files, run all tests 27 27 const tests 28 28 = testFile === '__vitest_all__'
+5
packages/ui/client/components/FileDetails.vue
··· 35 35 return current.value && hasFailedSnapshot(current.value) 36 36 }) 37 37 38 + const isTypecheck = computed(() => { 39 + return !!current.value?.meta?.typecheck 40 + }) 41 + 38 42 function open() { 39 43 const filePath = current.value?.filepath 40 44 if (filePath) { ··· 122 126 <div> 123 127 <div p="2" h-10 flex="~ gap-2" items-center bg-header border="b base"> 124 128 <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 /> 125 130 <div 126 131 v-if="current?.file.projectName" 127 132 font-light
+2 -1
packages/vitest/src/api/setup.ts
··· 103 103 }, 104 104 async getTestFiles() { 105 105 const spec = await ctx.globTestFiles() 106 - return spec.map(([project, file]) => [ 106 + return spec.map(([project, file, options]) => [ 107 107 { 108 108 name: project.config.name, 109 109 root: project.config.root, 110 110 }, 111 111 file, 112 + options, 112 113 ]) 113 114 }, 114 115 },
+72 -22
packages/vitest/src/node/core.ts
··· 17 17 import type { SerializedCoverageConfig } from '../runtime/config' 18 18 import type { SerializedSpec } from '../runtime/types/utils' 19 19 import type { ArgumentsType, OnServerRestartHandler, ProvidedContext, UserConsoleLog } from '../types/general' 20 - import { createPool } from './pool' 20 + import { createPool, getFilePoolName } from './pool' 21 21 import type { ProcessPool, WorkspaceSpec } from './pool' 22 22 import { createBenchmarkReporters, createReporters } from './reporters/utils' 23 23 import { StateManager } from './state' ··· 77 77 78 78 private resolvedProjects: WorkspaceProject[] = [] 79 79 public projects: WorkspaceProject[] = [] 80 - private projectsTestFiles = new Map<string, Set<WorkspaceProject>>() 81 80 82 81 public distPath!: string 82 + 83 + private _cachedSpecs = new Map<string, WorkspaceSpec[]>() 84 + 85 + /** @deprecated use `_cachedSpecs` */ 86 + projectTestFiles = this._cachedSpecs 83 87 84 88 constructor( 85 89 public readonly mode: VitestRunMode, ··· 103 107 this.coverageProvider = undefined 104 108 this.runningPromise = undefined 105 109 this.distPath = undefined! 106 - this.projectsTestFiles.clear() 110 + this._cachedSpecs.clear() 107 111 108 112 const resolved = resolveConfig(this.mode, options, server.config, this.logger) 109 113 ··· 191 195 } 192 196 193 197 /** 198 + * @deprecated internal, use `_createCoreProject` instead 199 + */ 200 + createCoreProject() { 201 + return this._createCoreProject() 202 + } 203 + 204 + /** 194 205 * @internal 195 206 */ 196 207 async _createCoreProject() { ··· 202 213 return this.coreWorkspaceProject 203 214 } 204 215 216 + /** 217 + * @deprecated use Reported Task API instead 218 + */ 205 219 public getProjectByTaskId(taskId: string): WorkspaceProject { 206 220 const task = this.state.idMap.get(taskId) 207 221 const projectName = (task as File).projectName || task?.file?.projectName || '' ··· 216 230 || this.projects[0] 217 231 } 218 232 219 - private async getWorkspaceConfigPath() { 233 + private async getWorkspaceConfigPath(): Promise<string | null> { 220 234 if (this.config.workspace) { 221 235 return this.config.workspace 222 236 } ··· 423 437 } 424 438 } 425 439 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) => { 428 442 if (deps.has(filepath)) { 429 443 return 430 444 } ··· 440 454 const path = await project.server.pluginContainer.resolveId(dep, filepath, { ssr: true }) 441 455 const fsPath = path && !path.external && path.id.split('?')[0] 442 456 if (fsPath && !fsPath.includes('node_modules') && !deps.has(fsPath) && existsSync(fsPath)) { 443 - await addImports([project, fsPath]) 457 + await addImports(project, fsPath) 444 458 } 445 459 })) 446 460 } 447 461 448 - await addImports(filepath) 449 - deps.delete(filepath[1]) 462 + await addImports(project, filepath) 463 + deps.delete(filepath) 450 464 451 465 return deps 452 466 } ··· 500 514 return runningTests 501 515 } 502 516 517 + /** 518 + * @deprecated remove when vscode extension supports "getFileWorkspaceSpecs" 519 + */ 503 520 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 507 528 } 508 - return Array.from(projects).map(project => [project, file] as WorkspaceSpec) 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 + } 539 + } 540 + specs.forEach(spec => this.ensureSpecCached(spec)) 541 + return specs 509 542 } 510 543 511 544 async initializeGlobalSetup(paths: WorkspaceSpec[]) { ··· 538 571 539 572 await this.report('onPathsCollected', filepaths) 540 573 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, 543 579 )) 544 580 545 581 // previous run ··· 856 892 })) 857 893 858 894 if (matchingProjects.length > 0) { 859 - this.projectsTestFiles.set(id, new Set(matchingProjects)) 860 895 this.changedTests.add(id) 861 896 this.scheduleRerun([id]) 862 897 } ··· 1054 1089 public async globTestFiles(filters: string[] = []) { 1055 1090 const files: WorkspaceSpec[] = [] 1056 1091 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) 1063 1103 }) 1064 1104 })) 1065 1105 return files 1106 + } 1107 + 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 + } 1066 1116 } 1067 1117 1068 1118 // The server needs to be running for communication
+4 -11
packages/vitest/src/node/pool.ts
··· 10 10 import { createTypecheckPool } from './pools/typecheck' 11 11 import { createVmForksPool } from './pools/vmForks' 12 12 13 - export type WorkspaceSpec = [project: WorkspaceProject, testFile: string] 13 + export type WorkspaceSpec = [project: WorkspaceProject, testFile: string, options: { pool: Pool }] 14 14 export type RunWithFiles = ( 15 15 files: WorkspaceSpec[], 16 16 invalidates?: string[] ··· 39 39 'typescript', 40 40 ] 41 41 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 { 50 43 if (project.config.browser.enabled) { 51 44 return 'browser' 52 45 } ··· 64 57 return pool as Pool 65 58 } 66 59 } 67 - return getDefaultPoolName(project, file) 60 + return getDefaultPoolName(project) 68 61 } 69 62 70 63 export function createPool(ctx: Vitest): ProcessPool { ··· 172 165 } 173 166 174 167 for (const spec of files) { 175 - const pool = getFilePoolName(spec[0], spec[1]) 168 + const { pool } = spec[2] 176 169 filesByPool[pool] ??= [] 177 170 filesByPool[pool].push(spec) 178 171 }
+14 -5
packages/vitest/src/node/state.ts
··· 78 78 .flat() 79 79 .filter(file => file && !file.local) 80 80 } 81 - return Array.from(this.filesMap.values()).flat().filter(file => !file.local) 81 + return Array.from(this.filesMap.values()).flat().filter(file => !file.local).sort((f1, f2) => { 82 + // print typecheck files first 83 + if (f1.meta?.typecheck && f2.meta?.typecheck) { 84 + return 0 85 + } 86 + if (f1.meta?.typecheck) { 87 + return -1 88 + } 89 + return 1 90 + }) 82 91 } 83 92 84 93 getFilepaths(): string[] { ··· 100 109 collectFiles(project: WorkspaceProject, files: File[] = []) { 101 110 files.forEach((file) => { 102 111 const existing = this.filesMap.get(file.filepath) || [] 103 - const otherProject = existing.filter( 104 - i => i.projectName !== file.projectName, 112 + const otherFiles = existing.filter( 113 + i => i.projectName !== file.projectName || i.meta.typecheck !== file.meta.typecheck, 105 114 ) 106 115 const currentFile = existing.find( 107 116 i => i.projectName === file.projectName, ··· 111 120 if (currentFile) { 112 121 file.logs = currentFile.logs 113 122 } 114 - otherProject.push(file) 115 - this.filesMap.set(file.filepath, otherProject) 123 + otherFiles.push(file) 124 + this.filesMap.set(file.filepath, otherFiles) 116 125 this.updateId(file, project) 117 126 }) 118 127 }
+20 -6
packages/vitest/src/node/workspace.ts
··· 97 97 closingPromise: Promise<unknown> | undefined 98 98 99 99 testFilesList: string[] | null = null 100 + typecheckFilesList: string[] | null = null 100 101 101 102 public testProject!: TestProject 102 103 ··· 225 226 ? [] 226 227 : this.globAllTestFiles(include, exclude, includeSource, dir), 227 228 typecheck.enabled 228 - ? this.globFiles(typecheck.include, typecheck.exclude, dir) 229 + ? (this.typecheckFilesList || this.globFiles(typecheck.include, typecheck.exclude, dir)) 229 230 : [], 230 231 ]) 231 232 232 - return this.filterFiles( 233 - [...testFiles, ...typecheckTestFiles], 234 - filters, 235 - dir, 236 - ) 233 + this.typecheckFilesList = typecheckTestFiles 234 + 235 + return { 236 + testFiles: this.filterFiles( 237 + testFiles, 238 + filters, 239 + dir, 240 + ), 241 + typecheckTestFiles: this.filterFiles( 242 + typecheckTestFiles, 243 + filters, 244 + dir, 245 + ), 246 + } 237 247 } 238 248 239 249 async globAllTestFiles( ··· 273 283 274 284 isTestFile(id: string) { 275 285 return this.testFilesList && this.testFilesList.includes(id) 286 + } 287 + 288 + isTypecheckFile(id: string) { 289 + return this.typecheckFilesList && this.typecheckFilesList.includes(id) 276 290 } 277 291 278 292 async globFiles(include: string[], exclude: string[], cwd: string) {
+2 -21
packages/vitest/src/public/config.ts
··· 1 1 import '../node/types/vite' 2 2 3 3 import type { ConfigEnv, UserConfig as ViteUserConfig } from 'vite' 4 - import type { ProjectConfig } from '../node/types/config' 5 - 6 - export interface UserWorkspaceConfig extends ViteUserConfig { 7 - test?: ProjectConfig 8 - } 4 + import type { UserProjectConfigExport, UserProjectConfigFn, UserWorkspaceConfig, WorkspaceProjectConfiguration } from '../node/types/config' 9 5 10 6 // will import vitest declare test in module 'vite' 11 7 export { ··· 20 16 export type { Plugin } from 'vite' 21 17 22 18 export type { ConfigEnv, ViteUserConfig as UserConfig } 19 + export type { UserProjectConfigExport, UserProjectConfigFn, UserWorkspaceConfig, WorkspaceProjectConfiguration } 23 20 export type UserConfigFnObject = (env: ConfigEnv) => ViteUserConfig 24 21 export type UserConfigFnPromise = (env: ConfigEnv) => Promise<ViteUserConfig> 25 22 export type UserConfigFn = ( ··· 31 28 | UserConfigFnObject 32 29 | UserConfigFnPromise 33 30 | UserConfigFn 34 - 35 - export type UserProjectConfigFn = ( 36 - env: ConfigEnv 37 - ) => UserWorkspaceConfig | Promise<UserWorkspaceConfig> 38 - export type UserProjectConfigExport = 39 - | UserWorkspaceConfig 40 - | Promise<UserWorkspaceConfig> 41 - | UserProjectConfigFn 42 31 43 32 export function defineConfig(config: ViteUserConfig): ViteUserConfig 44 33 export function defineConfig( ··· 57 46 export function defineProject(config: UserProjectConfigExport): UserProjectConfigExport { 58 47 return config 59 48 } 60 - 61 - export type WorkspaceProjectConfiguration = string | (UserProjectConfigExport & { 62 - /** 63 - * Relative path to the extendable config. All other options will be merged with this config. 64 - * @example '../vite.config.ts' 65 - */ 66 - extends?: string 67 - }) 68 49 69 50 export function defineWorkspace(config: WorkspaceProjectConfiguration[]): WorkspaceProjectConfiguration[] { 70 51 return config
+79 -17
packages/vitest/src/typecheck/collect.ts
··· 2 2 import { parseAstAsync } from 'vite' 3 3 import { ancestor as walkAst } from 'acorn-walk' 4 4 import type { RawSourceMap } from 'vite-node' 5 - 6 5 import { 7 6 calculateSuiteHash, 8 7 generateHash, ··· 54 53 } 55 54 const ast = await parseAstAsync(request.code) 56 55 const testFilepath = relative(ctx.config.root, filepath) 56 + const projectName = ctx.getName() 57 + const typecheckSubprojectName = projectName ? `${projectName}:__typecheck__` : '__typecheck__' 57 58 const file: ParsedFile = { 58 59 filepath, 59 60 type: 'suite', 60 - id: generateHash(`${testFilepath}${ctx.config.name || ''}`), 61 + id: generateHash(`${testFilepath}${typecheckSubprojectName}`), 61 62 name: testFilepath, 62 63 mode: 'run', 63 64 tasks: [], 64 65 start: ast.start, 65 66 end: ast.end, 66 - projectName: ctx.getName(), 67 + projectName, 67 68 meta: { typecheck: true }, 68 69 file: null!, 69 70 } ··· 76 77 if (callee.type === 'Identifier') { 77 78 return callee.name 78 79 } 80 + if (callee.type === 'CallExpression') { 81 + return getName(callee.callee) 82 + } 83 + if (callee.type === 'TaggedTemplateExpression') { 84 + return getName(callee.tag) 85 + } 79 86 if (callee.type === 'MemberExpression') { 80 87 // direct call as `__vite_ssr_exports_0__.test()` 81 88 if (callee.object?.name?.startsWith('__vite_ssr_')) { ··· 86 93 } 87 94 return null 88 95 } 96 + 89 97 walkAst(ast as any, { 90 98 CallExpression(node) { 91 99 const { callee } = node as any ··· 96 104 if (!['it', 'test', 'describe', 'suite'].includes(name)) { 97 105 return 98 106 } 99 - const { 100 - arguments: [{ value: message }], 101 - } = node as any 102 107 const property = callee?.property?.name 103 - let mode = !property || property === name ? 'run' : property 104 - if (!['run', 'skip', 'todo', 'only', 'skipIf', 'runIf'].includes(mode)) { 105 - throw new Error( 106 - `${name}.${mode} syntax is not supported when testing types`, 107 - ) 108 + const mode = !property || property === name ? 'run' : property 109 + // the test node for skipIf and runIf will be the next CallExpression 110 + if (mode === 'each' || mode === 'skipIf' || mode === 'runIf' || mode === 'for') { 111 + return 108 112 } 109 - // cannot statically analyze, so we always skip it 110 - if (mode === 'skipIf' || mode === 'runIf') { 111 - mode = 'skip' 113 + 114 + let start: number 115 + const end = node.end 116 + 117 + if (callee.type === 'CallExpression') { 118 + start = callee.end 112 119 } 120 + else if (callee.type === 'TaggedTemplateExpression') { 121 + start = callee.end + 1 122 + } 123 + else { 124 + start = node.start 125 + } 126 + 127 + const { 128 + arguments: [messageNode], 129 + } = node 130 + 131 + if (!messageNode) { 132 + // called as "test()" 133 + return 134 + } 135 + 136 + const message = getNodeAsString(messageNode, request.code) 137 + 113 138 definitions.push({ 114 - start: node.start, 115 - end: node.end, 139 + start, 140 + end, 116 141 name: message, 117 142 type: name === 'it' || name === 'test' ? 'test' : 'suite', 118 143 mode, 119 - } as LocalCallDefinition) 144 + task: null as any, 145 + } satisfies LocalCallDefinition) 120 146 }, 121 147 }) 122 148 let lastSuite: ParsedSuite = file ··· 188 214 map: request.map as RawSourceMap | null, 189 215 definitions, 190 216 } 217 + } 218 + 219 + function getNodeAsString(node: any, code: string): string { 220 + if (node.type === 'Literal') { 221 + return String(node.value) 222 + } 223 + else if (node.type === 'Identifier') { 224 + return node.name 225 + } 226 + else if (node.type === 'TemplateLiteral') { 227 + return mergeTemplateLiteral(node, code) 228 + } 229 + else { 230 + return code.slice(node.start, node.end) 231 + } 232 + } 233 + 234 + function mergeTemplateLiteral(node: any, code: string): string { 235 + let result = '' 236 + let expressionsIndex = 0 237 + 238 + for (let quasisIndex = 0; quasisIndex < node.quasis.length; quasisIndex++) { 239 + result += node.quasis[quasisIndex].value.raw 240 + if (expressionsIndex in node.expressions) { 241 + const expression = node.expressions[expressionsIndex] 242 + const string = expression.type === 'Literal' ? expression.raw : getNodeAsString(expression, code) 243 + if (expression.type === 'TemplateLiteral') { 244 + result += `\${\`${string}\`}` 245 + } 246 + else { 247 + result += `\${${string}}` 248 + } 249 + expressionsIndex++ 250 + } 251 + } 252 + return result 191 253 }
+1 -1
packages/vitest/src/typecheck/typechecker.ts
··· 145 145 ...definitions.sort((a, b) => b.start - a.start), 146 146 ] 147 147 // has no map for ".js" files that use // @ts-check 148 - const traceMap = map && new TraceMap(map as unknown as RawSourceMap) 148 + const traceMap = (map && new TraceMap(map as unknown as RawSourceMap)) 149 149 const indexMap = createIndexMap(parsed) 150 150 const markState = (task: Task, state: TaskState) => { 151 151 task.result = {
+2 -2
packages/vitest/src/utils/test-helpers.ts
··· 1 1 import { promises as fs } from 'node:fs' 2 2 import mm from 'micromatch' 3 - import type { WorkspaceProject } from '../node/workspace' 4 3 import type { EnvironmentOptions, TransformModePatterns, VitestEnvironment } from '../node/types/config' 5 4 import type { ContextTestEnvironment } from '../types/worker' 5 + import type { WorkspaceSpec } from '../node/pool' 6 6 import { groupBy } from './base' 7 7 8 8 export const envsOrder = ['node', 'jsdom', 'happy-dom', 'edge-runtime'] ··· 27 27 } 28 28 29 29 export async function groupFilesByEnv( 30 - files: (readonly [WorkspaceProject, string])[], 30 + files: Array<WorkspaceSpec>, 31 31 ) { 32 32 const filesWithEnv = await Promise.all( 33 33 files.map(async ([project, file]) => {
+12
test/typescript/fixtures/dynamic-title/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "noEmit": true, 4 + "target": "es2020", 5 + "module": "ESNext", 6 + "moduleResolution": "Bundler", 7 + "strict": true, 8 + "verbatimModuleSyntax": true 9 + }, 10 + "include": ["test"], 11 + "exclude": ["node_modules"] 12 + }
+10
test/typescript/fixtures/dynamic-title/vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config' 2 + 3 + export default defineConfig({ 4 + test: { 5 + typecheck: { 6 + enabled: true, 7 + tsconfig: './tsconfig.json', 8 + }, 9 + }, 10 + })
+1 -1
packages/ui/client/components/explorer/ExplorerItem.vue
··· 173 173 <div :class="opened ? 'i-carbon:chevron-down' : 'i-carbon:chevron-right op20'" op20 /> 174 174 </div> 175 175 <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 /> 177 176 <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 /> 178 178 <span text-sm truncate font-light> 179 179 <span v-if="type === 'file' && projectName" class="rounded-full p-1 mr-1 text-xs" :style="{ backgroundColor: projectNameColor, color: projectNameTextColor }"> 180 180 {{ projectName }}
+1 -1
packages/ui/client/composables/explorer/types.ts
··· 55 55 export interface SuiteTreeNode extends ParentTreeNode { 56 56 fileId: string 57 57 type: 'suite' 58 - typecheck?: boolean 59 58 } 60 59 61 60 export interface FileTreeNode extends ParentTreeNode { 62 61 type: 'file' 63 62 filepath: string 63 + typecheck: boolean | undefined 64 64 projectName?: string 65 65 projectNameColor: string 66 66 collectDuration?: number
+2 -4
packages/ui/client/composables/explorer/utils.ts
··· 46 46 let fileNode = explorerTree.nodes.get(file.id) as FileTreeNode | undefined 47 47 48 48 if (fileNode) { 49 + fileNode.typecheck = !!file.meta && 'typecheck' in file.meta 49 50 fileNode.state = file.result?.state 50 51 fileNode.mode = file.mode 51 52 fileNode.duration = file.result?.duration ··· 66 67 type: 'file', 67 68 children: new Set(), 68 69 tasks: [], 70 + typecheck: !!file.meta && 'typecheck' in file.meta, 69 71 indent: 0, 70 72 duration: file.result?.duration, 71 73 filepath: file.filepath, ··· 141 143 taskNode.mode = task.mode 142 144 taskNode.duration = task.result?.duration 143 145 taskNode.state = task.result?.state 144 - if (isSuiteNode(taskNode)) { 145 - taskNode.typecheck = !!task.meta && 'typecheck' in task.meta 146 - } 147 146 } 148 147 else { 149 148 if (isAtomTest(task)) { ··· 168 167 parentId, 169 168 name: task.name, 170 169 mode: task.mode, 171 - typecheck: !!task.meta && 'typecheck' in task.meta, 172 170 type: 'suite', 173 171 expandable: true, 174 172 // When the current run finish, we will expand all nodes when required, here we expand only the opened nodes
+4 -4
packages/vitest/src/node/pools/typecheck.ts
··· 10 10 11 11 export function createTypecheckPool(ctx: Vitest): ProcessPool { 12 12 const promisesMap = new WeakMap<WorkspaceProject, DeferPromise<void>>() 13 - const rerunTriggered = new WeakMap<WorkspaceProject, boolean>() 13 + const rerunTriggered = new WeakSet<WorkspaceProject>() 14 14 15 15 async function onParseEnd( 16 16 project: WorkspaceProject, ··· 36 36 37 37 promisesMap.get(project)?.resolve() 38 38 39 - rerunTriggered.set(project, false) 39 + rerunTriggered.delete(project) 40 40 41 41 // triggered by TSC watcher, not Vitest watcher, so we need to emulate what Vitest does in this case 42 42 if (ctx.config.watch && !ctx.runningPromise) { ··· 68 68 checker.onParseEnd(result => onParseEnd(project, result)) 69 69 70 70 checker.onWatcherRerun(async () => { 71 - rerunTriggered.set(project, true) 71 + rerunTriggered.add(project) 72 72 73 73 if (!ctx.runningPromise) { 74 74 ctx.state.clearErrors() ··· 123 123 // check that watcher actually triggered rerun 124 124 const _p = new Promise<boolean>((resolve) => { 125 125 const _i = setInterval(() => { 126 - if (!project.typechecker || rerunTriggered.get(project)) { 126 + if (!project.typechecker || rerunTriggered.has(project)) { 127 127 resolve(true) 128 128 clearInterval(_i) 129 129 }
+3
packages/vitest/src/node/reporters/base.ts
··· 154 154 } 155 155 156 156 let title = ` ${getStateSymbol(task)} ` 157 + if (task.meta.typecheck) { 158 + title += `${c.bgBlue(c.bold(' TS '))} ` 159 + } 157 160 if (task.projectName) { 158 161 title += formatProjectName(task.projectName) 159 162 }
+20 -2
packages/vitest/src/node/types/config.ts
··· 1 - import type { AliasOptions, DepOptimizationConfig, ServerOptions } from 'vite' 1 + import type { AliasOptions, ConfigEnv, DepOptimizationConfig, ServerOptions, UserConfig as ViteUserConfig } from 'vite' 2 2 import type { PrettyFormatOptions } from '@vitest/pretty-format' 3 3 import type { FakeTimerInstallOpts } from '@sinonjs/fake-timers' 4 4 import type { SequenceHooks, SequenceSetupFiles } from '@vitest/runner' ··· 1094 1094 NonProjectOptions 1095 1095 > 1096 1096 1097 - export type { UserWorkspaceConfig } from '../../public/config' 1097 + export interface UserWorkspaceConfig extends ViteUserConfig { 1098 + test?: ProjectConfig 1099 + } 1100 + 1101 + export type UserProjectConfigFn = ( 1102 + env: ConfigEnv 1103 + ) => UserWorkspaceConfig | Promise<UserWorkspaceConfig> 1104 + export type UserProjectConfigExport = 1105 + | UserWorkspaceConfig 1106 + | Promise<UserWorkspaceConfig> 1107 + | UserProjectConfigFn 1108 + 1109 + export type WorkspaceProjectConfiguration = string | (UserProjectConfigExport & { 1110 + /** 1111 + * Relative path to the extendable config. All other options will be merged with this config. 1112 + * @example '../vite.config.ts' 1113 + */ 1114 + extends?: string 1115 + })
+1 -2
packages/vitest/src/node/workspace/resolveWorkspace.ts
··· 3 3 import { dirname, relative, resolve } from 'pathe' 4 4 import { mergeConfig } from 'vite' 5 5 import fg from 'fast-glob' 6 - import type { UserWorkspaceConfig, WorkspaceProjectConfiguration } from '../../public/config' 7 6 import type { Vitest } from '../core' 8 - import type { UserConfig } from '../types/config' 7 + import type { UserConfig, UserWorkspaceConfig, WorkspaceProjectConfiguration } from '../types/config' 9 8 import type { WorkspaceProject } from '../workspace' 10 9 import { initializeProject } from '../workspace' 11 10 import { configFiles as defaultConfigFiles } from '../../constants'
+1
packages/vitest/src/runtime/types/utils.ts
··· 1 1 export type SerializedSpec = [ 2 2 project: { name: string | undefined; root: string }, 3 3 file: string, 4 + options: { pool: string }, 4 5 ]
+34
test/typescript/fixtures/dynamic-title/test/dynamic-title.test-d.ts
··· 1 + import { expectTypeOf, test } from 'vitest' 2 + 3 + test.each(['some-value'])('each: %s', () => { 4 + expectTypeOf(1).toEqualTypeOf(2) 5 + }) 6 + 7 + test.for(['some-value'])('for: %s', () => { 8 + expectTypeOf(1).toEqualTypeOf(2) 9 + }) 10 + 11 + test.skipIf(false)('dynamic skip', () => { 12 + expectTypeOf(1).toEqualTypeOf(2) 13 + }) 14 + 15 + test(`template string`, () => { 16 + expectTypeOf(1).toEqualTypeOf(2) 17 + }) 18 + 19 + test(`template ${'some value'} string`, () => { 20 + expectTypeOf(1).toEqualTypeOf(2) 21 + }) 22 + 23 + test(`template ${`literal`} string`, () => { 24 + expectTypeOf(1).toEqualTypeOf(2) 25 + }) 26 + 27 + const name = 'some value' 28 + test(name, () => { 29 + expectTypeOf(1).toEqualTypeOf(2) 30 + }) 31 + 32 + test((() => 'some name')(), () => { 33 + expectTypeOf(1).toEqualTypeOf(2) 34 + })
+5
packages/vitest/src/node/reporters/renderers/listRenderer.ts
··· 124 124 prefix += formatProjectName(task.projectName) 125 125 } 126 126 127 + if (level === 0 && task.type === 'suite' && task.meta.typecheck) { 128 + prefix += c.bgBlue(c.bold(' TS ')) 129 + prefix += ' ' 130 + } 131 + 127 132 if ( 128 133 task.type === 'test' 129 134 && task.result?.retryCount