From 10b30f6547a5afcd4bb05a7136b822391d41ceef Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 17 Oct 2023 13:22:43 +0200 Subject: [PATCH 01/11] feat(vitest): run typecheck during tests --- docs/advanced/api.md | 5 - packages/vitest/src/node/cli-api.ts | 3 + packages/vitest/src/node/cli.ts | 11 +- packages/vitest/src/node/config.ts | 9 +- packages/vitest/src/node/core.ts | 11 +- packages/vitest/src/node/logger.ts | 19 ++- packages/vitest/src/node/pool.ts | 19 ++- packages/vitest/src/node/pools/typecheck.ts | 123 +++++++++++++++++++ packages/vitest/src/node/reporters/base.ts | 20 +-- packages/vitest/src/node/stdin.ts | 4 - packages/vitest/src/node/workspace.ts | 75 ++--------- packages/vitest/src/typecheck/collect.ts | 2 + packages/vitest/src/typecheck/typechecker.ts | 23 +++- packages/vitest/src/types/config.ts | 7 +- packages/vitest/src/types/pool-options.ts | 2 +- test/core/test/types.test-d.ts | 6 + test/core/vite.config.ts | 3 + test/typescript/test/runner.test.ts | 7 +- test/typescript/test/vitest.custom.config.ts | 1 + test/typescript/test/vitest.empty.config.ts | 1 + 20 files changed, 227 insertions(+), 124 deletions(-) create mode 100644 packages/vitest/src/node/pools/typecheck.ts create mode 100644 test/core/test/types.test-d.ts diff --git a/docs/advanced/api.md b/docs/advanced/api.md index fa4990699fda..e21fe2f0ae5a 100644 --- a/docs/advanced/api.md +++ b/docs/advanced/api.md @@ -52,7 +52,6 @@ Vitest instance requires the current test mode. It can be either: - `test` when running runtime tests - `benchmark` when running benchmarks -- `typecheck` when running type tests ### mode @@ -64,10 +63,6 @@ Test mode will only call functions inside `test` or `it`, and throws an error wh Benchmark mode calls `bench` functions and throws an error, when it encounters `test` or `it`. This mode uses `benchmark.include` and `benchmark.exclude` options in the config to find benchmark files. -#### typecheck - -Typecheck mode doesn't _run_ tests. It only analyses types and gives a summary. This mode uses `typecheck.include` and `typecheck.exclude` options in the config to find files to analyze. - ### start You can start running tests or benchmarks with `start` method. You can pass an array of strings to filter test files. diff --git a/packages/vitest/src/node/cli-api.ts b/packages/vitest/src/node/cli-api.ts index a6456138948f..4d47ae90fea5 100644 --- a/packages/vitest/src/node/cli-api.ts +++ b/packages/vitest/src/node/cli-api.ts @@ -56,6 +56,9 @@ export async function startVitest( if (typeof options.browser === 'object' && !('enabled' in options.browser)) options.browser.enabled = true + if (typeof options.typecheck === 'boolean') + options.typecheck = { enabled: true } + const ctx = await createVitest(mode, options, viteOverrides) if (mode === 'test' && ctx.config.coverage.enabled) { diff --git a/packages/vitest/src/node/cli.ts b/packages/vitest/src/node/cli.ts index 02a6320cb096..092badc19ae8 100644 --- a/packages/vitest/src/node/cli.ts +++ b/packages/vitest/src/node/cli.ts @@ -51,6 +51,8 @@ cli .option('--bail ', 'Stop test execution when given number of tests have failed', { default: 0 }) .option('--retry ', 'Retry the test specific number of times if it fails', { default: 0 }) .option('--diff ', 'Path to a diff config that will be used to generate diff interface') + .option('--typecheck [options]', 'Custom options for typecheck pool') + .option('--typecheck.enabled', 'Enable typechecking alongside tests (default: false)') .help() cli @@ -73,10 +75,6 @@ cli .command('bench [...filters]') .action(benchmark) -cli - .command('typecheck [...filters]') - .action(typecheck) - cli .command('[...filters]') .action((filters, options) => start('test', filters, options)) @@ -130,11 +128,6 @@ async function benchmark(cliFilters: string[], options: CliOptions): Promise [resolve(resolved.root, i[0]), i[1]]) - if (mode === 'typecheck') { - resolved.include = resolved.typecheck.include - resolved.exclude = resolved.typecheck.exclude - } + resolved.typecheck ??= {} as any + resolved.typecheck.enabled ??= false + + if (resolved.typecheck.enabled) + console.warn(c.yellow('Testing types with tsc and vue-tsc is an experimental feature.\nBreaking changes might not follow semver, please pin Vitest\'s version when using it.')) resolved.browser ??= {} as any resolved.browser.enabled ??= false diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index d143c0f95d40..8e3987d986f4 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -93,7 +93,7 @@ export class Vitest { this.cache = new VitestCache() this.snapshot = new SnapshotManager({ ...resolved.snapshotOptions }) - if (this.config.watch && this.mode !== 'typecheck') + if (this.config.watch) this.registerWatcher() this.vitenode = new ViteNodeServer(server, this.config.server) @@ -308,16 +308,7 @@ export class Vitest { return Promise.all(this.projects.map(w => w.initBrowserProvider())) } - typecheck(filters?: string[]) { - return Promise.all(this.projects.map(project => project.typecheck(filters))) - } - async start(filters?: string[]) { - if (this.mode === 'typecheck') { - await this.typecheck(filters) - return - } - try { await this.initCoverageProvider() await this.coverageProvider?.clean(this.config.coverage.clean) diff --git a/packages/vitest/src/node/logger.ts b/packages/vitest/src/node/logger.ts index bd18b30861f4..bb40205bb42e 100644 --- a/packages/vitest/src/node/logger.ts +++ b/packages/vitest/src/node/logger.ts @@ -95,10 +95,21 @@ export class Logger { const comma = c.dim(', ') if (filters?.length) this.console.error(c.dim('filter: ') + c.yellow(filters.join(comma))) - if (config.include) - this.console.error(c.dim('include: ') + c.yellow(config.include.join(comma))) - if (config.exclude) - this.console.error(c.dim('exclude: ') + c.yellow(config.exclude.join(comma))) + this.ctx.projects.forEach((project) => { + const config = project.config + const name = project.getName() + const output = project.isCore() || !name ? '' : `[${name}]` + if (output) + this.console.error(c.bgCyan(`${output} Config`)) + if (config.include) + this.console.error(c.dim('include: ') + c.yellow(config.include.join(comma))) + if (config.exclude) + this.console.error(c.dim('exclude: ') + c.yellow(config.exclude.join(comma))) + if (config.typecheck.enabled) { + this.console.error(c.dim('typecheck include: ') + c.yellow(config.typecheck.include.join(comma))) + this.console.error(c.dim('typecheck exclude: ') + c.yellow(config.typecheck.exclude.join(comma))) + } + }) if (config.watchExclude) this.console.error(c.dim('watch exclude: ') + c.yellow(config.watchExclude.join(comma))) diff --git a/packages/vitest/src/node/pool.ts b/packages/vitest/src/node/pool.ts index a1abf634dfe7..f092473d1af7 100644 --- a/packages/vitest/src/node/pool.ts +++ b/packages/vitest/src/node/pool.ts @@ -9,6 +9,7 @@ import { createThreadsPool } from './pools/threads' import { createBrowserPool } from './pools/browser' import { createVmThreadsPool } from './pools/vm-threads' import type { WorkspaceProject } from './workspace' +import { createTypecheckPool } from './pools/typecheck' export type WorkspaceSpec = [project: WorkspaceProject, testFile: string] export type RunWithFiles = (files: WorkspaceSpec[], invalidates?: string[]) => Promise @@ -35,12 +36,20 @@ export function createPool(ctx: Vitest): ProcessPool { threads: null, browser: null, vmThreads: null, + typescript: null, } - function getDefaultPoolName(project: WorkspaceProject): Pool { + function getDefaultPoolName(project: WorkspaceProject, file: string): Pool { if (project.config.browser.enabled) return 'browser' + if (project.config.typecheck.enabled) { + for (const glob of project.config.typecheck.include) { + if (mm.isMatch(file, glob, { cwd: project.config.root })) + return 'typescript' + } + } + return project.config.pool } @@ -51,7 +60,7 @@ export function createPool(ctx: Vitest): ProcessPool { if (mm.isMatch(file, glob, { cwd: project.config.root })) return pool as Pool } - return getDefaultPoolName(project) + return getDefaultPoolName(project, file) } async function runTests(files: WorkspaceSpec[], invalidate?: string[]) { @@ -93,6 +102,7 @@ export function createPool(ctx: Vitest): ProcessPool { threads: [], browser: [], vmThreads: [], + typescript: [], } for (const spec of files) { @@ -123,6 +133,11 @@ export function createPool(ctx: Vitest): ProcessPool { return pools.threads.runTests(files, invalidate) } + if (pool === 'typescript') { + pools.typescript ??= createTypecheckPool(ctx) + return pools.typescript.runTests(files) + } + pools.forks ??= createChildProcessPool(ctx, options) return pools.forks.runTests(files, invalidate) })) diff --git a/packages/vitest/src/node/pools/typecheck.ts b/packages/vitest/src/node/pools/typecheck.ts new file mode 100644 index 000000000000..a794d604fdd9 --- /dev/null +++ b/packages/vitest/src/node/pools/typecheck.ts @@ -0,0 +1,123 @@ +import type { DeferPromise } from '@vitest/utils' +import { createDefer } from '@vitest/utils' +import type { TypecheckResults } from '../../typecheck/typechecker' +import { Typechecker } from '../../typecheck/typechecker' +import { groupBy } from '../../utils/base' +import { hasFailed } from '../../utils/tasks' +import type { Vitest } from '../core' +import type { ProcessPool, WorkspaceSpec } from '../pool' +import type { WorkspaceProject } from '../workspace' + +export function createTypecheckPool(ctx: Vitest): ProcessPool { + const promisesMap = new WeakMap>() + const rerunTriggered = new WeakMap() + + async function onParseEnd(project: WorkspaceProject, { files, sourceErrors }: TypecheckResults) { + const checker = project.typechecker! + + await ctx.report('onTaskUpdate', checker.getTestPacks()) + + if (!project.config.typecheck.ignoreSourceErrors) + sourceErrors.forEach(error => ctx.state.catchError(error, 'Unhandled Source Error')) + + const processError = !hasFailed(files) && checker.getExitCode() + if (processError) { + const error = new Error(checker.getOutput()) + error.stack = '' + ctx.state.catchError(error, 'Typecheck Error') + } + + promisesMap.get(project)?.resolve() + + rerunTriggered.set(project, false) + + // triggered by TSC watcher, not Vitest watcher, so we need to emulate what Vitest does in this case + if (ctx.config.watch && !ctx.runningPromise) { + await ctx.report('onFinished', files) + await ctx.report('onWatcherStart', files, [ + ...(project.config.typecheck.ignoreSourceErrors ? [] : sourceErrors), + ...ctx.state.getUnhandledErrors(), + ]) + } + } + + async function createWorkspaceTypechecker(project: WorkspaceProject, files: string[]) { + const checker = project.typechecker ?? new Typechecker(project) + if (project.typechecker) + return checker + + project.typechecker = checker + checker.setFiles(files) + + checker.onParseStart(async () => { + ctx.state.collectFiles(checker.getTestFiles()) + await ctx.report('onCollected') + }) + + checker.onParseEnd(result => onParseEnd(project, result)) + + checker.onWatcherRerun(async () => { + rerunTriggered.set(project, true) + + if (!ctx.runningPromise) { + ctx.state.clearErrors() + await ctx.report('onWatcherRerun', files, 'File change detected. Triggering rerun.') + } + + await checker.collectTests() + ctx.state.collectFiles(checker.getTestFiles()) + + await ctx.report('onTaskUpdate', checker.getTestPacks()) + await ctx.report('onCollected') + }) + + await checker.prepare() + await checker.collectTests() + checker.start() + return checker + } + + async function runTests(specs: WorkspaceSpec[]) { + const specsByProject = groupBy(specs, ([project]) => project.getName()) + const promises: Promise[] = [] + + for (const name in specsByProject) { + const project = specsByProject[name][0][0] + const files = specsByProject[name].map(([_, file]) => file) + const promise = createDefer() + // check that watcher actually triggered rerun + const _p = new Promise((resolve) => { + const _i = setInterval(() => { + if (!project.typechecker || rerunTriggered.get(project)) { + resolve(true) + clearInterval(_i) + } + }) + setTimeout(() => { + resolve(false) + clearInterval(_i) + }, 500) + }) + const triggered = await _p + if (project.typechecker && !triggered) { + ctx.state.collectFiles(project.typechecker.getTestFiles()) + await ctx.report('onCollected') + await onParseEnd(project, project.typechecker.getResult()) + continue + } + promises.push(promise) + promisesMap.set(project, promise) + createWorkspaceTypechecker(project, files) + } + + await Promise.all(promises) + } + + return { + runTests, + async close() { + const promises = ctx.projects.map(project => project.typechecker?.stop()) + await Promise.all(promises) + }, + } +} diff --git a/packages/vitest/src/node/reporters/base.ts b/packages/vitest/src/node/reporters/base.ts index 48e9882c0c9c..84a6990d265e 100644 --- a/packages/vitest/src/node/reporters/base.ts +++ b/packages/vitest/src/node/reporters/base.ts @@ -114,9 +114,7 @@ export abstract class BaseReporter implements Reporter { this.ctx.logger.log(WAIT_FOR_CHANGE_PASS) const hints: string[] = [] - // TODO typecheck doesn't support these for now - if (this.mode !== 'typecheck') - hints.push(HELP_HINT) + hints.push(HELP_HINT) if (failedSnap) hints.unshift(HELP_UPDATE_SNAP) else @@ -258,19 +256,23 @@ export abstract class BaseReporter implements Reporter { logger.log(padTitle('Test Files'), getStateString(files)) logger.log(padTitle('Tests'), getStateString(tests)) - if (this.mode === 'typecheck') { + if (this.ctx.projects.some(c => c.config.typecheck.enabled)) { const failed = tests.filter(t => t.meta?.typecheck && t.result?.errors?.length) logger.log(padTitle('Type Errors'), failed.length ? c.bold(c.red(`${failed.length} failed`)) : c.dim('no errors')) } if (errors.length) logger.log(padTitle('Errors'), c.bold(c.red(`${errors.length} error${errors.length > 1 ? 's' : ''}`))) logger.log(padTitle('Start at'), formatTimeString(this._timeStart)) - if (this.watchFilters) + if (this.watchFilters) { logger.log(padTitle('Duration'), time(threadTime)) - else if (this.mode === 'typecheck') - logger.log(padTitle('Duration'), time(executionTime)) - else - logger.log(padTitle('Duration'), time(executionTime) + c.dim(` (transform ${time(transformTime)}, setup ${time(setupTime)}, collect ${time(collectTime)}, tests ${time(testsTime)}, environment ${time(environmentTime)}, prepare ${time(prepareTime)})`)) + } + else { + let timers = `transform ${time(transformTime)}, setup ${time(setupTime)}, collect ${time(collectTime)}, tests ${time(testsTime)}, environment ${time(environmentTime)}, prepare ${time(prepareTime)}` + const typecheck = this.ctx.projects.reduce((acc, c) => acc + (c.typechecker?.getResult().time || 0), 0) + if (typecheck) + timers += `, typecheck ${time(typecheck)}` + logger.log(padTitle('Duration'), time(executionTime) + c.dim(` (${timers})`)) + } logger.log() } diff --git a/packages/vitest/src/node/stdin.ts b/packages/vitest/src/node/stdin.ts index f4b3b82dc8ef..e26a34ae69d2 100644 --- a/packages/vitest/src/node/stdin.ts +++ b/packages/vitest/src/node/stdin.ts @@ -65,10 +65,6 @@ export function registerConsoleShortcuts(ctx: Vitest) { if (name === 'q') return ctx.exit(true) - // TODO typechecking doesn't support shortcuts this yet - if (ctx.mode === 'typecheck') - return - // help if (name === 'h') return printShortcutsHelp() diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index 947c4018776c..d0a5341681cf 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -8,8 +8,8 @@ import { ViteNodeServer } from 'vite-node/server' import c from 'picocolors' import { createBrowserServer } from '../integrations/browser/server' import type { ArgumentsType, Reporter, ResolvedConfig, UserConfig, UserWorkspaceConfig, Vitest } from '../types' -import { deepMerge, hasFailed } from '../utils' -import { Typechecker } from '../typecheck/typechecker' +import { deepMerge } from '../utils' +import type { Typechecker } from '../typecheck/typechecker' import type { BrowserProvider } from '../types/browser' import { getBrowserProvider } from '../integrations/browser' import { isBrowserEnabled, resolveConfig } from './config' @@ -156,9 +156,14 @@ export class WorkspaceProject { async globTestFiles(filters: string[] = []) { const dir = this.config.dir || this.config.root - const testFiles = await this.globAllTestFiles(this.config, dir) + const typecheck = this.config.typecheck - return this.filterFiles(testFiles, filters, dir) + const [testFiles, typecheckTestFiles] = await Promise.all([ + this.globAllTestFiles(this.config, dir), + typecheck.enabled ? this.globFiles(typecheck.include, typecheck.exclude, dir) : [], + ]) + + return this.filterFiles([...testFiles, ...typecheckTestFiles], filters, dir) } async globAllTestFiles(config: ResolvedConfig, cwd: string) { @@ -275,68 +280,6 @@ export class WorkspaceProject { return this.ctx.report(name, ...args) } - async typecheck(filters: string[] = []) { - const dir = this.config.dir || this.config.root - const { include, exclude } = this.config.typecheck - - const testFiles = await this.globFiles(include, exclude, dir) - const testsFilesList = this.filterFiles(testFiles, filters, dir) - - const checker = new Typechecker(this, testsFilesList) - this.typechecker = checker - checker.onParseEnd(async ({ files, sourceErrors }) => { - this.ctx.state.collectFiles(checker.getTestFiles()) - await this.report('onTaskUpdate', checker.getTestPacks()) - await this.report('onCollected') - const failedTests = hasFailed(files) - const exitCode = !failedTests && checker.getExitCode() - if (exitCode) { - const error = new Error(checker.getOutput()) - error.stack = '' - this.ctx.state.catchError(error, 'Typecheck Error') - } - if (!files.length) { - this.ctx.logger.printNoTestFound() - } - else { - if (failedTests) - process.exitCode = 1 - await this.report('onFinished', files) - } - if (sourceErrors.length && !this.config.typecheck.ignoreSourceErrors) { - process.exitCode = 1 - await this.ctx.logger.printSourceTypeErrors(sourceErrors) - } - // if there are source errors, we are showing it, and then terminating process - if (!files.length) { - const exitCode = this.config.passWithNoTests ? (process.exitCode ?? 0) : 1 - await this.close() - process.exit(exitCode) - } - if (this.config.watch) { - await this.report('onWatcherStart', files, [ - ...(this.config.typecheck.ignoreSourceErrors ? [] : sourceErrors), - ...this.ctx.state.getUnhandledErrors(), - ]) - } - }) - checker.onParseStart(async () => { - await this.report('onInit', this.ctx) - this.ctx.state.collectFiles(checker.getTestFiles()) - await this.report('onCollected') - }) - checker.onWatcherRerun(async () => { - await this.report('onWatcherRerun', testsFilesList, 'File change detected. Triggering rerun.') - await checker.collectTests() - this.ctx.state.collectFiles(checker.getTestFiles()) - await this.report('onTaskUpdate', checker.getTestPacks()) - await this.report('onCollected') - }) - await checker.prepare() - await checker.collectTests() - await checker.start() - } - isBrowserEnabled() { return isBrowserEnabled(this.config) } diff --git a/packages/vitest/src/typecheck/collect.ts b/packages/vitest/src/typecheck/collect.ts index 97e079b44a0a..85be33376ba4 100644 --- a/packages/vitest/src/typecheck/collect.ts +++ b/packages/vitest/src/typecheck/collect.ts @@ -57,6 +57,7 @@ export async function collectTests(ctx: WorkspaceProject, filepath: string): Pro tasks: [], start: ast.start, end: ast.end, + projectName: ctx.getName(), meta: { typecheck: true }, } const definitions: LocalCallDefinition[] = [] @@ -122,6 +123,7 @@ export async function collectTests(ctx: WorkspaceProject, filepath: string): Pro name: definition.name, end: definition.end, start: definition.start, + projectName: ctx.getName(), meta: { typecheck: true, }, diff --git a/packages/vitest/src/typecheck/typechecker.ts b/packages/vitest/src/typecheck/typechecker.ts index e56ce36fc8a0..bee9a6fb631d 100644 --- a/packages/vitest/src/typecheck/typechecker.ts +++ b/packages/vitest/src/typecheck/typechecker.ts @@ -1,4 +1,5 @@ import { rm } from 'node:fs/promises' +import { performance } from 'node:perf_hooks' import type { ExecaChildProcess } from 'execa' import { execa } from 'execa' import { basename, extname, resolve } from 'pathe' @@ -21,35 +22,44 @@ export class TypeCheckError extends Error { } } -interface ErrorsCache { +export interface TypecheckResults { files: File[] sourceErrors: TypeCheckError[] + time: number } type Callback = []> = (...args: Args) => Awaitable export class Typechecker { private _onParseStart?: Callback - private _onParseEnd?: Callback<[ErrorsCache]> + private _onParseEnd?: Callback<[TypecheckResults]> private _onWatcherRerun?: Callback - private _result: ErrorsCache = { + private _result: TypecheckResults = { files: [], sourceErrors: [], + time: 0, } + private _startTime = 0 private _output = '' private _tests: Record | null = {} private tempConfigPath?: string private allowJs?: boolean private process?: ExecaChildProcess - constructor(protected ctx: WorkspaceProject, protected files: string[]) { } + protected files: string[] = [] + + constructor(protected ctx: WorkspaceProject) { } + + public setFiles(files: string[]) { + this.files = files + } public onParseStart(fn: Callback) { this._onParseStart = fn } - public onParseEnd(fn: Callback<[ErrorsCache]>) { + public onParseEnd(fn: Callback<[TypecheckResults]>) { this._onParseEnd = fn } @@ -165,6 +175,7 @@ export class Typechecker { return { files, sourceErrors, + time: performance.now() - this._startTime, } } @@ -243,6 +254,7 @@ export class Typechecker { if (typecheck.allowJs) args.push('--allowJs', '--checkJs') this._output = '' + this._startTime = performance.now() const child = execa(typecheck.checker, args, { cwd: root, stdout: 'pipe', @@ -257,6 +269,7 @@ export class Typechecker { return if (this._output.includes('File change detected') && !rerunTriggered) { this._onWatcherRerun?.() + this._startTime = performance.now() this._result.sourceErrors = [] this._result.files = [] this._tests = null // test structure might've changed diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts index 4bc1373290c5..d335e0a92900 100644 --- a/packages/vitest/src/types/config.ts +++ b/packages/vitest/src/types/config.ts @@ -37,7 +37,7 @@ export interface EnvironmentOptions { [x: string]: unknown } -export type VitestRunMode = 'test' | 'benchmark' | 'typecheck' +export type VitestRunMode = 'test' | 'benchmark' interface SequenceOptions { /** @@ -635,6 +635,7 @@ export interface InlineConfig { } export interface TypecheckConfig { + enabled?: boolean /** * What tools to use for type checking. */ @@ -749,7 +750,9 @@ export interface ResolvedConfig extends Omit, 'config' | 'f seed: number } - typecheck: TypecheckConfig + typecheck: Omit & { + enabled: boolean + } runner?: string } diff --git a/packages/vitest/src/types/pool-options.ts b/packages/vitest/src/types/pool-options.ts index c77b3c536623..eb874614e599 100644 --- a/packages/vitest/src/types/pool-options.ts +++ b/packages/vitest/src/types/pool-options.ts @@ -1,4 +1,4 @@ -export type Pool = 'browser' | 'threads' | 'forks' | 'vmThreads' // | 'vmForks' +export type Pool = 'browser' | 'threads' | 'forks' | 'vmThreads' | 'typescript' // | 'vmForks' export interface PoolOptions { /** diff --git a/test/core/test/types.test-d.ts b/test/core/test/types.test-d.ts new file mode 100644 index 000000000000..f5a8b7cb2638 --- /dev/null +++ b/test/core/test/types.test-d.ts @@ -0,0 +1,6 @@ +import { expectTypeOf, it } from 'vitest' + +it('typecheck works', () => { + expectTypeOf(4).toBeNumber() + expectTypeOf('').not.toBeNumber() +}) diff --git a/test/core/vite.config.ts b/test/core/vite.config.ts index a7f18bd70e70..e8ef072862a8 100644 --- a/test/core/vite.config.ts +++ b/test/core/vite.config.ts @@ -88,5 +88,8 @@ export default defineConfig({ customResolver: () => resolve(__dirname, 'src', 'aliased-mod.ts'), }, ], + typecheck: { + enabled: true, + }, }, }) diff --git a/test/typescript/test/runner.test.ts b/test/typescript/test/runner.test.ts index bbbc539f3ca3..1a7a11e206ec 100644 --- a/test/typescript/test/runner.test.ts +++ b/test/typescript/test/runner.test.ts @@ -13,10 +13,11 @@ describe('should fail', async () => { root, dir: './failing', typecheck: { + enabled: true, allowJs: true, include: ['**/*.test-d.*'], }, - }, [], 'typecheck') + }, []) expect(stderr).toBeTruthy() const lines = String(stderr).split(/\n/g) @@ -44,12 +45,12 @@ describe('should fail', async () => { it('typecheks with custom tsconfig', async () => { const { stderr } = await runVitestCli( { cwd: root, env: { ...process.env, CI: 'true' } }, - 'typecheck', '--run', '--dir', resolve(__dirname, '..', './failing'), '--config', resolve(__dirname, './vitest.custom.config.ts'), + '--typecheck.enabled', ) expect(stderr).toBeTruthy() @@ -85,12 +86,12 @@ describe('should fail', async () => { NO_COLOR: 'true', }, }, - 'typecheck', '--run', '--dir', resolve(__dirname, '..', './failing'), '--config', resolve(__dirname, './vitest.empty.config.ts'), + '--typecheck.enabled', ) expect(stderr.replace(resolve(__dirname, '..'), '')).toMatchSnapshot() diff --git a/test/typescript/test/vitest.custom.config.ts b/test/typescript/test/vitest.custom.config.ts index fe059cc66808..8cde5c059730 100644 --- a/test/typescript/test/vitest.custom.config.ts +++ b/test/typescript/test/vitest.custom.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { typecheck: { + enabled: true, allowJs: true, include: ['**/*.test-d.*'], tsconfig: '../tsconfig.custom.json', diff --git a/test/typescript/test/vitest.empty.config.ts b/test/typescript/test/vitest.empty.config.ts index 96d9349649a0..06ac35b4d106 100644 --- a/test/typescript/test/vitest.empty.config.ts +++ b/test/typescript/test/vitest.empty.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { typecheck: { + enabled: true, include: ['**/fail.test-d.ts'], tsconfig: '../tsconfig.empty.json', }, From d258e504e948906d688d8d414c90629960cf8a6f Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 17 Oct 2023 13:51:26 +0200 Subject: [PATCH 02/11] fix: improve type test output --- packages/runner/src/utils/tasks.ts | 4 +-- .../ui/client/components/views/ViewEditor.vue | 2 +- packages/ui/client/composables/summary.ts | 27 ++++++++++++++----- packages/vitest/src/typecheck/typechecker.ts | 11 ++++++-- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/packages/runner/src/utils/tasks.ts b/packages/runner/src/utils/tasks.ts index 40244da59c8a..d692a7995494 100644 --- a/packages/runner/src/utils/tasks.ts +++ b/packages/runner/src/utils/tasks.ts @@ -7,8 +7,8 @@ function isAtomTest(s: Task): s is Test | Custom { export function getTests(suite: Arrayable): (Test | Custom)[] { const tests: (Test | Custom)[] = [] - const suite_arr = toArray(suite) - for (const s of suite_arr) { + const arraySuites = toArray(suite) + for (const s of arraySuites) { if (isAtomTest(s)) { tests.push(s) } diff --git a/packages/ui/client/components/views/ViewEditor.vue b/packages/ui/client/components/views/ViewEditor.vue index 957fde0f2161..dd7ae679b85e 100644 --- a/packages/ui/client/components/views/ViewEditor.vue +++ b/packages/ui/client/components/views/ViewEditor.vue @@ -72,7 +72,7 @@ function createErrorElement(e: ErrorWithDiff) { div.className = 'op80 flex gap-x-2 items-center' const pre = document.createElement('pre') pre.className = 'c-red-600 dark:c-red-400' - pre.textContent = `${' '.repeat(stack.column)}^ ${e?.nameStr}: ${e?.message}` + pre.textContent = `${' '.repeat(stack.column)}^ ${e?.nameStr || e.name}: ${e?.message || ''}` div.appendChild(pre) const span = document.createElement('span') span.className = 'i-carbon-launch c-red-600 dark:c-red-400 hover:cursor-pointer min-w-1em min-h-1em' diff --git a/packages/ui/client/composables/summary.ts b/packages/ui/client/composables/summary.ts index 18e056779b59..68d57d13c787 100644 --- a/packages/ui/client/composables/summary.ts +++ b/packages/ui/client/composables/summary.ts @@ -1,5 +1,5 @@ import { hasFailedSnapshot } from '@vitest/ws-client' -import type { Benchmark, Task, Test, TypeCheck } from 'vitest/src' +import type { Custom, Task, Test } from 'vitest/src' import { files, testRunState } from '~/composables/client' type Nullable = T | null | undefined @@ -33,7 +33,7 @@ export const testsSkipped = computed(() => testsIgnore.value.filter(f => f.mode export const testsTodo = computed(() => testsIgnore.value.filter(f => f.mode === 'todo')) export const totalTests = computed(() => testsFailed.value.length + testsSuccess.value.length) export const time = computed(() => { - const t = getTests(tests.value).reduce((acc, t) => { + const t = files.value.reduce((acc, t) => { acc += Math.max(0, t.collectDuration || 0) acc += Math.max(0, t.setupDuration || 0) acc += Math.max(0, t.result?.duration || 0) @@ -53,10 +53,25 @@ function toArray(array?: Nullable>): Array { return [array] } -function isAtomTest(s: Task): s is Test | Benchmark | TypeCheck { - return (s.type === 'test' || s.type === 'benchmark' || s.type === 'typecheck') +function isAtomTest(s: Task): s is Test | Custom { + return (s.type === 'test' || s.type === 'custom') } -function getTests(suite: Arrayable): (Test | Benchmark | TypeCheck)[] { - return toArray(suite).flatMap(s => isAtomTest(s) ? [s] : s.tasks.flatMap(c => isAtomTest(c) ? [c] : getTests(c))) +function getTests(suite: Arrayable): (Test | Custom)[] { + const tests: (Test | Custom)[] = [] + const arraySuites = toArray(suite) + for (const s of arraySuites) { + if (isAtomTest(s)) { + tests.push(s) + } + else { + for (const task of s.tasks) { + if (isAtomTest(task)) + tests.push(task) + else + tests.push(...getTests(task)) + } + } + } + return tests } diff --git a/packages/vitest/src/typecheck/typechecker.ts b/packages/vitest/src/typecheck/typechecker.ts index bee9a6fb631d..5475d8a0e2ca 100644 --- a/packages/vitest/src/typecheck/typechecker.ts +++ b/packages/vitest/src/typecheck/typechecker.ts @@ -198,7 +198,14 @@ export class Typechecker { Error.stackTraceLimit = limit return { originalError: info, - error, + error: { + name: error.name, + nameStr: String(error.name), + message: info.errMsg, + stacks: error.stacks, + stack: '', + stackStr: '', + }, } }) typesErrors.set(filepath, suiteErrors) @@ -303,6 +310,6 @@ export class Typechecker { return Object.values(this._tests || {}) .map(({ file }) => getTasks(file)) .flat() - .map(i => [i.id, undefined, { typecheck: true }]) + .map(i => [i.id, i.result, { typecheck: true }]) } } From 55f366b30b2f798bdf99ca2bcb7cad711e68ec5f Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 17 Oct 2023 14:01:55 +0200 Subject: [PATCH 03/11] chore: add TS icon to typecheck tests in UI --- packages/ui/client/components/Suites.vue | 1 + packages/ui/client/components/TaskItem.vue | 1 + packages/ui/package.json | 1 + pnpm-lock.yaml | 13 +++++++++++-- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/ui/client/components/Suites.vue b/packages/ui/client/components/Suites.vue index 492b10d3bb3e..c9a1d8038194 100644 --- a/packages/ui/client/components/Suites.vue +++ b/packages/ui/client/components/Suites.vue @@ -23,6 +23,7 @@ async function onRunCurrent() {