From dd4f623d2e0759bf879205cfb13dccbaf369f948 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 25 Nov 2024 18:26:46 +0100 Subject: [PATCH 01/73] feat: prepare the Vitest API to be stable --- docs/advanced/api.md | 10 + docs/advanced/reporters.md | 4 + packages/vitest/src/api/setup.ts | 4 +- packages/vitest/src/node/cache/files.ts | 8 +- packages/vitest/src/node/cli/cli-api.ts | 90 +++-- packages/vitest/src/node/core.ts | 327 ++++++++++++------ packages/vitest/src/node/plugins/workspace.ts | 4 +- packages/vitest/src/node/pool.ts | 6 +- packages/vitest/src/node/pools/rpc.ts | 2 +- packages/vitest/src/node/pools/typecheck.ts | 11 +- packages/vitest/src/node/pools/vmThreads.ts | 2 +- .../src/node/reporters/reported-tasks.ts | 7 + packages/vitest/src/node/stdin.ts | 4 +- packages/vitest/src/node/types/tests.ts | 6 + packages/vitest/src/types/general.ts | 4 +- packages/vitest/src/utils/coverage.ts | 2 +- packages/vitest/src/utils/graph.ts | 2 +- packages/vitest/src/utils/test-helpers.ts | 4 +- 18 files changed, 325 insertions(+), 172 deletions(-) create mode 100644 packages/vitest/src/node/types/tests.ts diff --git a/docs/advanced/api.md b/docs/advanced/api.md index fafde8716ff2..8d840ce6d0bd 100644 --- a/docs/advanced/api.md +++ b/docs/advanced/api.md @@ -339,6 +339,16 @@ if (import.meta.vitest) { `) // true if `includeSource` is set ``` +### onTestsRerun + +This is a shorthand for `project.vitest.onTestsRerun`. It accepts a callback that will be awaited when the tests have been scheduled to rerun (usually, due to a file change). + +```ts +project.onTestsRerun((specs) => { + console.log(specs) +}) +``` + ### close Closes the project and all associated resources. This can only be called once; the closing promise is cached until the server restarts. If the resources are needed again, create a new project. diff --git a/docs/advanced/reporters.md b/docs/advanced/reporters.md index 49586526003c..fe95c8e05583 100644 --- a/docs/advanced/reporters.md +++ b/docs/advanced/reporters.md @@ -139,6 +139,10 @@ declare class TestCase { * If the test is not finished yet or was skipped, it will return `true`. */ ok(): boolean + /** + * Checks if the test was skipped. + */ + skipped(): boolean /** * Custom metadata that was attached to the test during its execution. */ diff --git a/packages/vitest/src/api/setup.ts b/packages/vitest/src/api/setup.ts index ea085f4376ef..262049c3973e 100644 --- a/packages/vitest/src/api/setup.ts +++ b/packages/vitest/src/api/setup.ts @@ -82,7 +82,7 @@ export function setup(ctx: Vitest, _server?: ViteDevServer) { return ctx.getRootTestProject().serializedConfig }, async getTransformResult(projectName: string, id, browser = false) { - const project = ctx.getProjectByName(projectName) + const project = ctx.getTestProjectByName(projectName) const result: TransformResultWithSource | null | undefined = browser ? await project.browser!.vite.transformRequest(id) : await project.vitenode.transformRequest(id) @@ -107,7 +107,7 @@ export function setup(ctx: Vitest, _server?: ViteDevServer) { return ctx.state.getUnhandledErrors() }, async getTestFiles() { - const spec = await ctx.globTestSpecs() + const spec = await ctx.globTestSpecifications() return spec.map(spec => [ { name: spec.project.config.name, diff --git a/packages/vitest/src/node/cache/files.ts b/packages/vitest/src/node/cache/files.ts index 689af665dd55..254dc45ec5ce 100644 --- a/packages/vitest/src/node/cache/files.ts +++ b/packages/vitest/src/node/cache/files.ts @@ -1,5 +1,5 @@ import type { Stats } from 'node:fs' -import type { WorkspaceSpec } from '../pool' +import type { TestSpecification } from '../spec' import fs from 'node:fs' import { relative } from 'pathe' @@ -12,10 +12,10 @@ export class FilesStatsCache { return this.cache.get(key) } - public async populateStats(root: string, specs: WorkspaceSpec[]) { + public async populateStats(root: string, specs: TestSpecification[]) { const promises = specs.map((spec) => { - const key = `${spec[0].name}:${relative(root, spec[1])}` - return this.updateStats(spec[1], key) + const key = `${spec[0].name}:${relative(root, spec.moduleId)}` + return this.updateStats(spec.moduleId, key) }) await Promise.all(promises) } diff --git a/packages/vitest/src/node/cli/cli-api.ts b/packages/vitest/src/node/cli/cli-api.ts index a0fb736c518c..c7d556ff0d95 100644 --- a/packages/vitest/src/node/cli/cli-api.ts +++ b/packages/vitest/src/node/cli/cli-api.ts @@ -1,11 +1,10 @@ -import type { File, Suite, Task } from '@vitest/runner' import type { UserConfig as ViteUserConfig } from 'vite' import type { environments } from '../../integrations/env' import type { Vitest, VitestOptions } from '../core' -import type { WorkspaceSpec } from '../pool' +import type { TestModule, TestSuite } from '../reporters' +import type { TestSpecification } from '../spec' import type { UserConfig, VitestEnvironment, VitestRunMode } from '../types/config' import { mkdirSync, writeFileSync } from 'node:fs' -import { getNames, getTests } from '@vitest/runner/utils' import { dirname, relative, resolve } from 'pathe' import { CoverageProviderMap } from '../../integrations/coverage' import { createVitest } from '../create' @@ -168,15 +167,15 @@ export async function prepareVitest( return ctx } -export function processCollected(ctx: Vitest, files: File[], options: CliOptions) { +export function processCollected(ctx: Vitest, files: TestModule[], options: CliOptions) { let errorsPrinted = false forEachSuite(files, (suite) => { - const errors = suite.result?.errors || [] + const errors = suite.task.result?.errors || [] errors.forEach((error) => { errorsPrinted = true ctx.logger.printError(error, { - project: ctx.getProjectByName(suite.file.projectName), + project: suite.project, }) }) }) @@ -192,7 +191,7 @@ export function processCollected(ctx: Vitest, files: File[], options: CliOptions return formatCollectedAsString(files).forEach(test => console.log(test)) } -export function outputFileList(files: WorkspaceSpec[], options: CliOptions) { +export function outputFileList(files: TestSpecification[], options: CliOptions) { if (typeof options.json !== 'undefined') { return outputJsonFileList(files, options) } @@ -200,7 +199,7 @@ export function outputFileList(files: WorkspaceSpec[], options: CliOptions) { return formatFilesAsString(files, options).map(file => console.log(file)) } -function outputJsonFileList(files: WorkspaceSpec[], options: CliOptions) { +function outputJsonFileList(files: TestSpecification[], options: CliOptions) { if (typeof options.json === 'boolean') { return console.log(JSON.stringify(formatFilesAsJSON(files), null, 2)) } @@ -211,7 +210,7 @@ function outputJsonFileList(files: WorkspaceSpec[], options: CliOptions) { } } -function formatFilesAsJSON(files: WorkspaceSpec[]) { +function formatFilesAsJSON(files: TestSpecification[]) { return files.map((file) => { const result: any = { file: file.moduleId, @@ -224,7 +223,7 @@ function formatFilesAsJSON(files: WorkspaceSpec[]) { }) } -function formatFilesAsString(files: WorkspaceSpec[], options: CliOptions) { +function formatFilesAsString(files: TestSpecification[], options: CliOptions) { return files.map((file) => { let name = relative(options.root || process.cwd(), file.moduleId) if (file.project.name) { @@ -234,7 +233,7 @@ function formatFilesAsString(files: WorkspaceSpec[], options: CliOptions) { }) } -function processJsonOutput(files: File[], options: CliOptions) { +function processJsonOutput(files: TestModule[], options: CliOptions) { if (typeof options.json === 'boolean') { return console.log(JSON.stringify(formatCollectedAsJSON(files), null, 2)) } @@ -246,45 +245,60 @@ function processJsonOutput(files: File[], options: CliOptions) { } } -function forEachSuite(tasks: Task[], callback: (suite: Suite) => void) { - tasks.forEach((task) => { - if (task.type === 'suite') { - callback(task) - forEachSuite(task.tasks, callback) +function forEachSuite(modules: TestModule[], callback: (suite: TestSuite) => void) { + modules.forEach((task) => { + for (const module of task.children.allSuites()) { + callback(module) } }) } -export function formatCollectedAsJSON(files: File[]) { - return files.map((file) => { - const tests = getTests(file).filter(test => test.mode === 'run' || test.mode === 'only') - return tests.map((test) => { - const result: any = { - name: getNames(test).slice(1).join(' > '), - file: file.filepath, +export interface TestCollectJSONResult { + name: string + file: string + projectName?: string + location?: { line: number; column: number } +} + +export function formatCollectedAsJSON(files: TestModule[]) { + const results: TestCollectJSONResult[] = [] + + files.forEach((file) => { + for (const test of file.children.allTests()) { + if (test.skipped()) { + continue } - if (test.file.projectName) { - result.projectName = test.file.projectName + const result: TestCollectJSONResult = { + name: test.fullName, + file: test.module.moduleId, + } + if (test.project.name) { + result.projectName = test.project.name } if (test.location) { result.location = test.location } - return result - }) - }).flat() + results.push(result) + } + }) + return results } -export function formatCollectedAsString(files: File[]) { - return files.map((file) => { - const tests = getTests(file).filter(test => test.mode === 'run' || test.mode === 'only') - return tests.map((test) => { - const name = getNames(test).join(' > ') - if (test.file.projectName) { - return `[${test.file.projectName}] ${name}` +export function formatCollectedAsString(files: TestModule[]) { + const results: string[] = [] + + files.forEach((file) => { + for (const test of file.children.allTests()) { + if (test.skipped()) { + continue } - return name - }) - }).flat() + results.push( + (test.project.name ? `[${test.project.name}] ` : '') + test.fullName, + ) + } + }) + + return results } const envPackageNames: Record< diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 2da9761b8760..16cb9a010e36 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -2,7 +2,6 @@ import type { CancelReason, File, TaskResultPack } from '@vitest/runner' import type { Writable } from 'node:stream' import type { ViteDevServer } from 'vite' import type { defineWorkspace } from 'vitest/config' -import type { RunnerTask, RunnerTestSuite } from '../public' import type { SerializedCoverageConfig } from '../runtime/config' import type { ArgumentsType, OnServerRestartHandler, OnTestsRerunHandler, ProvidedContext, UserConsoleLog } from '../types/general' import type { ProcessPool, WorkspaceSpec } from './pool' @@ -10,6 +9,7 @@ import type { TestSpecification } from './spec' import type { ResolvedConfig, UserConfig, VitestRunMode } from './types/config' import type { CoverageProvider } from './types/coverage' import type { Reporter } from './types/reporter' +import type { TestRunResult } from './types/tests' import { existsSync, promises as fs, readFileSync } from 'node:fs' import { resolve } from 'node:path' import { getTasks, hasFailed } from '@vitest/runner/utils' @@ -48,16 +48,23 @@ export interface VitestOptions { } export class Vitest { + /** + * Current Vitest version. + */ public readonly version = version static readonly version = version - config: ResolvedConfig = undefined! - configOverride: Partial = {} + public config: ResolvedConfig = undefined! + private configOverride: Partial = {} server: ViteDevServer = undefined! - state: StateManager = undefined! + /** + * @experimental The State API is experimental and not subject to semver. + */ + public state: StateManager = undefined! snapshot: SnapshotManager = undefined! cache: VitestCache = undefined! + /** @internal */ reporters: Reporter[] = undefined! coverageProvider: CoverageProvider | null | undefined logger: Logger @@ -69,25 +76,28 @@ export class Vitest { changedTests: Set = new Set() watchedTests: Set = new Set() filenamePattern?: string[] - runningPromise?: Promise + /** @internal */ + runningPromise?: Promise + /** @internal */ closingPromise?: Promise + /** @internal */ isCancelling = false + /** @internal */ isFirstRun = true + /** @internal */ restartsCount = 0 runner: ViteNodeRunner = undefined! public packageInstaller: VitestPackageInstaller + public projects: TestProject[] = [] + public distPath = distDir /** TODO: rename to `_coreRootProject` */ /** @internal */ - public coreWorkspaceProject!: TestProject - - /** @private */ + public coreWorkspaceProject: TestProject | undefined + /** @internal */ public resolvedProjects: TestProject[] = [] - public projects: TestProject[] = [] - - public distPath = distDir private _cachedSpecs = new Map() private _workspaceConfigPath?: string @@ -97,7 +107,6 @@ export class Vitest { /** @internal */ public _browserLastPort = defaultBrowserPort - /** @internal */ public _options: UserConfig = {} @@ -213,18 +222,22 @@ export class Vitest { await Promise.all(this._onSetServer.map(fn => fn())) } - public provide(key: T, value: ProvidedContext[T]) { + /** + * Provide a value to the test context. This value will be available to all tests with `inject`. + */ + public provide = (key: T, value: ProvidedContext[T]) => { this.getRootTestProject().provide(key, value) } - /** - * @internal - */ + /** @internal */ _createRootProject() { this.coreWorkspaceProject = TestProject._createBasicProject(this) return this.coreWorkspaceProject } + /** + * Return project that has the root (or "global") config. + */ public getRootTestProject(): TestProject { if (!this.coreWorkspaceProject) { throw new Error(`Root project is not initialized. This means that the Vite server was not established yet and the the workspace config is not resolved.`) @@ -243,10 +256,21 @@ export class Vitest { || this.projects[0] } - public getProjectByName(name: string = '') { - return this.projects.find(p => p.name === name) + /** + * @deprecated use `getTestProjectByName` instead + */ + public getProjectByName(name = ''): TestProject { + return this.getTestProjectByName(name) + } + + public getTestProjectByName(name: string = ''): TestProject { + const project = this.projects.find(p => p.name === name) || this.coreWorkspaceProject || this.projects[0] + if (!project) { + throw new Error(`Project "${name}" was not found.`) + } + return project } private async resolveWorkspaceConfigPath(): Promise { @@ -271,7 +295,7 @@ export class Vitest { return join(configDir, workspaceConfigName) } - private async resolveWorkspace(cliOptions: UserConfig) { + private async resolveWorkspace(cliOptions: UserConfig): Promise { if (Array.isArray(this.config.workspace)) { return resolveWorkspace( this, @@ -305,7 +329,7 @@ export class Vitest { ) } - private async initCoverageProvider() { + private async initCoverageProvider(): Promise { if (this.coverageProvider !== undefined) { return } @@ -320,7 +344,10 @@ export class Vitest { return this.coverageProvider } - async mergeReports() { + /** + * Merge reports from multiple runs located in the `--merge-reports` directory. + */ + public async mergeReports(): Promise { if (this.reporters.some(r => r instanceof BlobReporter)) { throw new Error('Cannot merge reports when `--reporter=blob` is used. Remove blob reporter from the config first.') } @@ -332,7 +359,7 @@ export class Vitest { const workspaceSpecs = new Map() for (const file of files) { - const project = this.getProjectByName(file.projectName) + const project = this.getTestProjectByName(file.projectName) const specs = workspaceSpecs.get(project) || [] specs.push(file) workspaceSpecs.set(project, specs) @@ -373,17 +400,22 @@ export class Vitest { process.exitCode = 1 } - this.checkUnhandledErrors(errors) + this._checkUnhandledErrors(errors) await this.report('onFinished', files, errors) await this.initCoverageProvider() await this.coverageProvider?.mergeReports?.(coverages) + + return { + tests: this.state.getTestModules(), + errors: this.state.getUnhandledErrors(), + } } - async collect(filters?: string[]) { + async collect(filters?: string[]): Promise { this._onClose = [] const files = await this.filterTestsBySource( - await this.globTestFiles(filters), + await this.globTestSpecifications(filters), ) // if run with --changed, don't exit if no tests are found @@ -391,22 +423,29 @@ export class Vitest { return { tests: [], errors: [] } } - await this.collectFiles(files) - - return { - tests: this.state.getFiles(), - errors: this.state.getUnhandledErrors(), - } + return await this.collectFiles(files) } - async listFiles(filters?: string[]) { + /** + * Returns the list of test files that match the config and filters. + * @param filters String filters to match the test files + */ + async listFiles(filters?: string[]): Promise { const files = await this.filterTestsBySource( - await this.globTestFiles(filters), + await this.globTestSpecifications(filters), ) return files } + /** + * Initialize reporters, the coverage provider, and run tests. + * This method can throw an error: + * - `FilesNotFoundError` if no tests are found + * - `GitNotFoundError` if `--related` flag is used, but git repository is not initialized + * - `Error` from the user reporters + * @param filters String filters to match the test files + */ async start(filters?: string[]) { this._onClose = [] @@ -420,7 +459,7 @@ export class Vitest { this.filenamePattern = filters && filters?.length > 0 ? filters : undefined const files = await this.filterTestsBySource( - await this.globTestFiles(filters), + await this.globTestSpecifications(filters), ) // if run with --changed, don't exit if no tests are found @@ -438,19 +477,30 @@ export class Vitest { } } + let testModules: TestRunResult = { + tests: [], + errors: [], + } + if (files.length) { // populate once, update cache on watch await this.cache.stats.populateStats(this.config.root, files) - await this.runFiles(files, true) + testModules = await this.runFiles(files, true) } if (this.config.watch) { await this.report('onWatcherStart') } + + return testModules } - async init() { + /** + * Initialize reporters and the coverage provider. This method doesn't run any tests. + * If the `--watch` flag is provided, Vitest will still run those tests. + */ + async init(): Promise { this._onClose = [] try { @@ -462,14 +512,14 @@ export class Vitest { } // populate test files cache so watch mode can trigger a file rerun - await this.globTestFiles() + await this.globTestSpecifications() if (this.config.watch) { await this.report('onWatcherStart') } } - private async getTestDependencies(spec: WorkspaceSpec, deps = new Set()) { + private async getTestDependencies(spec: TestSpecification, deps = new Set()): Promise> { const addImports = async (project: TestProject, filepath: string) => { if (deps.has(filepath)) { return @@ -497,7 +547,7 @@ export class Vitest { return deps } - async filterTestsBySource(specs: WorkspaceSpec[]) { + private async filterTestsBySource(specs: TestSpecification[]): Promise { if (this.config.changed && !this.config.related) { const { VitestGit } = await import('./git') const vitestGit = new VitestGit(this.config.root) @@ -536,10 +586,10 @@ export class Vitest { const runningTests = [] - for (const [filepath, deps] of testGraphs) { + for (const [specification, deps] of testGraphs) { // if deps or the test itself were changed - if (related.some(path => path === filepath[1] || deps.has(path))) { - runningTests.push(filepath) + if (related.some(path => path === specification.moduleId || deps.has(path))) { + runningTests.push(specification) } } @@ -547,32 +597,41 @@ export class Vitest { } /** - * @deprecated remove when vscode extension supports "getFileWorkspaceSpecs" + * @deprecated remove when vscode extension supports "getModuleTestSpecifications" */ - getProjectsByTestFile(file: string) { - return this.getFileWorkspaceSpecs(file) as WorkspaceSpec[] + getProjectsByTestFile(file: string): WorkspaceSpec[] { + return this.getModuleTestSpecifications(file) as WorkspaceSpec[] + } + + /** @deprecated */ + getFileWorkspaceSpecs(file: string): WorkspaceSpec[] { + return this.getModuleTestSpecifications(file) as WorkspaceSpec[] } - getFileWorkspaceSpecs(file: string) { - const _cached = this._cachedSpecs.get(file) + /** + * Get test specifications assosiated with the given module. If module is not a test file, an empty array is returned. + * @param moduleId The module ID to get test specifications for. + */ + public getModuleTestSpecifications(moduleId: string): TestSpecification[] { + const _cached = this._cachedSpecs.get(moduleId) if (_cached) { return _cached } const specs: TestSpecification[] = [] for (const project of this.projects) { - if (project.isTestFile(file)) { - specs.push(project.createSpecification(file)) + if (project.isTestFile(moduleId)) { + specs.push(project.createSpecification(moduleId)) } - if (project.isTypecheckFile(file)) { - specs.push(project.createSpecification(file, 'typescript')) + if (project.isTypecheckFile(moduleId)) { + specs.push(project.createSpecification(moduleId, 'typescript')) } } - specs.forEach(spec => this.ensureSpecCached(spec)) + specs.forEach(spec => this.ensureSpecificationCached(spec)) return specs } - async initializeGlobalSetup(paths: TestSpecification[]) { + private async initializeGlobalSetup(paths: TestSpecification[]): Promise { const projects = new Set(paths.map(spec => spec.project)) const coreProject = this.getRootTestProject() if (!projects.has(coreProject)) { @@ -583,7 +642,16 @@ export class Vitest { } } - async runFiles(specs: TestSpecification[], allTestsRun: boolean) { + /** + * Run tests for the given test specifications. + * @param specitifactions A list of specifications to run. + * @param allTestsRun Indicates whether all tests were run. This only matters for coverage. + */ + public async runTestSpecifications(specitifactions: TestSpecification[], allTestsRun = false): Promise { + return await this.runFiles(specitifactions, allTestsRun) + } + + private async runFiles(specs: TestSpecification[], allTestsRun: boolean): Promise { const filepaths = specs.map(spec => spec.moduleId) this.state.collectPaths(filepaths) @@ -628,6 +696,11 @@ export class Vitest { this.cache.results.updateResults(files) await this.cache.results.writeToCache() + + return { + tests: this.state.getTestModules(), + errors: this.state.getUnhandledErrors(), + } } finally { // can be duplicate files if different projects are using the same file @@ -635,7 +708,7 @@ export class Vitest { const errors = this.state.getUnhandledErrors() const coverage = await this.coverageProvider?.generateCoverage({ allTestsRun }) - this.checkUnhandledErrors(errors) + this._checkUnhandledErrors(errors) await this.report('onFinished', this.state.getFiles(files), errors, coverage) await this.reportCoverage(coverage, allTestsRun) } @@ -652,7 +725,7 @@ export class Vitest { return await this.runningPromise } - async collectFiles(specs: WorkspaceSpec[]) { + async collectFiles(specs: TestSpecification[]): Promise { const filepaths = specs.map(spec => spec.moduleId) this.state.collectPaths(filepaths) @@ -688,6 +761,11 @@ export class Vitest { if (hasFailed(files)) { process.exitCode = 1 } + + return { + tests: this.state.getTestModules(), + errors: this.state.getUnhandledErrors(), + } })() .finally(() => { this.runningPromise = undefined @@ -715,23 +793,20 @@ export class Vitest { } if (this.filenamePattern) { - const filteredFiles = await this.globTestFiles(this.filenamePattern) + const filteredFiles = await this.globTestSpecifications(this.filenamePattern) files = files.filter(file => filteredFiles.some(f => f[1] === file)) } + const specifications = files.flatMap(file => this.getModuleTestSpecifications(file)) await Promise.all([ this.report('onWatcherRerun', files, trigger), - ...this._onUserTestsRerun.map(fn => fn(files)), + ...this._onUserTestsRerun.map(fn => fn(specifications)), ]) - await this.runFiles(files.flatMap(file => this.getProjectsByTestFile(file)), allTestsRun) + await this.runFiles(specifications, allTestsRun) await this.report('onWatcherStart', this.state.getFiles(files)) } - private isSuite(task: RunnerTask): task is RunnerTestSuite { - return Object.hasOwnProperty.call(task, 'tasks') - } - async rerunTask(id: string) { const task = this.state.idMap.get(id) if (!task) { @@ -740,7 +815,7 @@ export class Vitest { await this.changeNamePattern( task.name, [task.file.filepath], - this.isSuite(task) ? 'rerun suite' : 'rerun test', + 'tasks' in task ? 'rerun suite' : 'rerun test', ) } @@ -753,7 +828,7 @@ export class Vitest { } this.projects = this.resolvedProjects.filter(p => p.name === pattern) - const files = (await this.globTestSpecs()).map(spec => spec.moduleId) + const files = (await this.globTestSpecifications()).map(spec => spec.moduleId) await this.rerunFiles(files, 'change project filter', pattern === '') } @@ -787,10 +862,18 @@ export class Vitest { } async rerunFailed() { - await this.rerunFiles(this.state.getFailedFilepaths(), 'rerun failed', false) + const failedTests = this.state.getFailedFilepaths() + if (!failedTests.length) { + return + } + await this.rerunFiles(failedTests, 'rerun failed', false) } - async updateSnapshot(files?: string[]) { + /** + * Run tests, and update snapshots for failed tests. + * @param files Files to update snapshot for. If not provided, all failed files and unchecked files will be updated. + */ + async updateSnapshot(files?: string[]): Promise { // default to failed files files = files || [ ...this.state.getFailedFilepaths(), @@ -812,7 +895,7 @@ export class Vitest { } private _rerunTimer: any - private async scheduleRerun(triggerId: string[]) { + private async scheduleRerun(triggerId: string[]): Promise { const currentCount = this.restartsCount clearTimeout(this._rerunTimer) await this.runningPromise @@ -862,26 +945,21 @@ export class Vitest { const triggerIds = new Set(triggerId.map(id => relative(this.config.root, id))) const triggerLabel = Array.from(triggerIds).join(', ') + const specifications = files.flatMap(file => this.getProjectsByTestFile(file)) await Promise.all([ this.report('onWatcherRerun', files, triggerLabel), - ...this._onUserTestsRerun.map(fn => fn(files)), + ...this._onUserTestsRerun.map(fn => fn(specifications)), ]) - await this.runFiles(files.flatMap(file => this.getProjectsByTestFile(file)), false) + await this.runFiles(specifications, false) await this.report('onWatcherStart', this.state.getFiles(files)) }, WATCHER_DEBOUNCE) } - public getModuleProjects(filepath: string) { - return this.projects.filter((project) => { - return project.getModulesByFilepath(filepath).size - // TODO: reevaluate || project.browser?.moduleGraph.getModulesByFile(id)?.size - }) - } - /** * Watch only the specified tests. If no tests are provided, all tests will be watched. + * @deprecated Do not use this method. It will be replaced with a different API in the future. */ public watchTests(tests: string[]) { this.watchedTests = new Set( @@ -889,11 +967,10 @@ export class Vitest { ) } - private updateLastChanged(filepath: string) { - const projects = this.getModuleProjects(filepath) - projects.forEach(({ server, browser }) => { - const serverMods = server.moduleGraph.getModulesByFile(filepath) - serverMods?.forEach(mod => server.moduleGraph.invalidateModule(mod)) + public invalidateFile(filepath: string): void { + this.projects.forEach(({ vite, browser }) => { + const serverMods = vite.moduleGraph.getModulesByFile(filepath) + serverMods?.forEach(mod => vite.moduleGraph.invalidateModule(mod)) if (browser) { const browserMods = browser.vite.moduleGraph.getModulesByFile(filepath) @@ -902,17 +979,22 @@ export class Vitest { }) } - onChange = (id: string) => { + /** @deprecated use `invalidateFile` */ + updateLastChanged(filepath: string): void { + this.invalidateFile(filepath) + } + + private onChange = (id: string): void => { id = slash(id) this.logger.clearHighlightCache(id) - this.updateLastChanged(id) + this.invalidateFile(id) const needsRerun = this.handleFileChanged(id) if (needsRerun.length) { this.scheduleRerun(needsRerun) } } - onUnlink = (id: string) => { + private onUnlink = (id: string): void => { id = slash(id) this.logger.clearHighlightCache(id) this.invalidates.add(id) @@ -926,9 +1008,9 @@ export class Vitest { } } - onAdd = async (id: string) => { + private onAdd = (id: string): void => { id = slash(id) - this.updateLastChanged(id) + this.invalidateFile(id) const fileContent = readFileSync(id, 'utf-8') const matchingProjects: TestProject[] = [] @@ -952,14 +1034,15 @@ export class Vitest { } } - checkUnhandledErrors(errors: unknown[]) { + /** @internal */ + public _checkUnhandledErrors(errors: unknown[]): void { if (errors.length && !this.config.dangerouslyIgnoreUnhandledErrors) { process.exitCode = 1 } } private unregisterWatcher = noop - private registerWatcher() { + private registerWatcher(): void { const watcher = this.server.watcher if (this.config.forceRerunTriggers.length) { @@ -991,7 +1074,10 @@ export class Vitest { return [filepath] } - const projects = this.getModuleProjects(filepath) + const projects = this.projects.filter((project) => { + const moduleGraph = project.browser?.vite.moduleGraph || project.vite.moduleGraph + return moduleGraph.getModulesByFile(filepath)?.size + }) if (!projects.length) { // if there are no modules it's possible that server was restarted // we don't have information about importers anymore, so let's check if the file is a test file at least @@ -1005,8 +1091,9 @@ export class Vitest { const files: string[] = [] for (const project of projects) { - const mods = project.getModulesByFilepath(filepath) - if (!mods.size) { + const mods = project.browser?.vite.moduleGraph.getModulesByFile(filepath) + || project.vite.moduleGraph.getModulesByFile(filepath) + if (!mods || !mods.size) { continue } @@ -1041,7 +1128,7 @@ export class Vitest { return Array.from(new Set(files)) } - private async reportCoverage(coverage: unknown, allTestsRun: boolean) { + private async reportCoverage(coverage: unknown, allTestsRun: boolean): Promise { if (this.state.getCountOfFailedTests() > 0) { await this.coverageProvider?.onTestFailure?.() @@ -1061,11 +1148,14 @@ export class Vitest { } } - async close() { + /** + * Closes all projects and their associated resources. This can only be called once; the closing promise is cached until the server restarts. + */ + public async close(): Promise { if (!this.closingPromise) { this.closingPromise = (async () => { const teardownProjects = [...this.projects] - if (!teardownProjects.includes(this.coreWorkspaceProject)) { + if (this.coreWorkspaceProject && !teardownProjects.includes(this.coreWorkspaceProject)) { teardownProjects.push(this.coreWorkspaceProject) } // do teardown before closing the server @@ -1076,7 +1166,7 @@ export class Vitest { const closePromises: unknown[] = this.resolvedProjects.map(w => w.close()) // close the core workspace server only once // it's possible that it's not initialized at all because it's not running any tests - if (!this.resolvedProjects.includes(this.coreWorkspaceProject)) { + if (this.coreWorkspaceProject && !this.resolvedProjects.includes(this.coreWorkspaceProject)) { closePromises.push(this.coreWorkspaceProject.close().then(() => this.server = undefined as any)) } @@ -1104,9 +1194,10 @@ export class Vitest { } /** - * Close the thread pool and exit the process + * Closes all projects and exit the process + * @param force If true, the process will exit immediately after closing the projects. */ - async exit(force = false) { + async exit(force = false): Promise { setTimeout(() => { this.report('onProcessTimeout').then(() => { console.warn(`close timed out after ${this.config.teardownTimeout}ms`) @@ -1121,7 +1212,9 @@ export class Vitest { else if (runningServers > 1) { console.warn(`Tests closed successfully but something prevents ${runningServers} Vite servers from exiting`) } - else { console.warn('Tests closed successfully but something prevents the main process from exiting') } + else { + console.warn('Tests closed successfully but something prevents the main process from exiting') + } console.warn('You can try to identify the cause by enabling "hanging-process" reporter. See https://vitest.dev/config/#reporters') } @@ -1136,6 +1229,7 @@ export class Vitest { } } + /** @internal */ async report(name: T, ...args: ArgumentsType) { await Promise.all(this.reporters.map(r => r[name]?.( // @ts-expect-error let me go @@ -1143,11 +1237,17 @@ export class Vitest { ))) } - public async getTestFilepaths() { - return this.globTestSpecs().then(specs => specs.map(spec => spec.moduleId)) + /** @internal */ + public async _globTestFilepaths() { + const specifications = await this.globTestSpecifications() + return Array.from(specifications.map(spec => spec.moduleId)) } - public async globTestSpecs(filters: string[] = []) { + /** + * Glob test files in every project and create a TestSpecification for each file and pool. + * @param filters String filters to match the test files. + */ + public async globTestSpecifications(filters: string[] = []): Promise { const files: TestSpecification[] = [] const dir = process.cwd() const parsedFilters = filters.map(f => parseFilter(f)) @@ -1177,7 +1277,7 @@ export class Vitest { testLocHasMatch[file] = true const spec = project.createSpecification(file, undefined, loc) - this.ensureSpecCached(spec) + this.ensureSpecificationCached(spec) files.push(spec) }) typecheckTestFiles.forEach((file) => { @@ -1185,7 +1285,7 @@ export class Vitest { testLocHasMatch[file] = true const spec = project.createSpecification(file, 'typescript', loc) - this.ensureSpecCached(spec) + this.ensureSpecificationCached(spec) files.push(spec) }) })) @@ -1202,14 +1302,21 @@ export class Vitest { } /** - * @deprecated use `globTestSpecs` instead + * @deprecated use `globTestSpecifications` instead + */ + public async globTestSpecs(filters: string[] = []) { + return this.globTestSpecifications(filters) + } + + /** + * @deprecated use `globTestSpecifications` instead */ public async globTestFiles(filters: string[] = []) { - return this.globTestSpecs(filters) + return this.globTestSpecifications(filters) } - private ensureSpecCached(spec: TestSpecification) { - const file = spec[1] + private ensureSpecificationCached(spec: TestSpecification) { + const file = spec.moduleId const specs = this._cachedSpecs.get(file) || [] const included = specs.some(_s => _s[0] === spec[0] && _s[2].pool === spec[2].pool) if (!included) { @@ -1218,7 +1325,9 @@ export class Vitest { } } - // The server needs to be running for communication + /** + * Should the server be kept running after the tests are done. + */ shouldKeepServer() { return !!this.config?.watch } diff --git a/packages/vitest/src/node/plugins/workspace.ts b/packages/vitest/src/node/plugins/workspace.ts index 39e2b6e3740e..a3e91bdcf29e 100644 --- a/packages/vitest/src/node/plugins/workspace.ts +++ b/packages/vitest/src/node/plugins/workspace.ts @@ -91,8 +91,8 @@ export function WorkspaceVitestPlugin( middlewareMode: true, fs: { allow: resolveFsAllow( - project.ctx.config.root, - project.ctx.server.config.configFile, + project.vitest.config.root, + project.vitest.server.config.configFile, ), }, }, diff --git a/packages/vitest/src/node/pool.ts b/packages/vitest/src/node/pool.ts index ef11f6167966..6562008b8989 100644 --- a/packages/vitest/src/node/pool.ts +++ b/packages/vitest/src/node/pool.ts @@ -30,7 +30,7 @@ export type WorkspaceSpec = TestSpecification & [ ] export type RunWithFiles = ( - files: WorkspaceSpec[], + files: TestSpecification[], invalidates?: string[] ) => Awaitable @@ -116,7 +116,7 @@ export function createPool(ctx: Vitest): ProcessPool { || execArg.startsWith('--diagnostic-dir'), ) - async function executeTests(method: 'runTests' | 'collectTests', files: WorkspaceSpec[], invalidate?: string[]) { + async function executeTests(method: 'runTests' | 'collectTests', files: TestSpecification[], invalidate?: string[]) { const options: PoolProcessOptions = { execArgv: [...execArgv, ...conditions], env: { @@ -166,7 +166,7 @@ export function createPool(ctx: Vitest): ProcessPool { return poolInstance as ProcessPool } - const filesByPool: Record = { + const filesByPool: Record = { forks: [], threads: [], vmThreads: [], diff --git a/packages/vitest/src/node/pools/rpc.ts b/packages/vitest/src/node/pools/rpc.ts index 4c41417fdae8..91cfc08f7f3b 100644 --- a/packages/vitest/src/node/pools/rpc.ts +++ b/packages/vitest/src/node/pools/rpc.ts @@ -98,7 +98,7 @@ export function createMethodsRPC(project: TestProject, options: MethodsOptions = }, onFinished(files) { const errors = ctx.state.getUnhandledErrors() - ctx.checkUnhandledErrors(errors) + ctx._checkUnhandledErrors(errors) return ctx.report('onFinished', files, errors) }, diff --git a/packages/vitest/src/node/pools/typecheck.ts b/packages/vitest/src/node/pools/typecheck.ts index 5a0bcae3a9c3..6bb0f5704ade 100644 --- a/packages/vitest/src/node/pools/typecheck.ts +++ b/packages/vitest/src/node/pools/typecheck.ts @@ -1,8 +1,9 @@ import type { DeferPromise } from '@vitest/utils' import type { TypecheckResults } from '../../typecheck/typechecker' import type { Vitest } from '../core' -import type { ProcessPool, WorkspaceSpec } from '../pool' +import type { ProcessPool } from '../pool' import type { TestProject } from '../project' +import type { TestSpecification } from '../spec' import { hasFailed } from '@vitest/runner/utils' import { createDefer } from '@vitest/utils' import { Typechecker } from '../../typecheck/typechecker' @@ -99,7 +100,7 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool { await checker.start() } - async function collectTests(specs: WorkspaceSpec[]) { + async function collectTests(specs: TestSpecification[]) { const specsByProject = groupBy(specs, spec => spec.project.name) for (const name in specsByProject) { const project = specsByProject[name][0].project @@ -112,13 +113,13 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool { } } - async function runTests(specs: WorkspaceSpec[]) { + async function runTests(specs: TestSpecification[]) { const specsByProject = groupBy(specs, spec => spec.project.name) const promises: Promise[] = [] for (const name in specsByProject) { - const project = specsByProject[name][0][0] - const files = specsByProject[name].map(([_, file]) => file) + const project = specsByProject[name][0].project + const files = specsByProject[name].map(spec => spec.moduleId) const promise = createDefer() // check that watcher actually triggered rerun const _p = new Promise((resolve) => { diff --git a/packages/vitest/src/node/pools/vmThreads.ts b/packages/vitest/src/node/pools/vmThreads.ts index bbdbbf7c00cb..7eb84e928019 100644 --- a/packages/vitest/src/node/pools/vmThreads.ts +++ b/packages/vitest/src/node/pools/vmThreads.ts @@ -165,7 +165,7 @@ export function createVmThreadsPool( return configs.get(project)! } - const config = project.getSerializableConfig() + const config = project.serializedConfig configs.set(project, config) return config } diff --git a/packages/vitest/src/node/reporters/reported-tasks.ts b/packages/vitest/src/node/reporters/reported-tasks.ts index cd918e7bfbd4..4072a08623bf 100644 --- a/packages/vitest/src/node/reporters/reported-tasks.ts +++ b/packages/vitest/src/node/reporters/reported-tasks.ts @@ -149,6 +149,13 @@ export class TestCase extends ReportedTaskImplementation { return !result || result.state !== 'failed' } + /** + * Checks if the test was skipped. + */ + public skipped(): boolean { + return this.task.mode !== 'run' && this.task.mode !== 'only' + } + /** * Custom metadata that was attached to the test during its execution. */ diff --git a/packages/vitest/src/node/stdin.ts b/packages/vitest/src/node/stdin.ts index 705980b1ecb3..f93c7ae56783 100644 --- a/packages/vitest/src/node/stdin.ts +++ b/packages/vitest/src/node/stdin.ts @@ -98,7 +98,7 @@ export function registerConsoleShortcuts( } // rerun all tests if (name === 'a' || name === 'return') { - const files = await ctx.getTestFilepaths() + const files = await ctx._globTestFilepaths() return ctx.changeNamePattern('', files, 'rerun all tests') } // rerun current pattern tests @@ -167,7 +167,7 @@ export function registerConsoleShortcuts( // if running in standalone mode, Vitest instance doesn't know about any test file const cliFiles = ctx.config.standalone && !files.length - ? await ctx.getTestFilepaths() + ? await ctx._globTestFilepaths() : undefined await ctx.changeNamePattern( diff --git a/packages/vitest/src/node/types/tests.ts b/packages/vitest/src/node/types/tests.ts new file mode 100644 index 000000000000..63b76515f3a7 --- /dev/null +++ b/packages/vitest/src/node/types/tests.ts @@ -0,0 +1,6 @@ +import type { TestModule } from '../reporters' + +export interface TestRunResult { + tests: TestModule[] + errors: unknown[] +} diff --git a/packages/vitest/src/types/general.ts b/packages/vitest/src/types/general.ts index 2d46006f0da4..ae9cca25fae0 100644 --- a/packages/vitest/src/types/general.ts +++ b/packages/vitest/src/types/general.ts @@ -1,3 +1,5 @@ +import type { TestSpecification } from '../node/spec' + export type { ErrorWithDiff, ParsedStack } from '@vitest/utils' export type Awaitable = T | PromiseLike @@ -46,5 +48,5 @@ export interface ModuleGraphData { } export type OnServerRestartHandler = (reason?: string) => Promise | void -export type OnTestsRerunHandler = (testFiles: string[]) => Promise | void +export type OnTestsRerunHandler = (testFiles: TestSpecification[]) => Promise | void export interface ProvidedContext {} diff --git a/packages/vitest/src/utils/coverage.ts b/packages/vitest/src/utils/coverage.ts index 2d60ef22cc63..ba470ebe8a1c 100644 --- a/packages/vitest/src/utils/coverage.ts +++ b/packages/vitest/src/utils/coverage.ts @@ -196,7 +196,7 @@ export class BaseCoverageProvider) { const filenames = Object.values(coverageByTestfiles) - const project = this.ctx.getProjectByName(projectName as string) + const project = this.ctx.getTestProjectByName(projectName as string) for (const chunk of this.toSlices(filenames, this.options.processingConcurrency)) { if (onDebug.enabled) { diff --git a/packages/vitest/src/utils/graph.ts b/packages/vitest/src/utils/graph.ts index 3e3cd783524e..840218fec19c 100644 --- a/packages/vitest/src/utils/graph.ts +++ b/packages/vitest/src/utils/graph.ts @@ -12,7 +12,7 @@ export async function getModuleGraph( const externalized = new Set() const inlined = new Set() - const project = ctx.getProjectByName(projectName) + const project = ctx.getTestProjectByName(projectName) async function get(mod?: ModuleNode, seen = new Map()) { if (!mod || !mod.id) { diff --git a/packages/vitest/src/utils/test-helpers.ts b/packages/vitest/src/utils/test-helpers.ts index b343d154eff6..9121c272696f 100644 --- a/packages/vitest/src/utils/test-helpers.ts +++ b/packages/vitest/src/utils/test-helpers.ts @@ -1,4 +1,4 @@ -import type { WorkspaceSpec } from '../node/pool' +import type { TestSpecification } from '../node/spec' import type { EnvironmentOptions, TransformModePatterns, VitestEnvironment } from '../node/types/config' import type { ContextTestEnvironment } from '../types/worker' import { promises as fs } from 'node:fs' @@ -27,7 +27,7 @@ function getTransformMode( } export async function groupFilesByEnv( - files: Array, + files: Array, ) { const filesWithEnv = await Promise.all( files.map(async (spec) => { From 250c4e45aefbe87978134cb15937388e40d26ed7 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 25 Nov 2024 18:28:33 +0100 Subject: [PATCH 02/73] chore: cleanup --- packages/vitest/src/node/core.ts | 10 ++++++++-- packages/vitest/src/node/plugins/index.ts | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 16cb9a010e36..a3c1f2ee3670 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -55,7 +55,7 @@ export class Vitest { static readonly version = version public config: ResolvedConfig = undefined! - private configOverride: Partial = {} + configOverride: Partial = {} server: ViteDevServer = undefined! /** @@ -124,7 +124,13 @@ export class Vitest { private _onCancelListeners: ((reason: CancelReason) => Promise | void)[] = [] private _onUserTestsRerun: OnTestsRerunHandler[] = [] - async setServer(options: UserConfig, server: ViteDevServer, cliOptions: UserConfig) { + /** @deprecated internal */ + setServer(options: UserConfig, server: ViteDevServer, cliOptions: UserConfig) { + return this._setServer(options, server, cliOptions) + } + + /** @internal */ + async _setServer(options: UserConfig, server: ViteDevServer, cliOptions: UserConfig) { this._options = options this.unregisterWatcher?.() clearTimeout(this._rerunTimer) diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index 74d9aabdc97c..56ec4a3d34cd 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -269,7 +269,7 @@ export async function VitestPlugin( console.log('[debug] watcher is ready') }) } - await ctx.setServer(options, server, userConfig) + await ctx._setServer(options, server, userConfig) if (options.api && options.watch) { (await import('../../api/setup')).setup(ctx) } From a455c9431cec8b5046fddf4821a2e9534c9e707a Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 26 Nov 2024 10:48:44 +0100 Subject: [PATCH 03/73] chore: add more type annotations, move globbing into a separate class --- packages/vitest/src/api/setup.ts | 2 +- packages/vitest/src/node/core.ts | 360 +++++++++----------------- packages/vitest/src/node/files.ts | 169 ++++++++++++ packages/vitest/src/utils/coverage.ts | 2 +- packages/vitest/src/utils/graph.ts | 2 +- 5 files changed, 300 insertions(+), 235 deletions(-) create mode 100644 packages/vitest/src/node/files.ts diff --git a/packages/vitest/src/api/setup.ts b/packages/vitest/src/api/setup.ts index 262049c3973e..c82fec2a6bd3 100644 --- a/packages/vitest/src/api/setup.ts +++ b/packages/vitest/src/api/setup.ts @@ -82,7 +82,7 @@ export function setup(ctx: Vitest, _server?: ViteDevServer) { return ctx.getRootTestProject().serializedConfig }, async getTransformResult(projectName: string, id, browser = false) { - const project = ctx.getTestProjectByName(projectName) + const project = ctx.getProjectByName(projectName) const result: TransformResultWithSource | null | undefined = browser ? await project.browser!.vite.transformRequest(id) : await project.vitenode.transformRequest(id) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index a3c1f2ee3670..ddf15b4c76b4 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -1,4 +1,5 @@ import type { CancelReason, File, TaskResultPack } from '@vitest/runner' +import type { Awaitable } from '@vitest/utils' import type { Writable } from 'node:stream' import type { ViteDevServer } from 'vite' import type { defineWorkspace } from 'vitest/config' @@ -10,8 +11,7 @@ import type { ResolvedConfig, UserConfig, VitestRunMode } from './types/config' import type { CoverageProvider } from './types/coverage' import type { Reporter } from './types/reporter' import type { TestRunResult } from './types/tests' -import { existsSync, promises as fs, readFileSync } from 'node:fs' -import { resolve } from 'node:path' +import { promises as fs, readFileSync } from 'node:fs' import { getTasks, hasFailed } from '@vitest/runner/utils' import { SnapshotManager } from '@vitest/snapshot/manager' import { noop, slash, toArray } from '@vitest/utils' @@ -26,9 +26,9 @@ import { getCoverageProvider } from '../integrations/coverage' import { distDir } from '../paths' import { wildcardPatternToRegExp } from '../utils/base' import { VitestCache } from './cache' -import { groupFilters, parseFilter } from './cli/filter' import { resolveConfig } from './config/resolveConfig' -import { FilesNotFoundError, GitNotFoundError, IncludeTaskLocationDisabledError, LocationFilterFileNotFoundError } from './errors' +import { FilesNotFoundError } from './errors' +import { VitestSpecifications } from './files' import { Logger } from './logger' import { VitestPackageInstaller } from './packageInstaller' import { createPool } from './pool' @@ -53,24 +53,29 @@ export class Vitest { */ public readonly version = version static readonly version = version + public readonly logger: Logger + public readonly packageInstaller: VitestPackageInstaller + public readonly distPath = distDir - public config: ResolvedConfig = undefined! + public projects: TestProject[] = [] + + /** @internal */ configOverride: Partial = {} + /** @internal */ + reporters: Reporter[] = undefined! server: ViteDevServer = undefined! /** * @experimental The State API is experimental and not subject to semver. */ - public state: StateManager = undefined! + state: StateManager = undefined! snapshot: SnapshotManager = undefined! cache: VitestCache = undefined! - /** @internal */ - reporters: Reporter[] = undefined! coverageProvider: CoverageProvider | null | undefined - logger: Logger pool: ProcessPool | undefined vitenode: ViteNodeServer = undefined! + runner: ViteNodeRunner = undefined! invalidates: Set = new Set() changedTests: Set = new Set() @@ -87,11 +92,6 @@ export class Vitest { isFirstRun = true /** @internal */ restartsCount = 0 - runner: ViteNodeRunner = undefined! - - public packageInstaller: VitestPackageInstaller - public projects: TestProject[] = [] - public distPath = distDir /** TODO: rename to `_coreRootProject` */ /** @internal */ @@ -99,11 +99,9 @@ export class Vitest { /** @internal */ public resolvedProjects: TestProject[] = [] - private _cachedSpecs = new Map() + private _config: ResolvedConfig | undefined private _workspaceConfigPath?: string - - /** @deprecated use `_cachedSpecs` */ - projectTestFiles = this._cachedSpecs + private specifications: VitestSpecifications /** @internal */ public _browserLastPort = defaultBrowserPort @@ -116,14 +114,23 @@ export class Vitest { ) { this.logger = new Logger(this, options.stdout, options.stderr) this.packageInstaller = options.packageInstaller || new VitestPackageInstaller() + this.specifications = new VitestSpecifications(this) } private _onRestartListeners: OnServerRestartHandler[] = [] - private _onClose: (() => Awaited)[] = [] + private _onClose: (() => Awaitable)[] = [] private _onSetServer: OnServerRestartHandler[] = [] - private _onCancelListeners: ((reason: CancelReason) => Promise | void)[] = [] + private _onCancelListeners: ((reason: CancelReason) => Awaitable)[] = [] private _onUserTestsRerun: OnTestsRerunHandler[] = [] + /** + * The global config. + */ + get config(): ResolvedConfig { + // FIXME: throw error if accessed before server is set + return this._config! + } + /** @deprecated internal */ setServer(options: UserConfig, server: ViteDevServer, cliOptions: UserConfig) { return this._setServer(options, server, cliOptions) @@ -144,13 +151,13 @@ export class Vitest { this._workspaceConfigPath = undefined this.coverageProvider = undefined this.runningPromise = undefined - this._cachedSpecs.clear() + this.specifications.clearCache() this._onUserTestsRerun = [] const resolved = resolveConfig(this.mode, options, server.config, this.logger) this.server = server - this.config = resolved + this._config = resolved this.state = new StateManager() this.cache = new VitestCache(this.version) this.snapshot = new SnapshotManager({ ...resolved.snapshotOptions }) @@ -262,14 +269,7 @@ export class Vitest { || this.projects[0] } - /** - * @deprecated use `getTestProjectByName` instead - */ - public getProjectByName(name = ''): TestProject { - return this.getTestProjectByName(name) - } - - public getTestProjectByName(name: string = ''): TestProject { + public getProjectByName(name: string = ''): TestProject { const project = this.projects.find(p => p.name === name) || this.coreWorkspaceProject || this.projects[0] @@ -365,7 +365,7 @@ export class Vitest { const workspaceSpecs = new Map() for (const file of files) { - const project = this.getTestProjectByName(file.projectName) + const project = this.getProjectByName(file.projectName) const specs = workspaceSpecs.get(project) || [] specs.push(file) workspaceSpecs.set(project, specs) @@ -420,16 +420,14 @@ export class Vitest { async collect(filters?: string[]): Promise { this._onClose = [] - const files = await this.filterTestsBySource( - await this.globTestSpecifications(filters), - ) + const files = await this.specifications.getRelevantTestSpecifications(filters) // if run with --changed, don't exit if no tests are found if (!files.length) { return { tests: [], errors: [] } } - return await this.collectFiles(files) + return this.collectTests(files) } /** @@ -437,11 +435,7 @@ export class Vitest { * @param filters String filters to match the test files */ async listFiles(filters?: string[]): Promise { - const files = await this.filterTestsBySource( - await this.globTestSpecifications(filters), - ) - - return files + return await this.specifications.getRelevantTestSpecifications(filters) } /** @@ -452,7 +446,7 @@ export class Vitest { * - `Error` from the user reporters * @param filters String filters to match the test files */ - async start(filters?: string[]) { + async start(filters?: string[]): Promise { this._onClose = [] try { @@ -464,9 +458,7 @@ export class Vitest { } this.filenamePattern = filters && filters?.length > 0 ? filters : undefined - const files = await this.filterTestsBySource( - await this.globTestSpecifications(filters), - ) + const files = await this.specifications.getRelevantTestSpecifications(filters) // if run with --changed, don't exit if no tests are found if (!files.length) { @@ -525,101 +517,24 @@ export class Vitest { } } - private async getTestDependencies(spec: TestSpecification, deps = new Set()): Promise> { - const addImports = async (project: TestProject, filepath: string) => { - if (deps.has(filepath)) { - return - } - deps.add(filepath) - - const mod = project.vite.moduleGraph.getModuleById(filepath) - const transformed = mod?.ssrTransformResult || await project.vitenode.transformRequest(filepath) - if (!transformed) { - return - } - const dependencies = [...transformed.deps || [], ...transformed.dynamicDeps || []] - await Promise.all(dependencies.map(async (dep) => { - const path = await project.vite.pluginContainer.resolveId(dep, filepath, { ssr: true }) - const fsPath = path && !path.external && path.id.split('?')[0] - if (fsPath && !fsPath.includes('node_modules') && !deps.has(fsPath) && existsSync(fsPath)) { - await addImports(project, fsPath) - } - })) - } - - await addImports(spec.project, spec.moduleId) - deps.delete(spec.moduleId) - - return deps - } - - private async filterTestsBySource(specs: TestSpecification[]): Promise { - if (this.config.changed && !this.config.related) { - const { VitestGit } = await import('./git') - const vitestGit = new VitestGit(this.config.root) - const related = await vitestGit.findChangedFiles({ - changedSince: this.config.changed, - }) - if (!related) { - process.exitCode = 1 - throw new GitNotFoundError() - } - this.config.related = Array.from(new Set(related)) - } - - const related = this.config.related - if (!related) { - return specs - } - - const forceRerunTriggers = this.config.forceRerunTriggers - if (forceRerunTriggers.length && mm(related, forceRerunTriggers).length) { - return specs - } - - // don't run anything if no related sources are found - // if we are in watch mode, we want to process all tests - if (!this.config.watch && !related.length) { - return [] - } - - const testGraphs = await Promise.all( - specs.map(async (spec) => { - const deps = await this.getTestDependencies(spec) - return [spec, deps] as const - }), - ) - - const runningTests = [] - - for (const [specification, deps] of testGraphs) { - // if deps or the test itself were changed - if (related.some(path => path === specification.moduleId || deps.has(path))) { - runningTests.push(specification) - } - } - - return runningTests - } - /** - * @deprecated remove when vscode extension supports "getModuleTestSpecifications" + * @deprecated remove when vscode extension supports "getModuleSpecifications" */ getProjectsByTestFile(file: string): WorkspaceSpec[] { - return this.getModuleTestSpecifications(file) as WorkspaceSpec[] + return this.getModuleSpecifications(file) as WorkspaceSpec[] } /** @deprecated */ getFileWorkspaceSpecs(file: string): WorkspaceSpec[] { - return this.getModuleTestSpecifications(file) as WorkspaceSpec[] + return this.getModuleSpecifications(file) as WorkspaceSpec[] } /** * Get test specifications assosiated with the given module. If module is not a test file, an empty array is returned. * @param moduleId The module ID to get test specifications for. */ - public getModuleTestSpecifications(moduleId: string): TestSpecification[] { - const _cached = this._cachedSpecs.get(moduleId) + public getModuleSpecifications(moduleId: string): TestSpecification[] { + const _cached = this.specifications.getCachedSpecifications(moduleId) if (_cached) { return _cached } @@ -633,28 +548,17 @@ export class Vitest { specs.push(project.createSpecification(moduleId, 'typescript')) } } - specs.forEach(spec => this.ensureSpecificationCached(spec)) + specs.forEach(spec => this.specifications.ensureSpecificationCached(spec)) return specs } - private async initializeGlobalSetup(paths: TestSpecification[]): Promise { - const projects = new Set(paths.map(spec => spec.project)) - const coreProject = this.getRootTestProject() - if (!projects.has(coreProject)) { - projects.add(coreProject) - } - for (const project of projects) { - await project._initializeGlobalSetup() - } - } - /** * Run tests for the given test specifications. - * @param specitifactions A list of specifications to run. + * @param specifications A list of specifications to run. * @param allTestsRun Indicates whether all tests were run. This only matters for coverage. */ - public async runTestSpecifications(specitifactions: TestSpecification[], allTestsRun = false): Promise { - return await this.runFiles(specitifactions, allTestsRun) + public runTestSpecifications(specifications: TestSpecification[], allTestsRun = false): Promise { + return this.runFiles(specifications, allTestsRun) } private async runFiles(specs: TestSpecification[], allTestsRun: boolean): Promise { @@ -688,7 +592,7 @@ export class Vitest { await this.initializeGlobalSetup(specs) try { - await this.pool.runTests(specs as WorkspaceSpec[], invalidates) + await this.pool.runTests(specs, invalidates) } catch (err) { this.state.catchError(err, 'Unhandled Error') @@ -731,8 +635,12 @@ export class Vitest { return await this.runningPromise } - async collectFiles(specs: TestSpecification[]): Promise { - const filepaths = specs.map(spec => spec.moduleId) + /** + * Collect tests in specified modules. Vitest will run the files to collect tests. + * @param specitifactions A list of specifications to run. + */ + public async collectTests(specitifactions: TestSpecification[]): Promise { + const filepaths = specitifactions.map(spec => spec.moduleId) this.state.collectPaths(filepaths) // previous run @@ -751,10 +659,10 @@ export class Vitest { this.snapshot.clear() this.state.clearErrors() - await this.initializeGlobalSetup(specs) + await this.initializeGlobalSetup(specitifactions) try { - await this.pool.collectTests(specs, invalidates) + await this.pool.collectTests(specitifactions, invalidates) } catch (err) { this.state.catchError(err, 'Unhandled Error') @@ -784,16 +692,49 @@ export class Vitest { return await this.runningPromise } - async cancelCurrentRun(reason: CancelReason) { + /** + * Gracefully cancel the current test run. Vitest will wait until all running tests are finished before cancelling. + */ + async cancelCurrentRun(reason: CancelReason): Promise { this.isCancelling = true await Promise.all(this._onCancelListeners.splice(0).map(listener => listener(reason))) } - async initBrowserServers() { + /** @internal */ + async initBrowserServers(): Promise { await Promise.all(this.projects.map(p => p._initBrowserServer())) } - async rerunFiles(files: string[] = this.state.getFilepaths(), trigger?: string, allTestsRun = true, resetTestNamePattern = false) { + private async initializeGlobalSetup(paths: TestSpecification[]): Promise { + const projects = new Set(paths.map(spec => spec.project)) + const coreProject = this.getRootTestProject() + if (!projects.has(coreProject)) { + projects.add(coreProject) + } + for (const project of projects) { + await project._initializeGlobalSetup() + } + } + + /** + * Rerun files and trigger `onWatcherRerun`, `onWatcherStart` and `onTestsRerun` events. + * @param specifications A list of specifications to run. + * @param allTestsRun Indicates whether all tests were run. This only matters for coverage. + */ + public async rerunTestSpecifications(specifications: TestSpecification[], allTestsRun = false): Promise { + this.configOverride.testNamePattern = undefined + const files = specifications.map(spec => spec.moduleId) + await Promise.all([ + this.report('onWatcherRerun', files, 'rerun test'), + ...this._onUserTestsRerun.map(fn => fn(specifications)), + ]) + await this.runTestSpecifications(specifications, allTestsRun) + + await this.report('onWatcherStart', this.state.getFiles(files)) + } + + /** @internal */ + async rerunFiles(files: string[] = this.state.getFilepaths(), trigger?: string, allTestsRun = true, resetTestNamePattern = false): Promise { if (resetTestNamePattern) { this.configOverride.testNamePattern = undefined } @@ -803,7 +744,7 @@ export class Vitest { files = files.filter(file => filteredFiles.some(f => f[1] === file)) } - const specifications = files.flatMap(file => this.getModuleTestSpecifications(file)) + const specifications = files.flatMap(file => this.getModuleSpecifications(file)) await Promise.all([ this.report('onWatcherRerun', files, trigger), ...this._onUserTestsRerun.map(fn => fn(specifications)), @@ -813,7 +754,8 @@ export class Vitest { await this.report('onWatcherStart', this.state.getFiles(files)) } - async rerunTask(id: string) { + /** @internal */ + async rerunTask(id: string): Promise { const task = this.state.idMap.get(id) if (!task) { throw new Error(`Task ${id} was not found`) @@ -825,7 +767,8 @@ export class Vitest { ) } - async changeProjectName(pattern: string) { + /** @internal */ + async changeProjectName(pattern: string): Promise { if (pattern === '') { delete this.configOverride.project } @@ -838,7 +781,8 @@ export class Vitest { await this.rerunFiles(files, 'change project filter', pattern === '') } - async changeNamePattern(pattern: string, files: string[] = this.state.getFilepaths(), trigger?: string) { + /** @internal */ + async changeNamePattern(pattern: string, files: string[] = this.state.getFilepaths(), trigger?: string): Promise { // Empty test name pattern should reset filename pattern as well if (pattern === '') { this.filenamePattern = undefined @@ -859,7 +803,8 @@ export class Vitest { await this.rerunFiles(files, trigger, pattern === '') } - async changeFilenamePattern(pattern: string, files: string[] = this.state.getFilepaths()) { + /** @internal */ + async changeFilenamePattern(pattern: string, files: string[] = this.state.getFilepaths()): Promise { this.filenamePattern = pattern ? [pattern] : [] const trigger = this.filenamePattern.length ? 'change filename pattern' : 'reset filename pattern' @@ -867,12 +812,9 @@ export class Vitest { await this.rerunFiles(files, trigger, pattern === '') } - async rerunFailed() { - const failedTests = this.state.getFailedFilepaths() - if (!failedTests.length) { - return - } - await this.rerunFiles(failedTests, 'rerun failed', false) + /** @internal */ + async rerunFailed(): Promise { + await this.rerunFiles(this.state.getFailedFilepaths(), 'rerun failed', false) } /** @@ -938,7 +880,7 @@ export class Vitest { let files = Array.from(this.changedTests) if (this.filenamePattern) { - const filteredFiles = await this.globTestFiles(this.filenamePattern) + const filteredFiles = await this.globTestSpecifications(this.filenamePattern) files = files.filter(file => filteredFiles.some(f => f[1] === file)) // A file that does not match the current filename pattern was changed @@ -951,7 +893,7 @@ export class Vitest { const triggerIds = new Set(triggerId.map(id => relative(this.config.root, id))) const triggerLabel = Array.from(triggerIds).join(', ') - const specifications = files.flatMap(file => this.getProjectsByTestFile(file)) + const specifications = files.flatMap(file => this.getModuleSpecifications(file)) await Promise.all([ this.report('onWatcherRerun', files, triggerLabel), ...this._onUserTestsRerun.map(fn => fn(specifications)), @@ -967,7 +909,7 @@ export class Vitest { * Watch only the specified tests. If no tests are provided, all tests will be watched. * @deprecated Do not use this method. It will be replaced with a different API in the future. */ - public watchTests(tests: string[]) { + public watchTests(tests: string[]): void { this.watchedTests = new Set( tests.map(test => slash(test)), ) @@ -1155,7 +1097,8 @@ export class Vitest { } /** - * Closes all projects and their associated resources. This can only be called once; the closing promise is cached until the server restarts. + * Closes all projects and their associated resources. + * This can only be called once; the closing promise is cached until the server restarts. */ public async close(): Promise { if (!this.closingPromise) { @@ -1246,7 +1189,7 @@ export class Vitest { /** @internal */ public async _globTestFilepaths() { const specifications = await this.globTestSpecifications() - return Array.from(specifications.map(spec => spec.moduleId)) + return Array.from(new Set(specifications.map(spec => spec.moduleId))) } /** @@ -1254,57 +1197,7 @@ export class Vitest { * @param filters String filters to match the test files. */ public async globTestSpecifications(filters: string[] = []): Promise { - const files: TestSpecification[] = [] - const dir = process.cwd() - const parsedFilters = filters.map(f => parseFilter(f)) - - // Require includeTaskLocation when a location filter is passed - if ( - !this.config.includeTaskLocation - && parsedFilters.some(f => f.lineNumber !== undefined) - ) { - throw new IncludeTaskLocationDisabledError() - } - - const testLocations = groupFilters(parsedFilters.map( - f => ({ ...f, filename: slash(resolve(dir, f.filename)) }), - )) - - // Key is file and val sepcifies whether we have matched this file with testLocation - const testLocHasMatch: { [f: string]: boolean } = {} - - await Promise.all(this.projects.map(async (project) => { - const { testFiles, typecheckTestFiles } = await project.globTestFiles( - parsedFilters.map(f => f.filename), - ) - - testFiles.forEach((file) => { - const loc = testLocations[file] - testLocHasMatch[file] = true - - const spec = project.createSpecification(file, undefined, loc) - this.ensureSpecificationCached(spec) - files.push(spec) - }) - typecheckTestFiles.forEach((file) => { - const loc = testLocations[file] - testLocHasMatch[file] = true - - const spec = project.createSpecification(file, 'typescript', loc) - this.ensureSpecificationCached(spec) - files.push(spec) - }) - })) - - Object.entries(testLocations).forEach(([filepath, loc]) => { - if (loc.length !== 0 && !testLocHasMatch[filepath]) { - throw new LocationFilterFileNotFoundError( - relative(dir, filepath), - ) - } - }) - - return files as WorkspaceSpec[] + return this.specifications.globTestSpecifications(filters) } /** @@ -1321,16 +1214,6 @@ export class Vitest { return this.globTestSpecifications(filters) } - private ensureSpecificationCached(spec: TestSpecification) { - const file = spec.moduleId - 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) - } - } - /** * Should the server be kept running after the tests are done. */ @@ -1338,23 +1221,36 @@ export class Vitest { return !!this.config?.watch } + /** + * Register a handler that will be called when the server is restarted due to a config change. + */ onServerRestart(fn: OnServerRestartHandler) { this._onRestartListeners.push(fn) } - onAfterSetServer(fn: OnServerRestartHandler) { - this._onSetServer.push(fn) - } - - onCancel(fn: (reason: CancelReason) => void) { + /** + * Register a handler that will be called when the test run was cancelled with `vitest.cancelCurrentRun`. + */ + onCancel(fn: (reason: CancelReason) => Awaitable) { this._onCancelListeners.push(fn) } - onClose(fn: () => void) { + /** + * Register a handler that will be called when the server is closed. + */ + onClose(fn: () => Awaitable) { this._onClose.push(fn) } + /** + * Register a handler that will be called when the tests are reruning. + */ onTestsRerun(fn: OnTestsRerunHandler): void { this._onUserTestsRerun.push(fn) } + + /** @internal */ + onAfterSetServer(fn: OnServerRestartHandler) { + this._onSetServer.push(fn) + } } diff --git a/packages/vitest/src/node/files.ts b/packages/vitest/src/node/files.ts new file mode 100644 index 000000000000..f3e5171712ed --- /dev/null +++ b/packages/vitest/src/node/files.ts @@ -0,0 +1,169 @@ +import type { Vitest } from './core' +import type { TestProject } from './reporters' +import type { TestSpecification } from './spec' +import { existsSync } from 'node:fs' +import mm from 'micromatch' +import { relative, resolve } from 'pathe' +import { groupFilters, parseFilter } from './cli/filter' +import { GitNotFoundError, IncludeTaskLocationDisabledError, LocationFilterFileNotFoundError } from './errors' + +export class VitestSpecifications { + private _cachedSpecs = new Map() + + constructor(private vitest: Vitest) {} + + public async getRelevantTestSpecifications(filters: string[] = []): Promise { + return this.filterTestsBySource( + await this.globTestSpecifications(filters), + ) + } + + public async globTestSpecifications(filters: string[] = []) { + const files: TestSpecification[] = [] + const dir = process.cwd() + const parsedFilters = filters.map(f => parseFilter(f)) + + // Require includeTaskLocation when a location filter is passed + if ( + !this.vitest.config.includeTaskLocation + && parsedFilters.some(f => f.lineNumber !== undefined) + ) { + throw new IncludeTaskLocationDisabledError() + } + + const testLocations = groupFilters(parsedFilters.map( + f => ({ ...f, filename: resolve(dir, f.filename) }), + )) + + // Key is file and val sepcifies whether we have matched this file with testLocation + const testLocHasMatch: { [f: string]: boolean } = {} + + await Promise.all(this.vitest.projects.map(async (project) => { + const { testFiles, typecheckTestFiles } = await project.globTestFiles( + parsedFilters.map(f => f.filename), + ) + + testFiles.forEach((file) => { + const loc = testLocations[file] + testLocHasMatch[file] = true + + const spec = project.createSpecification(file, undefined, loc) + this.ensureSpecificationCached(spec) + files.push(spec) + }) + typecheckTestFiles.forEach((file) => { + const loc = testLocations[file] + testLocHasMatch[file] = true + + const spec = project.createSpecification(file, 'typescript', loc) + this.ensureSpecificationCached(spec) + files.push(spec) + }) + })) + + Object.entries(testLocations).forEach(([filepath, loc]) => { + if (loc.length !== 0 && !testLocHasMatch[filepath]) { + throw new LocationFilterFileNotFoundError( + relative(dir, filepath), + ) + } + }) + + return files + } + + public getCachedSpecifications(moduleId: string): TestSpecification[] | undefined { + return this._cachedSpecs.get(moduleId) + } + + public clearCache(): void { + this._cachedSpecs.clear() + } + + public ensureSpecificationCached(spec: TestSpecification): void { + const file = spec.moduleId + const specs = this._cachedSpecs.get(file) || [] + const included = specs.some(_s => _s.project === spec.project && _s.pool === spec.pool) + if (!included) { + specs.push(spec) + this._cachedSpecs.set(file, specs) + } + } + + private async filterTestsBySource(specs: TestSpecification[]): Promise { + if (this.vitest.config.changed && !this.vitest.config.related) { + const { VitestGit } = await import('./git') + const vitestGit = new VitestGit(this.vitest.config.root) + const related = await vitestGit.findChangedFiles({ + changedSince: this.vitest.config.changed, + }) + if (!related) { + process.exitCode = 1 + throw new GitNotFoundError() + } + this.vitest.config.related = Array.from(new Set(related)) + } + + const related = this.vitest.config.related + if (!related) { + return specs + } + + const forceRerunTriggers = this.vitest.config.forceRerunTriggers + if (forceRerunTriggers.length && mm(related, forceRerunTriggers).length) { + return specs + } + + // don't run anything if no related sources are found + // if we are in watch mode, we want to process all tests + if (!this.vitest.config.watch && !related.length) { + return [] + } + + const testGraphs = await Promise.all( + specs.map(async (spec) => { + const deps = await this.getTestDependencies(spec) + return [spec, deps] as const + }), + ) + + const runningTests = [] + + for (const [specification, deps] of testGraphs) { + // if deps or the test itself were changed + if (related.some(path => path === specification.moduleId || deps.has(path))) { + runningTests.push(specification) + } + } + + return runningTests + } + + private async getTestDependencies(spec: TestSpecification, deps = new Set()): Promise> { + const addImports = async (project: TestProject, filepath: string) => { + if (deps.has(filepath)) { + return + } + deps.add(filepath) + + const mod = project.vite.moduleGraph.getModuleById(filepath) + const transformed = mod?.ssrTransformResult || await project.vitenode.transformRequest(filepath) + if (!transformed) { + return + } + const dependencies = [...transformed.deps || [], ...transformed.dynamicDeps || []] + await Promise.all(dependencies.map(async (dep) => { + const path = await project.vite.pluginContainer.resolveId(dep, filepath, { ssr: true }) + const fsPath = path && !path.external && path.id.split('?')[0] + if (fsPath && !fsPath.includes('node_modules') && !deps.has(fsPath) && existsSync(fsPath)) { + await addImports(project, fsPath) + } + })) + } + + await addImports(spec.project, spec.moduleId) + deps.delete(spec.moduleId) + + return deps + } +} diff --git a/packages/vitest/src/utils/coverage.ts b/packages/vitest/src/utils/coverage.ts index ba470ebe8a1c..2d60ef22cc63 100644 --- a/packages/vitest/src/utils/coverage.ts +++ b/packages/vitest/src/utils/coverage.ts @@ -196,7 +196,7 @@ export class BaseCoverageProvider) { const filenames = Object.values(coverageByTestfiles) - const project = this.ctx.getTestProjectByName(projectName as string) + const project = this.ctx.getProjectByName(projectName as string) for (const chunk of this.toSlices(filenames, this.options.processingConcurrency)) { if (onDebug.enabled) { diff --git a/packages/vitest/src/utils/graph.ts b/packages/vitest/src/utils/graph.ts index 840218fec19c..3e3cd783524e 100644 --- a/packages/vitest/src/utils/graph.ts +++ b/packages/vitest/src/utils/graph.ts @@ -12,7 +12,7 @@ export async function getModuleGraph( const externalized = new Set() const inlined = new Set() - const project = ctx.getTestProjectByName(projectName) + const project = ctx.getProjectByName(projectName) async function get(mod?: ModuleNode, seen = new Map()) { if (!mod || !mod.id) { From 7f48a4bfb4341df3f9b43b846827f9c24a6fb264 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 26 Nov 2024 10:49:27 +0100 Subject: [PATCH 04/73] chore: cleanup --- packages/vitest/src/node/core.ts | 2 +- packages/vitest/src/node/{files.ts => specifications.ts} | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) rename packages/vitest/src/node/{files.ts => specifications.ts} (98%) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index ddf15b4c76b4..6a6f7369e8d2 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -28,13 +28,13 @@ import { wildcardPatternToRegExp } from '../utils/base' import { VitestCache } from './cache' import { resolveConfig } from './config/resolveConfig' import { FilesNotFoundError } from './errors' -import { VitestSpecifications } from './files' import { Logger } from './logger' import { VitestPackageInstaller } from './packageInstaller' import { createPool } from './pool' import { TestProject } from './project' import { BlobReporter, readBlobs } from './reporters/blob' import { createBenchmarkReporters, createReporters } from './reporters/utils' +import { VitestSpecifications } from './specifications' import { StateManager } from './state' import { resolveWorkspace } from './workspace/resolveWorkspace' diff --git a/packages/vitest/src/node/files.ts b/packages/vitest/src/node/specifications.ts similarity index 98% rename from packages/vitest/src/node/files.ts rename to packages/vitest/src/node/specifications.ts index f3e5171712ed..74d65a9ab5d3 100644 --- a/packages/vitest/src/node/files.ts +++ b/packages/vitest/src/node/specifications.ts @@ -80,7 +80,7 @@ export class VitestSpecifications { this._cachedSpecs.clear() } - public ensureSpecificationCached(spec: TestSpecification): void { + public ensureSpecificationCached(spec: TestSpecification): TestSpecification[] { const file = spec.moduleId const specs = this._cachedSpecs.get(file) || [] const included = specs.some(_s => _s.project === spec.project && _s.pool === spec.pool) @@ -88,6 +88,7 @@ export class VitestSpecifications { specs.push(spec) this._cachedSpecs.set(file, specs) } + return specs } private async filterTestsBySource(specs: TestSpecification[]): Promise { From 414c965d5180c40f6f9a8429487bd79c20842788 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 26 Nov 2024 11:00:09 +0100 Subject: [PATCH 05/73] feat: hide the task in reported tasks --- docs/advanced/reporters.md | 17 +---------------- .../vitest/src/node/reporters/reported-tasks.ts | 12 +++++++++++- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/docs/advanced/reporters.md b/docs/advanced/reporters.md index fe95c8e05583..63f4ea78ade1 100644 --- a/docs/advanced/reporters.md +++ b/docs/advanced/reporters.md @@ -93,12 +93,7 @@ class MyReporter implements Reporter { ```ts declare class TestCase { - readonly type = 'test' | 'custom' - /** - * Task instance. - * @experimental Public task API is experimental and does not follow semver. - */ - readonly task: RunnerTestCase | RunnerCustomCase + readonly type = 'test' /** * The project associated with the test. */ @@ -236,11 +231,6 @@ export interface TestDiagnostic { ```ts declare class TestSuite { readonly type = 'suite' - /** - * Task instance. - * @experimental Public task API is experimental and does not follow semver. - */ - readonly task: RunnerTestSuite /** * The project associated with the test. */ @@ -286,11 +276,6 @@ declare class TestSuite { ```ts declare class TestModule extends SuiteImplementation { readonly type = 'module' - /** - * Task instance. - * @experimental Public task API is experimental and does not follow semver. - */ - readonly task: RunnerTestFile /** * Collection of suites and tests that are part of this module. */ diff --git a/packages/vitest/src/node/reporters/reported-tasks.ts b/packages/vitest/src/node/reporters/reported-tasks.ts index 4072a08623bf..6c8370a62f4a 100644 --- a/packages/vitest/src/node/reporters/reported-tasks.ts +++ b/packages/vitest/src/node/reporters/reported-tasks.ts @@ -11,7 +11,7 @@ import type { TestProject } from '../project' class ReportedTaskImplementation { /** * Task instance. - * @experimental Public runner task API is experimental and does not follow semver. + * @internal */ public readonly task: RunnerTask @@ -32,6 +32,7 @@ class ReportedTaskImplementation { */ public readonly location: { line: number; column: number } | undefined + /** @internal */ protected constructor( task: RunnerTask, project: TestProject, @@ -44,6 +45,7 @@ class ReportedTaskImplementation { /** * Creates a new reported task instance and stores it in the project's state for future use. + * @internal */ static register(task: RunnerTask, project: TestProject) { const state = new this(task, project) as TestCase | TestSuite | TestModule @@ -55,6 +57,7 @@ class ReportedTaskImplementation { export class TestCase extends ReportedTaskImplementation { #fullName: string | undefined + /** @internal */ declare public readonly task: RunnerTestCase public readonly type = 'test' @@ -78,6 +81,7 @@ export class TestCase extends ReportedTaskImplementation { */ public readonly parent: TestSuite | TestModule + /** @internal */ protected constructor(task: RunnerTestCase, project: TestProject) { super(task, project) @@ -294,6 +298,7 @@ class TestCollection { export type { TestCollection } abstract class SuiteImplementation extends ReportedTaskImplementation { + /** @internal */ declare public readonly task: RunnerTestSuite | RunnerTestFile /** @@ -301,6 +306,7 @@ abstract class SuiteImplementation extends ReportedTaskImplementation { */ public readonly children: TestCollection + /** @internal */ protected constructor(task: RunnerTestSuite | RunnerTestFile, project: TestProject) { super(task, project) this.children = new TestCollection(task, project) @@ -310,6 +316,7 @@ abstract class SuiteImplementation extends ReportedTaskImplementation { export class TestSuite extends SuiteImplementation { #fullName: string | undefined + /** @internal */ declare public readonly task: RunnerTestSuite public readonly type = 'suite' @@ -333,6 +340,7 @@ export class TestSuite extends SuiteImplementation { */ public readonly options: TaskOptions + /** @internal */ protected constructor(task: RunnerTestSuite, project: TestProject) { super(task, project) @@ -365,6 +373,7 @@ export class TestSuite extends SuiteImplementation { } export class TestModule extends SuiteImplementation { + /** @internal */ declare public readonly task: RunnerTestFile declare public readonly location: undefined public readonly type = 'module' @@ -376,6 +385,7 @@ export class TestModule extends SuiteImplementation { */ public readonly moduleId: string + /** @internal */ protected constructor(task: RunnerTestFile, project: TestProject) { super(task, project) this.moduleId = task.filepath From de9e6e3694374ddb7c8b9de83d4e4d5e3daf7a28 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 26 Nov 2024 11:02:42 +0100 Subject: [PATCH 06/73] chore: fix types in tests --- test/cli/test/create-vitest.test.ts | 4 ++-- test/reporters/src/context.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/cli/test/create-vitest.test.ts b/test/cli/test/create-vitest.test.ts index a4719cc1d2e6..ab18bca6ef5e 100644 --- a/test/cli/test/create-vitest.test.ts +++ b/test/cli/test/create-vitest.test.ts @@ -12,8 +12,8 @@ it(createVitest, async () => { }, ], }) - const testFiles = await ctx.globTestFiles() - await ctx.runFiles(testFiles, false) + const testFiles = await ctx.globTestSpecifications() + await ctx.runTestSpecifications(testFiles, false) expect(onFinished.mock.calls[0]).toMatchObject([ [ { diff --git a/test/reporters/src/context.ts b/test/reporters/src/context.ts index 26484c995c89..1806e0ef2c37 100644 --- a/test/reporters/src/context.ts +++ b/test/reporters/src/context.ts @@ -39,6 +39,7 @@ export function getContext(): Context { } as any, } + // @ts-expect-error logger is readonly context.logger = { ctx: context as Vitest, log: (text: string) => output += `${text}\n`, From 8749add0c3af0005c465b05a6fe1793f9644dd4b Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 26 Nov 2024 11:04:17 +0100 Subject: [PATCH 07/73] fix: fullName has file --- packages/vitest/src/node/cli/cli-api.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/vitest/src/node/cli/cli-api.ts b/packages/vitest/src/node/cli/cli-api.ts index c7d556ff0d95..8ef4272be443 100644 --- a/packages/vitest/src/node/cli/cli-api.ts +++ b/packages/vitest/src/node/cli/cli-api.ts @@ -268,8 +268,9 @@ export function formatCollectedAsJSON(files: TestModule[]) { if (test.skipped()) { continue } + const fullName = `${test.module.task.name} > ${test.fullName}` const result: TestCollectJSONResult = { - name: test.fullName, + name: fullName, file: test.module.moduleId, } if (test.project.name) { @@ -292,8 +293,9 @@ export function formatCollectedAsString(files: TestModule[]) { if (test.skipped()) { continue } + const fullName = `${test.module.task.name} > ${test.fullName}` results.push( - (test.project.name ? `[${test.project.name}] ` : '') + test.fullName, + (test.project.name ? `[${test.project.name}] ` : '') + fullName, ) } }) From b8830fb6bd9bbf9a543beb9569d98d1d4bc03e70 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 26 Nov 2024 11:58:04 +0100 Subject: [PATCH 08/73] refactor: remove the usage of getProjectByTaskId --- packages/vitest/src/api/setup.ts | 12 +- packages/vitest/src/node/core.ts | 153 +++++++++--------- packages/vitest/src/node/reporters/base.ts | 6 +- .../src/node/reporters/github-actions.ts | 2 +- packages/vitest/src/node/reporters/junit.ts | 2 +- packages/vitest/src/node/reporters/tap.ts | 2 +- packages/vitest/src/node/specifications.ts | 29 +++- packages/vitest/src/node/stdin.ts | 2 +- test/reporters/src/context.ts | 1 + 9 files changed, 111 insertions(+), 98 deletions(-) diff --git a/packages/vitest/src/api/setup.ts b/packages/vitest/src/api/setup.ts index c82fec2a6bd3..531dcf1bff8b 100644 --- a/packages/vitest/src/api/setup.ts +++ b/packages/vitest/src/api/setup.ts @@ -177,7 +177,6 @@ export class WebSocketReporter implements Reporter { } packs.forEach(([taskId, result]) => { - const project = this.ctx.getProjectByTaskId(taskId) const task = this.ctx.state.idMap.get(taskId) const isBrowser = task && task.file.pool === 'browser' @@ -186,10 +185,13 @@ export class WebSocketReporter implements Reporter { return } - const stacks = isBrowser - ? project.browser?.parseErrorStacktrace(error) - : parseErrorStacktrace(error) - error.stacks = stacks + if (isBrowser) { + const project = this.ctx.getProjectByName(task!.file.projectName || '') + error.stacks = project.browser?.parseErrorStacktrace(error) + } + else { + error.stacks = parseErrorStacktrace(error) + } }) }) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 6a6f7369e8d2..33c1bf5a35cd 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -57,6 +57,16 @@ export class Vitest { public readonly packageInstaller: VitestPackageInstaller public readonly distPath = distDir + /** + * @experimental The State API is experimental and not subject to semver. + */ + // TODO: getter + public state: StateManager = undefined! + // TODO: getter + public snapshot: SnapshotManager = undefined! + // TODO: getter + public cache: VitestCache = undefined! + public projects: TestProject[] = [] /** @internal */ @@ -64,22 +74,13 @@ export class Vitest { /** @internal */ reporters: Reporter[] = undefined! - server: ViteDevServer = undefined! - /** - * @experimental The State API is experimental and not subject to semver. - */ - state: StateManager = undefined! - snapshot: SnapshotManager = undefined! - cache: VitestCache = undefined! coverageProvider: CoverageProvider | null | undefined pool: ProcessPool | undefined vitenode: ViteNodeServer = undefined! runner: ViteNodeRunner = undefined! - invalidates: Set = new Set() - changedTests: Set = new Set() - watchedTests: Set = new Set() + /** @internal */ filenamePattern?: string[] /** @internal */ runningPromise?: Promise @@ -87,27 +88,28 @@ export class Vitest { closingPromise?: Promise /** @internal */ isCancelling = false - /** @internal */ - isFirstRun = true + coreWorkspaceProject: TestProject | undefined /** @internal */ - restartsCount = 0 - - /** TODO: rename to `_coreRootProject` */ + resolvedProjects: TestProject[] = [] /** @internal */ - public coreWorkspaceProject: TestProject | undefined + _browserLastPort = defaultBrowserPort /** @internal */ - public resolvedProjects: TestProject[] = [] + _options: UserConfig = {} + + private invalidates: Set = new Set() + private changedTests: Set = new Set() + // TODO: remove in 3.0 + private watchedTests: Set = new Set() + + private isFirstRun = true + private restartsCount = 0 private _config: ResolvedConfig | undefined + private _vite: ViteDevServer | undefined private _workspaceConfigPath?: string private specifications: VitestSpecifications - /** @internal */ - public _browserLastPort = defaultBrowserPort - /** @internal */ - public _options: UserConfig = {} - constructor( public readonly mode: VitestRunMode, options: VitestOptions = {}, @@ -127,8 +129,25 @@ export class Vitest { * The global config. */ get config(): ResolvedConfig { - // FIXME: throw error if accessed before server is set - return this._config! + if (!this._config) { + throw new Error('The config was not set. It means that `vitest.config` was called before the Vite server was established.') + } + return this._config + } + + /** @deprecated use `vitest.vite` instead */ + get server(): ViteDevServer { + return this._vite! + } + + /** + * Global Vite's dev server instance. + */ + get vite(): ViteDevServer { + if (!this._vite) { + throw new Error('The server was not set. It means that `vitest.vite` was called before the Vite server was established.') + } + return this._vite } /** @deprecated internal */ @@ -156,7 +175,7 @@ export class Vitest { const resolved = resolveConfig(this.mode, options, server.config, this.logger) - this.server = server + this._vite = server this._config = resolved this.state = new StateManager() this.cache = new VitestCache(this.version) @@ -269,7 +288,7 @@ export class Vitest { || this.projects[0] } - public getProjectByName(name: string = ''): TestProject { + public getProjectByName(name: string): TestProject { const project = this.projects.find(p => p.name === name) || this.coreWorkspaceProject || this.projects[0] @@ -284,8 +303,8 @@ export class Vitest { return this.config.workspace } - const configDir = this.server.config.configFile - ? dirname(this.server.config.configFile) + const configDir = this.vite.config.configFile + ? dirname(this.vite.config.configFile) : this.config.root const rootFiles = await fs.readdir(configDir) @@ -335,6 +354,14 @@ export class Vitest { ) } + /** + * Glob test files in every project and create a TestSpecification for each file and pool. + * @param filters String filters to match the test files. + */ + public async globTestSpecifications(filters: string[] = []): Promise { + return this.specifications.globTestSpecifications(filters) + } + private async initCoverageProvider(): Promise { if (this.coverageProvider !== undefined) { return @@ -365,7 +392,7 @@ export class Vitest { const workspaceSpecs = new Map() for (const file of files) { - const project = this.getProjectByName(file.projectName) + const project = this.getProjectByName(file.projectName || '') const specs = workspaceSpecs.get(project) || [] specs.push(file) workspaceSpecs.set(project, specs) @@ -434,8 +461,8 @@ export class Vitest { * Returns the list of test files that match the config and filters. * @param filters String filters to match the test files */ - async listFiles(filters?: string[]): Promise { - return await this.specifications.getRelevantTestSpecifications(filters) + listFiles(filters?: string[]): Promise { + return this.specifications.getRelevantTestSpecifications(filters) } /** @@ -534,26 +561,11 @@ export class Vitest { * @param moduleId The module ID to get test specifications for. */ public getModuleSpecifications(moduleId: string): TestSpecification[] { - const _cached = this.specifications.getCachedSpecifications(moduleId) - if (_cached) { - return _cached - } - - const specs: TestSpecification[] = [] - for (const project of this.projects) { - if (project.isTestFile(moduleId)) { - specs.push(project.createSpecification(moduleId)) - } - if (project.isTypecheckFile(moduleId)) { - specs.push(project.createSpecification(moduleId, 'typescript')) - } - } - specs.forEach(spec => this.specifications.ensureSpecificationCached(spec)) - return specs + return this.specifications.getModuleSpecifications(moduleId) } /** - * Run tests for the given test specifications. + * Run tests for the given test specifications. This does not trigger `onWatcher*` events. * @param specifications A list of specifications to run. * @param allTestsRun Indicates whether all tests were run. This only matters for coverage. */ @@ -637,10 +649,10 @@ export class Vitest { /** * Collect tests in specified modules. Vitest will run the files to collect tests. - * @param specitifactions A list of specifications to run. + * @param specifications A list of specifications to run. */ - public async collectTests(specitifactions: TestSpecification[]): Promise { - const filepaths = specitifactions.map(spec => spec.moduleId) + public async collectTests(specifications: TestSpecification[]): Promise { + const filepaths = specifications.map(spec => spec.moduleId) this.state.collectPaths(filepaths) // previous run @@ -659,10 +671,10 @@ export class Vitest { this.snapshot.clear() this.state.clearErrors() - await this.initializeGlobalSetup(specitifactions) + await this.initializeGlobalSetup(specifications) try { - await this.pool.collectTests(specitifactions, invalidates) + await this.pool.collectTests(specifications, invalidates) } catch (err) { this.state.catchError(err, 'Unhandled Error') @@ -701,7 +713,7 @@ export class Vitest { } /** @internal */ - async initBrowserServers(): Promise { + async _initBrowserServers(): Promise { await Promise.all(this.projects.map(p => p._initBrowserServer())) } @@ -741,7 +753,7 @@ export class Vitest { if (this.filenamePattern) { const filteredFiles = await this.globTestSpecifications(this.filenamePattern) - files = files.filter(file => filteredFiles.some(f => f[1] === file)) + files = files.filter(file => filteredFiles.some(f => f.moduleId === file)) } const specifications = files.flatMap(file => this.getModuleSpecifications(file)) @@ -855,15 +867,6 @@ export class Vitest { } this._rerunTimer = setTimeout(async () => { - // run only watched tests - if (this.watchedTests.size) { - this.changedTests.forEach((test) => { - if (!this.watchedTests.has(test)) { - this.changedTests.delete(test) - } - }) - } - if (this.changedTests.size === 0) { this.invalidates.clear() return @@ -881,7 +884,7 @@ export class Vitest { if (this.filenamePattern) { const filteredFiles = await this.globTestSpecifications(this.filenamePattern) - files = files.filter(file => filteredFiles.some(f => f[1] === file)) + files = files.filter(file => filteredFiles.some(f => f.moduleId === file)) // A file that does not match the current filename pattern was changed if (files.length === 0) { @@ -907,13 +910,9 @@ export class Vitest { /** * Watch only the specified tests. If no tests are provided, all tests will be watched. - * @deprecated Do not use this method. It will be replaced with a different API in the future. + * @deprecated This method does nothing. It will be remove in Vitest 3.0. */ - public watchTests(tests: string[]): void { - this.watchedTests = new Set( - tests.map(test => slash(test)), - ) - } + public watchTests(_tests: string[]): void {} public invalidateFile(filepath: string): void { this.projects.forEach(({ vite, browser }) => { @@ -1116,7 +1115,7 @@ export class Vitest { // close the core workspace server only once // it's possible that it's not initialized at all because it's not running any tests if (this.coreWorkspaceProject && !this.resolvedProjects.includes(this.coreWorkspaceProject)) { - closePromises.push(this.coreWorkspaceProject.close().then(() => this.server = undefined as any)) + closePromises.push(this.coreWorkspaceProject.close().then(() => this._vite = undefined as any)) } if (this.pool) { @@ -1192,14 +1191,6 @@ export class Vitest { return Array.from(new Set(specifications.map(spec => spec.moduleId))) } - /** - * Glob test files in every project and create a TestSpecification for each file and pool. - * @param filters String filters to match the test files. - */ - public async globTestSpecifications(filters: string[] = []): Promise { - return this.specifications.globTestSpecifications(filters) - } - /** * @deprecated use `globTestSpecifications` instead */ @@ -1229,7 +1220,7 @@ export class Vitest { } /** - * Register a handler that will be called when the test run was cancelled with `vitest.cancelCurrentRun`. + * Register a handler that will be called when the test run is cancelled with `vitest.cancelCurrentRun`. */ onCancel(fn: (reason: CancelReason) => Awaitable) { this._onCancelListeners.push(fn) diff --git a/packages/vitest/src/node/reporters/base.ts b/packages/vitest/src/node/reporters/base.ts index 71df7bf40885..ed9bbadc43ed 100644 --- a/packages/vitest/src/node/reporters/base.ts +++ b/packages/vitest/src/node/reporters/base.ts @@ -268,8 +268,8 @@ export abstract class BaseReporter implements Reporter { write('\n') } - const project = log.taskId - ? this.ctx.getProjectByTaskId(log.taskId) + const project = task + ? this.ctx.getProjectByName(task.file.projectName || '') : this.ctx.getRootTestProject() const stack = log.browser @@ -511,7 +511,7 @@ export abstract class BaseReporter implements Reporter { const screenshotPaths = tasks.map(t => t.meta?.failScreenshotPath).filter(screenshot => screenshot != null) this.ctx.logger.printError(error, { - project: this.ctx.getProjectByTaskId(tasks[0].id), + project: this.ctx.getProjectByName(tasks[0].file.projectName || ''), verbose: this.verbose, screenshotPaths, task: tasks[0], diff --git a/packages/vitest/src/node/reporters/github-actions.ts b/packages/vitest/src/node/reporters/github-actions.ts index 7d63b7a0bcc2..44c4d63ba228 100644 --- a/packages/vitest/src/node/reporters/github-actions.ts +++ b/packages/vitest/src/node/reporters/github-actions.ts @@ -30,7 +30,7 @@ export class GithubActionsReporter implements Reporter { } for (const file of files) { const tasks = getTasks(file) - const project = this.ctx.getProjectByTaskId(file.id) + const project = this.ctx.getProjectByName(file.projectName || '') for (const task of tasks) { if (task.result?.state !== 'fail') { continue diff --git a/packages/vitest/src/node/reporters/junit.ts b/packages/vitest/src/node/reporters/junit.ts index 86789b802090..e0902de041da 100644 --- a/packages/vitest/src/node/reporters/junit.ts +++ b/packages/vitest/src/node/reporters/junit.ts @@ -260,7 +260,7 @@ export class JUnitReporter implements Reporter { const result = capturePrintError( error, this.ctx, - { project: this.ctx.getProjectByTaskId(task.id), task }, + { project: this.ctx.getProjectByName(task.file.projectName || ''), task }, ) await this.baseLog( escapeXML(stripVTControlCharacters(result.output.trim())), diff --git a/packages/vitest/src/node/reporters/tap.ts b/packages/vitest/src/node/reporters/tap.ts index 65953b31a224..e5c4fe5a11cf 100644 --- a/packages/vitest/src/node/reporters/tap.ts +++ b/packages/vitest/src/node/reporters/tap.ts @@ -80,7 +80,7 @@ export class TapReporter implements Reporter { else { this.logger.log(`${ok} ${id} - ${tapString(task.name)}${comment}`) - const project = this.ctx.getProjectByTaskId(task.id) + const project = this.ctx.getProjectByName(task.file.projectName || '') if (task.result?.state === 'fail' && task.result.errors) { this.logger.indent() diff --git a/packages/vitest/src/node/specifications.ts b/packages/vitest/src/node/specifications.ts index 74d65a9ab5d3..3aacbdf15ab6 100644 --- a/packages/vitest/src/node/specifications.ts +++ b/packages/vitest/src/node/specifications.ts @@ -12,6 +12,25 @@ export class VitestSpecifications { constructor(private vitest: Vitest) {} + public getModuleSpecifications(moduleId: string): TestSpecification[] { + const _cached = this.getCachedSpecifications(moduleId) + if (_cached) { + return _cached + } + + const specs: TestSpecification[] = [] + for (const project of this.vitest.projects) { + if (project.isTestFile(moduleId)) { + specs.push(project.createSpecification(moduleId)) + } + if (project.isTypecheckFile(moduleId)) { + specs.push(project.createSpecification(moduleId, 'typescript')) + } + } + specs.forEach(spec => this.ensureSpecificationCached(spec)) + return specs + } + public async getRelevantTestSpecifications(filters: string[] = []): Promise { return this.filterTestsBySource( await this.globTestSpecifications(filters), @@ -72,15 +91,15 @@ export class VitestSpecifications { return files } - public getCachedSpecifications(moduleId: string): TestSpecification[] | undefined { - return this._cachedSpecs.get(moduleId) - } - public clearCache(): void { this._cachedSpecs.clear() } - public ensureSpecificationCached(spec: TestSpecification): TestSpecification[] { + private getCachedSpecifications(moduleId: string): TestSpecification[] | undefined { + return this._cachedSpecs.get(moduleId) + } + + private ensureSpecificationCached(spec: TestSpecification): TestSpecification[] { const file = spec.moduleId const specs = this._cachedSpecs.get(file) || [] const included = specs.some(_s => _s.project === spec.project && _s.pool === spec.pool) diff --git a/packages/vitest/src/node/stdin.ts b/packages/vitest/src/node/stdin.ts index f93c7ae56783..291cbe13119e 100644 --- a/packages/vitest/src/node/stdin.ts +++ b/packages/vitest/src/node/stdin.ts @@ -122,7 +122,7 @@ export function registerConsoleShortcuts( return inputFilePattern() } if (name === 'b') { - await ctx.initBrowserServers() + await ctx._initBrowserServers() ctx.projects.forEach((project) => { ctx.logger.log() ctx.logger.printBrowserBanner(project) diff --git a/test/reporters/src/context.ts b/test/reporters/src/context.ts index 1806e0ef2c37..0804dcbb9e1c 100644 --- a/test/reporters/src/context.ts +++ b/test/reporters/src/context.ts @@ -34,6 +34,7 @@ export function getContext(): Context { config: config as ResolvedConfig, server: server as ViteDevServer, getProjectByTaskId: () => ({ getBrowserSourceMapModuleById: () => undefined }) as any, + getProjectByName: () => ({ getBrowserSourceMapModuleById: () => undefined }) as any, snapshot: { summary: { added: 100, _test: true }, } as any, From 32e0a95535c0bfd8bb2bc689c7932f712fe82d0b Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 26 Nov 2024 12:37:57 +0100 Subject: [PATCH 09/73] refactor: move watcher functions into a separate class --- packages/vitest/src/node/core.ts | 181 +++++--------------------- packages/vitest/src/node/watcher.ts | 195 ++++++++++++++++++++++++++++ 2 files changed, 224 insertions(+), 152 deletions(-) create mode 100644 packages/vitest/src/node/watcher.ts diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 33c1bf5a35cd..20f486fa0931 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -11,11 +11,10 @@ import type { ResolvedConfig, UserConfig, VitestRunMode } from './types/config' import type { CoverageProvider } from './types/coverage' import type { Reporter } from './types/reporter' import type { TestRunResult } from './types/tests' -import { promises as fs, readFileSync } from 'node:fs' +import { promises as fs } from 'node:fs' import { getTasks, hasFailed } from '@vitest/runner/utils' import { SnapshotManager } from '@vitest/snapshot/manager' -import { noop, slash, toArray } from '@vitest/utils' -import mm from 'micromatch' +import { noop, toArray } from '@vitest/utils' import { dirname, join, normalize, relative } from 'pathe' import { ViteNodeRunner } from 'vite-node/client' import { ViteNodeServer } from 'vite-node/server' @@ -36,6 +35,7 @@ import { BlobReporter, readBlobs } from './reporters/blob' import { createBenchmarkReporters, createReporters } from './reporters/utils' import { VitestSpecifications } from './specifications' import { StateManager } from './state' +import { VitestWatcher } from './watcher' import { resolveWorkspace } from './workspace/resolveWorkspace' const WATCHER_DEBOUNCE = 100 @@ -57,6 +57,8 @@ export class Vitest { public readonly packageInstaller: VitestPackageInstaller public readonly distPath = distDir + /** @experimental */ + public watcher: VitestWatcher /** * @experimental The State API is experimental and not subject to semver. */ @@ -97,8 +99,6 @@ export class Vitest { /** @internal */ _options: UserConfig = {} - private invalidates: Set = new Set() - private changedTests: Set = new Set() // TODO: remove in 3.0 private watchedTests: Set = new Set() @@ -117,6 +117,9 @@ export class Vitest { this.logger = new Logger(this, options.stdout, options.stderr) this.packageInstaller = options.packageInstaller || new VitestPackageInstaller() this.specifications = new VitestSpecifications(this) + this.watcher = new VitestWatcher(this).onWatcherRerun(file => + this.scheduleRerun([file]), // TODO: error handling + ) } private _onRestartListeners: OnServerRestartHandler[] = [] @@ -125,6 +128,16 @@ export class Vitest { private _onCancelListeners: ((reason: CancelReason) => Awaitable)[] = [] private _onUserTestsRerun: OnTestsRerunHandler[] = [] + /** @deprecated will be removed in 3.0 */ + public get invalidates() { + return this.watcher.invalidates + } + + /** @deprecated will be removed in 3.0 */ + public get changedTests() { + return this.watcher.changedTests + } + /** * The global config. */ @@ -158,7 +171,7 @@ export class Vitest { /** @internal */ async _setServer(options: UserConfig, server: ViteDevServer, cliOptions: UserConfig) { this._options = options - this.unregisterWatcher?.() + this.watcher.unregisterWatcher() clearTimeout(this._rerunTimer) this.restartsCount += 1 this._browserLastPort = defaultBrowserPort @@ -182,7 +195,7 @@ export class Vitest { this.snapshot = new SnapshotManager({ ...resolved.snapshotOptions }) if (this.config.watch) { - this.registerWatcher() + this.watcher.registerWatcher() } this.vitenode = new ViteNodeServer(server, this.config.server) @@ -592,8 +605,8 @@ export class Vitest { this.pool = createPool(this) } - const invalidates = Array.from(this.invalidates) - this.invalidates.clear() + const invalidates = Array.from(this.watcher.invalidates) + this.watcher.invalidates.clear() this.snapshot.clear() this.state.clearErrors() @@ -666,8 +679,8 @@ export class Vitest { this.pool = createPool(this) } - const invalidates = Array.from(this.invalidates) - this.invalidates.clear() + const invalidates = Array.from(this.watcher.invalidates) + this.watcher.invalidates.clear() this.snapshot.clear() this.state.clearErrors() @@ -855,6 +868,7 @@ export class Vitest { } private _rerunTimer: any + // we can't use a single `triggerId` yet because vscode extension relies on this private async scheduleRerun(triggerId: string[]): Promise { const currentCount = this.restartsCount clearTimeout(this._rerunTimer) @@ -867,8 +881,8 @@ export class Vitest { } this._rerunTimer = setTimeout(async () => { - if (this.changedTests.size === 0) { - this.invalidates.clear() + if (this.watcher.changedTests.size === 0) { + this.watcher.invalidates.clear() return } @@ -880,7 +894,7 @@ export class Vitest { this.isFirstRun = false this.snapshot.clear() - let files = Array.from(this.changedTests) + let files = Array.from(this.watcher.changedTests) if (this.filenamePattern) { const filteredFiles = await this.globTestSpecifications(this.filenamePattern) @@ -892,7 +906,7 @@ export class Vitest { } } - this.changedTests.clear() + this.watcher.changedTests.clear() const triggerIds = new Set(triggerId.map(id => relative(this.config.root, id))) const triggerLabel = Array.from(triggerIds).join(', ') @@ -931,56 +945,6 @@ export class Vitest { this.invalidateFile(filepath) } - private onChange = (id: string): void => { - id = slash(id) - this.logger.clearHighlightCache(id) - this.invalidateFile(id) - const needsRerun = this.handleFileChanged(id) - if (needsRerun.length) { - this.scheduleRerun(needsRerun) - } - } - - private onUnlink = (id: string): void => { - id = slash(id) - this.logger.clearHighlightCache(id) - this.invalidates.add(id) - - if (this.state.filesMap.has(id)) { - this.state.filesMap.delete(id) - this.cache.results.removeFromCache(id) - this.cache.stats.removeStats(id) - this.changedTests.delete(id) - this.report('onTestRemoved', id) - } - } - - private onAdd = (id: string): void => { - id = slash(id) - this.invalidateFile(id) - const fileContent = readFileSync(id, 'utf-8') - - const matchingProjects: TestProject[] = [] - this.projects.forEach((project) => { - if (project.matchesTestGlob(id, fileContent)) { - matchingProjects.push(project) - project._markTestFile(id) - } - }) - - if (matchingProjects.length > 0) { - this.changedTests.add(id) - this.scheduleRerun([id]) - } - else { - // it's possible that file was already there but watcher triggered "add" event instead - const needsRerun = this.handleFileChanged(id) - if (needsRerun.length) { - this.scheduleRerun(needsRerun) - } - } - } - /** @internal */ public _checkUnhandledErrors(errors: unknown[]): void { if (errors.length && !this.config.dangerouslyIgnoreUnhandledErrors) { @@ -988,93 +952,6 @@ export class Vitest { } } - private unregisterWatcher = noop - private registerWatcher(): void { - const watcher = this.server.watcher - - if (this.config.forceRerunTriggers.length) { - watcher.add(this.config.forceRerunTriggers) - } - - watcher.on('change', this.onChange) - watcher.on('unlink', this.onUnlink) - watcher.on('add', this.onAdd) - - this.unregisterWatcher = () => { - watcher.off('change', this.onChange) - watcher.off('unlink', this.onUnlink) - watcher.off('add', this.onAdd) - this.unregisterWatcher = noop - } - } - - /** - * @returns A value indicating whether rerun is needed (changedTests was mutated) - */ - private handleFileChanged(filepath: string): string[] { - if (this.changedTests.has(filepath) || this.invalidates.has(filepath)) { - return [] - } - - if (mm.isMatch(filepath, this.config.forceRerunTriggers)) { - this.state.getFilepaths().forEach(file => this.changedTests.add(file)) - return [filepath] - } - - const projects = this.projects.filter((project) => { - const moduleGraph = project.browser?.vite.moduleGraph || project.vite.moduleGraph - return moduleGraph.getModulesByFile(filepath)?.size - }) - if (!projects.length) { - // if there are no modules it's possible that server was restarted - // we don't have information about importers anymore, so let's check if the file is a test file at least - if (this.state.filesMap.has(filepath) || this.projects.some(project => project.isTestFile(filepath))) { - this.changedTests.add(filepath) - return [filepath] - } - return [] - } - - const files: string[] = [] - - for (const project of projects) { - const mods = project.browser?.vite.moduleGraph.getModulesByFile(filepath) - || project.vite.moduleGraph.getModulesByFile(filepath) - if (!mods || !mods.size) { - continue - } - - this.invalidates.add(filepath) - - // one of test files that we already run, or one of test files that we can run - if (this.state.filesMap.has(filepath) || project.isTestFile(filepath)) { - this.changedTests.add(filepath) - files.push(filepath) - continue - } - - let rerun = false - for (const mod of mods) { - mod.importers.forEach((i) => { - if (!i.file) { - return - } - - const heedsRerun = this.handleFileChanged(i.file) - if (heedsRerun.length) { - rerun = true - } - }) - } - - if (rerun) { - files.push(filepath) - } - } - - return Array.from(new Set(files)) - } - private async reportCoverage(coverage: unknown, allTestsRun: boolean): Promise { if (this.state.getCountOfFailedTests() > 0) { await this.coverageProvider?.onTestFailure?.() diff --git a/packages/vitest/src/node/watcher.ts b/packages/vitest/src/node/watcher.ts new file mode 100644 index 000000000000..ec9723c25ac7 --- /dev/null +++ b/packages/vitest/src/node/watcher.ts @@ -0,0 +1,195 @@ +import type { Vitest } from './core' +import type { TestProject } from './reporters' +import { readFileSync } from 'node:fs' +import { noop, slash } from '@vitest/utils' +import mm from 'micromatch' + +export class VitestWatcher { + public invalidates: Set = new Set() + public changedTests: Set = new Set() + + private _onRerun: ((file: string) => void)[] = [] + private _onFilterTestFile: ((file: string) => boolean)[] = [] + + constructor(private vitest: Vitest) {} + + /** + * Register a handler that will be called when test files need to be rerun. + * The callback can receive several files in case the changed file is imported by several test files. + * Several invocations of this method will add multiple handlers. + * @internal + */ + onWatcherRerun(cb: (file: string) => void): this { + this._onRerun.push(cb) + return this + } + + /** + * Register a handler that will be called when a file is changed. + * This callback should return a value indicating whether the test file needs to be rerun. + */ + onFilterTestFile(cb: (file: string) => boolean): this { + this._onFilterTestFile.push(cb) + return this + } + + public invalidateFile(filepath: string): void { + this.vitest.projects.forEach(({ vite, browser }) => { + const serverMods = vite.moduleGraph.getModulesByFile(filepath) + serverMods?.forEach(mod => vite.moduleGraph.invalidateModule(mod)) + + if (browser) { + const browserMods = browser.vite.moduleGraph.getModulesByFile(filepath) + browserMods?.forEach(mod => browser.vite.moduleGraph.invalidateModule(mod)) + } + }) + } + + public unregisterWatcher: () => void = noop + public registerWatcher(): this { + const watcher = this.vitest.vite.watcher + + if (this.vitest.config.forceRerunTriggers.length) { + watcher.add(this.vitest.config.forceRerunTriggers) + } + + watcher.on('change', this.onChange) + watcher.on('unlink', this.onUnlink) + watcher.on('add', this.onAdd) + + this.unregisterWatcher = () => { + watcher.off('change', this.onChange) + watcher.off('unlink', this.onUnlink) + watcher.off('add', this.onAdd) + this.unregisterWatcher = noop + } + return this + } + + private scheduleRerun(file: string): void { + for (const testFile of this.changedTests) { + if (this._onFilterTestFile.some(cb => cb(testFile))) { + this.changedTests.delete(testFile) + } + } + + this._onRerun.forEach(cb => cb(file)) + } + + private onChange = (id: string): void => { + id = slash(id) + this.vitest.logger.clearHighlightCache(id) + this.invalidateFile(id) + const needsRerun = this.handleFileChanged(id) + if (needsRerun) { + this.scheduleRerun(id) + } + } + + private onUnlink = (id: string): void => { + id = slash(id) + this.vitest.logger.clearHighlightCache(id) + this.invalidates.add(id) + + if (this.vitest.state.filesMap.has(id)) { + this.vitest.state.filesMap.delete(id) + this.vitest.cache.results.removeFromCache(id) + this.vitest.cache.stats.removeStats(id) + this.changedTests.delete(id) + this.vitest.report('onTestRemoved', id) + } + } + + private onAdd = (id: string): void => { + id = slash(id) + this.invalidateFile(id) + const fileContent = readFileSync(id, 'utf-8') + + const matchingProjects: TestProject[] = [] + this.vitest.projects.forEach((project) => { + if (project.matchesTestGlob(id, fileContent)) { + matchingProjects.push(project) + project._markTestFile(id) + } + }) + + if (matchingProjects.length > 0) { + this.changedTests.add(id) + this.scheduleRerun(id) + } + else { + // it's possible that file was already there but watcher triggered "add" event instead + const needsRerun = this.handleFileChanged(id) + if (needsRerun) { + this.scheduleRerun(id) + } + } + } + + /** + * @returns A value indicating whether rerun is needed (changedTests was mutated) + */ + private handleFileChanged(filepath: string): boolean { + if (this.changedTests.has(filepath) || this.invalidates.has(filepath)) { + return false + } + + if (mm.isMatch(filepath, this.vitest.config.forceRerunTriggers)) { + this.vitest.state.getFilepaths().forEach(file => this.changedTests.add(file)) + return true + } + + const projects = this.vitest.projects.filter((project) => { + const moduleGraph = project.browser?.vite.moduleGraph || project.vite.moduleGraph + return moduleGraph.getModulesByFile(filepath)?.size + }) + if (!projects.length) { + // if there are no modules it's possible that server was restarted + // we don't have information about importers anymore, so let's check if the file is a test file at least + if (this.vitest.state.filesMap.has(filepath) || this.vitest.projects.some(project => project.isTestFile(filepath))) { + this.changedTests.add(filepath) + return true + } + return false + } + + const files: string[] = [] + + for (const project of projects) { + const mods = project.browser?.vite.moduleGraph.getModulesByFile(filepath) + || project.vite.moduleGraph.getModulesByFile(filepath) + if (!mods || !mods.size) { + continue + } + + this.invalidates.add(filepath) + + // one of test files that we already run, or one of test files that we can run + if (this.vitest.state.filesMap.has(filepath) || project.isTestFile(filepath)) { + this.changedTests.add(filepath) + files.push(filepath) + continue + } + + let rerun = false + for (const mod of mods) { + mod.importers.forEach((i) => { + if (!i.file) { + return + } + + const heedsRerun = this.handleFileChanged(i.file) + if (heedsRerun) { + rerun = true + } + }) + } + + if (rerun) { + files.push(filepath) + } + } + + return !!files.length + } +} From 2e2e70127ea027376914509bc2a48fb22a8ad412 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 26 Nov 2024 12:39:04 +0100 Subject: [PATCH 10/73] chore: cleanup --- packages/vitest/src/node/core.ts | 3 +-- packages/vitest/src/node/watcher.ts | 16 ++-------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 20f486fa0931..e57b7a68ef2b 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -55,10 +55,9 @@ export class Vitest { static readonly version = version public readonly logger: Logger public readonly packageInstaller: VitestPackageInstaller + public readonly watcher: VitestWatcher public readonly distPath = distDir - /** @experimental */ - public watcher: VitestWatcher /** * @experimental The State API is experimental and not subject to semver. */ diff --git a/packages/vitest/src/node/watcher.ts b/packages/vitest/src/node/watcher.ts index ec9723c25ac7..3e2b817245c2 100644 --- a/packages/vitest/src/node/watcher.ts +++ b/packages/vitest/src/node/watcher.ts @@ -33,18 +33,6 @@ export class VitestWatcher { return this } - public invalidateFile(filepath: string): void { - this.vitest.projects.forEach(({ vite, browser }) => { - const serverMods = vite.moduleGraph.getModulesByFile(filepath) - serverMods?.forEach(mod => vite.moduleGraph.invalidateModule(mod)) - - if (browser) { - const browserMods = browser.vite.moduleGraph.getModulesByFile(filepath) - browserMods?.forEach(mod => browser.vite.moduleGraph.invalidateModule(mod)) - } - }) - } - public unregisterWatcher: () => void = noop public registerWatcher(): this { const watcher = this.vitest.vite.watcher @@ -79,7 +67,7 @@ export class VitestWatcher { private onChange = (id: string): void => { id = slash(id) this.vitest.logger.clearHighlightCache(id) - this.invalidateFile(id) + this.vitest.invalidateFile(id) const needsRerun = this.handleFileChanged(id) if (needsRerun) { this.scheduleRerun(id) @@ -102,7 +90,7 @@ export class VitestWatcher { private onAdd = (id: string): void => { id = slash(id) - this.invalidateFile(id) + this.vitest.invalidateFile(id) const fileContent = readFileSync(id, 'utf-8') const matchingProjects: TestProject[] = [] From e46b2b0327b1d05b09f4ead2d222cd7c3cea93f1 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 26 Nov 2024 12:44:18 +0100 Subject: [PATCH 11/73] chore: get root --- packages/vitest/src/node/core.ts | 3 +++ packages/vitest/src/node/plugins/index.ts | 8 +++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index e57b7a68ef2b..bcd57a5a3495 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -927,6 +927,9 @@ export class Vitest { */ public watchTests(_tests: string[]): void {} + /** + * Invalidate a file in all projects. + */ public invalidateFile(filepath: string): void { this.projects.forEach(({ vite, browser }) => { const serverMods = vite.moduleGraph.getModulesByFile(filepath) diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index 56ec4a3d34cd..1189bc3b50f2 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -31,10 +31,8 @@ export async function VitestPlugin( ): Promise { const userConfig = deepMerge({}, options) as UserConfig - const getRoot = () => ctx.config?.root || options.root || process.cwd() - async function UIPlugin() { - await ctx.packageInstaller.ensureInstalled('@vitest/ui', getRoot(), ctx.version) + await ctx.packageInstaller.ensureInstalled('@vitest/ui', options.root || process.cwd(), ctx.version) return (await import('@vitest/ui')).default(ctx) } @@ -101,7 +99,7 @@ export async function VitestPlugin( ws: testConfig.api?.middlewareMode ? false : undefined, preTransformRequests: false, fs: { - allow: resolveFsAllow(getRoot(), testConfig.config), + allow: resolveFsAllow(options.root || process.cwd(), testConfig.config), }, }, build: { @@ -213,7 +211,7 @@ export async function VitestPlugin( name: string, filename: string, ) => { - const root = getRoot() + const root = ctx.config.root || options.root || process.cwd() return generateScopedClassName( classNameStrategy, name, From 3b2c25ff6689f5683509aad0a767cd9b2c578cbb Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 26 Nov 2024 12:49:04 +0100 Subject: [PATCH 12/73] chore: add ready() method --- packages/vitest/src/node/core.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index bcd57a5a3495..62882a6a21da 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -104,6 +104,7 @@ export class Vitest { private isFirstRun = true private restartsCount = 0 + private _ready = false private _config: ResolvedConfig | undefined private _vite: ViteDevServer | undefined private _workspaceConfigPath?: string @@ -162,6 +163,14 @@ export class Vitest { return this._vite } + /** + * Returns whether Vitest was fully initialised. This means that the Vite server was established and the workspace config was resolved. + * It's not necessary to call this method unless the instance was created manually via the public API, and the promise was not awaited. + */ + public ready(): boolean { + return this._ready + } + /** @deprecated internal */ setServer(options: UserConfig, server: ViteDevServer, cliOptions: UserConfig) { return this._setServer(options, server, cliOptions) @@ -169,6 +178,7 @@ export class Vitest { /** @internal */ async _setServer(options: UserConfig, server: ViteDevServer, cliOptions: UserConfig) { + this._ready = false this._options = options this.watcher.unregisterWatcher() clearTimeout(this._rerunTimer) @@ -215,6 +225,7 @@ export class Vitest { // hijack server restart const serverRestart = server.restart server.restart = async (...args) => { + this._ready = false await Promise.all(this._onRestartListeners.map(fn => fn())) this.report('onServerRestart') await this.close() @@ -228,6 +239,7 @@ export class Vitest { || this.resolvedProjects.some(p => p.vite.config.configFile === file) || file === this._workspaceConfigPath if (isConfig) { + this._ready = false await Promise.all(this._onRestartListeners.map(fn => fn('config'))) this.report('onServerRestart', 'config') await this.close() @@ -264,6 +276,7 @@ export class Vitest { } await Promise.all(this._onSetServer.map(fn => fn())) + this._ready = true } /** From 59dc0e716a5dcc0f15dcbac41e1418d54d06c2f7 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 26 Nov 2024 13:06:20 +0100 Subject: [PATCH 13/73] chore: use fullName --- packages/vitest/src/node/cli/cli-api.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/vitest/src/node/cli/cli-api.ts b/packages/vitest/src/node/cli/cli-api.ts index 8ef4272be443..e521eecf7298 100644 --- a/packages/vitest/src/node/cli/cli-api.ts +++ b/packages/vitest/src/node/cli/cli-api.ts @@ -268,9 +268,8 @@ export function formatCollectedAsJSON(files: TestModule[]) { if (test.skipped()) { continue } - const fullName = `${test.module.task.name} > ${test.fullName}` const result: TestCollectJSONResult = { - name: fullName, + name: test.fullName, file: test.module.moduleId, } if (test.project.name) { From 4d414c8a71200d4dc6740ad3dc57d157d6fa1e0e Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 26 Nov 2024 14:48:17 +0100 Subject: [PATCH 14/73] fix: correctly print errors --- docs/advanced/reporters.md | 6 +++++- packages/vitest/src/node/cli/cli-api.ts | 18 +++++++++--------- .../src/node/reporters/reported-tasks.ts | 7 +++++++ packages/vitest/src/node/specifications.ts | 2 +- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/docs/advanced/reporters.md b/docs/advanced/reporters.md index 63f4ea78ade1..ba0d19193a4a 100644 --- a/docs/advanced/reporters.md +++ b/docs/advanced/reporters.md @@ -266,6 +266,10 @@ declare class TestSuite { * Options that the suite was initiated with. */ readonly options: TaskOptions + /** + * Errors that happened outside of the test run during collection, like syntax errors. + */ + public errors(): TestError[] } ``` @@ -274,7 +278,7 @@ declare class TestSuite { `TestModule` represents a single file that contains suites and tests. ```ts -declare class TestModule extends SuiteImplementation { +declare class TestModule extends TestSuite { readonly type = 'module' /** * Collection of suites and tests that are part of this module. diff --git a/packages/vitest/src/node/cli/cli-api.ts b/packages/vitest/src/node/cli/cli-api.ts index e521eecf7298..4272c30064ad 100644 --- a/packages/vitest/src/node/cli/cli-api.ts +++ b/packages/vitest/src/node/cli/cli-api.ts @@ -171,8 +171,7 @@ export function processCollected(ctx: Vitest, files: TestModule[], options: CliO let errorsPrinted = false forEachSuite(files, (suite) => { - const errors = suite.task.result?.errors || [] - errors.forEach((error) => { + suite.errors().forEach((error) => { errorsPrinted = true ctx.logger.printError(error, { project: suite.project, @@ -245,10 +244,11 @@ function processJsonOutput(files: TestModule[], options: CliOptions) { } } -function forEachSuite(modules: TestModule[], callback: (suite: TestSuite) => void) { - modules.forEach((task) => { - for (const module of task.children.allSuites()) { - callback(module) +function forEachSuite(modules: TestModule[], callback: (suite: TestSuite | TestModule) => void) { + modules.forEach((testModule) => { + callback(testModule) + for (const suite of testModule.children.allSuites()) { + callback(suite) } }) } @@ -284,11 +284,11 @@ export function formatCollectedAsJSON(files: TestModule[]) { return results } -export function formatCollectedAsString(files: TestModule[]) { +export function formatCollectedAsString(testModules: TestModule[]) { const results: string[] = [] - files.forEach((file) => { - for (const test of file.children.allTests()) { + testModules.forEach((testModule) => { + for (const test of testModule.children.allTests()) { if (test.skipped()) { continue } diff --git a/packages/vitest/src/node/reporters/reported-tasks.ts b/packages/vitest/src/node/reporters/reported-tasks.ts index 6c8370a62f4a..6b37348c0f33 100644 --- a/packages/vitest/src/node/reporters/reported-tasks.ts +++ b/packages/vitest/src/node/reporters/reported-tasks.ts @@ -311,6 +311,13 @@ abstract class SuiteImplementation extends ReportedTaskImplementation { super(task, project) this.children = new TestCollection(task, project) } + + /** + * Errors that happened outside of the test run during collection, like syntax errors. + */ + public errors(): TestError[] { + return (this.task.result?.errors as TestError[] | undefined) || [] + } } export class TestSuite extends SuiteImplementation { diff --git a/packages/vitest/src/node/specifications.ts b/packages/vitest/src/node/specifications.ts index 3aacbdf15ab6..09d964d6cc65 100644 --- a/packages/vitest/src/node/specifications.ts +++ b/packages/vitest/src/node/specifications.ts @@ -147,7 +147,7 @@ export class VitestSpecifications { }), ) - const runningTests = [] + const runningTests: TestSpecification[] = [] for (const [specification, deps] of testGraphs) { // if deps or the test itself were changed From 46479886affb8195ceef409ce3da78546549dabf Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 26 Nov 2024 15:19:40 +0100 Subject: [PATCH 15/73] chore: ignore typecheck error --- test/config/vitest.config.ts | 3 +++ test/test-utils/index.ts | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/config/vitest.config.ts b/test/config/vitest.config.ts index 19ce7e1ff204..ac91604569ec 100644 --- a/test/config/vitest.config.ts +++ b/test/config/vitest.config.ts @@ -22,5 +22,8 @@ export default defineConfig({ // test that empty reporter does not throw reporter: [], }, + typecheck: { + ignoreSourceErrors: true, + }, }, }) diff --git a/test/test-utils/index.ts b/test/test-utils/index.ts index 1f5ffc38bad8..cf4eb1e793e9 100644 --- a/test/test-utils/index.ts +++ b/test/test-utils/index.ts @@ -109,14 +109,12 @@ export async function runVitest( if (getCurrentTest()) { onTestFinished(async () => { await ctx?.close() - await ctx?.closingPromise process.exit = exit }) } else { afterEach(async () => { await ctx?.close() - await ctx?.closingPromise process.exit = exit }) } From fa62000e5a9d3d6d6f8044856e443428ed82ca3b Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 26 Nov 2024 15:45:43 +0100 Subject: [PATCH 16/73] chore: make the rest into getters with a check --- packages/snapshot/src/manager.ts | 4 +- packages/vitest/src/node/core.ts | 120 ++++++++++-------- packages/vitest/src/node/watcher.ts | 4 +- .../src/node/workspace/resolveWorkspace.ts | 10 +- 4 files changed, 73 insertions(+), 65 deletions(-) diff --git a/packages/snapshot/src/manager.ts b/packages/snapshot/src/manager.ts index 11ed69847aab..3bf30e22e42e 100644 --- a/packages/snapshot/src/manager.ts +++ b/packages/snapshot/src/manager.ts @@ -6,8 +6,8 @@ import type { import { basename, dirname, isAbsolute, join, resolve } from 'pathe' export class SnapshotManager { - summary: SnapshotSummary = undefined! - extension = '.snap' + public summary!: SnapshotSummary + public extension = '.snap' constructor( public options: Omit, diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 62882a6a21da..3f9c5ba35984 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -58,45 +58,21 @@ export class Vitest { public readonly watcher: VitestWatcher public readonly distPath = distDir - /** - * @experimental The State API is experimental and not subject to semver. - */ - // TODO: getter - public state: StateManager = undefined! - // TODO: getter - public snapshot: SnapshotManager = undefined! - // TODO: getter - public cache: VitestCache = undefined! - public projects: TestProject[] = [] - /** @internal */ - configOverride: Partial = {} - /** @internal */ - reporters: Reporter[] = undefined! - - coverageProvider: CoverageProvider | null | undefined - pool: ProcessPool | undefined - - vitenode: ViteNodeServer = undefined! - runner: ViteNodeRunner = undefined! - - /** @internal */ - filenamePattern?: string[] - /** @internal */ - runningPromise?: Promise - /** @internal */ - closingPromise?: Promise - /** @internal */ - isCancelling = false - /** @internal */ - coreWorkspaceProject: TestProject | undefined - /** @internal */ - resolvedProjects: TestProject[] = [] - /** @internal */ - _browserLastPort = defaultBrowserPort - /** @internal */ - _options: UserConfig = {} + /** @internal */ coverageProvider: CoverageProvider | null | undefined + /** @internal */ filenamePattern?: string[] + /** @internal */ runningPromise?: Promise + /** @internal */ closingPromise?: Promise + /** @internal */ isCancelling = false + /** @internal */ coreWorkspaceProject: TestProject | undefined + /** @internal */ resolvedProjects: TestProject[] = [] + /** @internal */ _browserLastPort = defaultBrowserPort + /** @internal */ _options: UserConfig = {} + /** @internal */ configOverride: Partial = {} + /** @internal */ reporters: Reporter[] = undefined! + /** @internal */ vitenode: ViteNodeServer = undefined! + /** @internal */ runner: ViteNodeRunner = undefined! // TODO: remove in 3.0 private watchedTests: Set = new Set() @@ -104,11 +80,15 @@ export class Vitest { private isFirstRun = true private restartsCount = 0 + private specifications: VitestSpecifications + private pool: ProcessPool | undefined private _ready = false - private _config: ResolvedConfig | undefined - private _vite: ViteDevServer | undefined + private _config?: ResolvedConfig + private _vite?: ViteDevServer + private _state?: StateManager + private _cache?: VitestCache + private _snapshot?: SnapshotManager private _workspaceConfigPath?: string - private specifications: VitestSpecifications constructor( public readonly mode: VitestRunMode, @@ -128,12 +108,12 @@ export class Vitest { private _onCancelListeners: ((reason: CancelReason) => Awaitable)[] = [] private _onUserTestsRerun: OnTestsRerunHandler[] = [] - /** @deprecated will be removed in 3.0 */ + /** @deprecated will be removed in 3.0, use `vitest.watcher` */ public get invalidates() { return this.watcher.invalidates } - /** @deprecated will be removed in 3.0 */ + /** @deprecated will be removed in 3.0, use `vitest.watcher` */ public get changedTests() { return this.watcher.changedTests } @@ -142,9 +122,7 @@ export class Vitest { * The global config. */ get config(): ResolvedConfig { - if (!this._config) { - throw new Error('The config was not set. It means that `vitest.config` was called before the Vite server was established.') - } + assert(this._config, 'config') return this._config } @@ -157,12 +135,35 @@ export class Vitest { * Global Vite's dev server instance. */ get vite(): ViteDevServer { - if (!this._vite) { - throw new Error('The server was not set. It means that `vitest.vite` was called before the Vite server was established.') - } + assert(this._vite, 'vite', 'server') return this._vite } + /** + * The global test state manager. + * @experimental The State API is experimental and not subject to semver. + */ + get state(): StateManager { + assert(this._state, 'state') + return this._state + } + + /** + * The global snapshot manager. You can access the current state on `snapshot.summary`. + */ + get snapshot(): SnapshotManager { + assert(this._snapshot, 'snapshot', 'snapshot manager') + return this._snapshot + } + + /** + * Test results and test file stats cache. Primarily used by the sequencer to order tests. + */ + get cache(): VitestCache { + assert(this._cache, 'cache') + return this._cache + } + /** * Returns whether Vitest was fully initialised. This means that the Vite server was established and the workspace config was resolved. * It's not necessary to call this method unless the instance was created manually via the public API, and the promise was not awaited. @@ -199,9 +200,9 @@ export class Vitest { this._vite = server this._config = resolved - this.state = new StateManager() - this.cache = new VitestCache(this.version) - this.snapshot = new SnapshotManager({ ...resolved.snapshotOptions }) + this._state = new StateManager() + this._cache = new VitestCache(this.version) + this._snapshot = new SnapshotManager({ ...resolved.snapshotOptions }) if (this.config.watch) { this.watcher.registerWatcher() @@ -287,7 +288,10 @@ export class Vitest { } /** @internal */ - _createRootProject() { + _ensureRootProject() { + if (this.coreWorkspaceProject) { + return this.coreWorkspaceProject + } this.coreWorkspaceProject = TestProject._createBasicProject(this) return this.coreWorkspaceProject } @@ -308,9 +312,7 @@ export class Vitest { public getProjectByTaskId(taskId: string): TestProject { const task = this.state.idMap.get(taskId) const projectName = (task as File).projectName || task?.file?.projectName || '' - return this.projects.find(p => p.name === projectName) - || this.getRootTestProject() - || this.projects[0] + return this.getProjectByName(projectName) } public getProjectByName(name: string): TestProject { @@ -360,7 +362,7 @@ export class Vitest { this._workspaceConfigPath = workspaceConfigPath if (!workspaceConfigPath) { - return [this._createRootProject()] + return [this._ensureRootProject()] } const workspaceModule = await this.runner.executeFile(workspaceConfigPath) as { @@ -1137,3 +1139,9 @@ export class Vitest { this._onSetServer.push(fn) } } + +function assert(condition: unknown, property: string, name: string = property): asserts condition { + if (!condition) { + throw new Error(`The ${name} was not set. It means that \`vitest.${property}\` was called before the Vite server was established. Either await the Vitest promise or check that it is initialized with \`vitest.ready()\` before accessing \`vitest.${property}\`.`) + } +} diff --git a/packages/vitest/src/node/watcher.ts b/packages/vitest/src/node/watcher.ts index 3e2b817245c2..3a01e8c69752 100644 --- a/packages/vitest/src/node/watcher.ts +++ b/packages/vitest/src/node/watcher.ts @@ -5,8 +5,8 @@ import { noop, slash } from '@vitest/utils' import mm from 'micromatch' export class VitestWatcher { - public invalidates: Set = new Set() - public changedTests: Set = new Set() + public readonly invalidates: Set = new Set() + public readonly changedTests: Set = new Set() private _onRerun: ((file: string) => void)[] = [] private _onFilterTestFile: ((file: string) => boolean)[] = [] diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index c7216dae0610..765ce348eb4c 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -78,8 +78,8 @@ export async function resolveWorkspace( for (const path of fileProjects) { // if file leads to the root config, then we can just reuse it because we already initialized it - if (vitest.server.config.configFile === path) { - projectPromises.push(Promise.resolve(vitest._createRootProject())) + if (vitest.vite.config.configFile === path) { + projectPromises.push(Promise.resolve(vitest._ensureRootProject())) continue } @@ -97,7 +97,7 @@ export async function resolveWorkspace( // pretty rare case - the glob didn't match anything and there are no inline configs if (!projectPromises.length) { - return [vitest._createRootProject()] + return [vitest._ensureRootProject()] } const resolvedProjects = await Promise.all(projectPromises) @@ -193,8 +193,8 @@ async function resolveTestProjectConfigs( // if the config is inlined, we can resolve it immediately else if (typeof definition === 'function') { projectsOptions.push(await definition({ - command: vitest.server.config.command, - mode: vitest.server.config.mode, + command: vitest.vite.config.command, + mode: vitest.vite.config.mode, isPreview: false, isSsrBuild: false, })) From 8a9f47c041af2031a62fbcc8bcbda031f1da76c6 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 26 Nov 2024 16:12:45 +0100 Subject: [PATCH 17/73] fix: add a separate tsconfig for failing tests --- test/typescript/test/runner.test.ts | 3 ++- test/typescript/tsconfig.fails.json | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 test/typescript/tsconfig.fails.json diff --git a/test/typescript/test/runner.test.ts b/test/typescript/test/runner.test.ts index dadc444c1076..15a01de5aa3c 100644 --- a/test/typescript/test/runner.test.ts +++ b/test/typescript/test/runner.test.ts @@ -5,7 +5,7 @@ import { describe, expect, it } from 'vitest' import { runVitest } from '../../test-utils' describe('should fail', async () => { - const root = resolve(__dirname, '../failing') + const root = resolve(import.meta.dirname, '../failing') const files = await glob(['*.test-d.*'], { cwd: root, expandDirectories: false }) it('typecheck files', async () => { @@ -16,6 +16,7 @@ describe('should fail', async () => { enabled: true, allowJs: true, include: ['**/*.test-d.*'], + tsconfig: resolve(import.meta.dirname, '../tsconfig.fails.json'), }, }) diff --git a/test/typescript/tsconfig.fails.json b/test/typescript/tsconfig.fails.json new file mode 100644 index 000000000000..976ac5b3b7a2 --- /dev/null +++ b/test/typescript/tsconfig.fails.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [ + "./failing/*" + ], + "exclude": [ + "**/dist/**" + ] +} From 0931b3ca45c503e5b3411c9f83df2df3b7d49292 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 26 Nov 2024 16:54:05 +0100 Subject: [PATCH 18/73] refactor: add onFilterWatchedSpecification and getProvidedContext --- packages/vitest/src/node/core.ts | 58 +++++++++++++++++++--- packages/vitest/src/node/specifications.ts | 2 +- packages/vitest/src/node/watcher.ts | 24 +++------ 3 files changed, 60 insertions(+), 24 deletions(-) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 3f9c5ba35984..12fbe27cfb54 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -50,14 +50,33 @@ export interface VitestOptions { export class Vitest { /** * Current Vitest version. + * @example '2.0.0' */ public readonly version = version static readonly version = version + /** + * The logger instance used to log messages. It's recommended to use this logger instead of `console`. + * It's possible to override stdout and stderr streams when initiating Vitest. + * @example + * new Vitest('test', { + * stdout: new Writable(), + * }) + */ public readonly logger: Logger + /** + * The package installer instance used to install Vitest packages. + * @example + * await vitest.packageInstaller.ensureInstalled('@vitest/browser', process.cwd()) + */ public readonly packageInstaller: VitestPackageInstaller - public readonly watcher: VitestWatcher + /** + * A path to the built Vitest directory. This is usually a folder in `node_modules`. + */ public readonly distPath = distDir - + /** + * A list of projects that are currently running. + * If projects were filtered with `--project` flag, they won't appear here. + */ public projects: TestProject[] = [] /** @internal */ coverageProvider: CoverageProvider | null | undefined @@ -80,7 +99,8 @@ export class Vitest { private isFirstRun = true private restartsCount = 0 - private specifications: VitestSpecifications + private readonly specifications: VitestSpecifications + private readonly watcher: VitestWatcher private pool: ProcessPool | undefined private _ready = false private _config?: ResolvedConfig @@ -107,6 +127,7 @@ export class Vitest { private _onSetServer: OnServerRestartHandler[] = [] private _onCancelListeners: ((reason: CancelReason) => Awaitable)[] = [] private _onUserTestsRerun: OnTestsRerunHandler[] = [] + private _onFilterWatchedSpecification: ((spec: TestSpecification) => boolean)[] = [] /** @deprecated will be removed in 3.0, use `vitest.watcher` */ public get invalidates() { @@ -287,6 +308,13 @@ export class Vitest { this.getRootTestProject().provide(key, value) } + /** + * Get global provided context. + */ + public getProvidedContext(): ProvidedContext { + return this.getRootTestProject().getProvidedContext() + } + /** @internal */ _ensureRootProject() { if (this.coreWorkspaceProject) { @@ -760,16 +788,17 @@ export class Vitest { * @param specifications A list of specifications to run. * @param allTestsRun Indicates whether all tests were run. This only matters for coverage. */ - public async rerunTestSpecifications(specifications: TestSpecification[], allTestsRun = false): Promise { + public async rerunTestSpecifications(specifications: TestSpecification[], allTestsRun = false): Promise { this.configOverride.testNamePattern = undefined const files = specifications.map(spec => spec.moduleId) await Promise.all([ this.report('onWatcherRerun', files, 'rerun test'), ...this._onUserTestsRerun.map(fn => fn(specifications)), ]) - await this.runTestSpecifications(specifications, allTestsRun) + const result = await this.runTestSpecifications(specifications, allTestsRun) await this.report('onWatcherStart', this.state.getFiles(files)) + return result } /** @internal */ @@ -924,7 +953,13 @@ export class Vitest { const triggerIds = new Set(triggerId.map(id => relative(this.config.root, id))) const triggerLabel = Array.from(triggerIds).join(', ') - const specifications = files.flatMap(file => this.getModuleSpecifications(file)) + // get file specifications and filter them if needed + const specifications = files.flatMap(file => this.getModuleSpecifications(file)).filter((specification) => { + if (this._onFilterWatchedSpecification.length === 0) { + return true + } + return this._onFilterWatchedSpecification.every(fn => fn(specification)) + }) await Promise.all([ this.report('onWatcherRerun', files, triggerLabel), ...this._onUserTestsRerun.map(fn => fn(specifications)), @@ -1134,6 +1169,17 @@ export class Vitest { this._onUserTestsRerun.push(fn) } + /** + * Register a handler that will be called when a file is changed. + * This callback should return `true` of `false` indicating whether the test file needs to be rerun. + * @example + * const testsToRun = [resolve('./test.spec.ts')] + * vitest.onFilterWatchedSpecification(specification => testsToRun.includes(specification.moduleId)) + */ + onFilterWatchedSpecification(fn: (specification: TestSpecification) => boolean): void { + this._onFilterWatchedSpecification.push(fn) + } + /** @internal */ onAfterSetServer(fn: OnServerRestartHandler) { this._onSetServer.push(fn) diff --git a/packages/vitest/src/node/specifications.ts b/packages/vitest/src/node/specifications.ts index 09d964d6cc65..4a5db0168ada 100644 --- a/packages/vitest/src/node/specifications.ts +++ b/packages/vitest/src/node/specifications.ts @@ -8,7 +8,7 @@ import { groupFilters, parseFilter } from './cli/filter' import { GitNotFoundError, IncludeTaskLocationDisabledError, LocationFilterFileNotFoundError } from './errors' export class VitestSpecifications { - private _cachedSpecs = new Map() + private readonly _cachedSpecs = new Map() constructor(private vitest: Vitest) {} diff --git a/packages/vitest/src/node/watcher.ts b/packages/vitest/src/node/watcher.ts index 3a01e8c69752..dd35bb86ff23 100644 --- a/packages/vitest/src/node/watcher.ts +++ b/packages/vitest/src/node/watcher.ts @@ -5,11 +5,16 @@ import { noop, slash } from '@vitest/utils' import mm from 'micromatch' export class VitestWatcher { + /** + * Modules that will be invalidated on the next run. + */ public readonly invalidates: Set = new Set() + /** + * Test files that have changed and need to be rerun. + */ public readonly changedTests: Set = new Set() - private _onRerun: ((file: string) => void)[] = [] - private _onFilterTestFile: ((file: string) => boolean)[] = [] + private readonly _onRerun: ((file: string) => void)[] = [] constructor(private vitest: Vitest) {} @@ -24,15 +29,6 @@ export class VitestWatcher { return this } - /** - * Register a handler that will be called when a file is changed. - * This callback should return a value indicating whether the test file needs to be rerun. - */ - onFilterTestFile(cb: (file: string) => boolean): this { - this._onFilterTestFile.push(cb) - return this - } - public unregisterWatcher: () => void = noop public registerWatcher(): this { const watcher = this.vitest.vite.watcher @@ -55,12 +51,6 @@ export class VitestWatcher { } private scheduleRerun(file: string): void { - for (const testFile of this.changedTests) { - if (this._onFilterTestFile.some(cb => cb(testFile))) { - this.changedTests.delete(testFile) - } - } - this._onRerun.forEach(cb => cb(file)) } From 66f79dd5c5816beab709aa09af1529c0d0965180 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 26 Nov 2024 17:37:54 +0100 Subject: [PATCH 19/73] chore: some docs --- docs/advanced/api.md | 137 +++++++++++++++++++++++++++---- packages/vitest/src/node/core.ts | 49 +++++------ 2 files changed, 139 insertions(+), 47 deletions(-) diff --git a/docs/advanced/api.md b/docs/advanced/api.md index 8d840ce6d0bd..cb534f3f3842 100644 --- a/docs/advanced/api.md +++ b/docs/advanced/api.md @@ -4,10 +4,6 @@ outline: [2, 3] # Node API -::: warning -Vitest exposes experimental private API. Breaking changes might not follow SemVer, please pin Vitest's version when using it. -::: - ## startVitest You can start running Vitest tests using its Node API: @@ -98,7 +94,56 @@ Benchmark mode calls `bench` functions and throws an error, when it encounters ` You can start running tests or benchmarks with `start` method. You can pass an array of strings to filter test files. -### `provide` +### config + +The root (or global) config. If workspace feature is enabled, projects will reference this as `globalConfig`. + +::: warning +This is Vitest config, it doesn't extend _Vite_ config. It only has resolved values from the `test` property. +::: + +### vite + +This is a global [`ViteDevServer`](https://vite.dev/guide/api-javascript#vitedevserver). + +### state + +::: warning +Public state is an experimental state. Breaking changes might not follow SemVer, please pin Vitest's version when using it. +::: + +Global state stores information about the current tests. It uses the same API from `@vitest/runner` by default, but we recommend using the [Reported API](/advanced/reporters#reported-tasks) instead by calling `state.getReportedEntity()` on the `@vitest/runner` API: + +```ts +const task = vitest.state.idMap.get(taskId) // old API +const testCase = vitest.state.getReportedEntity(task) // new API +``` + +In the future, the old API won't be exposed anymore. + +### snapshot + +The global snapshot manager. Vitest keeps track of all snapshots using the `snapshot.add` method. + +You can get the latest summary of snapshots via the `vitest.snapshot.summay` property. + +### cache + +Cache manager that stores information about latest test results and test file stats. In Vitest itself this is only used by the default sequencer to sort tests. + +### ready + +Vitest needs to be resolved with the Vite server to be properly initialized. If the `Vitest` instance was created manually, you might need to check the `ready` status before accessing the `vite`, `state`, `cache`, `config`, and `snapshot` properties; otherwise, they will throw an error in the getter. + +In normal circumstances, you would never call this method because `createVitest` and `startVitest` return already resolved Vitest instance. + +### getRootTestProject + +This returns the root test project. The root project generally doesn't run any tests and is not included in `vitest.projects` unless the user explicitly includes the root config in their workspace. + +The primary goal of the root project is to setup the global config. In fact, `rootProject.config` references `rootProject.globalConfig` and `vitest.config` directly. + +### provide Vitest exposes `provide` method which is a shorthand for `vitest.getRootTestProject().provide`. With this method you can pass down values from the main thread to tests. All values are checked with `structuredClone` before they are stored, but the values themselves are not cloned. @@ -130,17 +175,63 @@ declare module 'vitest' { Technically, `provide` is a method of [`TestProject`](#testproject), so it is limited to the specific project. However, all projects inherit the values from the core project which makes `vitest.provide` universal way of passing down values to tests. ::: -::: tip -This method is also available to [global setup files](/config/#globalsetup) for cases where you cannot use the public API: +### getProvidedContext -```js -export default function setup({ provide }) { - provide('wsPort', 3000) -} +This returns the root context object. This is a shorthand for `vitest.getRootTestProject().getProvidedContext`. + +### getProjectByName + +This method returns the project by its name. Simillar to calling `vitest.projects.find`. + +::: warning +In case the project doesn't exist, this method will return the root project - make sure to check the names again if you need to make sure the project you are looking for is the one returned. +::: + +### globTestSpecifications + +This method constructs new [test specifications](#testspecification) by collecting every test in all projects with [`project.globTestFiles`](#globtestfiles). It accepts string filters to match the test files. + +::: warning +As of Vitest 2.2.0, it's possible to have multiple test specifications with the same module ID (file path) if `poolMatchGlob` has several pools or if `typecheck` is enabled. +::: + +```ts +const specifications = await vitest.globTestSpecifications(['my-filter']) +// [TestSpecification{ moduleId: '/tests/my-filter.test.ts', pool: 'forks' }] +console.log(specifications) ``` + +### mergeReports +### collect +### listFiles +### start +### init + +### getModuleSpecifications + +Returns a list of test specifications related to the module ID. The ID should already be resolved to an absolute file path. If ID doesn't match `include` or `includeSource` patterns, the returned array will be empty. + +::: warning +As of Vitest 2.2.0, this method uses a cache to check if the file is a test. To make sure that the cache is not empty, call `globTestSpecifications` at least once. ::: -## TestProject 3.0.0 {#testproject} +### runTestSpecifications +### rerunTestSpecifications +### collectTests +### cancelCurrentRun +### updateSnapshot +### invalidateFile +### close +### exit + +### shouldKeepServer +### onServerRestart +### onCancel +### onClose +### onTestsRerun +### onFilterWatchedSpecification + +## TestProject 2.2.0 {#testproject} - **Alias**: `WorkspaceProject` before 3.0.0 @@ -210,7 +301,7 @@ This is the project's resolved test config. ### vite -This is project's `ViteDevServer`. All projects have their own Vite servers. +This is project's [`ViteDevServer`](https://vite.dev/guide/api-javascript#vitedevserver). All projects have their own Vite servers. ### browser @@ -241,6 +332,16 @@ const value = inject('key') The values can be provided dynamicaly. Provided value in tests will be updated on their next run. +::: tip +This method is also available to [global setup files](/config/#globalsetup) for cases where you cannot use the public API: + +```js +export default function setup({ provide }) { + provide('wsPort', 3000) +} +``` +::: + ### getProvidedContext This returns the context object. Every project also inherits the global context set by `vitest.provide`. @@ -284,11 +385,7 @@ await vitest.runFiles([specification], true) ### isRootProject -Checks if the current project is the root project. You can also get the root project by calling `vitest.getRootTestProject()`. - -The root project generally doesn't run any tests and is not included in `vitest.projects` unless the user explicitly includes the root config in their workspace. - -The primary goal of the root project is to setup the global config. In fact, `rootProject.config` references `rootProject.globalConfig` and `vitest.config` directly. +Checks if the current project is the root project. You can also get the root project by calling [`vitest.getRootTestProject()`](#getroottestproject). ### globTestFiles @@ -354,3 +451,7 @@ project.onTestsRerun((specs) => { Closes the project and all associated resources. This can only be called once; the closing promise is cached until the server restarts. If the resources are needed again, create a new project. In detail, this method closes the Vite server, stops the typechecker service, closes the browser if it's running, deletes the temporary directory that holds the source code, and resets the provided context. + +## TestSpecification + + diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 12fbe27cfb54..64abae0638d1 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -93,9 +93,6 @@ export class Vitest { /** @internal */ vitenode: ViteNodeServer = undefined! /** @internal */ runner: ViteNodeRunner = undefined! - // TODO: remove in 3.0 - private watchedTests: Set = new Set() - private isFirstRun = true private restartsCount = 0 @@ -178,7 +175,7 @@ export class Vitest { } /** - * Test results and test file stats cache. Primarily used by the sequencer to order tests. + * Test results and test file stats cache. Primarily used by the sequencer to sort tests. */ get cache(): VitestCache { assert(this._cache, 'cache') @@ -316,7 +313,7 @@ export class Vitest { } /** @internal */ - _ensureRootProject() { + _ensureRootProject(): TestProject { if (this.coreWorkspaceProject) { return this.coreWorkspaceProject } @@ -628,6 +625,24 @@ export class Vitest { return this.runFiles(specifications, allTestsRun) } + /** + * Rerun files and trigger `onWatcherRerun`, `onWatcherStart` and `onTestsRerun` events. + * @param specifications A list of specifications to run. + * @param allTestsRun Indicates whether all tests were run. This only matters for coverage. + */ + public async rerunTestSpecifications(specifications: TestSpecification[], allTestsRun = false): Promise { + this.configOverride.testNamePattern = undefined + const files = specifications.map(spec => spec.moduleId) + await Promise.all([ + this.report('onWatcherRerun', files, 'rerun test'), + ...this._onUserTestsRerun.map(fn => fn(specifications)), + ]) + const result = await this.runTestSpecifications(specifications, allTestsRun) + + await this.report('onWatcherStart', this.state.getFiles(files)) + return result + } + private async runFiles(specs: TestSpecification[], allTestsRun: boolean): Promise { const filepaths = specs.map(spec => spec.moduleId) this.state.collectPaths(filepaths) @@ -783,24 +798,6 @@ export class Vitest { } } - /** - * Rerun files and trigger `onWatcherRerun`, `onWatcherStart` and `onTestsRerun` events. - * @param specifications A list of specifications to run. - * @param allTestsRun Indicates whether all tests were run. This only matters for coverage. - */ - public async rerunTestSpecifications(specifications: TestSpecification[], allTestsRun = false): Promise { - this.configOverride.testNamePattern = undefined - const files = specifications.map(spec => spec.moduleId) - await Promise.all([ - this.report('onWatcherRerun', files, 'rerun test'), - ...this._onUserTestsRerun.map(fn => fn(specifications)), - ]) - const result = await this.runTestSpecifications(specifications, allTestsRun) - - await this.report('onWatcherStart', this.state.getFiles(files)) - return result - } - /** @internal */ async rerunFiles(files: string[] = this.state.getFilepaths(), trigger?: string, allTestsRun = true, resetTestNamePattern = false): Promise { if (resetTestNamePattern) { @@ -971,12 +968,6 @@ export class Vitest { }, WATCHER_DEBOUNCE) } - /** - * Watch only the specified tests. If no tests are provided, all tests will be watched. - * @deprecated This method does nothing. It will be remove in Vitest 3.0. - */ - public watchTests(_tests: string[]): void {} - /** * Invalidate a file in all projects. */ From e23a4ebc38c4e600e9183e840e2606136a2d8f96 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 2 Dec 2024 00:17:20 +0100 Subject: [PATCH 20/73] docs: add more infor about test filtering --- docs/guide/cli.md | 25 +++++++++++++++++++++++++ docs/guide/filtering.md | 16 +++++++++++++--- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/docs/guide/cli.md b/docs/guide/cli.md index b4ac323c2187..789935c04760 100644 --- a/docs/guide/cli.md +++ b/docs/guide/cli.md @@ -19,6 +19,31 @@ vitest foobar Will run only the test file that contains `foobar` in their paths. This filter only checks inclusion and doesn't support regexp or glob patterns (unless your terminal processes it before Vitest receives the filter). +Since Vitest 2.2, you can also specify the test by filename and line number: + +```bash +$ vitest basic/foo.test.ts:10 +``` + +::: warning +Note that Vitest requires the full filename for this feature to work. It can be relative to the current working directory or an absolute file path. + +```bash +$ vitest basic/foo.js:10 # ✅ +$ vitest ./basic/foo.js:10 # ✅ +$ vitest /users/project/basic/foo.js:10 # ✅ +$ vitest foo:10 # ❌ +$ vitest ./basic/foo:10 # ❌ +``` + +At the moment Vitest also doesn't support ranges: + +```bash +$ vitest basic/foo.test.ts:10, basic/foo.test.ts:25 # ✅ +$ vitest basic/foo.test.ts:10-25 # ❌ +``` +::: + ### `vitest run` Perform a single run without watch mode. diff --git a/docs/guide/filtering.md b/docs/guide/filtering.md index 6d7bcebdddaa..44a99af64577 100644 --- a/docs/guide/filtering.md +++ b/docs/guide/filtering.md @@ -31,11 +31,21 @@ $ vitest basic/foo.test.ts:10 ``` ::: warning -Note that you have to specify the full filename, and specify the exact line number, i.e. you can't do +Note that Vitest requires the full filename for this feature to work. It can be relative to the current working directory or an absolute file path. ```bash -$ vitest foo:10 -$ vitest basic/foo.test.ts:10-25 +$ vitest basic/foo.js:10 # ✅ +$ vitest ./basic/foo.js:10 # ✅ +$ vitest /users/project/basic/foo.js:10 # ✅ +$ vitest foo:10 # ❌ +$ vitest ./basic/foo:10 # ❌ +``` + +At the moment Vitest also doesn't support ranges: + +```bash +$ vitest basic/foo.test.ts:10, basic/foo.test.ts:25 # ✅ +$ vitest basic/foo.test.ts:10-25 # ❌ ``` ::: From adaf11ec5f37dd8cdd0cbda6c348b5cdb39d13f9 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 2 Dec 2024 00:17:43 +0100 Subject: [PATCH 21/73] refactor: rename FileSpec to FileSpecification and Filter to FileFilter --- packages/runner/src/collect.ts | 4 +-- packages/runner/src/run.ts | 6 ++--- packages/runner/src/types.ts | 2 +- packages/runner/src/types/runner.ts | 5 +++- packages/vitest/src/node/cli/filter.ts | 6 ++--- packages/vitest/src/node/pools/forks.ts | 4 +-- packages/vitest/src/node/pools/threads.ts | 4 +-- packages/vitest/src/node/pools/vmForks.ts | 4 +-- packages/vitest/src/node/pools/vmThreads.ts | 4 +-- packages/vitest/src/node/project.ts | 4 +-- packages/vitest/src/node/spec.ts | 30 ++++++++++++--------- packages/vitest/src/runtime/runBaseTests.ts | 4 +-- packages/vitest/src/runtime/runVmTests.ts | 4 +-- packages/vitest/src/runtime/types/utils.ts | 2 +- packages/vitest/src/types/worker.ts | 4 +-- packages/vitest/src/utils/test-helpers.ts | 7 ++--- 16 files changed, 49 insertions(+), 45 deletions(-) diff --git a/packages/runner/src/collect.ts b/packages/runner/src/collect.ts index 8dd17a95cde9..33634d598806 100644 --- a/packages/runner/src/collect.ts +++ b/packages/runner/src/collect.ts @@ -1,4 +1,4 @@ -import type { FileSpec, VitestRunner } from './types/runner' +import type { FileSpecification, VitestRunner } from './types/runner' import type { File, SuiteHooks } from './types/tasks' import { toArray } from '@vitest/utils' import { processError } from '@vitest/utils/error' @@ -20,7 +20,7 @@ import { const now = globalThis.performance ? globalThis.performance.now.bind(globalThis.performance) : Date.now export async function collectTests( - specs: string[] | FileSpec[], + specs: string[] | FileSpecification[], runner: VitestRunner, ): Promise { const files: File[] = [] diff --git a/packages/runner/src/run.ts b/packages/runner/src/run.ts index 44c1baf3b189..d191091fd48e 100644 --- a/packages/runner/src/run.ts +++ b/packages/runner/src/run.ts @@ -1,6 +1,6 @@ import type { Awaitable } from '@vitest/utils' import type { DiffOptions } from '@vitest/utils/diff' -import type { FileSpec, VitestRunner } from './types/runner' +import type { FileSpecification, VitestRunner } from './types/runner' import type { ExtendedContext, File, @@ -514,7 +514,7 @@ export async function runFiles(files: File[], runner: VitestRunner): Promise { +export async function startTests(specs: string[] | FileSpecification[], runner: VitestRunner): Promise { const paths = specs.map(f => typeof f === 'string' ? f : f.filepath) await runner.onBeforeCollect?.(paths) @@ -532,7 +532,7 @@ export async function startTests(specs: string[] | FileSpec[], runner: VitestRun return files } -async function publicCollect(specs: string[] | FileSpec[], runner: VitestRunner): Promise { +async function publicCollect(specs: string[] | FileSpecification[], runner: VitestRunner): Promise { const paths = specs.map(f => typeof f === 'string' ? f : f.filepath) await runner.onBeforeCollect?.(paths) diff --git a/packages/runner/src/types.ts b/packages/runner/src/types.ts index 900fb8bd0755..778a86d01110 100644 --- a/packages/runner/src/types.ts +++ b/packages/runner/src/types.ts @@ -1,6 +1,6 @@ export type { CancelReason, - FileSpec, + FileSpecification, VitestRunner, VitestRunnerConfig, VitestRunnerConstructor, diff --git a/packages/runner/src/types/runner.ts b/packages/runner/src/types/runner.ts index a8571f7415b1..7d3011406380 100644 --- a/packages/runner/src/types/runner.ts +++ b/packages/runner/src/types/runner.ts @@ -39,7 +39,10 @@ export interface VitestRunnerConfig { diffOptions?: DiffOptions } -export interface FileSpec { +/** + * Possible options to run a single file in a test. + */ +export interface FileSpecification { filepath: string testLocations: number[] | undefined } diff --git a/packages/vitest/src/node/cli/filter.ts b/packages/vitest/src/node/cli/filter.ts index 0fcc577e5a3f..79594659b744 100644 --- a/packages/vitest/src/node/cli/filter.ts +++ b/packages/vitest/src/node/cli/filter.ts @@ -1,7 +1,7 @@ import { groupBy } from '../../utils/base' import { RangeLocationFilterProvidedError } from '../errors' -export function parseFilter(filter: string): Filter { +export function parseFilter(filter: string): FileFilter { const colonIndex = filter.lastIndexOf(':') if (colonIndex === -1) { return { filename: filter } @@ -26,12 +26,12 @@ export function parseFilter(filter: string): Filter { } } -interface Filter { +export interface FileFilter { filename: string lineNumber?: undefined | number } -export function groupFilters(filters: Filter[]) { +export function groupFilters(filters: FileFilter[]) { const groupedFilters_ = groupBy(filters, f => f.filename) const groupedFilters = Object.fromEntries(Object.entries(groupedFilters_) .map((entry) => { diff --git a/packages/vitest/src/node/pools/forks.ts b/packages/vitest/src/node/pools/forks.ts index 26172d6c2158..cba773dde790 100644 --- a/packages/vitest/src/node/pools/forks.ts +++ b/packages/vitest/src/node/pools/forks.ts @@ -1,4 +1,4 @@ -import type { FileSpec } from '@vitest/runner' +import type { FileSpecification } from '@vitest/runner' import type { TinypoolChannel, Options as TinypoolOptions } from 'tinypool' import type { RunnerRPC, RuntimeRPC } from '../../types/rpc' import type { ContextRPC, ContextTestEnvironment } from '../../types/worker' @@ -102,7 +102,7 @@ export function createForksPool( async function runFiles( project: TestProject, config: SerializedConfig, - files: FileSpec[], + files: FileSpecification[], environment: ContextTestEnvironment, invalidates: string[] = [], ) { diff --git a/packages/vitest/src/node/pools/threads.ts b/packages/vitest/src/node/pools/threads.ts index b1c8786163c9..e800ee14d0a4 100644 --- a/packages/vitest/src/node/pools/threads.ts +++ b/packages/vitest/src/node/pools/threads.ts @@ -1,4 +1,4 @@ -import type { FileSpec } from '@vitest/runner/types/runner' +import type { FileSpecification } from '@vitest/runner/types/runner' import type { Options as TinypoolOptions } from 'tinypool' import type { RunnerRPC, RuntimeRPC } from '../../types/rpc' import type { ContextTestEnvironment } from '../../types/worker' @@ -96,7 +96,7 @@ export function createThreadsPool( async function runFiles( project: TestProject, config: SerializedConfig, - files: FileSpec[], + files: FileSpecification[], environment: ContextTestEnvironment, invalidates: string[] = [], ) { diff --git a/packages/vitest/src/node/pools/vmForks.ts b/packages/vitest/src/node/pools/vmForks.ts index dd4e2ec4a909..75085af560ae 100644 --- a/packages/vitest/src/node/pools/vmForks.ts +++ b/packages/vitest/src/node/pools/vmForks.ts @@ -1,4 +1,4 @@ -import type { FileSpec } from '@vitest/runner' +import type { FileSpecification } from '@vitest/runner' import type { TinypoolChannel, Options as TinypoolOptions } from 'tinypool' import type { RunnerRPC, RuntimeRPC } from '../../types/rpc' import type { ContextRPC, ContextTestEnvironment } from '../../types/worker' @@ -110,7 +110,7 @@ export function createVmForksPool( async function runFiles( project: TestProject, config: SerializedConfig, - files: FileSpec[], + files: FileSpecification[], environment: ContextTestEnvironment, invalidates: string[] = [], ) { diff --git a/packages/vitest/src/node/pools/vmThreads.ts b/packages/vitest/src/node/pools/vmThreads.ts index 7eb84e928019..44c085a8aae2 100644 --- a/packages/vitest/src/node/pools/vmThreads.ts +++ b/packages/vitest/src/node/pools/vmThreads.ts @@ -1,4 +1,4 @@ -import type { FileSpec } from '@vitest/runner' +import type { FileSpecification } from '@vitest/runner' import type { Options as TinypoolOptions } from 'tinypool' import type { RunnerRPC, RuntimeRPC } from '../../types/rpc' import type { ContextTestEnvironment } from '../../types/worker' @@ -101,7 +101,7 @@ export function createVmThreadsPool( async function runFiles( project: TestProject, config: SerializedConfig, - files: FileSpec[], + files: FileSpecification[], environment: ContextTestEnvironment, invalidates: string[] = [], ) { diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts index fce4f51b954b..b850a02476e9 100644 --- a/packages/vitest/src/node/project.ts +++ b/packages/vitest/src/node/project.ts @@ -134,13 +134,13 @@ export class TestProject { public createSpecification( moduleId: string, pool?: string, - testLocations?: number[] | undefined, + locations?: number[] | undefined, ): TestSpecification { return new TestSpecification( this, moduleId, pool || getFilePoolName(this, moduleId), - testLocations, + locations, ) } diff --git a/packages/vitest/src/node/spec.ts b/packages/vitest/src/node/spec.ts index 9c74a8ca87d1..27872d6a0f6e 100644 --- a/packages/vitest/src/node/spec.ts +++ b/packages/vitest/src/node/spec.ts @@ -16,19 +16,29 @@ export class TestSpecification { */ public readonly 2: { pool: Pool } + /** + * The test project that the module belongs to. + */ public readonly project: TestProject + /** + * The ID of the module in the Vite module graph. It is usually an absolute file path. + */ public readonly moduleId: string + /** + * The current test pool. It's possible to have multiple pools in a single test project with `poolMatchGlob` and `typecheck.enabled`. + * @experimental In Vitest 3, the project will only support a single pool + */ public readonly pool: Pool - /** @private */ - public readonly testLocations: number[] | undefined - // public readonly location: WorkspaceSpecLocation | undefined + /** + * Line numbers of the test locations in the module to run. + */ + public readonly locations: number[] | undefined constructor( project: TestProject, moduleId: string, pool: Pool, - testLocations?: number[] | undefined, - // location?: WorkspaceSpecLocation | undefined, + locations?: number[] | undefined, ) { this[0] = project this[1] = moduleId @@ -36,8 +46,7 @@ export class TestSpecification { this.project = project this.moduleId = moduleId this.pool = pool - this.testLocations = testLocations - // this.location = location + this.locations = locations } toJSON(): SerializedTestSpecification { @@ -47,7 +56,7 @@ export class TestSpecification { root: this.project.config.root, }, this.moduleId, - { pool: this.pool }, + { pool: this.pool, locations: this.locations }, ] } @@ -61,8 +70,3 @@ export class TestSpecification { yield this.pool } } - -// interface WorkspaceSpecLocation { -// start: number -// end: number -// } diff --git a/packages/vitest/src/runtime/runBaseTests.ts b/packages/vitest/src/runtime/runBaseTests.ts index a00262950ac6..d1027988d7ba 100644 --- a/packages/vitest/src/runtime/runBaseTests.ts +++ b/packages/vitest/src/runtime/runBaseTests.ts @@ -1,4 +1,4 @@ -import type { FileSpec } from '@vitest/runner' +import type { FileSpecification } from '@vitest/runner' import type { ResolvedTestEnvironment } from '../types/environment' import type { SerializedConfig } from './config' import type { VitestExecutor } from './execute' @@ -18,7 +18,7 @@ import { getWorkerState, resetModules } from './utils' // browser shouldn't call this! export async function run( method: 'run' | 'collect', - files: FileSpec[], + files: FileSpecification[], config: SerializedConfig, environment: ResolvedTestEnvironment, executor: VitestExecutor, diff --git a/packages/vitest/src/runtime/runVmTests.ts b/packages/vitest/src/runtime/runVmTests.ts index 8193c0a34833..ea69c46a249e 100644 --- a/packages/vitest/src/runtime/runVmTests.ts +++ b/packages/vitest/src/runtime/runVmTests.ts @@ -1,4 +1,4 @@ -import type { FileSpec } from '@vitest/runner' +import type { FileSpecification } from '@vitest/runner' import type { SerializedConfig } from './config' import type { VitestExecutor } from './execute' import { createRequire } from 'node:module' @@ -22,7 +22,7 @@ import { getWorkerState } from './utils' export async function run( method: 'run' | 'collect', - files: FileSpec[], + files: FileSpecification[], config: SerializedConfig, executor: VitestExecutor, ): Promise { diff --git a/packages/vitest/src/runtime/types/utils.ts b/packages/vitest/src/runtime/types/utils.ts index 073327421d9f..2dcccbbbadb1 100644 --- a/packages/vitest/src/runtime/types/utils.ts +++ b/packages/vitest/src/runtime/types/utils.ts @@ -1,5 +1,5 @@ export type SerializedTestSpecification = [ project: { name: string | undefined; root: string }, file: string, - options: { pool: string }, + options: { pool: string; locations?: number[] | undefined }, ] diff --git a/packages/vitest/src/types/worker.ts b/packages/vitest/src/types/worker.ts index ee712b2b60c2..494bf7dac6db 100644 --- a/packages/vitest/src/types/worker.ts +++ b/packages/vitest/src/types/worker.ts @@ -1,4 +1,4 @@ -import type { CancelReason, FileSpec, Task } from '@vitest/runner' +import type { CancelReason, FileSpecification, Task } from '@vitest/runner' import type { BirpcReturn } from 'birpc' import type { ModuleCacheMap, ViteNodeResolveId } from 'vite-node' import type { SerializedConfig } from '../runtime/config' @@ -26,7 +26,7 @@ export interface ContextRPC { workerId: number config: SerializedConfig projectName: string - files: string[] | FileSpec[] + files: string[] | FileSpecification[] environment: ContextTestEnvironment providedContext: Record invalidates?: string[] diff --git a/packages/vitest/src/utils/test-helpers.ts b/packages/vitest/src/utils/test-helpers.ts index 9121c272696f..7db3549e0f72 100644 --- a/packages/vitest/src/utils/test-helpers.ts +++ b/packages/vitest/src/utils/test-helpers.ts @@ -30,10 +30,7 @@ export async function groupFilesByEnv( files: Array, ) { const filesWithEnv = await Promise.all( - files.map(async (spec) => { - const filepath = spec.moduleId - const { testLocations } = spec - const project = spec.project + files.map(async ({ moduleId: filepath, project, locations }) => { const code = await fs.readFile(filepath, 'utf-8') // 1. Check for control comments in the file @@ -74,7 +71,7 @@ export async function groupFilesByEnv( return { file: { filepath, - testLocations, + testLocations: locations, }, project, environment, From d65953fd69729f1590ba858957359c5a300ab57c Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 2 Dec 2024 17:12:30 +0100 Subject: [PATCH 22/73] docs: restructure the sudebar, update TestRunResult type --- docs/.vitepress/config.ts | 84 +++- docs/advanced/api.md | 457 --------------------- docs/advanced/api/index.md | 102 +++++ docs/advanced/api/test-case.md | 136 ++++++ docs/advanced/api/test-collection.md | 50 +++ docs/advanced/api/test-module.md | 46 +++ docs/advanced/api/test-project.md | 276 +++++++++++++ docs/advanced/api/test-specification.md | 3 + docs/advanced/api/test-suite.md | 46 +++ docs/advanced/api/vitest.md | 165 ++++++++ docs/advanced/pool.md | 2 +- docs/advanced/reporters.md | 294 +------------ docs/advanced/runner.md | 2 +- eslint.config.js | 1 + packages/vitest/src/node/cli/cac.ts | 8 +- packages/vitest/src/node/core.ts | 18 +- packages/vitest/src/node/project.ts | 8 +- packages/vitest/src/node/specifications.ts | 6 +- packages/vitest/src/node/types/tests.ts | 4 +- packages/vitest/src/public/node.ts | 4 +- 20 files changed, 922 insertions(+), 790 deletions(-) delete mode 100644 docs/advanced/api.md create mode 100644 docs/advanced/api/index.md create mode 100644 docs/advanced/api/test-case.md create mode 100644 docs/advanced/api/test-collection.md create mode 100644 docs/advanced/api/test-module.md create mode 100644 docs/advanced/api/test-project.md create mode 100644 docs/advanced/api/test-specification.md create mode 100644 docs/advanced/api/test-suite.md create mode 100644 docs/advanced/api/vitest.md diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 388181400ce4..fd0479d4f40e 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -146,7 +146,7 @@ export default ({ mode }: { mode: string }) => { items: [ { text: 'Advanced API', - link: '/advanced/api', + link: '/advanced/api/', activeMatch: '^/advanced/', }, { @@ -243,7 +243,15 @@ export default ({ mode }: { mode: string }) => { }, ], }, - footer(), + { + items: [ + ...footer(), + { + text: 'Node API Reference', + link: '/advanced/api/', + }, + ], + }, ], '/advanced': [ { @@ -251,8 +259,46 @@ export default ({ mode }: { mode: string }) => { collapsed: false, items: [ { - text: 'Vitest Node API', - link: '/advanced/api', + text: 'Node API', + items: [ + { + text: 'Getting Started', + link: '/advanced/api/', + }, + { + text: 'Vitest', + link: '/advanced/api/vitest', + }, + { + text: 'TestProject', + link: '/advanced/api/test-project', + }, + { + text: 'TestSpecification', + link: '/advanced/api/test-specification', + }, + ], + }, + { + text: 'Test Task API', + items: [ + { + text: 'TestCase', + link: '/advanced/api/test-case', + }, + { + text: 'TestSuite', + link: '/advanced/api/test-suite', + }, + { + text: 'TestModule', + link: '/advanced/api/test-module', + }, + { + text: 'TestCollection', + link: '/advanced/api/test-collection', + }, + ], }, { text: 'Runner API', @@ -282,7 +328,9 @@ export default ({ mode }: { mode: string }) => { }, ], }, - footer(), + { + items: footer(), + }, ], '/team': [], '/': [ @@ -308,7 +356,7 @@ export default ({ mode }: { mode: string }) => { link: '/guide/browser', }, { - text: 'Advanced API', + text: 'Node API Reference', link: '/advanced/api', }, { @@ -325,19 +373,17 @@ export default ({ mode }: { mode: string }) => { })) } -function footer(): DefaultTheme.SidebarItem { - return { - items: [ - { - text: 'Config Reference', - link: '/config/', - }, - { - text: 'Test API Reference', - link: '/api/', - }, - ], - } +function footer(): DefaultTheme.SidebarItem[] { + return [ + { + text: 'Config Reference', + link: '/config/', + }, + { + text: 'Test API Reference', + link: '/api/', + }, + ] } function introduction(): DefaultTheme.SidebarItem[] { diff --git a/docs/advanced/api.md b/docs/advanced/api.md deleted file mode 100644 index cb534f3f3842..000000000000 --- a/docs/advanced/api.md +++ /dev/null @@ -1,457 +0,0 @@ ---- -outline: [2, 3] ---- - -# Node API - -## startVitest - -You can start running Vitest tests using its Node API: - -```js -import { startVitest } from 'vitest/node' - -const vitest = await startVitest('test') - -await vitest?.close() -``` - -`startVitest` function returns `Vitest` instance if tests can be started. It returns `undefined`, if one of the following occurs: - -- Vitest didn't find the `vite` package (usually installed with Vitest) -- If coverage is enabled and run mode is "test", but the coverage package is not installed (`@vitest/coverage-v8` or `@vitest/coverage-istanbul`) -- If the environment package is not installed (`jsdom`/`happy-dom`/`@edge-runtime/vm`) - -If `undefined` is returned or tests failed during the run, Vitest sets `process.exitCode` to `1`. - -If watch mode is not enabled, Vitest will call `close` method. - -If watch mode is enabled and the terminal supports TTY, Vitest will register console shortcuts. - -You can pass down a list of filters as a second argument. Vitest will run only tests that contain at least one of the passed-down strings in their file path. - -Additionally, you can use the third argument to pass in CLI arguments, which will override any test config options. - -Alternatively, you can pass in the complete Vite config as the fourth argument, which will take precedence over any other user-defined options. - -After running the tests, you can get the results from the `state.getFiles` API: - -```ts -const vitest = await startVitest('test') - -console.log(vitest.state.getFiles()) // [{ type: 'file', ... }] -``` - -Since Vitest 2.1, it is recommended to use the ["Reported Tasks" API](/advanced/reporters#reported-tasks) together with the `state.getFiles`. In the future, Vitest will return those objects directly: - -```ts -const vitest = await startVitest('test') - -const [fileTask] = vitest.state.getFiles() -const testFile = vitest.state.getReportedEntity(fileTask) -``` - -## createVitest - -You can create Vitest instance yourself using `createVitest` function. It returns the same `Vitest` instance as `startVitest`, but it doesn't start tests and doesn't validate installed packages. - -```js -import { createVitest } from 'vitest/node' - -const vitest = await createVitest('test', { - watch: false, -}) -``` - -## parseCLI - -You can use this method to parse CLI arguments. It accepts a string (where arguments are split by a single space) or a strings array of CLI arguments in the same format that Vitest CLI uses. It returns a filter and `options` that you can later pass down to `createVitest` or `startVitest` methods. - -```ts -import { parseCLI } from 'vitest/node' - -parseCLI('vitest ./files.ts --coverage --browser=chrome') -``` - -## Vitest - -Vitest instance requires the current test mode. It can be either: - -- `test` when running runtime tests -- `benchmark` when running benchmarks - -### mode - -#### test - -Test mode will only call functions inside `test` or `it`, and throws an error when `bench` is encountered. This mode uses `include` and `exclude` options in the config to find test files. - -#### benchmark - -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. - -### start - -You can start running tests or benchmarks with `start` method. You can pass an array of strings to filter test files. - -### config - -The root (or global) config. If workspace feature is enabled, projects will reference this as `globalConfig`. - -::: warning -This is Vitest config, it doesn't extend _Vite_ config. It only has resolved values from the `test` property. -::: - -### vite - -This is a global [`ViteDevServer`](https://vite.dev/guide/api-javascript#vitedevserver). - -### state - -::: warning -Public state is an experimental state. Breaking changes might not follow SemVer, please pin Vitest's version when using it. -::: - -Global state stores information about the current tests. It uses the same API from `@vitest/runner` by default, but we recommend using the [Reported API](/advanced/reporters#reported-tasks) instead by calling `state.getReportedEntity()` on the `@vitest/runner` API: - -```ts -const task = vitest.state.idMap.get(taskId) // old API -const testCase = vitest.state.getReportedEntity(task) // new API -``` - -In the future, the old API won't be exposed anymore. - -### snapshot - -The global snapshot manager. Vitest keeps track of all snapshots using the `snapshot.add` method. - -You can get the latest summary of snapshots via the `vitest.snapshot.summay` property. - -### cache - -Cache manager that stores information about latest test results and test file stats. In Vitest itself this is only used by the default sequencer to sort tests. - -### ready - -Vitest needs to be resolved with the Vite server to be properly initialized. If the `Vitest` instance was created manually, you might need to check the `ready` status before accessing the `vite`, `state`, `cache`, `config`, and `snapshot` properties; otherwise, they will throw an error in the getter. - -In normal circumstances, you would never call this method because `createVitest` and `startVitest` return already resolved Vitest instance. - -### getRootTestProject - -This returns the root test project. The root project generally doesn't run any tests and is not included in `vitest.projects` unless the user explicitly includes the root config in their workspace. - -The primary goal of the root project is to setup the global config. In fact, `rootProject.config` references `rootProject.globalConfig` and `vitest.config` directly. - -### provide - -Vitest exposes `provide` method which is a shorthand for `vitest.getRootTestProject().provide`. With this method you can pass down values from the main thread to tests. All values are checked with `structuredClone` before they are stored, but the values themselves are not cloned. - -To recieve the values in the test, you need to import `inject` method from `vitest` entrypont: - -```ts -import { inject } from 'vitest' -const port = inject('wsPort') // 3000 -``` - -For better type safety, we encourage you to augment the type of `ProvidedContext`: - -```ts -import { createVitest } from 'vitest/node' - -const vitest = await createVitest('test', { - watch: false, -}) -vitest.provide('wsPort', 3000) - -declare module 'vitest' { - export interface ProvidedContext { - wsPort: number - } -} -``` - -::: warning -Technically, `provide` is a method of [`TestProject`](#testproject), so it is limited to the specific project. However, all projects inherit the values from the core project which makes `vitest.provide` universal way of passing down values to tests. -::: - -### getProvidedContext - -This returns the root context object. This is a shorthand for `vitest.getRootTestProject().getProvidedContext`. - -### getProjectByName - -This method returns the project by its name. Simillar to calling `vitest.projects.find`. - -::: warning -In case the project doesn't exist, this method will return the root project - make sure to check the names again if you need to make sure the project you are looking for is the one returned. -::: - -### globTestSpecifications - -This method constructs new [test specifications](#testspecification) by collecting every test in all projects with [`project.globTestFiles`](#globtestfiles). It accepts string filters to match the test files. - -::: warning -As of Vitest 2.2.0, it's possible to have multiple test specifications with the same module ID (file path) if `poolMatchGlob` has several pools or if `typecheck` is enabled. -::: - -```ts -const specifications = await vitest.globTestSpecifications(['my-filter']) -// [TestSpecification{ moduleId: '/tests/my-filter.test.ts', pool: 'forks' }] -console.log(specifications) -``` - -### mergeReports -### collect -### listFiles -### start -### init - -### getModuleSpecifications - -Returns a list of test specifications related to the module ID. The ID should already be resolved to an absolute file path. If ID doesn't match `include` or `includeSource` patterns, the returned array will be empty. - -::: warning -As of Vitest 2.2.0, this method uses a cache to check if the file is a test. To make sure that the cache is not empty, call `globTestSpecifications` at least once. -::: - -### runTestSpecifications -### rerunTestSpecifications -### collectTests -### cancelCurrentRun -### updateSnapshot -### invalidateFile -### close -### exit - -### shouldKeepServer -### onServerRestart -### onCancel -### onClose -### onTestsRerun -### onFilterWatchedSpecification - -## TestProject 2.2.0 {#testproject} - -- **Alias**: `WorkspaceProject` before 3.0.0 - -### name - -The name is a unique string assigned by the user or interpreted by Vitest. If user did not provide a name, Vitest tries to load a `package.json` in the root of the project and takes the `name` property from there. If there is no `package.json`, Vitest uses the name of the folder by default. Inline projects use numbers as the name (converted to string). - -::: code-group -```ts [node.js] -import { createVitest } from 'vitest/node' - -const vitest = await createVitest('test') -vitest.projects.map(p => p.name) === [ - '@pkg/server', - 'utils', - '2', - 'custom' -] -``` -```ts [vitest.workspace.js] -export default [ - './packages/server', // has package.json with "@pkg/server" - './utils', // doesn't have a package.json file - { - // doesn't customize the name - test: { - pool: 'threads', - }, - }, - { - // customized the name - test: { - name: 'custom', - }, - }, -] -``` -::: - -### vitest - -`vitest` references the global [`vitest`](#vitest) process. - -### serializedConfig - -This is the test config that all tests will receive. Vitest [serializes config](https://github.com/vitest-dev/vitest/blob/main/packages/vitest/src/node/config/serializeConfig.ts) manually by removing all functions and properties that are not possible to serialize. Since this value is available in both tests and node, it is exported from the main entry point. - -```ts -import type { SerializedConfig } from 'vitest' - -const config: SerializedConfig = vitest.projects[0].serializedConfig -``` - -### globalConfig - -The test config that `vitest` was initialized with. If this is the root project, `globalConfig` and `config` will reference the same object. This config is useful for values that cannot be set on the project level, like `coverage` or `reporters`. - -```ts -import type { ResolvedConfig } from 'vitest/node' - -vitest.config === vitest.projects[0].globalConfig -``` - -### config - -This is the project's resolved test config. - -### vite - -This is project's [`ViteDevServer`](https://vite.dev/guide/api-javascript#vitedevserver). All projects have their own Vite servers. - -### browser - -This value will be set only if tests are running in the browser. If `browser` is enabled, but tests didn't run yet, this will be `undefined`. If you need to check if the project supports browser tests, use `project.isBrowserSupported()` method. - -::: warning -The browser API is even more experimental and doesn't follow SemVer. The browser API will be standardized separately from the rest of the APIs. -::: - -### provide - -A way to provide custom values to tests in addition to [`config.provide`](/config/#provide) field. All values are validated with [`structuredClone`](https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone) before they are stored, but the values on `providedContext` themselves are not cloned. - -::: code-group -```ts [node.js] -import { createVitest } from 'vitest/node' - -const vitest = await createVitest('test') -const project = vitest.projects.find(p => p.name === 'custom') -project.provide('key', 'value') -await vitest.start() -``` -```ts [test.spec.js] -import { inject } from 'vitest' -const value = inject('key') -``` -::: - -The values can be provided dynamicaly. Provided value in tests will be updated on their next run. - -::: tip -This method is also available to [global setup files](/config/#globalsetup) for cases where you cannot use the public API: - -```js -export default function setup({ provide }) { - provide('wsPort', 3000) -} -``` -::: - -### getProvidedContext - -This returns the context object. Every project also inherits the global context set by `vitest.provide`. - -```ts -import { createVitest } from 'vitest/node' - -const vitest = await createVitest('test') -vitest.provide('global', true) -const project = vitest.projects.find(p => p.name === 'custom') -project.provide('key', 'value') - -// { global: true, key: 'value' } -const context = project.getProvidedContext() -``` - -::: tip -Project context values will always override global ones. -::: - -### createSpecification - -Create a test specification that can be used in `vitest.runFiles`. Specification scopes the test file to a specific `project` and `pool` (optionally). - -```ts -import { createVitest } from 'vitest/node' -import { resolve } from 'node:path/posix' - -const vitest = await createVitest('test') -const project = vitest.projects[0] -const specification = project.createSpecification( - resolve('./basic.test.ts'), - 'threads', // optional override -) -await vitest.runFiles([specification], true) -``` - -::: warning -`createSpecification` expects an absolute file path. It doesn't resolve the file or check that it exists on the file system. -::: - -### isRootProject - -Checks if the current project is the root project. You can also get the root project by calling [`vitest.getRootTestProject()`](#getroottestproject). - -### globTestFiles - -Globs all test files. This function returns an object with regular tests and typecheck tests: - -```ts -interface GlobReturn { - /** - * Test files that match the filters. - */ - testFiles: string[] - /** - * Typecheck test files that match the filters. This will be empty unless `typecheck.enabled` is `true`. - */ - typecheckTestFiles: string[] -} -``` - -::: tip -Vitest uses [fast-glob](https://www.npmjs.com/package/fast-glob) to find test files. `test.dir`, `test.root`, `root` or `process.cwd()` define the `cwd` option. - -This method looks at several config options: - -- `test.include`, `test.exclude` to find regular test files -- `test.includeSource`, `test.exclude` to find in-source tests -- `test.typecheck.include`, `test.typecheck.exclude` to find typecheck tests -::: - -### matchesTestGlob - -This method checks if the file is a regular test file. It uses the same config properties that `globTestFiles` uses for validation. - -This method also accepts a second parameter, which is the source code. This is used to validate if the file is an in-source test. If you are calling this method several times for several projects it is recommended to read the file once and pass it down directly. - -```ts -import { createVitest } from 'vitest/node' -import { resolve } from 'node:path/posix' - -const vitest = await createVitest('test') -const project = vitest.projects[0] - -project.matchesTestGlob(resolve('./basic.test.ts')) // true -project.matchesTestGlob(resolve('./basic.ts')) // false -project.matchesTestGlob(resolve('./basic.ts'), ` -if (import.meta.vitest) { - // ... -} -`) // true if `includeSource` is set -``` - -### onTestsRerun - -This is a shorthand for `project.vitest.onTestsRerun`. It accepts a callback that will be awaited when the tests have been scheduled to rerun (usually, due to a file change). - -```ts -project.onTestsRerun((specs) => { - console.log(specs) -}) -``` - -### close - -Closes the project and all associated resources. This can only be called once; the closing promise is cached until the server restarts. If the resources are needed again, create a new project. - -In detail, this method closes the Vite server, stops the typechecker service, closes the browser if it's running, deletes the temporary directory that holds the source code, and resets the provided context. - -## TestSpecification - - diff --git a/docs/advanced/api/index.md b/docs/advanced/api/index.md new file mode 100644 index 000000000000..cffbd2175d86 --- /dev/null +++ b/docs/advanced/api/index.md @@ -0,0 +1,102 @@ +# Getting Started + +::: warning +This guide lists advanced APIs to run tests via a Node.js script. If you just want to [run tests](/guide/), you probably don't need this. It is primarily used by library authors. +::: + +## startVitest + +```ts +function startVitest( + mode: VitestRunMode, + cliFilters: string[] = [], + options: CliOptions = {}, + viteOverrides?: ViteUserConfig, + vitestOptions?: VitestOptions, +): Promise +``` + +You can start running Vitest tests using its Node API: + +```js +import { startVitest } from 'vitest/node' + +const vitest = await startVitest('test') + +await vitest.close() +``` + +`startVitest` function returns [`Vitest`](/advanced/api/vitest) instance if tests can be started. + +If watch mode is not enabled, Vitest will call `close` method automatically. + +If watch mode is enabled and the terminal supports TTY, Vitest will register console shortcuts. + +You can pass down a list of filters as a second argument. Vitest will run only tests that contain at least one of the passed-down strings in their file path. + +Additionally, you can use the third argument to pass in CLI arguments, which will override any test config options. Alternatively, you can pass in the complete Vite config as the fourth argument, which will take precedence over any other user-defined options. + +After running the tests, you can get the results from the [`state.getTestModules`](/advanced/reporters#reported-tasks) API: + +```ts +import type { TestModule } from 'vitest/node' + +const vitest = await startVitest('test') + +console.log(vitest.state.getTestModules()) // [TestModule] +``` + +::: tip +The ["Running Tests"](/advanced/guide/tests#startvitest) guide has a usage example. +::: + +## createVitest + +```ts +function createVitest( + mode: VitestRunMode, + options: UserConfig, + viteOverrides: ViteUserConfig = {}, + vitestOptions: VitestOptions = {}, +): Promise +``` + +You can create Vitest instance by using `createVitest` function. It returns the same [`Vitest`](/advanced/api/vitest) instance as `startVitest`, but it doesn't start tests and doesn't validate installed packages. + +```js +import { createVitest } from 'vitest/node' + +const vitest = await createVitest('test', { + watch: false, +}) +``` + +::: tip +The ["Running Tests"](/advanced/guide/tests#createvitest) guide has a usage example. +::: + +## parseCLI + +```ts +function parseCLI(argv: string | string[], config: CliParseOptions = {}): { + filter: string[] + options: CliOptions +} +``` + +You can use this method to parse CLI arguments. It accepts a string (where arguments are split by a single space) or a strings array of CLI arguments in the same format that Vitest CLI uses. It returns a filter and `options` that you can later pass down to `createVitest` or `startVitest` methods. + +```ts +import { parseCLI } from 'vitest/node' + +const result = parseCLI('vitest ./files.ts --coverage --browser=chrome') + +result.options +// { +// coverage: { enabled: true }, +// browser: { name: 'chrome', enabled: true } +// } + +result.filter +// ['./files.ts'] +``` diff --git a/docs/advanced/api/test-case.md b/docs/advanced/api/test-case.md new file mode 100644 index 000000000000..d6f4004f6ea3 --- /dev/null +++ b/docs/advanced/api/test-case.md @@ -0,0 +1,136 @@ +# TestCase + +The `TestCase` class represents a single test. + +```ts +declare class TestCase { + readonly type = 'test' + /** + * The project associated with the test. + */ + readonly project: TestProject + /** + * Direct reference to the test module where the test is defined. + */ + readonly module: TestModule + /** + * Name of the test. + */ + readonly name: string + /** + * Full name of the test including all parent suites separated with `>`. + */ + readonly fullName: string + /** + * Unique identifier. + * This ID is deterministic and will be the same for the same test across multiple runs. + * The ID is based on the project name, module id and test position. + */ + readonly id: string + /** + * Location in the module where the test was defined. + * Locations are collected only if `includeTaskLocation` is enabled in the config. + */ + readonly location: { line: number; column: number } | undefined + /** + * Parent suite. If the test was called directly inside the module, the parent will be the module itself. + */ + readonly parent: TestSuite | TestModule + /** + * Options that test was initiated with. + */ + readonly options: TaskOptions + /** + * Checks if the test did not fail the suite. + * If the test is not finished yet or was skipped, it will return `true`. + */ + ok(): boolean + /** + * Checks if the test was skipped. + */ + skipped(): boolean + /** + * Custom metadata that was attached to the test during its execution. + */ + meta(): TaskMeta + /** + * Test results. Will be `undefined` if test is not finished yet or was just collected. + */ + result(): TestResult | undefined + /** + * Useful information about the test like duration, memory usage, etc. + */ + diagnostic(): TestDiagnostic | undefined +} + +export type TestResult = TestResultPassed | TestResultFailed | TestResultSkipped + +export interface TestResultPassed { + /** + * The test passed successfully. + */ + state: 'passed' + /** + * Errors that were thrown during the test execution. + * + * **Note**: If test was retried successfully, errors will still be reported. + */ + errors: TestError[] | undefined +} + +export interface TestResultFailed { + /** + * The test failed to execute. + */ + state: 'failed' + /** + * Errors that were thrown during the test execution. + */ + errors: TestError[] +} + +export interface TestResultSkipped { + /** + * The test was skipped with `only`, `skip` or `todo` flag. + * You can see which one was used in the `mode` option. + */ + state: 'skipped' + /** + * Skipped tests have no errors. + */ + errors: undefined +} + +export interface TestDiagnostic { + /** + * If the duration of the test is above `slowTestThreshold`. + */ + slow: boolean + /** + * The amount of memory used by the test in bytes. + * This value is only available if the test was executed with `logHeapUsage` flag. + */ + heap: number | undefined + /** + * The time it takes to execute the test in ms. + */ + duration: number + /** + * The time in ms when the test started. + */ + startTime: number + /** + * The amount of times the test was retried. + */ + retryCount: number + /** + * The amount of times the test was repeated as configured by `repeats` option. + * This value can be lower if the test failed during the repeat and no `retry` is configured. + */ + repeatCount: number + /** + * If test passed on a second retry. + */ + flaky: boolean +} +``` diff --git a/docs/advanced/api/test-collection.md b/docs/advanced/api/test-collection.md new file mode 100644 index 000000000000..d01e84962c61 --- /dev/null +++ b/docs/advanced/api/test-collection.md @@ -0,0 +1,50 @@ +# TestCollection + +`TestCollection` represents a collection of suites and tests. It also provides useful methods to iterate over itself. + +```ts +declare class TestCollection { + /** + * Returns the test or suite at a specific index in the array. + */ + at(index: number): TestCase | TestSuite | undefined + /** + * The number of tests and suites in the collection. + */ + size: number + /** + * Returns the collection in array form for easier manipulation. + */ + array(): (TestCase | TestSuite)[] + /** + * Filters all suites that are part of this collection and its children. + */ + allSuites(): IterableIterator + /** + * Filters all tests that are part of this collection and its children. + */ + allTests(state?: TestResult['state'] | 'running'): IterableIterator + /** + * Filters only the tests that are part of this collection. + */ + tests(state?: TestResult['state'] | 'running'): IterableIterator + /** + * Filters only the suites that are part of this collection. + */ + suites(): IterableIterator; + [Symbol.iterator](): IterableIterator +} +``` + +For example, you can iterate over all tests inside a module by calling `testModule.children.allTests()`: + +```ts +function onFileCollected(testModule: TestModule): void { + console.log('collecting tests in', testModule.moduleId) + + // iterate over all tests and suites in the module + for (const task of testModule.children.allTests()) { + console.log('collected', task.type, task.fullName) + } +} +``` diff --git a/docs/advanced/api/test-module.md b/docs/advanced/api/test-module.md new file mode 100644 index 000000000000..76ad8fb7a3ae --- /dev/null +++ b/docs/advanced/api/test-module.md @@ -0,0 +1,46 @@ +# TestModule + +```ts +declare class TestModule extends TestSuite { + readonly type = 'module' + /** + * Collection of suites and tests that are part of this module. + */ + readonly children: TestCollection + /** + * This is usually an absolute Unix file path. + * It can be a virtual id if the file is not on the disk. + * This value corresponds to Vite's `ModuleGraph` id. + */ + readonly moduleId: string + /** + * Useful information about the module like duration, memory usage, etc. + * If the module was not executed yet, all diagnostic values will return `0`. + */ + diagnostic(): ModuleDiagnostic +} + +export interface ModuleDiagnostic { + /** + * The time it takes to import and initiate an environment. + */ + environmentSetupDuration: number + /** + * The time it takes Vitest to setup test harness (runner, mocks, etc.). + */ + prepareDuration: number + /** + * The time it takes to import the test module. + * This includes importing everything in the module and executing suite callbacks. + */ + collectDuration: number + /** + * The time it takes to import the setup module. + */ + setupDuration: number + /** + * Accumulated duration of all tests and hooks in the module. + */ + duration: number +} +``` diff --git a/docs/advanced/api/test-project.md b/docs/advanced/api/test-project.md new file mode 100644 index 000000000000..eaedd5d9afc2 --- /dev/null +++ b/docs/advanced/api/test-project.md @@ -0,0 +1,276 @@ +--- +title: TestProject +--- + +# TestProject 2.2.0 {#testproject} + +- **Alias**: `WorkspaceProject` before 2.2.0 + +::: warning +This guide describes the advanced Node.js API. If you just want to create a workspace, follow the ["Workspace"](/guide/workspace) guide. +::: + +## name + +The name is a unique string assigned by the user or interpreted by Vitest. If user did not provide a name, Vitest tries to load a `package.json` in the root of the project and takes the `name` property from there. If there is no `package.json`, Vitest uses the name of the folder by default. Inline projects use numbers as the name (converted to string). + +::: code-group +```ts [node.js] +import { createVitest } from 'vitest/node' + +const vitest = await createVitest('test') +vitest.projects.map(p => p.name) === [ + '@pkg/server', + 'utils', + '2', + 'custom' +] +``` +```ts [vitest.workspace.js] +export default [ + './packages/server', // has package.json with "@pkg/server" + './utils', // doesn't have a package.json file + { + // doesn't customize the name + test: { + pool: 'threads', + }, + }, + { + // customized the name + test: { + name: 'custom', + }, + }, +] +``` +::: + +::: info +If the [root project](/advanced/api/vitest#getroottestproject) is not part of a user workspace, its `name` will not be resolved. +::: + +## vitest + +`vitest` references the global [`Vitest`](/advanced/api/vitest) process. + +## serializedConfig + +This is the config that test processes receive. Vitest [serializes config](https://github.com/vitest-dev/vitest/blob/main/packages/vitest/src/node/config/serializeConfig.ts) manually by removing all functions and properties that are not possible to serialize. Since this value is available in both tests and node, its type is exported from the main entry point. + +```ts +import type { SerializedConfig } from 'vitest' + +const config: SerializedConfig = vitest.projects[0].serializedConfig +``` + +::: warning +The `serializedConfig` property is a getter. Every time it's accessed Vitest serializes the config again in case it was changed. This also means that it always returns a different reference: + +```ts +project.serializedConfig === project.serializedConfig // false +``` +::: + +## globalConfig + +The test config that [`Vitest`](/advanced/api/vitest) was initialized with. If this is the [root project](/advanced/api/vitest#getroottestproject), `globalConfig` and `config` will reference the same object. This config is useful for values that cannot be set on the project level, like `coverage` or `reporters`. + +```ts +import type { ResolvedConfig } from 'vitest/node' + +vitest.config === vitest.projects[0].globalConfig +``` + +## config + +This is the project's resolved test config. + +## vite + +This is project's [`ViteDevServer`](https://vite.dev/guide/api-javascript#vitedevserver). All projects have their own Vite servers. + +## browser + +This value will be set only if tests are running in the browser. If `browser` is enabled, but tests didn't run yet, this will be `undefined`. If you need to check if the project supports browser tests, use `project.isBrowserSupported()` method. + +::: warning +The browser API is even more experimental and doesn't follow SemVer. The browser API will be standardized separately from the rest of the APIs. +::: + +## provide + +```ts +function provide( + key: T, + value: ProvidedContext[T], +): void +``` + +A way to provide custom values to tests in addition to [`config.provide`](/config/#provide) field. All values are validated with [`structuredClone`](https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone) before they are stored, but the values on `providedContext` themselves are not cloned. + +::: code-group +```ts [node.js] +import { createVitest } from 'vitest/node' + +const vitest = await createVitest('test') +const project = vitest.projects.find(p => p.name === 'custom') +project.provide('key', 'value') +await vitest.start() +``` +```ts [test.spec.js] +import { inject } from 'vitest' +const value = inject('key') +``` +::: + +The values can be provided dynamicaly. Provided value in tests will be updated on their next run. + +::: tip +This method is also available to [global setup files](/config/#globalsetup) for cases where you cannot use the public API: + +```js +export default function setup({ provide }) { + provide('wsPort', 3000) +} +``` +::: + +## getProvidedContext + +```ts +function getProvidedContext(): ProvidedContext +``` + +This returns the context object. Every project also inherits the global context set by `vitest.provide`. + +```ts +import { createVitest } from 'vitest/node' + +const vitest = await createVitest('test') +vitest.provide('global', true) +const project = vitest.projects.find(p => p.name === 'custom') +project.provide('key', 'value') + +// { global: true, key: 'value' } +const context = project.getProvidedContext() +``` + +::: tip +Project context values will always override root project's context. +::: + +## createSpecification + +```ts +function createSpecification( + moduleId: string, + locations?: number[], + pool?: string, +): TestSpecification +``` + +Create a [test specification](/advanced/api/test-specification) that can be used in [`vitest.runTestSpecifications`](/advanced/api/vitest#runtestspecifications). Specification scopes the test file to a specific `project`, `locations` (optional) and `pool` (optional). + +```ts +import { createVitest } from 'vitest/node' +import { resolve } from 'node:path/posix' + +const vitest = await createVitest('test') +const project = vitest.projects[0] +const specification = project.createSpecification( + resolve('./basic.test.ts'), + [20, 40], // optional test lines + 'threads', // optional override +) +await vitest.runFiles([specification], true) +``` + +::: warning +`createSpecification` expects an absolute file path. It doesn't resolve the file or check that it exists on the file system. +::: + +## isRootProject + +```ts +function isRootProject(): boolean +``` + +Checks if the current project is the root project. You can also get the root project by calling [`vitest.getRootTestProject()`](#getroottestproject). + +## globTestFiles + +```ts +function globTestFiles(filters?: string[]): { + /** + * Test files that match the filters. + */ + testFiles: string[] + /** + * Typecheck test files that match the filters. This will be empty unless `typecheck.enabled` is `true`. + */ + typecheckTestFiles: string[] +} +``` + +Globs all test files. This function returns an object with regular tests and typecheck tests. + +::: tip +Vitest uses [fast-glob](https://www.npmjs.com/package/fast-glob) to find test files. `test.dir`, `test.root`, `root` or `process.cwd()` define the `cwd` option. + +This method looks at several config options: + +- `test.include`, `test.exclude` to find regular test files +- `test.includeSource`, `test.exclude` to find in-source tests +- `test.typecheck.include`, `test.typecheck.exclude` to find typecheck tests +::: + +## matchesTestGlob + +```ts +function matchesTestGlob(moduleId: string, source?: string): boolean +``` + +This method checks if the file is a regular test file. It uses the same config properties that `globTestFiles` uses for validation. + +This method also accepts a second parameter, which is the source code. This is used to validate if the file is an in-source test. If you are calling this method several times for several projects it is recommended to read the file once and pass it down directly. If the file is not a test file, but matches the `includeSource` glob, Vitest will synchronously read the file unless the `source` is provided. + +```ts +import { createVitest } from 'vitest/node' +import { resolve } from 'node:path/posix' + +const vitest = await createVitest('test') +const project = vitest.projects[0] + +project.matchesTestGlob(resolve('./basic.test.ts')) // true +project.matchesTestGlob(resolve('./basic.ts')) // false +project.matchesTestGlob(resolve('./basic.ts'), ` +if (import.meta.vitest) { + // ... +} +`) // true if `includeSource` is set +``` + +## onTestsRerun + +```ts +function onTestsRerun(cb: OnTestsRerunHandler): void +``` + +This is a shorthand for `project.vitest.onTestsRerun`. It accepts a callback that will be awaited when the tests have been scheduled to rerun (usually, due to a file change). + +```ts +project.onTestsRerun((specs) => { + console.log(specs) +}) +``` + +## close + +```ts +function close(): Promise +``` + +Closes the project and all associated resources. This can only be called once; the closing promise is cached until the server restarts. If the resources are needed again, create a new project. + +In detail, this method closes the Vite server, stops the typechecker service, closes the browser if it's running, deletes the temporary directory that holds the source code, and resets the provided context. diff --git a/docs/advanced/api/test-specification.md b/docs/advanced/api/test-specification.md new file mode 100644 index 000000000000..112b3c678c5c --- /dev/null +++ b/docs/advanced/api/test-specification.md @@ -0,0 +1,3 @@ +# TestSpecification + + diff --git a/docs/advanced/api/test-suite.md b/docs/advanced/api/test-suite.md new file mode 100644 index 000000000000..f47beff9d492 --- /dev/null +++ b/docs/advanced/api/test-suite.md @@ -0,0 +1,46 @@ +# TestSuite + +```ts +declare class TestSuite { + readonly type = 'suite' + /** + * The project associated with the test. + */ + readonly project: TestProject + /** + * Direct reference to the test module where the suite is defined. + */ + readonly module: TestModule + /** + * Name of the suite. + */ + readonly name: string + /** + * Full name of the suite including all parent suites separated with `>`. + */ + readonly fullName: string + /** + * Unique identifier. + * This ID is deterministic and will be the same for the same test across multiple runs. + * The ID is based on the project name, module id and test position. + */ + readonly id: string + /** + * Location in the module where the suite was defined. + * Locations are collected only if `includeTaskLocation` is enabled in the config. + */ + readonly location: { line: number; column: number } | undefined + /** + * Collection of suites and tests that are part of this suite. + */ + readonly children: TaskCollection + /** + * Options that the suite was initiated with. + */ + readonly options: TaskOptions + /** + * Errors that happened outside of the test run during collection, like syntax errors. + */ + public errors(): TestError[] +} +``` diff --git a/docs/advanced/api/vitest.md b/docs/advanced/api/vitest.md new file mode 100644 index 000000000000..ae81cd5dd2df --- /dev/null +++ b/docs/advanced/api/vitest.md @@ -0,0 +1,165 @@ +--- +outline: deep +--- + +# Vitest + +Vitest instance requires the current test mode. It can be either: + +- `test` when running runtime tests +- `benchmark` when running benchmarks + +## mode + +### test + +Test mode will only call functions inside `test` or `it`, and throws an error when `bench` is encountered. This mode uses `include` and `exclude` options in the config to find test files. + +### benchmark + +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. + +## start + +```ts +function start(filters: string[]): Promise +``` + +You can start running tests or benchmarks with `start` method. You can pass an array of strings to filter test files. + +## config + +The root (or global) config. If workspace feature is enabled, projects will reference this as `globalConfig`. + +::: warning +This is Vitest config, it doesn't extend _Vite_ config. It only has resolved values from the `test` property. +::: + +## vite + +This is a global [`ViteDevServer`](https://vite.dev/guide/api-javascript#vitedevserver). + +## state + +::: warning +Public `state` is an experimental API. Breaking changes might not follow SemVer, please pin Vitest's version when using it. +::: + +Global state stores information about the current tests. It uses the same API from `@vitest/runner` by default, but we recommend using the [Reported API](/advanced/reporters#reported-tasks) instead by calling `state.getReportedEntity()` on the `@vitest/runner` API: + +```ts +const task = vitest.state.idMap.get(taskId) // old API +const testCase = vitest.state.getReportedEntity(task) // new API +``` + +In the future, the old API won't be exposed anymore. + +## snapshot + +The global snapshot manager. Vitest keeps track of all snapshots using the `snapshot.add` method. + +You can get the latest summary of snapshots via the `vitest.snapshot.summay` property. + +## cache + +Cache manager that stores information about latest test results and test file stats. In Vitest itself this is only used by the default sequencer to sort tests. + +## ready + +Vitest needs to be resolved with the Vite server to be properly initialized. If the `Vitest` instance was created manually, you might need to check the `ready` status before accessing the `vite`, `state`, `cache`, `config`, and `snapshot` properties; otherwise, they will throw an error in the getter. + +In normal circumstances, you would never call this method because `createVitest` and `startVitest` return already resolved Vitest instance. + +## getRootTestProject + +This returns the root test project. The root project generally doesn't run any tests and is not included in `vitest.projects` unless the user explicitly includes the root config in their workspace. + +The primary goal of the root project is to setup the global config. In fact, `rootProject.config` references `rootProject.globalConfig` and `vitest.config` directly. + +## provide + +Vitest exposes `provide` method which is a shorthand for `vitest.getRootTestProject().provide`. With this method you can pass down values from the main thread to tests. All values are checked with `structuredClone` before they are stored, but the values themselves are not cloned. + +To recieve the values in the test, you need to import `inject` method from `vitest` entrypont: + +```ts +import { inject } from 'vitest' +const port = inject('wsPort') // 3000 +``` + +For better type safety, we encourage you to augment the type of `ProvidedContext`: + +```ts +import { createVitest } from 'vitest/node' + +const vitest = await createVitest('test', { + watch: false, +}) +vitest.provide('wsPort', 3000) + +declare module 'vitest' { + export interface ProvidedContext { + wsPort: number + } +} +``` + +::: warning +Technically, `provide` is a method of [`TestProject`](#testproject), so it is limited to the specific project. However, all projects inherit the values from the core project which makes `vitest.provide` universal way of passing down values to tests. +::: + +## getProvidedContext + +This returns the root context object. This is a shorthand for `vitest.getRootTestProject().getProvidedContext`. + +## getProjectByName + +This method returns the project by its name. Simillar to calling `vitest.projects.find`. + +::: warning +In case the project doesn't exist, this method will return the root project - make sure to check the names again if you need to make sure the project you are looking for is the one returned. +::: + +## globTestSpecifications + +This method constructs new [test specifications](#testspecification) by collecting every test in all projects with [`project.globTestFiles`](#globtestfiles). It accepts string filters to match the test files. + +::: warning +As of Vitest 2.2.0, it's possible to have multiple test specifications with the same module ID (file path) if `poolMatchGlob` has several pools or if `typecheck` is enabled. This possibility will be removed in Vitest 3. +::: + +```ts +const specifications = await vitest.globTestSpecifications(['my-filter']) +// [TestSpecification{ moduleId: '/tests/my-filter.test.ts' }] +console.log(specifications) +``` + +## mergeReports +## collect +## listFiles +## start +## init + +## getModuleSpecifications + +Returns a list of test specifications related to the module ID. The ID should already be resolved to an absolute file path. If ID doesn't match `include` or `includeSource` patterns, the returned array will be empty. + +::: warning +As of Vitest 2.2.0, this method uses a cache to check if the file is a test. To make sure that the cache is not empty, call `globTestSpecifications` at least once. +::: + +## runTestSpecifications +## rerunTestSpecifications +## collectTests +## cancelCurrentRun +## updateSnapshot +## invalidateFile +## close +## exit + +## shouldKeepServer +## onServerRestart +## onCancel +## onClose +## onTestsRerun +## onFilterWatchedSpecification diff --git a/docs/advanced/pool.md b/docs/advanced/pool.md index 36c5a6c9cdd7..31f850527257 100644 --- a/docs/advanced/pool.md +++ b/docs/advanced/pool.md @@ -1,7 +1,7 @@ # Custom Pool ::: warning -This is advanced API. If you just want to [run tests](/guide/), you probably don't need this. It is primarily used by library authors. +This is an advanced API. If you just want to [run tests](/guide/), you probably don't need this. It is primarily used by library authors. ::: Vitest runs tests in pools. By default, there are several pools: diff --git a/docs/advanced/reporters.md b/docs/advanced/reporters.md index ba0d19193a4a..38027752033f 100644 --- a/docs/advanced/reporters.md +++ b/docs/advanced/reporters.md @@ -1,5 +1,9 @@ # Extending Reporters +::: warning +This is an advanced API. If you just want to configure built-in reporters, read the ["Reporters"](/guide/reporters) guide. +::: + You can import reporters from `vitest/reporters` and extend them to create your custom reporters. ## Extending Built-in Reporters @@ -56,8 +60,7 @@ export default defineConfig({ ## Reported Tasks -::: warning -This is an experimental API. Breaking changes might not follow SemVer. Please pin Vitest's version when using it. +Instead of using the tasks that reporters receive, it is recommended to use the Reported Tasks API instead. You can get access to this API by calling `vitest.state.getReportedEntity(runnerTask)`: @@ -85,293 +88,6 @@ class MyReporter implements Reporter { } } ``` -::: - -### TestCase - -`TestCase` represents a single test. - -```ts -declare class TestCase { - readonly type = 'test' - /** - * The project associated with the test. - */ - readonly project: TestProject - /** - * Direct reference to the test module where the test is defined. - */ - readonly module: TestModule - /** - * Name of the test. - */ - readonly name: string - /** - * Full name of the test including all parent suites separated with `>`. - */ - readonly fullName: string - /** - * Unique identifier. - * This ID is deterministic and will be the same for the same test across multiple runs. - * The ID is based on the project name, module id and test position. - */ - readonly id: string - /** - * Location in the module where the test was defined. - * Locations are collected only if `includeTaskLocation` is enabled in the config. - */ - readonly location: { line: number; column: number } | undefined - /** - * Parent suite. If the test was called directly inside the module, the parent will be the module itself. - */ - readonly parent: TestSuite | TestModule - /** - * Options that test was initiated with. - */ - readonly options: TaskOptions - /** - * Checks if the test did not fail the suite. - * If the test is not finished yet or was skipped, it will return `true`. - */ - ok(): boolean - /** - * Checks if the test was skipped. - */ - skipped(): boolean - /** - * Custom metadata that was attached to the test during its execution. - */ - meta(): TaskMeta - /** - * Test results. Will be `undefined` if test is not finished yet or was just collected. - */ - result(): TestResult | undefined - /** - * Useful information about the test like duration, memory usage, etc. - */ - diagnostic(): TestDiagnostic | undefined -} - -export type TestResult = TestResultPassed | TestResultFailed | TestResultSkipped - -export interface TestResultPassed { - /** - * The test passed successfully. - */ - state: 'passed' - /** - * Errors that were thrown during the test execution. - * - * **Note**: If test was retried successfully, errors will still be reported. - */ - errors: TestError[] | undefined -} - -export interface TestResultFailed { - /** - * The test failed to execute. - */ - state: 'failed' - /** - * Errors that were thrown during the test execution. - */ - errors: TestError[] -} - -export interface TestResultSkipped { - /** - * The test was skipped with `only`, `skip` or `todo` flag. - * You can see which one was used in the `mode` option. - */ - state: 'skipped' - /** - * Skipped tests have no errors. - */ - errors: undefined -} - -export interface TestDiagnostic { - /** - * If the duration of the test is above `slowTestThreshold`. - */ - slow: boolean - /** - * The amount of memory used by the test in bytes. - * This value is only available if the test was executed with `logHeapUsage` flag. - */ - heap: number | undefined - /** - * The time it takes to execute the test in ms. - */ - duration: number - /** - * The time in ms when the test started. - */ - startTime: number - /** - * The amount of times the test was retried. - */ - retryCount: number - /** - * The amount of times the test was repeated as configured by `repeats` option. - * This value can be lower if the test failed during the repeat and no `retry` is configured. - */ - repeatCount: number - /** - * If test passed on a second retry. - */ - flaky: boolean -} -``` - -### TestSuite - -`TestSuite` represents a single suite that contains tests and other suites. - -```ts -declare class TestSuite { - readonly type = 'suite' - /** - * The project associated with the test. - */ - readonly project: TestProject - /** - * Direct reference to the test module where the suite is defined. - */ - readonly module: TestModule - /** - * Name of the suite. - */ - readonly name: string - /** - * Full name of the suite including all parent suites separated with `>`. - */ - readonly fullName: string - /** - * Unique identifier. - * This ID is deterministic and will be the same for the same test across multiple runs. - * The ID is based on the project name, module id and test position. - */ - readonly id: string - /** - * Location in the module where the suite was defined. - * Locations are collected only if `includeTaskLocation` is enabled in the config. - */ - readonly location: { line: number; column: number } | undefined - /** - * Collection of suites and tests that are part of this suite. - */ - readonly children: TaskCollection - /** - * Options that the suite was initiated with. - */ - readonly options: TaskOptions - /** - * Errors that happened outside of the test run during collection, like syntax errors. - */ - public errors(): TestError[] -} -``` - -### TestModule - -`TestModule` represents a single file that contains suites and tests. - -```ts -declare class TestModule extends TestSuite { - readonly type = 'module' - /** - * Collection of suites and tests that are part of this module. - */ - readonly children: TestCollection - /** - * This is usually an absolute Unix file path. - * It can be a virtual id if the file is not on the disk. - * This value corresponds to Vite's `ModuleGraph` id. - */ - readonly moduleId: string - /** - * Useful information about the module like duration, memory usage, etc. - * If the module was not executed yet, all diagnostic values will return `0`. - */ - diagnostic(): ModuleDiagnostic -} - -export interface ModuleDiagnostic { - /** - * The time it takes to import and initiate an environment. - */ - environmentSetupDuration: number - /** - * The time it takes Vitest to setup test harness (runner, mocks, etc.). - */ - prepareDuration: number - /** - * The time it takes to import the test module. - * This includes importing everything in the module and executing suite callbacks. - */ - collectDuration: number - /** - * The time it takes to import the setup module. - */ - setupDuration: number - /** - * Accumulated duration of all tests and hooks in the module. - */ - duration: number -} -``` - -### TestCollection - -`TestCollection` represents a collection of suites and tests. It also provides useful methods to iterate over itself. - -```ts -declare class TestCollection { - /** - * Returns the test or suite at a specific index in the array. - */ - at(index: number): TestCase | TestSuite | undefined - /** - * The number of tests and suites in the collection. - */ - size: number - /** - * Returns the collection in array form for easier manipulation. - */ - array(): (TestCase | TestSuite)[] - /** - * Filters all suites that are part of this collection and its children. - */ - allSuites(): IterableIterator - /** - * Filters all tests that are part of this collection and its children. - */ - allTests(state?: TestResult['state'] | 'running'): IterableIterator - /** - * Filters only the tests that are part of this collection. - */ - tests(state?: TestResult['state'] | 'running'): IterableIterator - /** - * Filters only the suites that are part of this collection. - */ - suites(): IterableIterator; - [Symbol.iterator](): IterableIterator -} -``` - -For example, you can iterate over all tests inside a module by calling `testModule.children.allTests()`: - -```ts -function onFileCollected(testModule: TestModule): void { - console.log('collecting tests in', testModule.moduleId) - - // iterate over all tests and suites in the module - for (const task of testModule.children.allTests()) { - console.log('collected', task.type, task.fullName) - } -} -``` ## Exported Reporters diff --git a/docs/advanced/runner.md b/docs/advanced/runner.md index ab5fb77e510b..fa58f3f983cb 100644 --- a/docs/advanced/runner.md +++ b/docs/advanced/runner.md @@ -1,4 +1,4 @@ -# Test Runner +# Runner API ::: warning This is advanced API. If you just want to [run tests](/guide/), you probably don't need this. It is primarily used by library authors. diff --git a/eslint.config.js b/eslint.config.js index fca823e74f11..e2adb7ed01fc 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -107,6 +107,7 @@ export default antfu( 'import/first': 'off', 'unused-imports/no-unused-imports': 'off', 'ts/method-signature-style': 'off', + 'no-self-compare': 'off', }, }, { diff --git a/packages/vitest/src/node/cli/cac.ts b/packages/vitest/src/node/cli/cac.ts index d1c8e78b103d..925bf21ba0ad 100644 --- a/packages/vitest/src/node/cli/cac.ts +++ b/packages/vitest/src/node/cli/cac.ts @@ -58,7 +58,7 @@ function addCommand(cli: CAC | Command, name: string, option: CLIOption) { } } -interface CLIOptions { +export interface CliParseOptions { allowUnknownOptions?: boolean } @@ -70,7 +70,7 @@ function addCliOptions(cli: CAC | Command, options: CLIOptionsConfig) { } } -export function createCLI(options: CLIOptions = {}) { +export function createCLI(options: CliParseOptions = {}) { const cli = cac('vitest') cli.version(version) @@ -196,7 +196,7 @@ export function createCLI(options: CLIOptions = {}) { return cli } -export function parseCLI(argv: string | string[], config: CLIOptions = {}): { +export function parseCLI(argv: string | string[], config: CliParseOptions = {}): { filter: string[] options: CliOptions } { @@ -307,7 +307,7 @@ async function collect(mode: VitestRunMode, cliFilters: string[], options: CliOp run: true, }) if (!options.filesOnly) { - const { tests, errors } = await ctx.collect(cliFilters.map(normalize)) + const { testModules: tests, unhandledErrors: errors } = await ctx.collect(cliFilters.map(normalize)) if (errors.length) { console.error('\nThere were unhandled errors during test collection') diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 64abae0638d1..37ee5a7ceff4 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -491,8 +491,8 @@ export class Vitest { await this.coverageProvider?.mergeReports?.(coverages) return { - tests: this.state.getTestModules(), - errors: this.state.getUnhandledErrors(), + testModules: this.state.getTestModules(), + unhandledErrors: this.state.getUnhandledErrors(), } } @@ -503,7 +503,7 @@ export class Vitest { // if run with --changed, don't exit if no tests are found if (!files.length) { - return { tests: [], errors: [] } + return { testModules: [], unhandledErrors: [] } } return this.collectTests(files) @@ -555,8 +555,8 @@ export class Vitest { } let testModules: TestRunResult = { - tests: [], - errors: [], + testModules: [], + unhandledErrors: [], } if (files.length) { @@ -690,8 +690,8 @@ export class Vitest { await this.cache.results.writeToCache() return { - tests: this.state.getTestModules(), - errors: this.state.getUnhandledErrors(), + testModules: this.state.getTestModules(), + unhandledErrors: this.state.getUnhandledErrors(), } } finally { @@ -759,8 +759,8 @@ export class Vitest { } return { - tests: this.state.getTestModules(), - errors: this.state.getUnhandledErrors(), + testModules: this.state.getTestModules(), + unhandledErrors: this.state.getUnhandledErrors(), } })() .finally(() => { diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts index b850a02476e9..e5bae926bdb2 100644 --- a/packages/vitest/src/node/project.ts +++ b/packages/vitest/src/node/project.ts @@ -133,8 +133,8 @@ export class TestProject { */ public createSpecification( moduleId: string, - pool?: string, locations?: number[] | undefined, + pool?: string, ): TestSpecification { return new TestSpecification( this, @@ -417,8 +417,8 @@ export class TestProject { /** * Test if a file matches the test globs. This does the actual glob matching unlike `isTestFile`. */ - public matchesTestGlob(filepath: string, source?: string): boolean { - const relativeId = relative(this.config.dir || this.config.root, filepath) + public matchesTestGlob(moduleId: string, source?: string): boolean { + const relativeId = relative(this.config.dir || this.config.root, moduleId) if (mm.isMatch(relativeId, this.config.exclude)) { return false } @@ -429,7 +429,7 @@ export class TestProject { this.config.includeSource?.length && mm.isMatch(relativeId, this.config.includeSource) ) { - const code = source || readFileSync(filepath, 'utf-8') + const code = source || readFileSync(moduleId, 'utf-8') return this.isInSourceTestCode(code) } return false diff --git a/packages/vitest/src/node/specifications.ts b/packages/vitest/src/node/specifications.ts index 4a5db0168ada..a0e0ce6d50a2 100644 --- a/packages/vitest/src/node/specifications.ts +++ b/packages/vitest/src/node/specifications.ts @@ -24,7 +24,7 @@ export class VitestSpecifications { specs.push(project.createSpecification(moduleId)) } if (project.isTypecheckFile(moduleId)) { - specs.push(project.createSpecification(moduleId, 'typescript')) + specs.push(project.createSpecification(moduleId, [], 'typescript')) } } specs.forEach(spec => this.ensureSpecificationCached(spec)) @@ -66,7 +66,7 @@ export class VitestSpecifications { const loc = testLocations[file] testLocHasMatch[file] = true - const spec = project.createSpecification(file, undefined, loc) + const spec = project.createSpecification(file, loc) this.ensureSpecificationCached(spec) files.push(spec) }) @@ -74,7 +74,7 @@ export class VitestSpecifications { const loc = testLocations[file] testLocHasMatch[file] = true - const spec = project.createSpecification(file, 'typescript', loc) + const spec = project.createSpecification(file, loc, 'typescript') this.ensureSpecificationCached(spec) files.push(spec) }) diff --git a/packages/vitest/src/node/types/tests.ts b/packages/vitest/src/node/types/tests.ts index 63b76515f3a7..9b05e1f97ee0 100644 --- a/packages/vitest/src/node/types/tests.ts +++ b/packages/vitest/src/node/types/tests.ts @@ -1,6 +1,6 @@ import type { TestModule } from '../reporters' export interface TestRunResult { - tests: TestModule[] - errors: unknown[] + testModules: TestModule[] + unhandledErrors: unknown[] } diff --git a/packages/vitest/src/public/node.ts b/packages/vitest/src/public/node.ts index edb086a81e57..fd3fbc42da9a 100644 --- a/packages/vitest/src/public/node.ts +++ b/packages/vitest/src/public/node.ts @@ -6,6 +6,7 @@ import { TestModule as _TestFile } from '../node/reporters/reported-tasks' export const version = Vitest.version export { parseCLI } from '../node/cli/cac' +export type { CliParseOptions } from '../node/cli/cac' export { startVitest } from '../node/cli/cli-api' export { resolveApiServerConfig, resolveConfig } from '../node/config/resolveConfig' export type { Vitest } from '../node/core' @@ -94,7 +95,6 @@ export type { VitestEnvironment, VitestRunMode, } from '../node/types/config' - export type { BaseCoverageOptions, CoverageIstanbulOptions, @@ -107,6 +107,8 @@ export type { ReportContext, ResolvedCoverageOptions, } from '../node/types/coverage' + +export type { TestRunResult } from '../node/types/tests' /** * @deprecated Use `TestModule` instead */ From 38d12f310ca5fccc1fd5b18dce794788988bd2fc Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 3 Dec 2024 10:25:30 +0100 Subject: [PATCH 23/73] perf: speed up getTestDependencies --- packages/vitest/src/node/specifications.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/vitest/src/node/specifications.ts b/packages/vitest/src/node/specifications.ts index a0e0ce6d50a2..00c03ec384eb 100644 --- a/packages/vitest/src/node/specifications.ts +++ b/packages/vitest/src/node/specifications.ts @@ -3,7 +3,8 @@ import type { TestProject } from './reporters' import type { TestSpecification } from './spec' import { existsSync } from 'node:fs' import mm from 'micromatch' -import { relative, resolve } from 'pathe' +import { join, relative, resolve } from 'pathe' +import { isWindows } from '../utils/env' import { groupFilters, parseFilter } from './cli/filter' import { GitNotFoundError, IncludeTaskLocationDisabledError, LocationFilterFileNotFoundError } from './errors' @@ -173,9 +174,10 @@ export class VitestSpecifications { } const dependencies = [...transformed.deps || [], ...transformed.dynamicDeps || []] await Promise.all(dependencies.map(async (dep) => { - const path = await project.vite.pluginContainer.resolveId(dep, filepath, { ssr: true }) - const fsPath = path && !path.external && path.id.split('?')[0] - if (fsPath && !fsPath.includes('node_modules') && !deps.has(fsPath) && existsSync(fsPath)) { + const fsPath = dep.startsWith('/@fs/') + ? dep.slice(isWindows ? 5 : 4) + : join(project.config.root, dep) + if (!fsPath.includes('node_modules') && !deps.has(fsPath) && existsSync(fsPath)) { await addImports(project, fsPath) } })) From 3f31c8d4a26dfb0600d96732338194096c0bbee0 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 3 Dec 2024 11:31:13 +0100 Subject: [PATCH 24/73] docs: more documented methods --- docs/advanced/api/index.md | 2 +- docs/advanced/api/test-project.md | 12 +- docs/advanced/api/vitest.md | 178 +++++++++++++++++++++++++++++- 3 files changed, 185 insertions(+), 7 deletions(-) diff --git a/docs/advanced/api/index.md b/docs/advanced/api/index.md index cffbd2175d86..c64e7eda7ec7 100644 --- a/docs/advanced/api/index.md +++ b/docs/advanced/api/index.md @@ -36,7 +36,7 @@ You can pass down a list of filters as a second argument. Vitest will run only t Additionally, you can use the third argument to pass in CLI arguments, which will override any test config options. Alternatively, you can pass in the complete Vite config as the fourth argument, which will take precedence over any other user-defined options. -After running the tests, you can get the results from the [`state.getTestModules`](/advanced/reporters#reported-tasks) API: +After running the tests, you can get the results from the [`state.getTestModules`](/advanced/api/test-module) API: ```ts import type { TestModule } from 'vitest/node' diff --git a/docs/advanced/api/test-project.md b/docs/advanced/api/test-project.md index eaedd5d9afc2..2c9b9043a896 100644 --- a/docs/advanced/api/test-project.md +++ b/docs/advanced/api/test-project.md @@ -92,7 +92,7 @@ This is project's [`ViteDevServer`](https://vite.dev/guide/api-javascript#vitede ## browser -This value will be set only if tests are running in the browser. If `browser` is enabled, but tests didn't run yet, this will be `undefined`. If you need to check if the project supports browser tests, use `project.isBrowserSupported()` method. +This value will be set only if tests are running in the browser. If `browser` is enabled, but tests didn't run yet, this will be `undefined`. If you need to check if the project supports browser tests, use `project.isBrowserEnabled()` method. ::: warning The browser API is even more experimental and doesn't follow SemVer. The browser API will be standardized separately from the rest of the APIs. @@ -188,6 +188,8 @@ await vitest.runFiles([specification], true) ::: warning `createSpecification` expects an absolute file path. It doesn't resolve the file or check that it exists on the file system. + +Also note that `project.createSpecification` always returns a new instance. ::: ## isRootProject @@ -265,6 +267,14 @@ project.onTestsRerun((specs) => { }) ``` +## isBrowserEnabled + +```ts +function isBrowserEnabled(): boolean +``` + +Returns `true` if this project runs tests in the browser. + ## close ```ts diff --git a/docs/advanced/api/vitest.md b/docs/advanced/api/vitest.md index ae81cd5dd2df..137e4770be42 100644 --- a/docs/advanced/api/vitest.md +++ b/docs/advanced/api/vitest.md @@ -66,18 +66,39 @@ Cache manager that stores information about latest test results and test file st ## ready +```ts +function ready(): boolean +``` + Vitest needs to be resolved with the Vite server to be properly initialized. If the `Vitest` instance was created manually, you might need to check the `ready` status before accessing the `vite`, `state`, `cache`, `config`, and `snapshot` properties; otherwise, they will throw an error in the getter. +::: tip In normal circumstances, you would never call this method because `createVitest` and `startVitest` return already resolved Vitest instance. +::: ## getRootTestProject +```ts +function getRootTestProject(): TestProject +``` + This returns the root test project. The root project generally doesn't run any tests and is not included in `vitest.projects` unless the user explicitly includes the root config in their workspace. -The primary goal of the root project is to setup the global config. In fact, `rootProject.config` references `rootProject.globalConfig` and `vitest.config` directly. +The primary goal of the root project is to setup the global config. In fact, `rootProject.config` references `rootProject.globalConfig` and `vitest.config` directly: + +```ts +rootProject.config === rootProject.globalConfig === rootProject.vitest.config +``` ## provide +```ts +function provide( + key: T, + value: ProvidedContext[T], +): void +``` + Vitest exposes `provide` method which is a shorthand for `vitest.getRootTestProject().provide`. With this method you can pass down values from the main thread to tests. All values are checked with `structuredClone` before they are stored, but the values themselves are not cloned. To recieve the values in the test, you need to import `inject` method from `vitest` entrypont: @@ -105,24 +126,42 @@ declare module 'vitest' { ``` ::: warning -Technically, `provide` is a method of [`TestProject`](#testproject), so it is limited to the specific project. However, all projects inherit the values from the core project which makes `vitest.provide` universal way of passing down values to tests. +Technically, `provide` is a method of [`TestProject`](/advanced/api/testproject), so it is limited to the specific project. However, all projects inherit the values from the core project which makes `vitest.provide` universal way of passing down values to tests. ::: ## getProvidedContext +```ts +function getProvidedContext(): ProvidedContext +``` + This returns the root context object. This is a shorthand for `vitest.getRootTestProject().getProvidedContext`. ## getProjectByName +```ts +function getProjectByName(name: string): ProvidedContext +``` + This method returns the project by its name. Simillar to calling `vitest.projects.find`. ::: warning -In case the project doesn't exist, this method will return the root project - make sure to check the names again if you need to make sure the project you are looking for is the one returned. +In case the project doesn't exist, this method will return the root project - make sure to check the names again if the project you are looking for is the one returned. + +If user didn't customize a name, the Vitest will assign an empty string as a name. ::: ## globTestSpecifications -This method constructs new [test specifications](#testspecification) by collecting every test in all projects with [`project.globTestFiles`](#globtestfiles). It accepts string filters to match the test files. +```ts +function globTestSpecifications( + filters?: string[], +): Promise +``` + +This method constructs new [test specifications](/advanced/api/test-specification) by collecting every test in all projects with [`project.globTestFiles`](/advanced/api/test-project#globtestfiles). It accepts string filters to match the test files - these are the same filters that [CLI supports](/guide/filtering#cli). + +This method automatically caches all test specifications. When you call [`getModuleSpecifications`](#getmodulespecifications) next time, it will return the same specifications unless [`clearSpecificationsCache`](#clearspecificationscache) was called before that. ::: warning As of Vitest 2.2.0, it's possible to have multiple test specifications with the same module ID (file path) if `poolMatchGlob` has several pools or if `typecheck` is enabled. This possibility will be removed in Vitest 3. @@ -142,24 +181,153 @@ console.log(specifications) ## getModuleSpecifications +```ts +function getModuleSpecifications(moduleId: string): TestSpecification[] +``` + Returns a list of test specifications related to the module ID. The ID should already be resolved to an absolute file path. If ID doesn't match `include` or `includeSource` patterns, the returned array will be empty. +This method can return already cached specifications based on the `moduleId` and `pool`. But note that [`project.createSpecification`](/advanced/api/test-project#createspecification) always returns a new instance and it's not cached automatically. However, specifications are automatically cached when [`runTestSpecifications`](#runtestspecifications) is called. + ::: warning -As of Vitest 2.2.0, this method uses a cache to check if the file is a test. To make sure that the cache is not empty, call `globTestSpecifications` at least once. +As of Vitest 2.2.0, this method uses a cache to check if the file is a test. To make sure that the cache is not empty, call [`globTestSpecifications`](#globtestspecifications) at least once. ::: +## clearSpecificationsCache + +```ts +function clearSpecificationsCache(moduleId?: string): void +``` + +Vitest automatically caches test specifications for each file when [`globTestSpecifications`](#globtestspecifications) or [`runTestSpecifications`](#runtestspecifications) is called. This method clears the cache for the given file or the whole cache alltogether depending on the first argument. + ## runTestSpecifications + +```ts +function runTestSpecifications( + specifications: TestSpecification[], + allTestsRun = false +): Promise +``` + +This method runs every test based on the received [specifications](/advanced/api/test-specification). The second argument, `allTestsRun`, is used by the coverage provider to determine if it needs to instrument coverage on _every_ file in the root (this only matters if coverage is enabled and `coverage.all` is set to `true`). + +::: warning +This method doesn't trigger `onWatcherRerun`, `onWatcherStart` and `onTestsRerun` callbacks. If you are rerunning tests based on the file change, consider using [`rerunTestSpecifications`](#reruntestspecifications) instead. +::: + ## rerunTestSpecifications + +```ts +function runTestSpecifications( + specifications: TestSpecification[], + allTestsRun = false +): Promise +``` + +This method emits `reporter.onWatcherRerun` and `onTestsRerun` events, then it runs tests with [`runTestSpecifications`](#runtestspecifications). If there were no errors in the main process, it will emit `reporter.onWatcherStart` event. + ## collectTests ## cancelCurrentRun + +```ts +function cancelCurrentRun(reason: CancelReason): Promise +``` + +This method will gracefully cancel all ongoing tests. It will wait for started tests to finish running and will not run tests that were scheduled to run but haven't started yet. + ## updateSnapshot ## invalidateFile + +```ts +function invalidateFile(filepath: string): void +``` + +This method invalidates the file in the cache of every project. It is mostly useful if you rely on your own watcher because Vite's cache persist in memory. + +::: danger +If you disable Vitest's watcher but keep Vitest running, it is important to manually clear the cache with this method because there is no way to disable the cache. This method will also invalidate file's importers. +::: + ## close + +```ts +function close(): Promise +``` + +Closes all projects and their associated resources. This can only be called once; the closing promise is cached until the server restarts. + ## exit +```ts +function exit(force = false): Promise +``` + +Closes all projects and exit the process. If `force` is set to `true`, the process will exit immediately after closing the projects. + +This method will also forcefuly call `process.exit()` if the process is still active after [`config.teardownTimeout`](/config/#teardowntimeout) milliseconds. + ## shouldKeepServer + +```ts +function shouldKeepServer(): boolean +``` + +This method will return `true` if the server should be kept running after the tests are done. This usually means that the `watch` mode was enabled. + ## onServerRestart + +```ts +function onServerRestart(fn: OnServerRestartHandler): void +``` + +Register a handler that will be called when the server is restarted due to a config change. + ## onCancel + +```ts +function onCancel(fn: (reason: CancelReason) => Awaitable): void +``` + +Register a handler that will be called when the test run is cancelled with [`vitest.cancelCurrentRun`](#cancelcurrentrun). + ## onClose + +```ts +function onClose(fn: () => Awaitable): void +``` + +Register a handler that will be called when the server is closed. + ## onTestsRerun + +```ts +function onTestsRerun(fn: OnTestsRerunHandler): void +``` + +Register a handler that will be called when the tests are rerunning. The tests can rerun when [`rerunTestSpecifications`](#reruntestspecifications) is called manually or when a file is changed and the built-in watcher schedules a rerun. + ## onFilterWatchedSpecification + +```ts +function onFilterWatchedSpecification( + fn: (specification: TestSpecification) => boolean +): void +``` +Register a handler that will be called when a file is changed. This callback should return `true` or `false`, indicating whether the test file needs to be rerun. + +With this method, you can hook into the default watcher logic to delay or discard tests that the user doesn't want to keep track of at the moment: + +```ts +const continuesTests: string[] = [] + +myCustomWrapper.onContinuesRunEnabled(testItem => + continuesTests.push(item.fsPath) +) + +vitest.onFilterWatchedSpecification(specification => + continuesTests.includes(specification.moduleId) +) +``` + +Vitest can create different specifications for the same file depending on the `pool` or `locations` options, so do not rely on the reference. Vitest can also return cached specification from [`vitest.getModuleSpecifications`](#getmodulespecifications) - the cache is based on the `moduleId` and `pool`. Note that [`project.createSpecification`](/advanced/api/test-project#createspecification) always returns a new instance. From e84f549e14bb41ded8c5c5329f00ffd9440a20e8 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 3 Dec 2024 11:31:36 +0100 Subject: [PATCH 25/73] feat: add clearSpecificationsCache --- packages/vitest/src/node/cli/cac.ts | 4 ++-- packages/vitest/src/node/core.ts | 20 ++++++++++++++------ packages/vitest/src/node/specifications.ts | 18 +++++++++++++----- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/packages/vitest/src/node/cli/cac.ts b/packages/vitest/src/node/cli/cac.ts index 925bf21ba0ad..c54e87012799 100644 --- a/packages/vitest/src/node/cli/cac.ts +++ b/packages/vitest/src/node/cli/cac.ts @@ -265,8 +265,8 @@ async function start(mode: VitestRunMode, cliFilters: string[], options: CliOpti try { const { startVitest } = await import('./cli-api') const ctx = await startVitest(mode, cliFilters.map(normalize), normalizeCliOptions(options)) - if (!ctx?.shouldKeepServer()) { - await ctx?.exit() + if (!ctx.shouldKeepServer()) { + await ctx.exit() } } catch (e) { diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 37ee5a7ceff4..5496c4a4254e 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -616,12 +616,20 @@ export class Vitest { return this.specifications.getModuleSpecifications(moduleId) } + /** + * Vitest automatically caches test specifications for each file. This method clears the cache for the given file or the whole cache alltogether. + */ + public clearSpecificationsCache(moduleId?: string) { + this.specifications.clearCache(moduleId) + } + /** * Run tests for the given test specifications. This does not trigger `onWatcher*` events. * @param specifications A list of specifications to run. * @param allTestsRun Indicates whether all tests were run. This only matters for coverage. */ public runTestSpecifications(specifications: TestSpecification[], allTestsRun = false): Promise { + specifications.forEach(spec => this.specifications.ensureSpecificationCached(spec)) return this.runFiles(specifications, allTestsRun) } @@ -1072,7 +1080,7 @@ export class Vitest { this.state.getProcessTimeoutCauses().forEach(cause => console.warn(cause)) if (!this.pool) { - const runningServers = [this.server, ...this.resolvedProjects.map(p => p.server)].filter(Boolean).length + const runningServers = [this.vite, ...this.resolvedProjects.map(p => p.vite)].filter(Boolean).length if (runningServers === 1) { console.warn('Tests closed successfully but something prevents Vite server from exiting') @@ -1128,28 +1136,28 @@ export class Vitest { /** * Should the server be kept running after the tests are done. */ - shouldKeepServer() { + shouldKeepServer(): boolean { return !!this.config?.watch } /** * Register a handler that will be called when the server is restarted due to a config change. */ - onServerRestart(fn: OnServerRestartHandler) { + onServerRestart(fn: OnServerRestartHandler): void { this._onRestartListeners.push(fn) } /** * Register a handler that will be called when the test run is cancelled with `vitest.cancelCurrentRun`. */ - onCancel(fn: (reason: CancelReason) => Awaitable) { + onCancel(fn: (reason: CancelReason) => Awaitable): void { this._onCancelListeners.push(fn) } /** * Register a handler that will be called when the server is closed. */ - onClose(fn: () => Awaitable) { + onClose(fn: () => Awaitable): void { this._onClose.push(fn) } @@ -1172,7 +1180,7 @@ export class Vitest { } /** @internal */ - onAfterSetServer(fn: OnServerRestartHandler) { + onAfterSetServer(fn: OnServerRestartHandler): void { this._onSetServer.push(fn) } } diff --git a/packages/vitest/src/node/specifications.ts b/packages/vitest/src/node/specifications.ts index 00c03ec384eb..9ea5ee79f162 100644 --- a/packages/vitest/src/node/specifications.ts +++ b/packages/vitest/src/node/specifications.ts @@ -92,22 +92,30 @@ export class VitestSpecifications { return files } - public clearCache(): void { - this._cachedSpecs.clear() + public clearCache(moduleId?: string): void { + if (moduleId) { + this._cachedSpecs.delete(moduleId) + } + else { + this._cachedSpecs.clear() + } } private getCachedSpecifications(moduleId: string): TestSpecification[] | undefined { return this._cachedSpecs.get(moduleId) } - private ensureSpecificationCached(spec: TestSpecification): TestSpecification[] { + public ensureSpecificationCached(spec: TestSpecification): TestSpecification[] { const file = spec.moduleId const specs = this._cachedSpecs.get(file) || [] - const included = specs.some(_s => _s.project === spec.project && _s.pool === spec.pool) - if (!included) { + const index = specs.findIndex(_s => _s.project === spec.project && _s.pool === spec.pool) + if (index === -1) { specs.push(spec) this._cachedSpecs.set(file, specs) } + else { + specs.splice(index, 1, spec) + } return specs } From c10df8abfa0e9802ab9a059e5ff20b07f44d79aa Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 3 Dec 2024 11:36:54 +0100 Subject: [PATCH 26/73] docs: fix link --- docs/advanced/api/vitest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced/api/vitest.md b/docs/advanced/api/vitest.md index 137e4770be42..ddac32ceabb9 100644 --- a/docs/advanced/api/vitest.md +++ b/docs/advanced/api/vitest.md @@ -126,7 +126,7 @@ declare module 'vitest' { ``` ::: warning -Technically, `provide` is a method of [`TestProject`](/advanced/api/testproject), so it is limited to the specific project. However, all projects inherit the values from the core project which makes `vitest.provide` universal way of passing down values to tests. +Technically, `provide` is a method of [`TestProject`](/advanced/api/test-project), so it is limited to the specific project. However, all projects inherit the values from the core project which makes `vitest.provide` universal way of passing down values to tests. ::: ## getProvidedContext From 43b51e6a657f8d5fd9914b74004e9acee79aed49 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 3 Dec 2024 12:51:01 +0100 Subject: [PATCH 27/73] chore: cleanup --- docs/advanced/api/test-case.md | 38 ++++++++++++++----- .../src/node/reporters/reported-tasks.ts | 5 ++- packages/vitest/src/public/node.ts | 5 ++- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/docs/advanced/api/test-case.md b/docs/advanced/api/test-case.md index d6f4004f6ea3..1683a926f9df 100644 --- a/docs/advanced/api/test-case.md +++ b/docs/advanced/api/test-case.md @@ -1,17 +1,37 @@ # TestCase -The `TestCase` class represents a single test. +The `TestCase` class represents a single test. This class is only available in the main thread. Refer to the ["Runner API"](/advanced/runner#tasks) if you are working with runtime tasks. + +The `TestCase` instance always has a `type` property with the value of `test`. You can use it to distinguish between different task types: + +```ts +if (task.type === 'test') { + task // TestCase +} +``` + +## project + +This references the [`TestProject`](/advanced/api/test-project) that the test belongs to. + +## module + +This is a direct reference to the [`TestModule`](/advanced/api/test-module) where the test is defined. + +## name + +This is a test name that was passed to the `test` function: + +```ts +import { test } from 'vitest' + +test('the validation works correctly', () => { + // ... +}) +``` ```ts declare class TestCase { - readonly type = 'test' - /** - * The project associated with the test. - */ - readonly project: TestProject - /** - * Direct reference to the test module where the test is defined. - */ readonly module: TestModule /** * Name of the test. diff --git a/packages/vitest/src/node/reporters/reported-tasks.ts b/packages/vitest/src/node/reporters/reported-tasks.ts index 6b37348c0f33..06211aba48b9 100644 --- a/packages/vitest/src/node/reporters/reported-tasks.ts +++ b/packages/vitest/src/node/reporters/reported-tasks.ts @@ -154,10 +154,11 @@ export class TestCase extends ReportedTaskImplementation { } /** - * Checks if the test was skipped. + * Checks if the test was skipped during collection or dynamically with `ctx.skip()`. */ public skipped(): boolean { - return this.task.mode !== 'run' && this.task.mode !== 'only' + const mode = this.task?.result?.state || this.task.mode + return mode === 'skip' || mode === 'todo' } /** diff --git a/packages/vitest/src/public/node.ts b/packages/vitest/src/public/node.ts index fd3fbc42da9a..abbb84ffb636 100644 --- a/packages/vitest/src/public/node.ts +++ b/packages/vitest/src/public/node.ts @@ -26,18 +26,19 @@ export type { JsonOptions } from '../node/reporters/json' export type { JUnitOptions } from '../node/reporters/junit' -export { TestCase, TestModule, TestSuite } from '../node/reporters/reported-tasks' - export type { ModuleDiagnostic, TaskOptions, + TestCase, TestCollection, TestDiagnostic, + TestModule, TestResult, TestResultFailed, TestResultPassed, TestResultSkipped, + TestSuite, } from '../node/reporters/reported-tasks' export { BaseSequencer } from '../node/sequencers/BaseSequencer' From ce8733fb03d524e2fa043d3d16039b5fce227a7d Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 3 Dec 2024 13:35:44 +0100 Subject: [PATCH 28/73] feat: add generateFileHash utility --- packages/runner/src/utils/collect.ts | 14 +++++++++++++- packages/runner/src/utils/index.ts | 1 + packages/vitest/src/public/node.ts | 2 ++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/runner/src/utils/collect.ts b/packages/runner/src/utils/collect.ts index f492cc5c789b..468f6200aaa3 100644 --- a/packages/runner/src/utils/collect.ts +++ b/packages/runner/src/utils/collect.ts @@ -169,7 +169,7 @@ export function createFileTask( ): File { const path = relative(root, filepath) const file: File = { - id: generateHash(`${path}${projectName || ''}`), + id: generateFileHash(path, projectName), name: path, type: 'suite', mode: 'run', @@ -183,3 +183,15 @@ export function createFileTask( file.file = file return file } + +/** + * Generate a unique ID for a file based on its path and project name + * @param file File relative to the root of the project to keep ID the same between different machines + * @param projectName The name of the test project + */ +export function generateFileHash( + file: string, + projectName: string | undefined, +): string { + return generateHash(`${file}${projectName || ''}`) +} diff --git a/packages/runner/src/utils/index.ts b/packages/runner/src/utils/index.ts index 1faffeb65163..2d793f4a5d2a 100644 --- a/packages/runner/src/utils/index.ts +++ b/packages/runner/src/utils/index.ts @@ -2,6 +2,7 @@ export { type ChainableFunction, createChainable } from './chain' export { calculateSuiteHash, createFileTask, + generateFileHash, generateHash, interpretTaskModes, someTasksAreOnly, diff --git a/packages/vitest/src/public/node.ts b/packages/vitest/src/public/node.ts index abbb84ffb636..53542058a61d 100644 --- a/packages/vitest/src/public/node.ts +++ b/packages/vitest/src/public/node.ts @@ -140,6 +140,8 @@ export type { export { createDebugger } from '../utils/debugger' +export { generateFileHash } from '@vitest/runner/utils' + export { esbuildVersion, isFileServingAllowed, From 27fc3957ab96230622518a8776a748e6cf7fd130 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 3 Dec 2024 13:36:06 +0100 Subject: [PATCH 29/73] feat: allow --includeTaskLocation flag --- packages/vitest/src/node/cli/cli-config.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index 8aa7118b6206..48a27ad7578a 100644 --- a/packages/vitest/src/node/cli/cli-config.ts +++ b/packages/vitest/src/node/cli/cli-config.ts @@ -780,6 +780,9 @@ export const cliOptionsConfig: VitestCLIOptions = { printConsoleTrace: { description: 'Always print console stack traces', }, + includeTaskLocation: { + description: 'Collect test and suite locations in the `location` property', + }, // CLI only options run: { @@ -839,7 +842,6 @@ export const cliOptionsConfig: VitestCLIOptions = { poolMatchGlobs: null, deps: null, name: null, - includeTaskLocation: null, snapshotEnvironment: null, compare: null, outputJson: null, From e401be33ba75e71c2621bf66540390e99504576c Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 3 Dec 2024 13:36:18 +0100 Subject: [PATCH 30/73] docs: document TestCase --- docs/advanced/api/test-case.md | 227 +++++++++++++----- .../src/node/reporters/reported-tasks.ts | 10 +- 2 files changed, 166 insertions(+), 71 deletions(-) diff --git a/docs/advanced/api/test-case.md b/docs/advanced/api/test-case.md index 1683a926f9df..f640301afd97 100644 --- a/docs/advanced/api/test-case.md +++ b/docs/advanced/api/test-case.md @@ -20,9 +20,65 @@ This is a direct reference to the [`TestModule`](/advanced/api/test-module) wher ## name -This is a test name that was passed to the `test` function: +This is a test name that was passed to the `test` function. This test has a name "the validation works correctly": + +```ts{3} +import { test } from 'vitest' + +test('the validation works correctly', () => { + // ... +}) +``` + +## fullName + +The name of the test including all parent suites separated with `>` symbol. This test has a full name "the validation logic > the validation works correctly": + +```ts{3,6} +import { test, describe } from 'vitest' + +describe('the validation logic', () => { + // ... + + test('the validation works correctly', () => { + // ... + }) +}) +``` + +## id + +This is test's unique identifier. This ID is deterministic and will be the same for the same test across multiple runs. The ID is based on the [project](/advanced/api/test-project) name, module ID and test order. + +The ID looks like this: + +``` +1223128da3_0_0 +^^^^^^^^^^ the file hash + ^ suite index + ^ test index +``` + +::: tip +You can generate file hash with `generateFileHash` function from `vitest/node` which is available since Vitest 2.2: ```ts +import { generateFileHash } from 'vitest/node' + +const hash = generateFileHash( + '/file/path.js', // relative path + undefined, // the project name or `undefined` is not set +) +``` +::: + +## location + +The location in the module where the test was defined. Locations are collected only if [`includeTaskLocation`](/config/#includetasklocation) is enabled in the config. Note that this option is automatically enabled if `--reporter=html`, `--ui` or `--browser` flags are used. + +The location of this test will be equal to `{ line: 3, column: 1 }`: + +```ts:line-numbers {3} import { test } from 'vitest' test('the validation works correctly', () => { @@ -30,75 +86,97 @@ test('the validation works correctly', () => { }) ``` +## parent + +Parent [suite](/advanced/api/test-suite). If the test was called directly inside the [module](/advanced/api/test-module), the parent will be the module itself. + +## options + ```ts -declare class TestCase { - readonly module: TestModule - /** - * Name of the test. - */ - readonly name: string - /** - * Full name of the test including all parent suites separated with `>`. - */ - readonly fullName: string - /** - * Unique identifier. - * This ID is deterministic and will be the same for the same test across multiple runs. - * The ID is based on the project name, module id and test position. - */ - readonly id: string - /** - * Location in the module where the test was defined. - * Locations are collected only if `includeTaskLocation` is enabled in the config. - */ - readonly location: { line: number; column: number } | undefined - /** - * Parent suite. If the test was called directly inside the module, the parent will be the module itself. - */ - readonly parent: TestSuite | TestModule - /** - * Options that test was initiated with. - */ - readonly options: TaskOptions - /** - * Checks if the test did not fail the suite. - * If the test is not finished yet or was skipped, it will return `true`. - */ - ok(): boolean - /** - * Checks if the test was skipped. - */ - skipped(): boolean +interface TaskOptions { + each: boolean | undefined + concurrent: boolean | undefined + shuffle: boolean | undefined + retry: number | undefined + repeats: number | undefined + mode: 'run' | 'only' | 'skip' | 'todo' +} +``` + +The options that test was collected with. + +## ok + +```ts +function ok(): boolean +``` + +Checks if the test did not fail the suite. If the test is not finished yet or was skipped, it will return `true`. + +## skipped + +```ts +function skipped(): boolean +``` + +Checks if the test was skipped during collection or dynamically with `ctx.skip()`. + +## meta + +```ts +function meta(): TaskMeta +``` + +Custom metadata that was attached to the test during its execution. The meta can be attached by assigning a property to the `ctx.task.meta` object during a test run: + +```ts:line-numbers {3,6} +import { test } from 'vitest' + +test('the validation works correctly', ({ task }) => { + // ... + + task.meta.decorated = false +}) +``` + +If the test did not finish running yet, the meta will be an empty object. + +## result + +```ts +function result(): TestResult | undefined +``` + +Test results. It will be `undefined` if test is not finished yet or was just collected. + +If the test was skipped, the return value will be `TestResultSkipped`: + +```ts +interface TestResultSkipped { /** - * Custom metadata that was attached to the test during its execution. + * The test was skipped with `skip` or `todo` flag. + * You can see which one was used in the `options.mode` option. */ - meta(): TaskMeta + state: 'skipped' /** - * Test results. Will be `undefined` if test is not finished yet or was just collected. + * Skipped tests have no errors. */ - result(): TestResult | undefined + errors: undefined /** - * Useful information about the test like duration, memory usage, etc. + * A custom note passed down to `ctx.skip(note)`. */ - diagnostic(): TestDiagnostic | undefined + note: string | undefined } +``` -export type TestResult = TestResultPassed | TestResultFailed | TestResultSkipped +::: tip +If the test was skipped because another test has `only` flag, the `options.mode` will be equal to `skip`. +::: -export interface TestResultPassed { - /** - * The test passed successfully. - */ - state: 'passed' - /** - * Errors that were thrown during the test execution. - * - * **Note**: If test was retried successfully, errors will still be reported. - */ - errors: TestError[] | undefined -} +If the test failed, the return value will be `TestResultFailed`: -export interface TestResultFailed { +```ts +interface TestResultFailed { /** * The test failed to execute. */ @@ -108,20 +186,37 @@ export interface TestResultFailed { */ errors: TestError[] } +``` -export interface TestResultSkipped { +If the test passed, the retunr value will be `TestResultPassed`: + +```ts +interface TestResultPassed { /** - * The test was skipped with `only`, `skip` or `todo` flag. - * You can see which one was used in the `mode` option. + * The test passed successfully. */ - state: 'skipped' + state: 'passed' /** - * Skipped tests have no errors. + * Errors that were thrown during the test execution. */ - errors: undefined + errors: TestError[] | undefined } +``` -export interface TestDiagnostic { +::: warning +Note that the test with `passed` state can still have errors attached - this can happen if `retry` was triggered at least once. +::: + +## diagnostic + +```ts +function diagnostic(): TestDiagnostic | undefined +``` + +Useful information about the test like duration, memory usage, etc: + +```ts +interface TestDiagnostic { /** * If the duration of the test is above `slowTestThreshold`. */ diff --git a/packages/vitest/src/node/reporters/reported-tasks.ts b/packages/vitest/src/node/reporters/reported-tasks.ts index 06211aba48b9..87da4879bef6 100644 --- a/packages/vitest/src/node/reporters/reported-tasks.ts +++ b/packages/vitest/src/node/reporters/reported-tasks.ts @@ -23,7 +23,7 @@ class ReportedTaskImplementation { /** * Unique identifier. * This ID is deterministic and will be the same for the same test across multiple runs. - * The ID is based on the project name, module url and test position. + * The ID is based on the project name, module url and test order. */ public readonly id: string @@ -157,7 +157,7 @@ export class TestCase extends ReportedTaskImplementation { * Checks if the test was skipped during collection or dynamically with `ctx.skip()`. */ public skipped(): boolean { - const mode = this.task?.result?.state || this.task.mode + const mode = this.task.result?.state || this.task.mode return mode === 'skip' || mode === 'todo' } @@ -469,8 +469,8 @@ export interface TestResultFailed { export interface TestResultSkipped { /** - * The test was skipped with `only`, `skip` or `todo` flag. - * You can see which one was used in the `mode` option. + * The test was skipped with `only` (on another test), `skip` or `todo` flag. + * You can see which one was used in the `options.mode` option. */ state: 'skipped' /** @@ -478,7 +478,7 @@ export interface TestResultSkipped { */ errors: undefined /** - * A custom note. + * A custom note passed down to `ctx.skip(note)`. */ note: string | undefined } From d6017cafcf4f169009690f6128a3928333ee6f59 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 3 Dec 2024 14:39:53 +0100 Subject: [PATCH 31/73] docs: add task test api --- docs/.vitepress/config.ts | 4 +- docs/.vitepress/style/main.css | 6 + docs/advanced/api/test-case.md | 19 +- docs/advanced/api/test-collection.md | 121 +++++++---- docs/advanced/api/test-module.md | 41 ++-- docs/advanced/api/test-suite.md | 188 ++++++++++++++---- docs/package.json | 1 + .../src/node/reporters/reported-tasks.ts | 2 +- pnpm-lock.yaml | 12 +- 9 files changed, 285 insertions(+), 109 deletions(-) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index fd0479d4f40e..337d842a69f2 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -1,4 +1,5 @@ import { transformerTwoslash } from '@shikijs/vitepress-twoslash' +import { transformerNotationWordHighlight } from '@shikijs/transformers' import { withPwa } from '@vite-pwa/vitepress' import type { DefaultTheme } from 'vitepress' import { defineConfig } from 'vitepress' @@ -87,8 +88,9 @@ export default ({ mode }: { mode: string }) => { dark: 'github-dark', }, codeTransformers: mode === 'development' - ? [] + ? [transformerNotationWordHighlight()] : [ + transformerNotationWordHighlight(), transformerTwoslash({ processHoverInfo: (info) => { if (info.includes(process.cwd())) { diff --git a/docs/.vitepress/style/main.css b/docs/.vitepress/style/main.css index 735f705c9940..251138d121b7 100644 --- a/docs/.vitepress/style/main.css +++ b/docs/.vitepress/style/main.css @@ -170,3 +170,9 @@ img.resizable-img { min-height: unset; } } + +.highlighted-word { + background-color: var(--vp-code-line-highlight-color); + transition: background-color 0.5s; + display: inline-block; +} diff --git a/docs/advanced/api/test-case.md b/docs/advanced/api/test-case.md index f640301afd97..e80569d06874 100644 --- a/docs/advanced/api/test-case.md +++ b/docs/advanced/api/test-case.md @@ -20,11 +20,12 @@ This is a direct reference to the [`TestModule`](/advanced/api/test-module) wher ## name -This is a test name that was passed to the `test` function. This test has a name "the validation works correctly": +This is a test name that was passed to the `test` function. -```ts{3} +```ts import { test } from 'vitest' +// [!code word:'the validation works correctly'] test('the validation works correctly', () => { // ... }) @@ -34,12 +35,12 @@ test('the validation works correctly', () => { The name of the test including all parent suites separated with `>` symbol. This test has a full name "the validation logic > the validation works correctly": -```ts{3,6} -import { test, describe } from 'vitest' +```ts +import { describe, test } from 'vitest' +// [!code word:'the validation works correctly'] +// [!code word:'the validation logic'] describe('the validation logic', () => { - // ... - test('the validation works correctly', () => { // ... }) @@ -72,6 +73,10 @@ const hash = generateFileHash( ``` ::: +::: danger +Don't try to parse the ID. It can have a minus at the start: `-1223128da3_0_0_0`. +::: + ## location The location in the module where the test was defined. Locations are collected only if [`includeTaskLocation`](/config/#includetasklocation) is enabled in the config. Note that this option is automatically enabled if `--reporter=html`, `--ui` or `--browser` flags are used. @@ -129,7 +134,7 @@ function meta(): TaskMeta Custom metadata that was attached to the test during its execution. The meta can be attached by assigning a property to the `ctx.task.meta` object during a test run: -```ts:line-numbers {3,6} +```ts {3,6} import { test } from 'vitest' test('the validation works correctly', ({ task }) => { diff --git a/docs/advanced/api/test-collection.md b/docs/advanced/api/test-collection.md index d01e84962c61..03bb1d45322a 100644 --- a/docs/advanced/api/test-collection.md +++ b/docs/advanced/api/test-collection.md @@ -1,50 +1,93 @@ # TestCollection -`TestCollection` represents a collection of suites and tests. It also provides useful methods to iterate over itself. - -```ts -declare class TestCollection { - /** - * Returns the test or suite at a specific index in the array. - */ - at(index: number): TestCase | TestSuite | undefined - /** - * The number of tests and suites in the collection. - */ - size: number - /** - * Returns the collection in array form for easier manipulation. - */ - array(): (TestCase | TestSuite)[] - /** - * Filters all suites that are part of this collection and its children. - */ - allSuites(): IterableIterator - /** - * Filters all tests that are part of this collection and its children. - */ - allTests(state?: TestResult['state'] | 'running'): IterableIterator - /** - * Filters only the tests that are part of this collection. - */ - tests(state?: TestResult['state'] | 'running'): IterableIterator - /** - * Filters only the suites that are part of this collection. - */ - suites(): IterableIterator; - [Symbol.iterator](): IterableIterator +`TestCollection` represents a collection of top-level [suites](/advanced/api/test-suite) and [tests](/advanced/api/test-case) in a suite or a module. It also provides useful methods to iterate over itself. + +::: info +Most methods return an iterator instead of an array for better performance in case you don't need every item in the collection. If you prefer working with array, you can spread the iterator: `[...children.allSuites()]`. + +Also note that the collection itself is an iterator: + +```ts +for (const task of module.children) { + console.log(task.type, task.name) } ``` +::: + +## size + +The number of tests and suites in the collection. -For example, you can iterate over all tests inside a module by calling `testModule.children.allTests()`: +::: warning +This number includes only tests and suites at the top-level, it doesn't include nested suites and tests. +::: + +## at ```ts -function onFileCollected(testModule: TestModule): void { - console.log('collecting tests in', testModule.moduleId) +function at(index: number): TestCase | TestSuite | undefined +``` + +Returns the test or suite at a specific index. This method accepts negative indexes. - // iterate over all tests and suites in the module - for (const task of testModule.children.allTests()) { - console.log('collected', task.type, task.fullName) +## array + +```ts +function array(): (TestCase | TestSuite)[] +``` + +The same collection but as an array. This is useful if you want to use `Array` methods like `map` and `filter` that are not supported by the `TaskCollection` implementation. + +## allSuites + +```ts +function allSuites(): Generator +``` + +Filters all suites that are part of this collection and its children. + +```ts +for (const suite of module.children.allSuites()) { + if (suite.errors().length) { + console.log('failed to collect', suite.errors()) } } ``` + +## allTests + +```ts +function allTests( + state?: TestResult['state'] | 'running' +): Generator +``` + +Filters all tests that are part of this collection and its children. + +```ts +for (const test of module.children.allTests()) { + if (!test.result()) { + console.log('test', test.fullName, 'did not finish') + } +} +``` + +You can pass down a `state` value to filter tests by the state. + +## tests + +```ts +function tests( + state?: TestResult['state'] | 'running' +): Generator +``` + +Filters only the tests that are part of this collection. You can pass down a `state` value to filter tests by the state. + +## suites + +```ts +function suites(): Generator +``` + +Filters only the suites that are part of this collection. diff --git a/docs/advanced/api/test-module.md b/docs/advanced/api/test-module.md index 76ad8fb7a3ae..bacddef2d092 100644 --- a/docs/advanced/api/test-module.md +++ b/docs/advanced/api/test-module.md @@ -1,26 +1,31 @@ # TestModule +The `TestModule` class represents a single module in a single project. This class is only available in the main thread. Refer to the ["Runner API"](/advanced/runner#tasks) if you are working with runtime tasks. + +The `TestModule` instance always has a `type` property with the value of `module`. You can use it to distinguish between different task types: + ```ts -declare class TestModule extends TestSuite { - readonly type = 'module' - /** - * Collection of suites and tests that are part of this module. - */ - readonly children: TestCollection - /** - * This is usually an absolute Unix file path. - * It can be a virtual id if the file is not on the disk. - * This value corresponds to Vite's `ModuleGraph` id. - */ - readonly moduleId: string - /** - * Useful information about the module like duration, memory usage, etc. - * If the module was not executed yet, all diagnostic values will return `0`. - */ - diagnostic(): ModuleDiagnostic +if (task.type === 'module') { + task // TestModule } +``` -export interface ModuleDiagnostic { +The `TestModule` inherits all methods and properties from the [`TestSuite`](/advanced/api/test-module). This guide will only list methods and properties unique to the `TestModule` + +## moduleId + +This is usually an absolute unix file path (even on Windows). It can be a virtual id if the file is not on the disk. This value corresponds to Vite's `ModuleGraph` id. + +## diagnostic + +```ts +function diagnostic(): ModuleDiagnostic +``` + +Useful information about the module like duration, memory usage, etc. If the module was not executed yet, all diagnostic values will return `0`. + +```ts +interface ModuleDiagnostic { /** * The time it takes to import and initiate an environment. */ diff --git a/docs/advanced/api/test-suite.md b/docs/advanced/api/test-suite.md index f47beff9d492..9e94f75f8271 100644 --- a/docs/advanced/api/test-suite.md +++ b/docs/advanced/api/test-suite.md @@ -1,46 +1,152 @@ # TestSuite +The `TestSuite` class represents a single suite. This class is only available in the main thread. Refer to the ["Runner API"](/advanced/runner#tasks) if you are working with runtime tasks. + +The `TestSuite` instance always has a `type` property with the value of `suite`. You can use it to distinguish between different task types: + +```ts +if (task.type === 'suite') { + task // TestSuite +} +``` + +## project + +This references the [`TestProject`](/advanced/api/test-project) that the test belongs to. + +## module + +This is a direct reference to the [`TestModule`](/advanced/api/test-module) where the test is defined. + +## name + +This is a suite name that was passed to the `describe` function. + +```ts +import { describe } from 'vitest' + +// [!code word:'the validation logic'] +describe('the validation logic', () => { + // ... +}) +``` + +## fullName + +The name of the suite including all parent suites separated with `>` symbol. This suite has a full name "the validation logic > validating cities": + +```ts +import { describe, test } from 'vitest' + +// [!code word:'the validation logic'] +// [!code word:'validating cities'] +describe('the validation logic', () => { + describe('validating cities', () => { + // ... + }) +}) +``` + +## id + +This is suite's unique identifier. This ID is deterministic and will be the same for the same suite across multiple runs. The ID is based on the [project](/advanced/api/test-project) name, module ID and suite order. + +The ID looks like this: + +``` +1223128da3_0_0_0 +^^^^^^^^^^ the file hash + ^ suite index + ^ nested suite index + ^ test index +``` + +::: tip +You can generate file hash with `generateFileHash` function from `vitest/node` which is available since Vitest 2.2: + +```ts +import { generateFileHash } from 'vitest/node' + +const hash = generateFileHash( + '/file/path.js', // relative path + undefined, // the project name or `undefined` is not set +) +``` +::: + +::: danger +Don't try to parse the ID. It can have a minus at the start: `-1223128da3_0_0_0`. +::: + +## location + +The location in the module where the suite was defined. Locations are collected only if [`includeTaskLocation`](/config/#includetasklocation) is enabled in the config. Note that this option is automatically enabled if `--reporter=html`, `--ui` or `--browser` flags are used. + +The location of this suite will be equal to `{ line: 3, column: 1 }`: + +```ts:line-numbers {3} +import { describe } from 'vitest' + +describe('the validation works correctly', () => { + // ... +}) +``` + +## parent + +Parent suite. If the suite was called directly inside the [module](/advanced/api/test-module), the parent will be the module itself. + +## options + +```ts +interface TaskOptions { + each: boolean | undefined + concurrent: boolean | undefined + shuffle: boolean | undefined + retry: number | undefined + repeats: number | undefined + mode: 'run' | 'only' | 'skip' | 'todo' +} +``` + +The options that suite was collected with. + +## children + +This is a [collection](/advanced/api/test-collection) of all suites and tests inside the current suite. + ```ts -declare class TestSuite { - readonly type = 'suite' - /** - * The project associated with the test. - */ - readonly project: TestProject - /** - * Direct reference to the test module where the suite is defined. - */ - readonly module: TestModule - /** - * Name of the suite. - */ - readonly name: string - /** - * Full name of the suite including all parent suites separated with `>`. - */ - readonly fullName: string - /** - * Unique identifier. - * This ID is deterministic and will be the same for the same test across multiple runs. - * The ID is based on the project name, module id and test position. - */ - readonly id: string - /** - * Location in the module where the suite was defined. - * Locations are collected only if `includeTaskLocation` is enabled in the config. - */ - readonly location: { line: number; column: number } | undefined - /** - * Collection of suites and tests that are part of this suite. - */ - readonly children: TaskCollection - /** - * Options that the suite was initiated with. - */ - readonly options: TaskOptions - /** - * Errors that happened outside of the test run during collection, like syntax errors. - */ - public errors(): TestError[] +for (const task of suite.children) { + if (task.type === 'test') { + console.log('test', task.fullName) + } + else { + // task is TaskSuite + console.log('suite', task.name) + } } ``` + +::: warning +Note that `suite.children` will only iterate the first level of nesting, it won't go deeper. +::: + +## errors + +```ts +function errors(): TestError[] +``` + +Errors that happened outside of the test run during collection, like syntax errors. + +```ts {4} +import { describe } from 'vitest' + +describe('collection failed', () => { + throw new Error('a custom error') +}) +``` + +::: warning +Note that errors are serialized into simple object: `instanceof Error` will always return `false`. +::: diff --git a/docs/package.json b/docs/package.json index 9949cae14cdd..85273cf5bfa5 100644 --- a/docs/package.json +++ b/docs/package.json @@ -20,6 +20,7 @@ "devDependencies": { "@iconify-json/carbon": "^1.2.4", "@iconify-json/logos": "^1.2.3", + "@shikijs/transformers": "^1.24.0", "@shikijs/vitepress-twoslash": "^1.24.1", "@unocss/reset": "^0.65.1", "@vite-pwa/assets-generator": "^0.2.6", diff --git a/packages/vitest/src/node/reporters/reported-tasks.ts b/packages/vitest/src/node/reporters/reported-tasks.ts index 87da4879bef6..0adff228c321 100644 --- a/packages/vitest/src/node/reporters/reported-tasks.ts +++ b/packages/vitest/src/node/reporters/reported-tasks.ts @@ -202,7 +202,7 @@ class TestCollection { } /** - * Returns the test or suite at a specific index in the array. + * Returns the test or suite at a specific index. */ at(index: number): TestCase | TestSuite | undefined { if (index < 0) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6652bd3a173..512816c0c95d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -140,6 +140,9 @@ importers: '@iconify-json/logos': specifier: ^1.2.3 version: 1.2.3 + '@shikijs/transformers': + specifier: ^1.24.0 + version: 1.24.0 '@shikijs/vitepress-twoslash': specifier: ^1.24.1 version: 1.24.1(typescript@5.7.2) @@ -11768,7 +11771,12 @@ snapshots: '@shikijs/transformers@1.22.2': dependencies: - shiki: 1.22.2 + '@shikijs/types': 1.24.0 + '@shikijs/vscode-textmate': 9.3.0 + + '@shikijs/transformers@1.24.0': + dependencies: + shiki: 1.24.0 '@shikijs/twoslash@1.24.1(typescript@5.7.2)': dependencies: @@ -18869,7 +18877,7 @@ snapshots: '@docsearch/js': 3.6.2(@algolia/client-search@4.20.0)(@types/react@18.2.79)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.9.0) '@iconify-json/simple-icons': 1.2.11 '@shikijs/core': 1.22.2 - '@shikijs/transformers': 1.22.2 + '@shikijs/transformers': 1.24.0 '@shikijs/types': 1.22.2 '@types/markdown-it': 14.1.2 '@vitejs/plugin-vue': 5.2.1(vite@5.4.0(@types/node@22.10.1)(terser@5.36.0))(vue@3.5.12(typescript@5.7.2)) From bcf2c45b375f057aa4217b2baec77986e80ac1da Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 3 Dec 2024 14:47:32 +0100 Subject: [PATCH 32/73] test: add tests for filters --- docs/advanced/api/test-case.md | 2 +- packages/vitest/src/node/reporters/reported-tasks.ts | 5 ++++- test/cli/test/reported-tasks.test.ts | 5 +++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/advanced/api/test-case.md b/docs/advanced/api/test-case.md index e80569d06874..7453da2eaf60 100644 --- a/docs/advanced/api/test-case.md +++ b/docs/advanced/api/test-case.md @@ -152,7 +152,7 @@ If the test did not finish running yet, the meta will be an empty object. function result(): TestResult | undefined ``` -Test results. It will be `undefined` if test is not finished yet or was just collected. +Test results. It will be `undefined` if test is skipped during collection, not finished yet or was just collected. If the test was skipped, the return value will be `TestResultSkipped`: diff --git a/packages/vitest/src/node/reporters/reported-tasks.ts b/packages/vitest/src/node/reporters/reported-tasks.ts index 0adff228c321..4482baeea676 100644 --- a/packages/vitest/src/node/reporters/reported-tasks.ts +++ b/packages/vitest/src/node/reporters/reported-tasks.ts @@ -113,7 +113,7 @@ export class TestCase extends ReportedTaskImplementation { } /** - * Test results. Will be `undefined` if test is not finished yet or was just collected. + * Test results. Will be `undefined` if test is skipped, not finished yet or was just collected. */ public result(): TestResult | undefined { const result = this.task.result @@ -541,6 +541,9 @@ export interface ModuleDiagnostic { } function getTestState(test: TestCase): TestResult['state'] | 'running' { + if (test.skipped()) { + return 'skipped' + } const result = test.result() return result ? result.state : 'running' } diff --git a/test/cli/test/reported-tasks.test.ts b/test/cli/test/reported-tasks.test.ts index 15b0b768ef84..b0d15b0e2c2d 100644 --- a/test/cli/test/reported-tasks.test.ts +++ b/test/cli/test/reported-tasks.test.ts @@ -63,6 +63,11 @@ it('correctly reports a file', () => { const deepTests = [...testModule.children.allTests()] expect(deepTests).toHaveLength(19) + expect([...testModule.children.allTests('skipped')]).toHaveLength(5) + expect([...testModule.children.allTests('passed')]).toHaveLength(9) + expect([...testModule.children.allTests('failed')]).toHaveLength(5) + expect([...testModule.children.allTests('running')]).toHaveLength(0) + const suites = [...testModule.children.suites()] expect(suites).toHaveLength(3) const deepSuites = [...testModule.children.allSuites()] From bf4d32e7e332a44b49aa52740ed6757a220255bb Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 3 Dec 2024 14:48:16 +0100 Subject: [PATCH 33/73] docs: cleanup --- docs/advanced/api/test-collection.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/advanced/api/test-collection.md b/docs/advanced/api/test-collection.md index 03bb1d45322a..974f37dbd11d 100644 --- a/docs/advanced/api/test-collection.md +++ b/docs/advanced/api/test-collection.md @@ -8,8 +8,8 @@ Most methods return an iterator instead of an array for better performance in ca Also note that the collection itself is an iterator: ```ts -for (const task of module.children) { - console.log(task.type, task.name) +for (const child of module.children) { + console.log(child.type, child.name) } ``` ::: From b150754ecc5402055fa54ee7205d5908454aff07 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 3 Dec 2024 15:26:30 +0100 Subject: [PATCH 34/73] feat: add ok/skipped to test suite --- docs/advanced/api/test-project.md | 2 +- docs/advanced/api/test-suite.md | 16 +++++++ docs/advanced/api/vitest.md | 2 +- docs/advanced/guide/tests.md | 13 +++--- .../src/node/reporters/reported-tasks.ts | 43 +++++++++++++++---- 5 files changed, 60 insertions(+), 16 deletions(-) diff --git a/docs/advanced/api/test-project.md b/docs/advanced/api/test-project.md index 2c9b9043a896..5211827b6299 100644 --- a/docs/advanced/api/test-project.md +++ b/docs/advanced/api/test-project.md @@ -259,7 +259,7 @@ if (import.meta.vitest) { function onTestsRerun(cb: OnTestsRerunHandler): void ``` -This is a shorthand for `project.vitest.onTestsRerun`. It accepts a callback that will be awaited when the tests have been scheduled to rerun (usually, due to a file change). +This is a shorthand for [`project.vitest.onTestsRerun`](/advanced/api/vitest#ontestsrerun). It accepts a callback that will be awaited when the tests have been scheduled to rerun (usually, due to a file change). ```ts project.onTestsRerun((specs) => { diff --git a/docs/advanced/api/test-suite.md b/docs/advanced/api/test-suite.md index 9e94f75f8271..614c197d26c7 100644 --- a/docs/advanced/api/test-suite.md +++ b/docs/advanced/api/test-suite.md @@ -131,6 +131,22 @@ for (const task of suite.children) { Note that `suite.children` will only iterate the first level of nesting, it won't go deeper. ::: +## ok + +```ts +function ok(): boolean +``` + +Checks if the suite has any failed tests. This will also return `false` if suite failed during collection. In that case, check the [`errors()`](#errors) for thrown errors. + +## skipped + +```ts +function skipped(): boolean +``` + +Checks if the suite was skipped during collection. + ## errors ```ts diff --git a/docs/advanced/api/vitest.md b/docs/advanced/api/vitest.md index ddac32ceabb9..8dfab0843b31 100644 --- a/docs/advanced/api/vitest.md +++ b/docs/advanced/api/vitest.md @@ -140,7 +140,7 @@ This returns the root context object. This is a shorthand for `vitest.getRootTes ## getProjectByName ```ts -function getProjectByName(name: string): ProvidedContext +function getProjectByName(name: string): TestProject ``` This method returns the project by its name. Simillar to calling `vitest.projects.find`. diff --git a/docs/advanced/guide/tests.md b/docs/advanced/guide/tests.md index ce768f7815f3..9028c42c0160 100644 --- a/docs/advanced/guide/tests.md +++ b/docs/advanced/guide/tests.md @@ -25,17 +25,19 @@ const vitest = await startVitest( ) const testModules = vitest.state.getTestModules() for (const testModule of testModules) { - console.log(testModule.moduleId, 'results', testModule.result()) + console.log(testModule.moduleId, testModule.ok() ? 'passed' : 'failed') } ``` ::: tip -[`TestModule`](/advanced/reporters#TestModule), [`TestSuite`](/advanced/reporters#TestSuite) and [`TestCase`](/advanced/reporters#TestCase) APIs are not experimental and follow SemVer since Vitest 2.1. +[`TestModule`](/advanced/api/test-module), [`TestSuite`](/advanced/api/test-suite) and [`TestCase`](/advanced/api/test-case) APIs are not experimental and follow SemVer since Vitest 2.1. ::: ## `createVitest` -`createVitest` method doesn't validate that required packages are installed. This method also doesn't respect `config.standalone` or `config.mergeReports`. Vitest also won't be closed automatically even if `watch` is disabled. +Creates a [Vitest](/advanced/api/vitest) instances without running tests. + +`createVitest` method doesn't validate that required packages are installed. It also doesn't respect `config.standalone` or `config.mergeReports`. Vitest won't be closed automatically even if `watch` is disabled. ```ts import { createVitest } from 'vitest/node' @@ -55,13 +57,14 @@ vitest.onClose(() => {}) vitest.onTestsRerun((files) => {}) try { - // this will set process.exitCode to 1 if tests failed + // this will set process.exitCode to 1 if tests failed, + // and won't close the process automatically await vitest.start(['my-filter']) } catch (err) { // this can throw // "FilesNotFoundError" if no files were found - // "GitNotFoundError" if `--changed` is enabled and repository is not initialized + // "GitNotFoundError" with `--changed` and repository is not initialized } finally { await vitest.close() diff --git a/packages/vitest/src/node/reporters/reported-tasks.ts b/packages/vitest/src/node/reporters/reported-tasks.ts index 4482baeea676..fa881accdb99 100644 --- a/packages/vitest/src/node/reporters/reported-tasks.ts +++ b/packages/vitest/src/node/reporters/reported-tasks.ts @@ -43,6 +43,15 @@ class ReportedTaskImplementation { this.location = task.location } + /** + * Checks if the test did not fail the suite. + * If the test is not finished yet or was skipped, it will return `true`. + */ + public ok(): boolean { + const result = this.task.result + return !result || result.state !== 'fail' + } + /** * Creates a new reported task instance and stores it in the project's state for future use. * @internal @@ -144,15 +153,6 @@ export class TestCase extends ReportedTaskImplementation { } satisfies TestResultFailed } - /** - * Checks if the test did not fail the suite. - * If the test is not finished yet or was skipped, it will return `true`. - */ - public ok(): boolean { - const result = this.result() - return !result || result.state !== 'failed' - } - /** * Checks if the test was skipped during collection or dynamically with `ctx.skip()`. */ @@ -313,6 +313,14 @@ abstract class SuiteImplementation extends ReportedTaskImplementation { this.children = new TestCollection(task, project) } + /** + * Checks if the suite was skipped during collection. + */ + public skipped(): boolean { + const mode = this.task.mode + return mode === 'skip' || mode === 'todo' + } + /** * Errors that happened outside of the test run during collection, like syntax errors. */ @@ -364,6 +372,12 @@ export class TestSuite extends SuiteImplementation { this.options = buildOptions(task) } + /** + * Checks if the suite has any failed tests. + * This will also return `false` if suite failed during collection. + */ + declare public ok: () => boolean + /** * Full name of the suite including all parent suites separated with `>`. */ @@ -399,6 +413,17 @@ export class TestModule extends SuiteImplementation { this.moduleId = task.filepath } + /** + * Checks if the module has any failed tests. + * This will also return `false` if module failed during collection. + */ + declare public ok: () => boolean + + /** + * Checks if the module was skipped and didn't run. + */ + declare public skipped: () => boolean + /** * Useful information about the module like duration, memory usage, etc. * If the module was not executed yet, all diagnostic values will return `0`. From cb0c7e73bc6520120dbb0e1f32eeb6d20b97e923 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 3 Dec 2024 15:26:54 +0100 Subject: [PATCH 35/73] chore: enable `includeTaskLocation` if Vitest received filters with : --- packages/vitest/src/node/cli/cac.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/vitest/src/node/cli/cac.ts b/packages/vitest/src/node/cli/cac.ts index c54e87012799..2660b8af2e3c 100644 --- a/packages/vitest/src/node/cli/cac.ts +++ b/packages/vitest/src/node/cli/cac.ts @@ -247,11 +247,14 @@ async function benchmark(cliFilters: string[], options: CliOptions): Promise filter.includes(':'))) { + argv.includeTaskLocation ??= true + } return argv } @@ -264,7 +267,7 @@ async function start(mode: VitestRunMode, cliFilters: string[], options: CliOpti try { const { startVitest } = await import('./cli-api') - const ctx = await startVitest(mode, cliFilters.map(normalize), normalizeCliOptions(options)) + const ctx = await startVitest(mode, cliFilters.map(normalize), normalizeCliOptions(cliFilters, options)) if (!ctx.shouldKeepServer()) { await ctx.exit() } @@ -302,7 +305,7 @@ async function collect(mode: VitestRunMode, cliFilters: string[], options: CliOp try { const { prepareVitest, processCollected, outputFileList } = await import('./cli-api') const ctx = await prepareVitest(mode, { - ...normalizeCliOptions(options), + ...normalizeCliOptions(cliFilters, options), watch: false, run: true, }) From e8cc0d244016efcb776eb683f40312ab292bcd76 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 3 Dec 2024 15:39:05 +0100 Subject: [PATCH 36/73] test: disbale irrelevant tests --- test/cli/test/location-filters.test.ts | 27 -------------------------- 1 file changed, 27 deletions(-) diff --git a/test/cli/test/location-filters.test.ts b/test/cli/test/location-filters.test.ts index 6bbc0fa177a4..d1738709637e 100644 --- a/test/cli/test/location-filters.test.ts +++ b/test/cli/test/location-filters.test.ts @@ -134,19 +134,6 @@ describe('location filter with list command', () => { expect(stderr).not.toContain('Error: Found "-"') }) - test('erorrs if includeTaskLocation is not enabled', async () => { - const { stdout, stderr } = await runVitestCli( - 'list', - `-r=${fixturePath}`, - '--config=no-task-location.config.ts', - `${fixturePath}/a/file/that/doesnt/exist:5`, - ) - - expect(stdout).toEqual('') - expect(stderr).toContain('Collect Error') - expect(stderr).toContain('IncludeTaskLocationDisabledError') - }) - test('fails on part of filename with location filter', async () => { const { stdout, stderr } = await runVitestCli( 'list', @@ -267,20 +254,6 @@ describe('location filter with run command', () => { expect(stderr).not.toContain('Error: Found "-"') }) - test('errors if includeTaskLocation is not enabled', async () => { - const { stderr } = await runVitestCli( - 'run', - `-r=${fixturePath}`, - `--config=no-task-location.config.ts`, - `${fixturePath}/a/file/that/doesnt/exist:5`, - ) - - expect(stderr).toMatchInlineSnapshot(` - "Error: Recieved line number filters while \`includeTaskLocation\` option is disabled - " - `) - }) - test('fails on part of filename with location filter', async () => { const { stdout, stderr } = await runVitestCli( 'run', From 4af492bb07d9348deeb0c89dba225833ce766855 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 3 Dec 2024 16:48:40 +0100 Subject: [PATCH 37/73] chore: make hooks internal --- docs/advanced/runner.md | 8 -------- packages/runner/src/types/tasks.ts | 2 ++ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/docs/advanced/runner.md b/docs/advanced/runner.md index fa58f3f983cb..ed32093d574f 100644 --- a/docs/advanced/runner.md +++ b/docs/advanced/runner.md @@ -179,14 +179,6 @@ interface Test extends TaskBase { * Whether the task should succeed if it fails. If the task fails, it will be marked as passed. */ fails?: boolean - /** - * Hooks that will run if the task fails. The order depends on the `sequence.hooks` option. - */ - onFailed?: OnTestFailedHandler[] - /** - * Hooks that will run after the task finishes. The order depends on the `sequence.hooks` option. - */ - onFinished?: OnTestFinishedHandler[] /** * Store promises (from async expects) to wait for them before finishing the test */ diff --git a/packages/runner/src/types/tasks.ts b/packages/runner/src/types/tasks.ts index db53a597d28b..692a9cc4a5e0 100644 --- a/packages/runner/src/types/tasks.ts +++ b/packages/runner/src/types/tasks.ts @@ -87,10 +87,12 @@ export interface TaskPopulated extends TaskBase { fails?: boolean /** * Hooks that will run if the task fails. The order depends on the `sequence.hooks` option. + * @internal */ onFailed?: OnTestFailedHandler[] /** * Hooks that will run after the task finishes. The order depends on the `sequence.hooks` option. + * @internal */ onFinished?: OnTestFinishedHandler[] /** From 59b08c8c3e7fe76e1ec1dcc1dc22c66d07b7c948 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 5 Dec 2024 17:48:27 +0100 Subject: [PATCH 38/73] chore: fix pnpm-lockfile --- pnpm-lock.yaml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 512816c0c95d..89d2f2ac5907 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3391,8 +3391,8 @@ packages: '@shikijs/engine-oniguruma@1.24.1': resolution: {integrity: sha512-KdrTIBIONWd+Xs61eh8HdIpfigtrseat9dpARvaOe2x0g/FNTbwbkGr3y92VSOVD1XotzEskh3v/nCzyWjkf7g==} - '@shikijs/transformers@1.22.2': - resolution: {integrity: sha512-8f78OiBa6pZDoZ53lYTmuvpFPlWtevn23bzG+azpPVvZg7ITax57o/K3TC91eYL3OMJOO0onPbgnQyZjRos8XQ==} + '@shikijs/transformers@1.24.0': + resolution: {integrity: sha512-Qf/hby+PRPkoHncjYnJf5svK1aCsOUtQhuLzKPnmeXJtuUZCmbH0pTpdNtXe9tgln/RHlyRJnv7q46HHS1sO0Q==} '@shikijs/twoslash@1.24.1': resolution: {integrity: sha512-TbXYtUREusATSCAWLw5dSwmc54Ga9wYF1gTfrOTEQJB3iFejtjA6VFZSpIGnmnQemVr4NNBTK6+4yxcFIZXD7A==} @@ -11769,11 +11769,6 @@ snapshots: '@shikijs/types': 1.24.1 '@shikijs/vscode-textmate': 9.3.0 - '@shikijs/transformers@1.22.2': - dependencies: - '@shikijs/types': 1.24.0 - '@shikijs/vscode-textmate': 9.3.0 - '@shikijs/transformers@1.24.0': dependencies: shiki: 1.24.0 From 18c53b0b8d21da4ae59bb98334417e43d4fcde95 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 09:52:52 +0100 Subject: [PATCH 39/73] chore: keep exposing getCoreWorkspaceProject --- packages/vitest/src/node/core.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 5496c4a4254e..2503a3d05d29 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -321,6 +321,11 @@ export class Vitest { return this.coreWorkspaceProject } + /** @deprecated use `getRootTestProject` instead */ + public getCoreWorkspaceProject(): TestProject { + return this.getRootTestProject() + } + /** * Return project that has the root (or "global") config. */ From f14b12919aee847851b7b7425ececd1d48f650cb Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 09:58:22 +0100 Subject: [PATCH 40/73] chore: 2.2 -> 3.0 --- docs/advanced/api/test-case.md | 2 +- docs/advanced/api/test-project.md | 4 ++-- docs/advanced/api/test-suite.md | 2 +- docs/advanced/api/vitest.md | 4 ++-- docs/guide/cli.md | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/advanced/api/test-case.md b/docs/advanced/api/test-case.md index 7453da2eaf60..f1d2c82695c9 100644 --- a/docs/advanced/api/test-case.md +++ b/docs/advanced/api/test-case.md @@ -61,7 +61,7 @@ The ID looks like this: ``` ::: tip -You can generate file hash with `generateFileHash` function from `vitest/node` which is available since Vitest 2.2: +You can generate file hash with `generateFileHash` function from `vitest/node` which is available since Vitest 3: ```ts import { generateFileHash } from 'vitest/node' diff --git a/docs/advanced/api/test-project.md b/docs/advanced/api/test-project.md index 5211827b6299..23856316d312 100644 --- a/docs/advanced/api/test-project.md +++ b/docs/advanced/api/test-project.md @@ -2,9 +2,9 @@ title: TestProject --- -# TestProject 2.2.0 {#testproject} +# TestProject 3.0.0 {#testproject} -- **Alias**: `WorkspaceProject` before 2.2.0 +- **Alias**: `WorkspaceProject` before 3.0.0 ::: warning This guide describes the advanced Node.js API. If you just want to create a workspace, follow the ["Workspace"](/guide/workspace) guide. diff --git a/docs/advanced/api/test-suite.md b/docs/advanced/api/test-suite.md index 614c197d26c7..6e33f5d8c10f 100644 --- a/docs/advanced/api/test-suite.md +++ b/docs/advanced/api/test-suite.md @@ -62,7 +62,7 @@ The ID looks like this: ``` ::: tip -You can generate file hash with `generateFileHash` function from `vitest/node` which is available since Vitest 2.2: +You can generate file hash with `generateFileHash` function from `vitest/node` which is available since Vitest 3: ```ts import { generateFileHash } from 'vitest/node' diff --git a/docs/advanced/api/vitest.md b/docs/advanced/api/vitest.md index 8dfab0843b31..892780bc7b30 100644 --- a/docs/advanced/api/vitest.md +++ b/docs/advanced/api/vitest.md @@ -164,7 +164,7 @@ This method constructs new [test specifications](/advanced/api/test-specificatio This method automatically caches all test specifications. When you call [`getModuleSpecifications`](#getmodulespecifications) next time, it will return the same specifications unless [`clearSpecificationsCache`](#clearspecificationscache) was called before that. ::: warning -As of Vitest 2.2.0, it's possible to have multiple test specifications with the same module ID (file path) if `poolMatchGlob` has several pools or if `typecheck` is enabled. This possibility will be removed in Vitest 3. +As of Vitest 3, it's possible to have multiple test specifications with the same module ID (file path) if `poolMatchGlob` has several pools or if `typecheck` is enabled. This possibility will be removed in Vitest 4. ::: ```ts @@ -190,7 +190,7 @@ Returns a list of test specifications related to the module ID. The ID should al This method can return already cached specifications based on the `moduleId` and `pool`. But note that [`project.createSpecification`](/advanced/api/test-project#createspecification) always returns a new instance and it's not cached automatically. However, specifications are automatically cached when [`runTestSpecifications`](#runtestspecifications) is called. ::: warning -As of Vitest 2.2.0, this method uses a cache to check if the file is a test. To make sure that the cache is not empty, call [`globTestSpecifications`](#globtestspecifications) at least once. +As of Vitest 3, this method uses a cache to check if the file is a test. To make sure that the cache is not empty, call [`globTestSpecifications`](#globtestspecifications) at least once. ::: ## clearSpecificationsCache diff --git a/docs/guide/cli.md b/docs/guide/cli.md index 789935c04760..062ddcd27d1c 100644 --- a/docs/guide/cli.md +++ b/docs/guide/cli.md @@ -19,7 +19,7 @@ vitest foobar Will run only the test file that contains `foobar` in their paths. This filter only checks inclusion and doesn't support regexp or glob patterns (unless your terminal processes it before Vitest receives the filter). -Since Vitest 2.2, you can also specify the test by filename and line number: +Since Vitest 3, you can also specify the test by filename and line number: ```bash $ vitest basic/foo.test.ts:10 From cd5363b7752b953c813b398a3360908b08a6658f Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 10:34:26 +0100 Subject: [PATCH 41/73] chore: add experimental tag --- packages/vitest/src/node/project.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts index e5bae926bdb2..d54b62c5168b 100644 --- a/packages/vitest/src/node/project.ts +++ b/packages/vitest/src/node/project.ts @@ -62,8 +62,11 @@ export class TestProject { */ public readonly tmpDir = join(tmpdir(), nanoid()) + /** @experimental This will be removed */ vitenode!: ViteNodeServer + /** @experimental This will be removed */ runner!: ViteNodeRunner + /** @experimental This will be removed */ typechecker?: Typechecker private closingPromise: Promise | undefined From c3e1ed5cbd1df3b150b8ab23a9aac59324c96a3f Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 10:59:51 +0100 Subject: [PATCH 42/73] feat: add .import method and hide the project.runner --- docs/advanced/api/import-example.md | 3 +++ docs/advanced/api/test-project.md | 21 +++++++++++++++++++++ docs/advanced/api/vitest.md | 21 +++++++++++++++++++++ docs/config/index.md | 6 ++++++ eslint.config.js | 2 ++ packages/browser/src/node/utils.ts | 4 ++-- packages/vitest/src/node/core.ts | 12 ++++++++++-- packages/vitest/src/node/project.ts | 18 ++++++++++++------ 8 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 docs/advanced/api/import-example.md diff --git a/docs/advanced/api/import-example.md b/docs/advanced/api/import-example.md new file mode 100644 index 000000000000..68f6258892fe --- /dev/null +++ b/docs/advanced/api/import-example.md @@ -0,0 +1,3 @@ +```ts +function import(moduleId: string): Promise +``` diff --git a/docs/advanced/api/test-project.md b/docs/advanced/api/test-project.md index 23856316d312..6d1372314acf 100644 --- a/docs/advanced/api/test-project.md +++ b/docs/advanced/api/test-project.md @@ -253,6 +253,27 @@ if (import.meta.vitest) { `) // true if `includeSource` is set ``` +## import + + + +Import a file using Vite module runner. The file will be transformed by Vite with provided project's config and executed in a separate context. Note that `moduleId` will be relative to the `config.root`. + +::: danger +`project.import` reuses Vite's module graph, so importing the same module using a regular import will return a different module: + +```ts +import * as staticExample from './example.js' +const dynamicExample = await project.import('./example.js') + +dynamicExample !== staticExample // ✅ +``` +::: + +::: info +Internally, Vitest uses this method to import global setups, custom coverage providers, workspace file, and custom reporters, meaning all of them share the same module graph as long as they belong to the same Vite server. +::: + ## onTestsRerun ```ts diff --git a/docs/advanced/api/vitest.md b/docs/advanced/api/vitest.md index 892780bc7b30..7207f772ad23 100644 --- a/docs/advanced/api/vitest.md +++ b/docs/advanced/api/vitest.md @@ -249,6 +249,27 @@ This method invalidates the file in the cache of every project. It is mostly use If you disable Vitest's watcher but keep Vitest running, it is important to manually clear the cache with this method because there is no way to disable the cache. This method will also invalidate file's importers. ::: +## import + + + +Import a file using Vite module runner. The file will be transformed by Vite with the global config and executed in a separate context. Note that `moduleId` will be relative to the `config.root`. + +::: danger +`project.import` reuses Vite's module graph, so importing the same module using a regular import will return a different module: + +```ts +import * as staticExample from './example.js' +const dynamicExample = await vitest.import('./example.js') + +dynamicExample !== staticExample // ✅ +``` +::: + +::: info +Internally, Vitest uses this method to import global setups, custom coverage providers, workspace file, and custom reporters, meaning all of them share the same module graph as long as they belong to the same Vite server. +::: + ## close ```ts diff --git a/docs/config/index.md b/docs/config/index.md index 347a174c274b..c4d6ed1ca9b5 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -144,6 +144,12 @@ Include globs for in-source test files. When defined, Vitest will run all matched files with `import.meta.vitest` inside. +### name + +- **Type:** `string` + +Assign a custom name to the test project or Vitest process. The name will be visible in the CLI and available in the Node.js API via [`project.name`](/advanced/api/test-project#name). + ### server {#server} - **Type:** `{ sourcemap?, deps?, ... }` diff --git a/eslint.config.js b/eslint.config.js index e2adb7ed01fc..2b17354694d6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -28,6 +28,8 @@ export default antfu( 'packages/browser/**/esm-client-injector.js', // contains technically invalid code to display pretty diff 'docs/guide/snapshot.md', + // uses invalid js example + 'docs/advanced/api/import-example.md', ], }, { diff --git a/packages/browser/src/node/utils.ts b/packages/browser/src/node/utils.ts index 3d3e92ad37c5..2b729ab3e1d4 100644 --- a/packages/browser/src/node/utils.ts +++ b/packages/browser/src/node/utils.ts @@ -22,9 +22,9 @@ export async function getBrowserProvider( let customProviderModule try { - customProviderModule = (await project.runner.executeId( + customProviderModule = (await project.import<{ default: BrowserProviderModule }>( options.provider, - )) as { default: BrowserProviderModule } + )) } catch (error) { throw new Error( diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 2503a3d05d29..963ea68f39b5 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -355,6 +355,14 @@ export class Vitest { return project } + /** + * Import a file using Vite module runner. The file will be transformed by Vite and executed in a separate context. + * @param moduleId The ID of the module in Vite module graph + */ + public import(moduleId: string): Promise { + return this.runner.executeId(moduleId) + } + private async resolveWorkspaceConfigPath(): Promise { if (typeof this.config.workspace === 'string') { return this.config.workspace @@ -395,9 +403,9 @@ export class Vitest { return [this._ensureRootProject()] } - const workspaceModule = await this.runner.executeFile(workspaceConfigPath) as { + const workspaceModule = await this.import<{ default: ReturnType - } + }>(workspaceConfigPath) if (!workspaceModule.default || !Array.isArray(workspaceModule.default)) { throw new TypeError(`Workspace config file "${workspaceConfigPath}" must export a default array of project paths.`) diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts index d54b62c5168b..b0fe140f6d7c 100644 --- a/packages/vitest/src/node/project.ts +++ b/packages/vitest/src/node/project.ts @@ -62,12 +62,10 @@ export class TestProject { */ public readonly tmpDir = join(tmpdir(), nanoid()) - /** @experimental This will be removed */ - vitenode!: ViteNodeServer - /** @experimental This will be removed */ - runner!: ViteNodeRunner - /** @experimental This will be removed */ - typechecker?: Typechecker + /** @internal */ vitenode!: ViteNodeServer + /** @internal */ typechecker?: Typechecker + + private runner!: ViteNodeRunner private closingPromise: Promise | undefined @@ -524,6 +522,14 @@ export class TestProject { return this.closingPromise } + /** + * Import a file using Vite module runner. + * @param moduleId The ID of the module in Vite module graph + */ + public import(moduleId: string): Promise { + return this.runner.executeId(moduleId) + } + /** @deprecated use `name` instead */ public getName(): string { return this.config.name || '' From c5c9a54d1fc6dd9f1807f68a8d7b63c5c3c0d225 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 11:02:35 +0100 Subject: [PATCH 43/73] refactor: getRootTestProject -> getRootProject --- docs/advanced/api/test-project.md | 2 +- docs/advanced/api/vitest.md | 8 ++++---- packages/coverage-v8/src/provider.ts | 2 +- packages/ui/node/reporter.ts | 2 +- packages/vitest/src/api/setup.ts | 2 +- packages/vitest/src/node/core.ts | 12 ++++++------ packages/vitest/src/node/project.ts | 4 ++-- packages/vitest/src/node/reporters/base.ts | 2 +- packages/vitest/src/node/reporters/github-actions.ts | 2 +- test/cli/test/reported-tasks.test.ts | 2 +- test/coverage-test/test/threshold-100.test.ts | 2 +- 11 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/advanced/api/test-project.md b/docs/advanced/api/test-project.md index 6d1372314acf..590637859922 100644 --- a/docs/advanced/api/test-project.md +++ b/docs/advanced/api/test-project.md @@ -198,7 +198,7 @@ Also note that `project.createSpecification` always returns a new instance. function isRootProject(): boolean ``` -Checks if the current project is the root project. You can also get the root project by calling [`vitest.getRootTestProject()`](#getroottestproject). +Checks if the current project is the root project. You can also get the root project by calling [`vitest.getRootProject()`](#getrootproject). ## globTestFiles diff --git a/docs/advanced/api/vitest.md b/docs/advanced/api/vitest.md index 7207f772ad23..e18f3f917a05 100644 --- a/docs/advanced/api/vitest.md +++ b/docs/advanced/api/vitest.md @@ -76,10 +76,10 @@ Vitest needs to be resolved with the Vite server to be properly initialized. If In normal circumstances, you would never call this method because `createVitest` and `startVitest` return already resolved Vitest instance. ::: -## getRootTestProject +## getRootProject ```ts -function getRootTestProject(): TestProject +function getRootProject(): TestProject ``` This returns the root test project. The root project generally doesn't run any tests and is not included in `vitest.projects` unless the user explicitly includes the root config in their workspace. @@ -99,7 +99,7 @@ function provide( ): void ``` -Vitest exposes `provide` method which is a shorthand for `vitest.getRootTestProject().provide`. With this method you can pass down values from the main thread to tests. All values are checked with `structuredClone` before they are stored, but the values themselves are not cloned. +Vitest exposes `provide` method which is a shorthand for `vitest.getRootProject().provide`. With this method you can pass down values from the main thread to tests. All values are checked with `structuredClone` before they are stored, but the values themselves are not cloned. To recieve the values in the test, you need to import `inject` method from `vitest` entrypont: @@ -135,7 +135,7 @@ Technically, `provide` is a method of [`TestProject`](/advanced/api/test-project function getProvidedContext(): ProvidedContext ``` -This returns the root context object. This is a shorthand for `vitest.getRootTestProject().getProvidedContext`. +This returns the root context object. This is a shorthand for `vitest.getRootProject().getProvidedContext`. ## getProjectByName diff --git a/packages/coverage-v8/src/provider.ts b/packages/coverage-v8/src/provider.ts index f5942989a7d4..f65f0533fdb5 100644 --- a/packages/coverage-v8/src/provider.ts +++ b/packages/coverage-v8/src/provider.ts @@ -288,7 +288,7 @@ export class V8CoverageProvider extends BaseCoverageProvider { let fetchCache = project.vitenode.fetchCache diff --git a/packages/ui/node/reporter.ts b/packages/ui/node/reporter.ts index 696c13e7e0b4..1e53756ad9a5 100644 --- a/packages/ui/node/reporter.ts +++ b/packages/ui/node/reporter.ts @@ -63,7 +63,7 @@ export default class HTMLReporter implements Reporter { const result: HTMLReportData = { paths: this.ctx.state.getPaths(), files: this.ctx.state.getFiles(), - config: this.ctx.getRootTestProject().serializedConfig, + config: this.ctx.getRootProject().serializedConfig, unhandledErrors: this.ctx.state.getUnhandledErrors(), moduleGraph: {}, sources: {}, diff --git a/packages/vitest/src/api/setup.ts b/packages/vitest/src/api/setup.ts index 531dcf1bff8b..879d662b60ad 100644 --- a/packages/vitest/src/api/setup.ts +++ b/packages/vitest/src/api/setup.ts @@ -79,7 +79,7 @@ export function setup(ctx: Vitest, _server?: ViteDevServer) { await ctx.rerunTask(id) }, getConfig() { - return ctx.getRootTestProject().serializedConfig + return ctx.getRootProject().serializedConfig }, async getTransformResult(projectName: string, id, browser = false) { const project = ctx.getProjectByName(projectName) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 963ea68f39b5..dd7cd290b78f 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -302,14 +302,14 @@ export class Vitest { * Provide a value to the test context. This value will be available to all tests with `inject`. */ public provide = (key: T, value: ProvidedContext[T]) => { - this.getRootTestProject().provide(key, value) + this.getRootProject().provide(key, value) } /** * Get global provided context. */ public getProvidedContext(): ProvidedContext { - return this.getRootTestProject().getProvidedContext() + return this.getRootProject().getProvidedContext() } /** @internal */ @@ -321,15 +321,15 @@ export class Vitest { return this.coreWorkspaceProject } - /** @deprecated use `getRootTestProject` instead */ + /** @deprecated use `getRootProject` instead */ public getCoreWorkspaceProject(): TestProject { - return this.getRootTestProject() + return this.getRootProject() } /** * Return project that has the root (or "global") config. */ - public getRootTestProject(): TestProject { + public getRootProject(): TestProject { if (!this.coreWorkspaceProject) { throw new Error(`Root project is not initialized. This means that the Vite server was not established yet and the the workspace config is not resolved.`) } @@ -810,7 +810,7 @@ export class Vitest { private async initializeGlobalSetup(paths: TestSpecification[]): Promise { const projects = new Set(paths.map(spec => spec.project)) - const coreProject = this.getRootTestProject() + const coreProject = this.getRootProject() if (!projects.has(coreProject)) { projects.add(coreProject) } diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts index b0fe140f6d7c..0520ca80cbf2 100644 --- a/packages/vitest/src/node/project.ts +++ b/packages/vitest/src/node/project.ts @@ -123,7 +123,7 @@ export class TestProject { // globalSetup can run even if core workspace is not part of the test run // so we need to inherit its provided context return { - ...this.vitest.getRootTestProject().getProvidedContext(), + ...this.vitest.getRootProject().getProvidedContext(), ...this._provided, } } @@ -196,7 +196,7 @@ export class TestProject { * Check if this is the root project. The root project is the one that has the root config. */ public isRootProject(): boolean { - return this.vitest.getRootTestProject() === this + return this.vitest.getRootProject() === this } /** @deprecated use `isRootProject` instead */ diff --git a/packages/vitest/src/node/reporters/base.ts b/packages/vitest/src/node/reporters/base.ts index ed9bbadc43ed..e4c9d42abb2d 100644 --- a/packages/vitest/src/node/reporters/base.ts +++ b/packages/vitest/src/node/reporters/base.ts @@ -270,7 +270,7 @@ export abstract class BaseReporter implements Reporter { const project = task ? this.ctx.getProjectByName(task.file.projectName || '') - : this.ctx.getRootTestProject() + : this.ctx.getRootProject() const stack = log.browser ? (project.browser?.parseStacktrace(log.origin) || []) diff --git a/packages/vitest/src/node/reporters/github-actions.ts b/packages/vitest/src/node/reporters/github-actions.ts index 44c4d63ba228..cfa35f50a4c0 100644 --- a/packages/vitest/src/node/reporters/github-actions.ts +++ b/packages/vitest/src/node/reporters/github-actions.ts @@ -23,7 +23,7 @@ export class GithubActionsReporter implements Reporter { }>() for (const error of errors) { projectErrors.push({ - project: this.ctx.getRootTestProject(), + project: this.ctx.getRootProject(), title: 'Unhandled error', error, }) diff --git a/test/cli/test/reported-tasks.test.ts b/test/cli/test/reported-tasks.test.ts index b0d15b0e2c2d..2a9b58aa7733 100644 --- a/test/cli/test/reported-tasks.test.ts +++ b/test/cli/test/reported-tasks.test.ts @@ -35,7 +35,7 @@ beforeAll(async () => { logHeapUsage: true, }) state = ctx!.state - project = ctx!.getRootTestProject() + project = ctx!.getRootProject() files = state.getFiles() expect(files).toHaveLength(1) testModule = state.getReportedEntity(files[0])! as TestModule diff --git a/test/coverage-test/test/threshold-100.test.ts b/test/coverage-test/test/threshold-100.test.ts index 9edf7ae7efaa..5d8145cc8c49 100644 --- a/test/coverage-test/test/threshold-100.test.ts +++ b/test/coverage-test/test/threshold-100.test.ts @@ -20,7 +20,7 @@ test('{ threshold: { 100: true }}', async () => { 'verbose', { onInit(ctx) { - ctx.getRootTestProject().provide('coverage', { + ctx.getRootProject().provide('coverage', { provider: ctx.config.coverage.provider, thresholds: (ctx.config.coverage as any).thresholds, }) From 0acc20b20d3103b56e7e527a24d64f79e1067723 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 11:05:31 +0100 Subject: [PATCH 44/73] feat: change source to () => source --- docs/advanced/api/test-project.md | 7 +++++-- packages/vitest/src/node/project.ts | 6 +++--- packages/vitest/src/node/watcher.ts | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/advanced/api/test-project.md b/docs/advanced/api/test-project.md index 590637859922..873204ac4344 100644 --- a/docs/advanced/api/test-project.md +++ b/docs/advanced/api/test-project.md @@ -230,7 +230,10 @@ This method looks at several config options: ## matchesTestGlob ```ts -function matchesTestGlob(moduleId: string, source?: string): boolean +function matchesTestGlob( + moduleId: string, + source?: () => string +): boolean ``` This method checks if the file is a regular test file. It uses the same config properties that `globTestFiles` uses for validation. @@ -246,7 +249,7 @@ const project = vitest.projects[0] project.matchesTestGlob(resolve('./basic.test.ts')) // true project.matchesTestGlob(resolve('./basic.ts')) // false -project.matchesTestGlob(resolve('./basic.ts'), ` +project.matchesTestGlob(resolve('./basic.ts'), () => ` if (import.meta.vitest) { // ... } diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts index 0520ca80cbf2..e236f6d22e94 100644 --- a/packages/vitest/src/node/project.ts +++ b/packages/vitest/src/node/project.ts @@ -418,7 +418,7 @@ export class TestProject { /** * Test if a file matches the test globs. This does the actual glob matching unlike `isTestFile`. */ - public matchesTestGlob(moduleId: string, source?: string): boolean { + public matchesTestGlob(moduleId: string, source?: () => string): boolean { const relativeId = relative(this.config.dir || this.config.root, moduleId) if (mm.isMatch(relativeId, this.config.exclude)) { return false @@ -430,7 +430,7 @@ export class TestProject { this.config.includeSource?.length && mm.isMatch(relativeId, this.config.includeSource) ) { - const code = source || readFileSync(moduleId, 'utf-8') + const code = source?.() || readFileSync(moduleId, 'utf-8') return this.isInSourceTestCode(code) } return false @@ -438,7 +438,7 @@ export class TestProject { /** @deprecated use `matchesTestGlob` instead */ async isTargetFile(id: string, source?: string): Promise { - return this.matchesTestGlob(id, source) + return this.matchesTestGlob(id, source ? () => source : undefined) } private isInSourceTestCode(code: string): boolean { diff --git a/packages/vitest/src/node/watcher.ts b/packages/vitest/src/node/watcher.ts index dd35bb86ff23..79c40fc00896 100644 --- a/packages/vitest/src/node/watcher.ts +++ b/packages/vitest/src/node/watcher.ts @@ -81,11 +81,11 @@ export class VitestWatcher { private onAdd = (id: string): void => { id = slash(id) this.vitest.invalidateFile(id) - const fileContent = readFileSync(id, 'utf-8') + let _fileContent: string | undefined const matchingProjects: TestProject[] = [] this.vitest.projects.forEach((project) => { - if (project.matchesTestGlob(id, fileContent)) { + if (project.matchesTestGlob(id, () => (_fileContent = readFileSync(id, 'utf-8')))) { matchingProjects.push(project) project._markTestFile(id) } From c6e2833976f63fe193c2d41c0435ca5d201dad8b Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 11:14:01 +0100 Subject: [PATCH 45/73] docs: mention that the new task API is not used in the current API --- docs/advanced/api/test-case.md | 25 +++++++++++++++++++++++++ docs/advanced/api/test-module.md | 23 +++++++++++++++++++++++ docs/advanced/api/test-suite.md | 25 +++++++++++++++++++++++++ 3 files changed, 73 insertions(+) diff --git a/docs/advanced/api/test-case.md b/docs/advanced/api/test-case.md index f1d2c82695c9..3f5046685f89 100644 --- a/docs/advanced/api/test-case.md +++ b/docs/advanced/api/test-case.md @@ -10,6 +10,31 @@ if (task.type === 'test') { } ``` +::: warning +We are planning to introduce a new Reporter API that will be using this API by default. For now, the Reporter API uses [runner tasks](/advanced/runner#tasks), but you can still access `TestCase` via `vitest.state.getReportedEntity` method: + +```ts +import type { RunnerTestFile, TestModule, Vitest } from 'vitest/node' + +class Reporter { + private vitest!: Vitest + + onInit(vitest: Vitest) { + this.vitest = vitest + } + + onFinished(files: RunnerTestFile[]) { + for (const file of files) { + const testModule = this.vitest.getReportedEntity(file) as TestModule + for (const test of testModule.children.allTests()) { + console.log(test) // TestCase + } + } + } +} +``` +::: + ## project This references the [`TestProject`](/advanced/api/test-project) that the test belongs to. diff --git a/docs/advanced/api/test-module.md b/docs/advanced/api/test-module.md index bacddef2d092..d3f69f0c4bb7 100644 --- a/docs/advanced/api/test-module.md +++ b/docs/advanced/api/test-module.md @@ -12,6 +12,29 @@ if (task.type === 'module') { The `TestModule` inherits all methods and properties from the [`TestSuite`](/advanced/api/test-module). This guide will only list methods and properties unique to the `TestModule` +::: warning +We are planning to introduce a new Reporter API that will be using this API by default. For now, the Reporter API uses [runner tasks](/advanced/runner#tasks), but you can still access `TestModule` via `vitest.state.getReportedEntity` method: + +```ts +import type { RunnerTestFile, TestModule, Vitest } from 'vitest/node' + +class Reporter { + private vitest!: Vitest + + onInit(vitest: Vitest) { + this.vitest = vitest + } + + onFinished(files: RunnerTestFile[]) { + for (const file of files) { + const testModule = this.vitest.getReportedEntity(file) as TestModule + console.log(testModule) // TestModule + } + } +} +``` +::: + ## moduleId This is usually an absolute unix file path (even on Windows). It can be a virtual id if the file is not on the disk. This value corresponds to Vite's `ModuleGraph` id. diff --git a/docs/advanced/api/test-suite.md b/docs/advanced/api/test-suite.md index 6e33f5d8c10f..0a7ddb4d13e4 100644 --- a/docs/advanced/api/test-suite.md +++ b/docs/advanced/api/test-suite.md @@ -10,6 +10,31 @@ if (task.type === 'suite') { } ``` +::: warning +We are planning to introduce a new Reporter API that will be using this API by default. For now, the Reporter API uses [runner tasks](/advanced/runner#tasks), but you can still access `TestSuite` via `vitest.state.getReportedEntity` method: + +```ts +import type { RunnerTestFile, TestModule, Vitest } from 'vitest/node' + +class Reporter { + private vitest!: Vitest + + onInit(vitest: Vitest) { + this.vitest = vitest + } + + onFinished(files: RunnerTestFile[]) { + for (const file of files) { + const testModule = this.vitest.getReportedEntity(file) as TestModule + for (const suite of testModule.children.allSuites()) { + console.log(suite) // TestSuite + } + } + } +} +``` +::: + ## project This references the [`TestProject`](/advanced/api/test-project) that the test belongs to. From 54ba3e380ae537ed3f52bf53d78688c8b14e30e4 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 15:11:53 +0100 Subject: [PATCH 46/73] docs: add test spec docs --- docs/advanced/api/test-project.md | 10 ++-- docs/advanced/api/test-specification.md | 70 ++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/docs/advanced/api/test-project.md b/docs/advanced/api/test-project.md index 873204ac4344..69169491fbbc 100644 --- a/docs/advanced/api/test-project.md +++ b/docs/advanced/api/test-project.md @@ -166,11 +166,10 @@ Project context values will always override root project's context. function createSpecification( moduleId: string, locations?: number[], - pool?: string, ): TestSpecification ``` -Create a [test specification](/advanced/api/test-specification) that can be used in [`vitest.runTestSpecifications`](/advanced/api/vitest#runtestspecifications). Specification scopes the test file to a specific `project`, `locations` (optional) and `pool` (optional). +Create a [test specification](/advanced/api/test-specification) that can be used in [`vitest.runTestSpecifications`](/advanced/api/vitest#runtestspecifications). Specification scopes the test file to a specific `project` and test `locations` (optional). Test [locations](/advanced/api/test-case#location) are code lines where the test is defined in the source code. If locations are provided, Vitest will only run tests defined on those lines. Note that if [`testNamePattern`](/config/#testnamepattern) is defined, then it will also be applied. ```ts import { createVitest } from 'vitest/node' @@ -179,15 +178,14 @@ import { resolve } from 'node:path/posix' const vitest = await createVitest('test') const project = vitest.projects[0] const specification = project.createSpecification( - resolve('./basic.test.ts'), + resolve('./example.test.ts'), [20, 40], // optional test lines - 'threads', // optional override ) -await vitest.runFiles([specification], true) +await vitest.runTestSpecifications([specification]) ``` ::: warning -`createSpecification` expects an absolute file path. It doesn't resolve the file or check that it exists on the file system. +`createSpecification` expects resolved [module ID](/advanced/api/test-specification#moduleid). It doesn't auto-resolve the file or check that it exists on the file system. Also note that `project.createSpecification` always returns a new instance. ::: diff --git a/docs/advanced/api/test-specification.md b/docs/advanced/api/test-specification.md index 112b3c678c5c..46dd0d4a1755 100644 --- a/docs/advanced/api/test-specification.md +++ b/docs/advanced/api/test-specification.md @@ -1,3 +1,71 @@ # TestSpecification - +The `TestSpecification` class describes what module to run as a test and its parameters. + +You can only create a specification by calling [`createSpecification`](/advanced/api/test-project#createspecification) method on a test project: + +```ts +const specification = project.createSpecification( + resolve('./example.test.ts'), + [20, 40], // optional test lines +) +``` + +`createSpecification` expects resolved module ID. It doesn't auto-resolve the file or check that it exists on the file system. + +## project + +This references the [`TestProject`](/advanced/api/test-project) that the test module belongs to. + +## moduleId + +The ID of the module in Vite's module graph. Usually, it's an absolute file path using posix separator: + +```ts +'C:/Users/Documents/project/example.test.ts' // ✅ +'/Users/mac/project/example.test.ts' // ✅ +'C:\\Users\\Documents\\project\\example.test.ts' // ❌ +``` + +## pool experimental {#pool} + +The [`pool`](/config/#pool) in which the test module will run. + +::: danger +It's possible to have multiple pools in a single test project with [`poolMatchGlob`](/config/#poolmatchglob) and [`typecheck.enabled`](/config/#typecheck-enabled). This means it's possible to have several specifications with the same `moduleId` but different `pool`. In Vitest 4, the project will only support a single pool, and this property will be removed. +::: + +## testLines + +This is an array of lines in the source code where the test files are defined. This field is defined only if the `createSpecification` method received an array. + +Note that if there is no test on at least one of the lines, the whole suite will fail. An example of a correct `testLines` configuration: + +::: code-group +```ts [script.js] +const specification = project.createSpecification( + resolve('./example.test.ts'), + [3, 8, 9], +) +``` +```ts:line-numbers{3,8,9} [example.test.js] +import { test, describe } from 'vitest' + +test('verification works') + +describe('a group of tests', () => { // [!code error] + // ... + + test('nested test') + test.skip('skipped test') +}) +``` +::: + +## toJSON + +```ts +function toJSON(): SerializedTestSpecification +``` + +`toJSON` generates a JSON-friendly object that can be consumed by the [Browser Mode](/guide/browser) or [Vitest UI](/guide/ui). From e111ab125d552e093451f660fc83a85c6e991938 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 15:12:03 +0100 Subject: [PATCH 47/73] docs: cleanup --- docs/guide/migration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/migration.md b/docs/guide/migration.md index ea1398229a4e..fc63df1e4ef3 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -29,7 +29,7 @@ test('validation works', () => { }, 1000) // Ok ✅ ``` -### `Custom` Type is Deprecated experimental API {#custom-type-is-deprecated} +### `Custom` Type is Deprecated experimental {#custom-type-is-deprecated} The `Custom` type is now equal to the `Test` type. Note that Vitest updated the public types in 2.1 and changed exported names to `RunnerCustomCase` and `RunnerTestCase`: From bd32b215d3b3f3665cbe329780ebf142425212cf Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 15:12:45 +0100 Subject: [PATCH 48/73] refactor: rename locations to testLines --- packages/vitest/src/node/project.ts | 1 + packages/vitest/src/node/spec.ts | 12 ++++++------ packages/vitest/src/node/specifications.ts | 12 ++++++------ packages/vitest/src/runtime/types/utils.ts | 2 +- packages/vitest/src/utils/test-helpers.ts | 4 ++-- 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts index e236f6d22e94..098451d189f9 100644 --- a/packages/vitest/src/node/project.ts +++ b/packages/vitest/src/node/project.ts @@ -135,6 +135,7 @@ export class TestProject { public createSpecification( moduleId: string, locations?: number[] | undefined, + /** @internal */ pool?: string, ): TestSpecification { return new TestSpecification( diff --git a/packages/vitest/src/node/spec.ts b/packages/vitest/src/node/spec.ts index 27872d6a0f6e..8ae14ec121dc 100644 --- a/packages/vitest/src/node/spec.ts +++ b/packages/vitest/src/node/spec.ts @@ -26,19 +26,19 @@ export class TestSpecification { public readonly moduleId: string /** * The current test pool. It's possible to have multiple pools in a single test project with `poolMatchGlob` and `typecheck.enabled`. - * @experimental In Vitest 3, the project will only support a single pool + * @experimental In Vitest 4, the project will only support a single pool and this property will be removed. */ public readonly pool: Pool /** - * Line numbers of the test locations in the module to run. + * Line numbers of the test locations to run. */ - public readonly locations: number[] | undefined + public readonly testLines: number[] | undefined constructor( project: TestProject, moduleId: string, pool: Pool, - locations?: number[] | undefined, + testLines?: number[] | undefined, ) { this[0] = project this[1] = moduleId @@ -46,7 +46,7 @@ export class TestSpecification { this.project = project this.moduleId = moduleId this.pool = pool - this.locations = locations + this.testLines = testLines } toJSON(): SerializedTestSpecification { @@ -56,7 +56,7 @@ export class TestSpecification { root: this.project.config.root, }, this.moduleId, - { pool: this.pool, locations: this.locations }, + { pool: this.pool, testLines: this.testLines }, ] } diff --git a/packages/vitest/src/node/specifications.ts b/packages/vitest/src/node/specifications.ts index 9ea5ee79f162..e6b03db7ec2a 100644 --- a/packages/vitest/src/node/specifications.ts +++ b/packages/vitest/src/node/specifications.ts @@ -51,7 +51,7 @@ export class VitestSpecifications { throw new IncludeTaskLocationDisabledError() } - const testLocations = groupFilters(parsedFilters.map( + const testLines = groupFilters(parsedFilters.map( f => ({ ...f, filename: resolve(dir, f.filename) }), )) @@ -64,24 +64,24 @@ export class VitestSpecifications { ) testFiles.forEach((file) => { - const loc = testLocations[file] + const lines = testLines[file] testLocHasMatch[file] = true - const spec = project.createSpecification(file, loc) + const spec = project.createSpecification(file, lines) this.ensureSpecificationCached(spec) files.push(spec) }) typecheckTestFiles.forEach((file) => { - const loc = testLocations[file] + const lines = testLines[file] testLocHasMatch[file] = true - const spec = project.createSpecification(file, loc, 'typescript') + const spec = project.createSpecification(file, lines, 'typescript') this.ensureSpecificationCached(spec) files.push(spec) }) })) - Object.entries(testLocations).forEach(([filepath, loc]) => { + Object.entries(testLines).forEach(([filepath, loc]) => { if (loc.length !== 0 && !testLocHasMatch[filepath]) { throw new LocationFilterFileNotFoundError( relative(dir, filepath), diff --git a/packages/vitest/src/runtime/types/utils.ts b/packages/vitest/src/runtime/types/utils.ts index 2dcccbbbadb1..1f47382133a7 100644 --- a/packages/vitest/src/runtime/types/utils.ts +++ b/packages/vitest/src/runtime/types/utils.ts @@ -1,5 +1,5 @@ export type SerializedTestSpecification = [ project: { name: string | undefined; root: string }, file: string, - options: { pool: string; locations?: number[] | undefined }, + options: { pool: string; testLines?: number[] | undefined }, ] diff --git a/packages/vitest/src/utils/test-helpers.ts b/packages/vitest/src/utils/test-helpers.ts index 7db3549e0f72..750fe32181f5 100644 --- a/packages/vitest/src/utils/test-helpers.ts +++ b/packages/vitest/src/utils/test-helpers.ts @@ -30,7 +30,7 @@ export async function groupFilesByEnv( files: Array, ) { const filesWithEnv = await Promise.all( - files.map(async ({ moduleId: filepath, project, locations }) => { + files.map(async ({ moduleId: filepath, project, testLines }) => { const code = await fs.readFile(filepath, 'utf-8') // 1. Check for control comments in the file @@ -71,7 +71,7 @@ export async function groupFilesByEnv( return { file: { filepath, - testLocations: locations, + testLocations: testLines, }, project, environment, From 253140c6a94d81f8d3bb90bc650c2fbc6cd32efe Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 15:12:56 +0100 Subject: [PATCH 49/73] fix: export TestSpecification only as a type --- packages/vitest/src/public/node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vitest/src/public/node.ts b/packages/vitest/src/public/node.ts index 53542058a61d..34f8f859da30 100644 --- a/packages/vitest/src/public/node.ts +++ b/packages/vitest/src/public/node.ts @@ -46,7 +46,7 @@ export type { TestSequencer, TestSequencerConstructor, } from '../node/sequencers/types' -export { TestSpecification } from '../node/spec' +export type { TestSpecification } from '../node/spec' export { registerConsoleShortcuts } from '../node/stdin' export type { BenchmarkUserOptions } from '../node/types/benchmark' From 094e9a65c9a25c423ce41c00cf6a81fb2ffb3a3b Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 15:38:03 +0100 Subject: [PATCH 50/73] docs: update runner API docs --- docs/advanced/api/test-specification.md | 2 +- docs/advanced/runner.md | 82 ++++++++++++++++++------- docs/guide/cli-generated.md | 7 +++ 3 files changed, 68 insertions(+), 23 deletions(-) diff --git a/docs/advanced/api/test-specification.md b/docs/advanced/api/test-specification.md index 46dd0d4a1755..3fefba0c8954 100644 --- a/docs/advanced/api/test-specification.md +++ b/docs/advanced/api/test-specification.md @@ -68,4 +68,4 @@ describe('a group of tests', () => { // [!code error] function toJSON(): SerializedTestSpecification ``` -`toJSON` generates a JSON-friendly object that can be consumed by the [Browser Mode](/guide/browser) or [Vitest UI](/guide/ui). +`toJSON` generates a JSON-friendly object that can be consumed by the [Browser Mode](/guide/browser/) or [Vitest UI](/guide/ui). diff --git a/docs/advanced/runner.md b/docs/advanced/runner.md index ed32093d574f..0e4e2be53b38 100644 --- a/docs/advanced/runner.md +++ b/docs/advanced/runner.md @@ -64,7 +64,7 @@ export interface VitestRunner { /** * Called, when a task is updated. The same as "onTaskUpdate" in a reporter, but this is running in the same thread as tests. */ - onTaskUpdate?: (task: [string, TaskResult | undefined][]) => Promise + onTaskUpdate?: (task: [string, TaskResult | undefined, TaskMeta | undefined][]) => Promise /** * Called before running all tests in collected paths. @@ -77,29 +77,65 @@ export interface VitestRunner { /** * Called when new context for a test is defined. Useful, if you want to add custom properties to the context. * If you only want to define custom context with a runner, consider using "beforeAll" in "setupFiles" instead. - * - * This method is called for both "test" and "custom" handlers. - * - * @see https://vitest.dev/advanced/runner.html#your-task-function */ - extendTaskContext?: (context: TaskContext) => TaskContext + extendTaskContext?: (context: TestContext) => TestContext /** - * Called, when certain files are imported. Can be called in two situations: when collecting tests and when importing setup files. + * Called when certain files are imported. Can be called in two situations: to collect tests and to import setup files. */ importFile: (filepath: string, source: VitestRunnerImportSource) => unknown + /** + * Function that is called when the runner attempts to get the value when `test.extend` is used with `{ injected: true }` + */ + injectValue?: (key: string) => unknown /** * Publicly available configuration. */ config: VitestRunnerConfig + /** + * The name of the current pool. Can affect how stack trace is inferred on the server side. + */ + pool?: string } ``` -When initiating this class, Vitest passes down Vitest config, - you should expose it as a `config` property. +When initiating this class, Vitest passes down Vitest config, - you should expose it as a `config` property: + +```ts [runner.ts] +import type { RunnerTestFile } from 'vitest' +import type { VitestRunner, VitestRunnerConfig } from 'vitest/suite' +import { VitestTestRunner } from 'vitest/runners' + +class CustomRunner extends VitestTestRunner implements VitestRunner { + public config: VitestRunnerConfig + + constructor(config: VitestRunnerConfig) { + this.config = config + } + + onAfterRunFiles(files: RunnerTestFile[]) { + console.log('finished running', files) + } +} + +export default CustomRunner +``` ::: warning Vitest also injects an instance of `ViteNodeRunner` as `__vitest_executor` property. You can use it to process files in `importFile` method (this is default behavior of `TestRunner` and `BenchmarkRunner`). -`ViteNodeRunner` exposes `executeId` method, which is used to import test files in a Vite-friendly environment. Meaning, it will resolve imports and transform file content at runtime so that Node can understand it. +`ViteNodeRunner` exposes `executeId` method, which is used to import test files in a Vite-friendly environment. Meaning, it will resolve imports and transform file content at runtime so that Node can understand it: + +```ts +export default class Runner { + async importFile(filepath: string) { + await this.__vitest_executor.executeId(filepath) + } +} +``` +::: + +::: warning +If you don't have a custom runner or didn't define `runTest` method, Vitest will try to retrieve a task automatically. If you didn't add a function with `setFn`, it will fail. ::: ::: tip @@ -108,6 +144,12 @@ Snapshot support and some other features depend on the runner. If you don't want ## Tasks +::: warning +The "Runner Tasks API" is experimental and should primarily be used only in the test runtime. Vitest also exposes the ["Reported Tasks API"](/advanced/api/test-module), which should be preferred when working in the main thread (inside the reporter, for example). + +The team is currently discussing if "Runner Tasks" should be replaced by "Reported Tasks" in the future. +::: + Suites and tests are called `tasks` internally. Vitest runner initiates a `File` task before collecting any tests - this is a superset of `Suite` with a few additional properties. It is available on every task (including `File`) as a `file` property. ```ts @@ -134,11 +176,6 @@ interface File extends Suite { * The time it took to import the setup file. */ setupDuration?: number - /** - * Whether the file is initiated without running any tests. - * This is done to populate state on the server side by Vitest. - */ - local?: boolean } ``` @@ -166,7 +203,7 @@ interface Test extends TaskBase { /** * Test context that will be passed to the test function. */ - context: TaskContext & ExtraContext & TestContext + context: TestContext & ExtraContext /** * File task. It's the root task of the file. */ @@ -234,12 +271,12 @@ export interface TaskResult { ## Your Task Function -Vitest exposes a `Custom` task type that allows users to reuse built-int reporters. It is virtually the same as `Test`, but has a type of `'custom'`. +Vitest exposes `createTaskCollector` utility to create your own `test` method. It behaves the same way as a test, but calls a custom method during collection. A task is an object that is part of a suite. It is automatically added to the current suite with a `suite.task` method: ```js [custom.js] -import { createTaskCollector, getCurrentSuite, setFn } from 'vitest/suite' +import { createTaskCollector, getCurrentSuite } from 'vitest/suite' export { afterAll, beforeAll, describe } from 'vitest' @@ -262,7 +299,12 @@ export const myCustomTask = createTaskCollector( ``` ```js [tasks.test.js] -import { afterAll, beforeAll, describe, myCustomTask } from './custom.js' +import { + afterAll, + beforeAll, + describe, + myCustomTask +} from './custom.js' import { gardener } from './gardener.js' describe('take care of the garden', () => { @@ -289,7 +331,3 @@ describe('take care of the garden', () => { ```bash vitest ./garden/tasks.test.js ``` - -::: warning -If you don't have a custom runner or didn't define `runTest` method, Vitest will try to retrieve a task automatically. If you didn't add a function with `setFn`, it will fail. -::: diff --git a/docs/guide/cli-generated.md b/docs/guide/cli-generated.md index 350a178d8bf4..9acbdfb0421d 100644 --- a/docs/guide/cli-generated.md +++ b/docs/guide/cli-generated.md @@ -887,6 +887,13 @@ Poll timeout in milliseconds for `expect.poll()` assertions (default: `1000`) Always print console stack traces +### includeTaskLocation + +- **CLI:** `--includeTaskLocation` +- **Config:** [includeTaskLocation](/config/#includetasklocation) + +Collect test and suite locations in the `location` property + ### run - **CLI:** `--run` From da7e9996b28690683c2f9c302714dd39fe258d97 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 15:46:18 +0100 Subject: [PATCH 51/73] refactor: deprecate ExtendedContext --- packages/runner/src/context.ts | 13 +++---- packages/runner/src/run.ts | 4 +- packages/runner/src/types/runner.ts | 11 ++---- packages/runner/src/types/tasks.ts | 42 +++++++++++---------- packages/vitest/src/node/core.ts | 5 ++- packages/vitest/src/node/project.ts | 4 +- packages/vitest/src/public/index.ts | 3 -- packages/vitest/src/public/node.ts | 11 +++--- packages/vitest/src/public/suite.ts | 1 + packages/vitest/src/runtime/runners/test.ts | 10 ++--- packages/vitest/src/types/general.ts | 4 -- packages/vitest/src/types/global.ts | 3 +- 12 files changed, 49 insertions(+), 62 deletions(-) diff --git a/packages/runner/src/context.ts b/packages/runner/src/context.ts index a4fe8436d42f..d0145e99a5e0 100644 --- a/packages/runner/src/context.ts +++ b/packages/runner/src/context.ts @@ -1,11 +1,10 @@ import type { Awaitable } from '@vitest/utils' import type { VitestRunner } from './types/runner' import type { - ExtendedContext, RuntimeContext, SuiteCollector, - TaskContext, Test, + TestContext, } from './types/tasks' import { getSafeTimers } from '@vitest/utils' import { PendingError } from './errors' @@ -58,13 +57,13 @@ export function withTimeout any>( }) as T } -export function createTestContext( - test: T, +export function createTestContext( + test: Test, runner: VitestRunner, -): ExtendedContext { +): TestContext { const context = function () { throw new Error('done() callback is deprecated, use promise instead') - } as unknown as TaskContext + } as unknown as TestContext context.task = test @@ -87,7 +86,7 @@ export function createTestContext( ) } - return (runner.extendTaskContext?.(context) as ExtendedContext) || context + return runner.extendTaskContext?.(context) || context } function makeTimeoutMsg(isHook: boolean, timeout: number) { diff --git a/packages/runner/src/run.ts b/packages/runner/src/run.ts index d191091fd48e..bb3fa918ee8f 100644 --- a/packages/runner/src/run.ts +++ b/packages/runner/src/run.ts @@ -2,7 +2,6 @@ import type { Awaitable } from '@vitest/utils' import type { DiffOptions } from '@vitest/utils/diff' import type { FileSpecification, VitestRunner } from './types/runner' import type { - ExtendedContext, File, HookCleanupCallback, HookListener, @@ -15,6 +14,7 @@ import type { TaskResultPack, TaskState, Test, + TestContext, } from './types/tasks' import { getSafeTimers, shuffle } from '@vitest/utils' import { processError } from '@vitest/utils/error' @@ -64,7 +64,7 @@ function getSuiteHooks( async function callTestHooks( runner: VitestRunner, test: Test, - hooks: ((context: ExtendedContext) => Awaitable)[], + hooks: ((context: TestContext) => Awaitable)[], sequence: SequenceHooks, ) { if (sequence === 'stack') { diff --git a/packages/runner/src/types/runner.ts b/packages/runner/src/types/runner.ts index 7d3011406380..4a52a7139a1c 100644 --- a/packages/runner/src/types/runner.ts +++ b/packages/runner/src/types/runner.ts @@ -1,14 +1,13 @@ import type { DiffOptions } from '@vitest/utils/diff' import type { - ExtendedContext, File, SequenceHooks, SequenceSetupFiles, Suite, Task, - TaskContext, TaskResultPack, Test, + TestContext, } from './tasks' /** @@ -143,13 +142,9 @@ export interface VitestRunner { * Called when new context for a test is defined. Useful if you want to add custom properties to the context. * If you only want to define custom context, consider using "beforeAll" in "setupFiles" instead. * - * This method is called for both "test" and "custom" handlers. - * - * @see https://vitest.dev/advanced/runner.html#your-task-function + * @see https://vitest.dev/advanced/runner#your-task-function */ - extendTaskContext?: ( - context: TaskContext - ) => ExtendedContext + extendTaskContext?: (context: TestContext) => TestContext /** * Called when test and setup files are imported. Can be called in two situations: when collecting tests and when importing setup files. */ diff --git a/packages/runner/src/types/tasks.ts b/packages/runner/src/types/tasks.ts index 692a9cc4a5e0..1c593cb744ae 100644 --- a/packages/runner/src/types/tasks.ts +++ b/packages/runner/src/types/tasks.ts @@ -119,7 +119,7 @@ export interface TaskResult { state: TaskState /** * Errors that occurred during the task execution. It is possible to have several errors - * if `expect.soft()` failed multiple times. + * if `expect.soft()` failed multiple times or `retry` was triggered. */ errors?: ErrorWithDiff[] /** @@ -210,6 +210,7 @@ export interface File extends Suite { /** * Whether the file is initiated without running any tests. * This is done to populate state on the server side by Vitest. + * @internal */ local?: boolean } @@ -219,7 +220,7 @@ export interface Test extends TaskPopulated { /** * Test context that will be passed to the test function. */ - context: TaskContext & ExtraContext & TestContext + context: TestContext & ExtraContext } /** @@ -234,7 +235,7 @@ export type Task = Test | Suite | File */ export type DoneCallback = (error?: any) => void export type TestFunction = ( - context: ExtendedContext & ExtraContext + context: TestContext & ExtraContext ) => Awaitable | void // jest's ExtractEachCallbackArgs @@ -319,7 +320,7 @@ interface TestForFunction { // test.for([[1, 2], [3, 4, 5]]) (cases: ReadonlyArray): TestForFunctionReturn< T, - ExtendedContext & ExtraContext + TestContext & ExtraContext > // test.for` @@ -329,7 +330,7 @@ interface TestForFunction { // ` (strings: TemplateStringsArray, ...values: any[]): TestForFunctionReturn< any, - ExtendedContext & ExtraContext + TestContext & ExtraContext > } @@ -462,8 +463,8 @@ export type Fixture = (( : never) export type Fixtures, ExtraContext = object> = { [K in keyof T]: - | Fixture> - | [Fixture>, FixtureOptions?]; + | Fixture + | [Fixture, FixtureOptions?]; } export type InferFixturesTypes = T extends TestAPI ? C : T @@ -524,14 +525,14 @@ export interface AfterAllListener { export interface BeforeEachListener { ( - context: ExtendedContext & ExtraContext, + context: TestContext & ExtraContext, suite: Readonly ): Awaitable } export interface AfterEachListener { ( - context: ExtendedContext & ExtraContext, + context: TestContext & ExtraContext, suite: Readonly ): Awaitable } @@ -561,7 +562,7 @@ export interface TaskCustomOptions extends TestOptions { * If nothing is provided, the runner will try to get the function using `getFn(task)`. * If the runner cannot find the function, the task will be marked as failed. */ - handler?: (context: TaskContext) => Awaitable + handler?: (context: TestContext) => Awaitable } export interface SuiteCollector { @@ -596,12 +597,7 @@ export interface RuntimeContext { /** * User's custom test context. */ -export interface TestContext {} - -/** - * Context that's always available in the test function. - */ -export interface TaskContext { +export interface TestContext { /** * Metadata of the current test */ @@ -624,11 +620,17 @@ export interface TaskContext { skip: (note?: string) => void } -export type ExtendedContext = TaskContext & - TestContext +/** + * Context that's always available in the test function. + * @deprecated use `TestContext` instead + */ +export interface TaskContext extends TestContext {} + +/** @deprecated use `TestContext` instead */ +export type ExtendedContext = TaskContext & TestContext -export type OnTestFailedHandler = (context: ExtendedContext) => Awaitable -export type OnTestFinishedHandler = (context: ExtendedContext) => Awaitable +export type OnTestFailedHandler = (context: TestContext) => Awaitable +export type OnTestFinishedHandler = (context: TestContext) => Awaitable export interface TaskHook { (fn: HookListener, timeout?: number): void diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index dd7cd290b78f..a10309601155 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -4,7 +4,7 @@ import type { Writable } from 'node:stream' import type { ViteDevServer } from 'vite' import type { defineWorkspace } from 'vitest/config' import type { SerializedCoverageConfig } from '../runtime/config' -import type { ArgumentsType, OnServerRestartHandler, OnTestsRerunHandler, ProvidedContext, UserConsoleLog } from '../types/general' +import type { ArgumentsType, ProvidedContext, UserConsoleLog } from '../types/general' import type { ProcessPool, WorkspaceSpec } from './pool' import type { TestSpecification } from './spec' import type { ResolvedConfig, UserConfig, VitestRunMode } from './types/config' @@ -1203,3 +1203,6 @@ function assert(condition: unknown, property: string, name: string = property): throw new Error(`The ${name} was not set. It means that \`vitest.${property}\` was called before the Vite server was established. Either await the Vitest promise or check that it is initialized with \`vitest.ready()\` before accessing \`vitest.${property}\`.`) } } + +export type OnServerRestartHandler = (reason?: string) => Promise | void +export type OnTestsRerunHandler = (testFiles: TestSpecification[]) => Promise | void diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts index 098451d189f9..64363a7aaf8d 100644 --- a/packages/vitest/src/node/project.ts +++ b/packages/vitest/src/node/project.ts @@ -5,8 +5,8 @@ import type { InlineConfig as ViteInlineConfig, } from 'vite' import type { Typechecker } from '../typecheck/typechecker' -import type { OnTestsRerunHandler, ProvidedContext } from '../types/general' -import type { Vitest } from './core' +import type { ProvidedContext } from '../types/general' +import type { OnTestsRerunHandler, Vitest } from './core' import type { GlobalSetupFile } from './globalSetup' import type { Logger } from './logger' import type { BrowserServer } from './types/browser' diff --git a/packages/vitest/src/public/index.ts b/packages/vitest/src/public/index.ts index 6a02c1759f6d..f5b6ec05947d 100644 --- a/packages/vitest/src/public/index.ts +++ b/packages/vitest/src/public/index.ts @@ -90,7 +90,6 @@ import type { Constructable as Constructable_, MutableArray as MutableArray_, Nullable as Nullable_, - OnServerRestartHandler as OnServerRestartHandler_, } from '../types/general' import type { WorkerRPC as WorkerRPC_, @@ -211,8 +210,6 @@ export type ArgumentsType = ArgumentsType_ export type MutableArray = MutableArray_ /** @deprecated do not use, internal helper */ export type Constructable = Constructable_ -/** @deprecated import from `vitest/node` instead */ -export type OnServerRestartHandler = OnServerRestartHandler_ export type { RunnerRPC, diff --git a/packages/vitest/src/public/node.ts b/packages/vitest/src/public/node.ts index 34f8f859da30..fc1d721ac028 100644 --- a/packages/vitest/src/public/node.ts +++ b/packages/vitest/src/public/node.ts @@ -9,7 +9,11 @@ export { parseCLI } from '../node/cli/cac' export type { CliParseOptions } from '../node/cli/cac' export { startVitest } from '../node/cli/cli-api' export { resolveApiServerConfig, resolveConfig } from '../node/config/resolveConfig' -export type { Vitest } from '../node/core' +export type { + OnServerRestartHandler, + OnTestsRerunHandler, + Vitest, +} from '../node/core' export { createVitest } from '../node/create' export { GitNotFoundError, FilesNotFoundError as TestsNotFoundError } from '../node/errors' export type { GlobalSetupContext } from '../node/globalSetup' @@ -133,11 +137,6 @@ export type { RootAndTarget as TypeCheckRootAndTarget, } from '../typecheck/types' -export type { - OnServerRestartHandler, - OnTestsRerunHandler, -} from '../types/general' - export { createDebugger } from '../utils/debugger' export { generateFileHash } from '@vitest/runner/utils' diff --git a/packages/vitest/src/public/suite.ts b/packages/vitest/src/public/suite.ts index da7ce0ef7859..9e65e3953446 100644 --- a/packages/vitest/src/public/suite.ts +++ b/packages/vitest/src/public/suite.ts @@ -8,4 +8,5 @@ export { setFn, setHooks, } from '@vitest/runner' +export type { VitestRunner, VitestRunnerConfig } from '@vitest/runner' export { createChainable } from '@vitest/runner/utils' diff --git a/packages/vitest/src/runtime/runners/test.ts b/packages/vitest/src/runtime/runners/test.ts index 54c46adf3d08..b9b1cc106b7b 100644 --- a/packages/vitest/src/runtime/runners/test.ts +++ b/packages/vitest/src/runtime/runners/test.ts @@ -1,12 +1,10 @@ import type { ExpectStatic } from '@vitest/expect' import type { CancelReason, - ExtendedContext, File, Suite, Task, - TaskContext, - Test, + TestContext, VitestRunner, VitestRunnerImportSource, } from '@vitest/runner' @@ -161,9 +159,7 @@ export class VitestTestRunner implements VitestRunner { } } - extendTaskContext( - context: TaskContext, - ): ExtendedContext { + extendTaskContext(context: TestContext): TestContext { // create error during the test initialization so we have a nice stack trace if (this.config.expect.requireAssertions) { this.assertionsErrors.set( @@ -185,7 +181,7 @@ export class VitestTestRunner implements VitestRunner { return _expect != null }, }) - return context as ExtendedContext + return context } } diff --git a/packages/vitest/src/types/general.ts b/packages/vitest/src/types/general.ts index ae9cca25fae0..a423417a1d88 100644 --- a/packages/vitest/src/types/general.ts +++ b/packages/vitest/src/types/general.ts @@ -1,5 +1,3 @@ -import type { TestSpecification } from '../node/spec' - export type { ErrorWithDiff, ParsedStack } from '@vitest/utils' export type Awaitable = T | PromiseLike @@ -47,6 +45,4 @@ export interface ModuleGraphData { inlined: string[] } -export type OnServerRestartHandler = (reason?: string) => Promise | void -export type OnTestsRerunHandler = (testFiles: TestSpecification[]) => Promise | void export interface ProvidedContext {} diff --git a/packages/vitest/src/types/global.ts b/packages/vitest/src/types/global.ts index c9ba65420ac3..1b039f61e0fa 100644 --- a/packages/vitest/src/types/global.ts +++ b/packages/vitest/src/types/global.ts @@ -1,7 +1,6 @@ import type { ExpectStatic, PromisifyAssertion, Tester } from '@vitest/expect' import type { Plugin as PrettyFormatPlugin } from '@vitest/pretty-format' import type { SnapshotState } from '@vitest/snapshot' -import type { VitestEnvironment } from '../node/types/config' import type { BenchmarkResult } from '../runtime/types/benchmark' import type { UserConsoleLog } from './general' @@ -36,7 +35,7 @@ interface InlineSnapshotMatcher { declare module '@vitest/expect' { interface MatcherState { - environment: VitestEnvironment + environment: string snapshotState: SnapshotState } From e2598ccce8d436bb7a28cf0986df7ac19c7c0ca6 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 15:58:01 +0100 Subject: [PATCH 52/73] refactor: remove deprecated APIs from custom-pool --- .../fixtures/custom-pool/pool/custom-pool.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/test/cli/fixtures/custom-pool/pool/custom-pool.ts b/test/cli/fixtures/custom-pool/pool/custom-pool.ts index 5f40830acbea..614b1363add6 100644 --- a/test/cli/fixtures/custom-pool/pool/custom-pool.ts +++ b/test/cli/fixtures/custom-pool/pool/custom-pool.ts @@ -1,30 +1,30 @@ -import type { File, Test } from 'vitest' +import type { RunnerTestFile, RunnerTestCase } from 'vitest' import type { ProcessPool, Vitest } from 'vitest/node' import { createMethodsRPC } from 'vitest/node' import { getTasks } from '@vitest/runner/utils' import { normalize, relative } from 'pathe' -export default (ctx: Vitest): ProcessPool => { - const options = ctx.config.poolOptions?.custom as any +export default (vitest: Vitest): ProcessPool => { + const options = vitest.config.poolOptions?.custom as any return { name: 'custom', async collectTests() { throw new Error('Not implemented') }, async runTests(specs) { - ctx.logger.console.warn('[pool] printing:', options.print) - ctx.logger.console.warn('[pool] array option', options.array) - for await (const [project, file] of specs) { - ctx.state.clearFiles(project) + vitest.logger.console.warn('[pool] printing:', options.print) + vitest.logger.console.warn('[pool] array option', options.array) + for (const [project, file] of specs) { + vitest.state.clearFiles(project) const methods = createMethodsRPC(project) - ctx.logger.console.warn('[pool] running tests for', project.getName(), 'in', normalize(file).toLowerCase().replace(normalize(process.cwd()).toLowerCase(), '')) + vitest.logger.console.warn('[pool] running tests for', project.name, 'in', normalize(file).toLowerCase().replace(normalize(process.cwd()).toLowerCase(), '')) const path = relative(project.config.root, file) - const taskFile: File = { - id: `${path}${project.getName()}`, + const taskFile: RunnerTestFile = { + id: `${path}${project.name}`, name: path, mode: 'run', meta: {}, - projectName: project.getName(), + projectName: project.name, filepath: file, type: 'suite', tasks: [], @@ -34,7 +34,7 @@ export default (ctx: Vitest): ProcessPool => { file: null!, } taskFile.file = taskFile - const taskTest: Test = { + const taskTest: RunnerTestCase = { type: 'test', name: 'custom test', id: 'custom-test', @@ -53,7 +53,7 @@ export default (ctx: Vitest): ProcessPool => { } }, close() { - ctx.logger.console.warn('[pool] custom pool is closed!') + vitest.logger.console.warn('[pool] custom pool is closed!') }, } } From 1eaa705e0ae2bd5eaad8a6827320e82662e67636 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 15:58:25 +0100 Subject: [PATCH 53/73] refactor: remove deprecated APIs from sequencers --- packages/vitest/src/node/sequencers/BaseSequencer.ts | 6 +++--- packages/vitest/src/node/sequencers/RandomSequencer.ts | 4 ++-- packages/vitest/src/node/sequencers/types.ts | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/vitest/src/node/sequencers/BaseSequencer.ts b/packages/vitest/src/node/sequencers/BaseSequencer.ts index dcdda705d663..0a00f18e4fbb 100644 --- a/packages/vitest/src/node/sequencers/BaseSequencer.ts +++ b/packages/vitest/src/node/sequencers/BaseSequencer.ts @@ -1,5 +1,5 @@ import type { Vitest } from '../core' -import type { WorkspaceSpec } from '../pool' +import type { TestSpecification } from '../spec' import type { TestSequencer } from './types' import { relative, resolve } from 'pathe' import { slash } from 'vite-node/utils' @@ -13,7 +13,7 @@ export class BaseSequencer implements TestSequencer { } // async so it can be extended by other sequelizers - public async shard(files: WorkspaceSpec[]): Promise { + public async shard(files: TestSpecification[]): Promise { const { config } = this.ctx const { index, count } = config.shard! const shardSize = Math.ceil(files.length / count) @@ -34,7 +34,7 @@ export class BaseSequencer implements TestSequencer { } // async so it can be extended by other sequelizers - public async sort(files: WorkspaceSpec[]): Promise { + public async sort(files: TestSpecification[]): Promise { const cache = this.ctx.cache return [...files].sort((a, b) => { const keyA = `${a.project.name}:${relative(this.ctx.config.root, a.moduleId)}` diff --git a/packages/vitest/src/node/sequencers/RandomSequencer.ts b/packages/vitest/src/node/sequencers/RandomSequencer.ts index f4aa9d787e6d..262f2b0fe590 100644 --- a/packages/vitest/src/node/sequencers/RandomSequencer.ts +++ b/packages/vitest/src/node/sequencers/RandomSequencer.ts @@ -1,9 +1,9 @@ -import type { WorkspaceSpec } from '../pool' +import type { TestSpecification } from '../spec' import { shuffle } from '@vitest/utils' import { BaseSequencer } from './BaseSequencer' export class RandomSequencer extends BaseSequencer { - public async sort(files: WorkspaceSpec[]) { + public async sort(files: TestSpecification[]) { const { sequence } = this.ctx.config return shuffle(files, sequence.seed) diff --git a/packages/vitest/src/node/sequencers/types.ts b/packages/vitest/src/node/sequencers/types.ts index 5e10b2c00a24..9cecf309a438 100644 --- a/packages/vitest/src/node/sequencers/types.ts +++ b/packages/vitest/src/node/sequencers/types.ts @@ -1,14 +1,14 @@ import type { Awaitable } from '../../types/general' import type { Vitest } from '../core' -import type { WorkspaceSpec } from '../pool' +import type { TestSpecification } from '../spec' export interface TestSequencer { /** * Slicing tests into shards. Will be run before `sort`. * Only run, if `shard` is defined. */ - shard: (files: WorkspaceSpec[]) => Awaitable - sort: (files: WorkspaceSpec[]) => Awaitable + shard: (files: TestSpecification[]) => Awaitable + sort: (files: TestSpecification[]) => Awaitable } export interface TestSequencerConstructor { From 47e5484b3c7bc541dd4e625579def5d0adb2c0db Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 15:58:43 +0100 Subject: [PATCH 54/73] docs: update pool docs --- docs/advanced/pool.md | 24 ++++++------------------ docs/advanced/reporters.md | 10 +++++----- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/docs/advanced/pool.md b/docs/advanced/pool.md index 31f850527257..35b0cfeeb717 100644 --- a/docs/advanced/pool.md +++ b/docs/advanced/pool.md @@ -1,7 +1,7 @@ # Custom Pool ::: warning -This is an advanced API. If you just want to [run tests](/guide/), you probably don't need this. It is primarily used by library authors. +This is an advanced and very low-level API. If you just want to [run tests](/guide/), you probably don't need this. It is primarily used by library authors. ::: Vitest runs tests in pools. By default, there are several pools: @@ -49,7 +49,7 @@ export default defineConfig({ ``` ::: info -The `workspace` field was introduced in Vitest 3. To define a workspace in [Vitest <3](https://v2.vitest.dev/), create a separate `vitest.workspace.ts` file. +The `workspace` field was introduced in Vitest 3. To define a workspace in [Vitest 2](https://v2.vitest.dev/), create a separate `vitest.workspace.ts` file. ::: ## API @@ -57,7 +57,7 @@ The `workspace` field was introduced in Vitest 3. To define a workspace in [Vite The file specified in `pool` option should export a function (can be async) that accepts `Vitest` interface as its first option. This function needs to return an object matching `ProcessPool` interface: ```ts -import { ProcessPool, TestSpecification } from 'vitest/node' +import type { ProcessPool, TestSpecification } from 'vitest/node' export interface ProcessPool { name: string @@ -69,9 +69,9 @@ export interface ProcessPool { The function is called only once (unless the server config was updated), and it's generally a good idea to initialize everything you need for tests inside that function and reuse it when `runTests` is called. -Vitest calls `runTest` when new tests are scheduled to run. It will not call it if `files` is empty. The first argument is an array of tuples: the first element is a reference to a workspace project and the second one is an absolute path to a test file. Files are sorted using [`sequencer`](/config/#sequence-sequencer) before `runTests` is called. It's possible (but unlikely) to have the same file twice, but it will always have a different project - this is implemented via [`vitest.workspace.ts`](/guide/workspace) configuration. +Vitest calls `runTest` when new tests are scheduled to run. It will not call it if `files` is empty. The first argument is an array of [TestSpecifications](/advanced/api/test-specification). Files are sorted using [`sequencer`](/config/#sequence-sequencer) before `runTests` is called. It's possible (but unlikely) to have the same file twice, but it will always have a different project - this is implemented via [`vitest.workspace.ts`](/guide/workspace) configuration. -Vitest will wait until `runTests` is executed before finishing a run (i.e., it will emit [`onFinished`](/guide/reporters) only after `runTests` is resolved). +Vitest will wait until `runTests` is executed before finishing a run (i.e., it will emit [`onFinished`](/advanced/reporters) only after `runTests` is resolved). If you are using a custom pool, you will have to provide test files and their results yourself - you can reference [`vitest.state`](https://github.com/vitest-dev/vitest/blob/main/packages/vitest/src/node/state.ts) for that (most important are `collectFiles` and `updateTasks`). Vitest uses `startTests` function from `@vitest/runner` package to do that. @@ -97,16 +97,4 @@ function createRpc(project: TestProject, wss: WebSocketServer) { } ``` -To make sure every test is collected, you would call `ctx.state.collectFiles` and report it to Vitest reporters: - -```ts -async function runTests(project: TestProject, tests: string[]) { - // ... running tests, put into "files" and "tasks" - const methods = createMethodsRPC(project) - await methods.onCollected(files) - // most reporters rely on results being updated in "onTaskUpdate" - await methods.onTaskUpdate(tasks) -} -``` - -You can see a simple example in [pool/custom-pool.ts](https://github.com/vitest-dev/vitest/blob/main/test/run/pool-custom-fixtures/pool/custom-pool.ts). +You can see a simple example of a pool made from scratch that doesn't run tests but marks them as collected in [pool/custom-pool.ts](https://github.com/vitest-dev/vitest/blob/main/test/cli/fixtures/custom-pool/pool/custom-pool.ts). diff --git a/docs/advanced/reporters.md b/docs/advanced/reporters.md index 38027752033f..494c1fba1a11 100644 --- a/docs/advanced/reporters.md +++ b/docs/advanced/reporters.md @@ -70,16 +70,16 @@ import type { RunnerTestFile } from 'vitest' import type { Reporter, TestModule } from 'vitest/reporters' class MyReporter implements Reporter { - ctx!: Vitest + private vitest!: Vitest - onInit(ctx: Vitest) { - this.ctx = ctx + onInit(vitest: Vitest) { + this.vitest = vitest } onFinished(files: RunnerTestFile[]) { - for (const fileTask of files) { + for (const file of files) { // note that the old task implementation uses "file" instead of "module" - const testModule = this.ctx.state.getReportedEntity(fileTask) as TestModule + const testModule = this.vitest.state.getReportedEntity(file) as TestModule for (const task of testModule.children) { // ^? console.log('finished', task.type, task.fullName) From 6c4030af02310c4c0651c3f769043eab4716fb32 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 16:05:08 +0100 Subject: [PATCH 55/73] refactor: remove `ready` API --- docs/advanced/api/vitest.md | 21 +++++---------------- packages/vitest/src/node/core.ts | 13 ------------- 2 files changed, 5 insertions(+), 29 deletions(-) diff --git a/docs/advanced/api/vitest.md b/docs/advanced/api/vitest.md index e18f3f917a05..6b2607e5b8fe 100644 --- a/docs/advanced/api/vitest.md +++ b/docs/advanced/api/vitest.md @@ -1,5 +1,6 @@ --- outline: deep +title: Vitest API --- # Vitest @@ -7,7 +8,7 @@ outline: deep Vitest instance requires the current test mode. It can be either: - `test` when running runtime tests -- `benchmark` when running benchmarks +- `benchmark` when running benchmarks experimental ## mode @@ -15,7 +16,7 @@ Vitest instance requires the current test mode. It can be either: Test mode will only call functions inside `test` or `it`, and throws an error when `bench` is encountered. This mode uses `include` and `exclude` options in the config to find test files. -### benchmark +### benchmark experimental 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. @@ -39,10 +40,10 @@ This is Vitest config, it doesn't extend _Vite_ config. It only has resolved val This is a global [`ViteDevServer`](https://vite.dev/guide/api-javascript#vitedevserver). -## state +## state experimental ::: warning -Public `state` is an experimental API. Breaking changes might not follow SemVer, please pin Vitest's version when using it. +Public `state` is an experimental API (except `vitest.state.getReportedEntity`). Breaking changes might not follow SemVer, please pin Vitest's version when using it. ::: Global state stores information about the current tests. It uses the same API from `@vitest/runner` by default, but we recommend using the [Reported API](/advanced/reporters#reported-tasks) instead by calling `state.getReportedEntity()` on the `@vitest/runner` API: @@ -64,18 +65,6 @@ You can get the latest summary of snapshots via the `vitest.snapshot.summay` pro Cache manager that stores information about latest test results and test file stats. In Vitest itself this is only used by the default sequencer to sort tests. -## ready - -```ts -function ready(): boolean -``` - -Vitest needs to be resolved with the Vite server to be properly initialized. If the `Vitest` instance was created manually, you might need to check the `ready` status before accessing the `vite`, `state`, `cache`, `config`, and `snapshot` properties; otherwise, they will throw an error in the getter. - -::: tip -In normal circumstances, you would never call this method because `createVitest` and `startVitest` return already resolved Vitest instance. -::: - ## getRootProject ```ts diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index a10309601155..7e2bbccd3bdf 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -99,7 +99,6 @@ export class Vitest { private readonly specifications: VitestSpecifications private readonly watcher: VitestWatcher private pool: ProcessPool | undefined - private _ready = false private _config?: ResolvedConfig private _vite?: ViteDevServer private _state?: StateManager @@ -182,14 +181,6 @@ export class Vitest { return this._cache } - /** - * Returns whether Vitest was fully initialised. This means that the Vite server was established and the workspace config was resolved. - * It's not necessary to call this method unless the instance was created manually via the public API, and the promise was not awaited. - */ - public ready(): boolean { - return this._ready - } - /** @deprecated internal */ setServer(options: UserConfig, server: ViteDevServer, cliOptions: UserConfig) { return this._setServer(options, server, cliOptions) @@ -197,7 +188,6 @@ export class Vitest { /** @internal */ async _setServer(options: UserConfig, server: ViteDevServer, cliOptions: UserConfig) { - this._ready = false this._options = options this.watcher.unregisterWatcher() clearTimeout(this._rerunTimer) @@ -244,7 +234,6 @@ export class Vitest { // hijack server restart const serverRestart = server.restart server.restart = async (...args) => { - this._ready = false await Promise.all(this._onRestartListeners.map(fn => fn())) this.report('onServerRestart') await this.close() @@ -258,7 +247,6 @@ export class Vitest { || this.resolvedProjects.some(p => p.vite.config.configFile === file) || file === this._workspaceConfigPath if (isConfig) { - this._ready = false await Promise.all(this._onRestartListeners.map(fn => fn('config'))) this.report('onServerRestart', 'config') await this.close() @@ -295,7 +283,6 @@ export class Vitest { } await Promise.all(this._onSetServer.map(fn => fn())) - this._ready = true } /** From e9cf357c7f6d3eff105edbc8e1b742cf995ba63b Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 16:06:04 +0100 Subject: [PATCH 56/73] refactor: resolve workspace before reporters --- packages/vitest/src/node/core.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 7e2bbccd3bdf..fcae75632f6e 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -255,10 +255,6 @@ export class Vitest { }) } - this.reporters = resolved.mode === 'benchmark' - ? await createBenchmarkReporters(toArray(resolved.benchmark?.reporters), this.runner) - : await createReporters(resolved.reporters, this) - this.cache.results.setConfig(resolved.root, resolved.cache) try { await this.cache.results.readFromCache() @@ -282,6 +278,10 @@ export class Vitest { this.configOverride.testNamePattern = this.config.testNamePattern } + this.reporters = resolved.mode === 'benchmark' + ? await createBenchmarkReporters(toArray(resolved.benchmark?.reporters), this.runner) + : await createReporters(resolved.reporters, this) + await Promise.all(this._onSetServer.map(fn => fn())) } From 693b23f75c9297a57d63ad3b34182244f56c4884 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 16:39:54 +0100 Subject: [PATCH 57/73] feat: expose `resolveConfig` --- docs/.vitepress/config.ts | 19 +++++-- docs/guide/migration.md | 16 ++++-- packages/vitest/src/node/plugins/index.ts | 6 ++ .../vitest/src/node/plugins/publicConfig.ts | 57 +++++++++++++++++++ packages/vitest/src/node/pool.ts | 10 ++-- packages/vitest/src/public/node.ts | 3 +- .../fixtures/public-config/vitest.config.ts | 7 +++ .../public-config/vitest.custom.config.ts | 7 +++ test/config/test/public.test.ts | 42 ++++++++++++++ 9 files changed, 152 insertions(+), 15 deletions(-) create mode 100644 packages/vitest/src/node/plugins/publicConfig.ts create mode 100644 test/config/fixtures/public-config/vitest.config.ts create mode 100644 test/config/fixtures/public-config/vitest.custom.config.ts create mode 100644 test/config/test/public.test.ts diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 337d842a69f2..246443e63853 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -471,14 +471,25 @@ function guide(): DefaultTheme.SidebarItem[] { text: 'Debugging', link: '/guide/debugging', }, - { - text: 'Migration Guide', - link: '/guide/migration', - }, { text: 'Common Errors', link: '/guide/common-errors', }, + { + text: 'Migration Guide', + link: '/guide/migration', + collapsed: false, + items: [ + { + text: 'Migrating to Vitest 3.0', + link: '/guide/migration#vitest-3', + }, + { + text: 'Migrating from Jest', + link: '/guide/migration#jest', + }, + ], + }, { text: 'Performance', collapsed: false, diff --git a/docs/guide/migration.md b/docs/guide/migration.md index fc63df1e4ef3..0faaae8d7bfd 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -5,7 +5,7 @@ outline: deep # Migration Guide -## Migrating to Vitest 3.0 +## Migrating to Vitest 3.0 {#vitest-3} ### Test Options as a Third Argument @@ -29,9 +29,9 @@ test('validation works', () => { }, 1000) // Ok ✅ ``` -### `Custom` Type is Deprecated experimental {#custom-type-is-deprecated} +### `Custom` Type is Deprecated API {#custom-type-is-deprecated} -The `Custom` type is now equal to the `Test` type. Note that Vitest updated the public types in 2.1 and changed exported names to `RunnerCustomCase` and `RunnerTestCase`: +The `Custom` type is now an alias for the `Test` type. Note that Vitest updated the public types in 2.1 and changed exported names to `RunnerCustomCase` and `RunnerTestCase`: ```ts import { @@ -46,7 +46,13 @@ If you are using `getCurrentSuite().custom()`, the `type` of the returned task i The [`onTestFinished`](/api/#ontestfinished) and [`onTestFailed`](/api/#ontestfailed) hooks previously received a test result as the first argument. Now, they receive a test context, like `beforeEach` and `afterEach`. -## Migrating to Vitest 2.0 +### Changes to `resolveConfig` Type Signature API {#changes-to-resolveconfig-type-signature} + +The [`resolveConfig`](/advanced/api/#resolveconfig) is now more useful. Instead of accepting already resolved Vite config, it now accepts a user config and returns resolved config. + +This function is not used internally and exposed exclusively as a public API. + +## Migrating to Vitest 2.0 {#vitest-2} ### Default Pool is `forks` @@ -328,7 +334,7 @@ It is still possible to mock `process.nextTick` by explicitly specifying it by u However, mocking `process.nextTick` is not possible when using `--pool=forks`. Use a different `--pool` option if you need `process.nextTick` mocking. -## Migrating from Jest +## Migrating from Jest {#jest} Vitest has been designed with a Jest compatible API, in order to make the migration from Jest as simple as possible. Despite those efforts, you may still run into the following differences: diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index 1189bc3b50f2..66119740c772 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -256,6 +256,12 @@ export async function VitestPlugin( } hijackVitePluginInject(viteConfig) + + Object.defineProperty(viteConfig, '_vitest', { + value: options, + enumerable: false, + configurable: true, + }) }, configureServer: { // runs after vite:import-analysis as it relies on `server` instance on Vite 5 diff --git a/packages/vitest/src/node/plugins/publicConfig.ts b/packages/vitest/src/node/plugins/publicConfig.ts new file mode 100644 index 000000000000..2365a56fbe66 --- /dev/null +++ b/packages/vitest/src/node/plugins/publicConfig.ts @@ -0,0 +1,57 @@ +import type { + ResolvedConfig as ResolvedViteConfig, + UserConfig as ViteUserConfig, +} from 'vite' +import type { ResolvedConfig, UserConfig } from '../types/config' +import { slash } from '@vitest/utils' +import { findUp } from 'find-up' +import { resolve } from 'pathe' +import { mergeConfig, resolveConfig as resolveViteConfig } from 'vite' +import { configFiles } from '../../constants' +import { resolveConfig as resolveVitestConfig } from '../config/resolveConfig' +import { Vitest } from '../core' +import { VitestPlugin } from './index' + +// this is only exported as a public function and not used inside vitest +export async function resolveConfig( + options: UserConfig = {}, + viteOverrides: ViteUserConfig = {}, +): Promise<{ vitestConfig: ResolvedConfig; viteConfig: ResolvedViteConfig }> { + const root = slash(resolve(options.root || process.cwd())) + + const configPath + = options.config === false + ? false + : options.config + ? resolve(root, options.config) + : await findUp(configFiles, { cwd: root } as any) + options.config = configPath + + const vitest = new Vitest('test') + const config = await resolveViteConfig( + mergeConfig( + { + configFile: configPath, + // this will make "mode": "test" | "benchmark" inside defineConfig + mode: options.mode || 'test', + plugins: [ + await VitestPlugin(options, vitest), + ], + }, + mergeConfig(viteOverrides, { root: options.root }), + ), + 'serve', + ) + // Reflect just to avoid type error + const updatedOptions = Reflect.get(config, '_vitest') as UserConfig + const vitestConfig = resolveVitestConfig( + 'test', + updatedOptions, + config, + vitest.logger, + ) + return { + viteConfig: config, + vitestConfig, + } +} diff --git a/packages/vitest/src/node/pool.ts b/packages/vitest/src/node/pool.ts index 6562008b8989..7cb70a5c3317 100644 --- a/packages/vitest/src/node/pool.ts +++ b/packages/vitest/src/node/pool.ts @@ -93,15 +93,15 @@ export function createPool(ctx: Vitest): ProcessPool { const potentialConditions = new Set([ 'production', 'development', - ...ctx.server.config.resolve.conditions, + ...ctx.vite.config.resolve.conditions, ]) const conditions = [...potentialConditions] .filter((condition) => { if (condition === 'production') { - return ctx.server.config.isProduction + return ctx.vite.config.isProduction } if (condition === 'development') { - return !ctx.server.config.isProduction + return !ctx.vite.config.isProduction } return true }) @@ -191,7 +191,7 @@ export function createPool(ctx: Vitest): ProcessPool { const Sequencer = ctx.config.sequence.sequencer const sequencer = new Sequencer(ctx) - async function sortSpecs(specs: WorkspaceSpec[]) { + async function sortSpecs(specs: TestSpecification[]) { if (ctx.config.shard) { specs = await sequencer.shard(specs) } @@ -200,7 +200,7 @@ export function createPool(ctx: Vitest): ProcessPool { await Promise.all( Object.entries(filesByPool).map(async (entry) => { - const [pool, files] = entry as [Pool, WorkspaceSpec[]] + const [pool, files] = entry as [Pool, TestSpecification[]] if (!files.length) { return null diff --git a/packages/vitest/src/public/node.ts b/packages/vitest/src/public/node.ts index fc1d721ac028..b85293dda07b 100644 --- a/packages/vitest/src/public/node.ts +++ b/packages/vitest/src/public/node.ts @@ -8,7 +8,7 @@ export const version = Vitest.version export { parseCLI } from '../node/cli/cac' export type { CliParseOptions } from '../node/cli/cac' export { startVitest } from '../node/cli/cli-api' -export { resolveApiServerConfig, resolveConfig } from '../node/config/resolveConfig' +export { resolveApiServerConfig } from '../node/config/resolveConfig' export type { OnServerRestartHandler, OnTestsRerunHandler, @@ -19,6 +19,7 @@ export { GitNotFoundError, FilesNotFoundError as TestsNotFoundError } from '../n export type { GlobalSetupContext } from '../node/globalSetup' export { VitestPackageInstaller } from '../node/packageInstaller' export { VitestPlugin } from '../node/plugins' +export { resolveConfig } from '../node/plugins/publicConfig' export { resolveFsAllow } from '../node/plugins/utils' export type { ProcessPool, WorkspaceSpec } from '../node/pool' export { getFilePoolName } from '../node/pool' diff --git a/test/config/fixtures/public-config/vitest.config.ts b/test/config/fixtures/public-config/vitest.config.ts new file mode 100644 index 000000000000..c067847db847 --- /dev/null +++ b/test/config/fixtures/public-config/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + name: 'root config' + } +}) \ No newline at end of file diff --git a/test/config/fixtures/public-config/vitest.custom.config.ts b/test/config/fixtures/public-config/vitest.custom.config.ts new file mode 100644 index 000000000000..f97daa7da87d --- /dev/null +++ b/test/config/fixtures/public-config/vitest.custom.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + name: 'custom config' + } +}) \ No newline at end of file diff --git a/test/config/test/public.test.ts b/test/config/test/public.test.ts new file mode 100644 index 000000000000..e72048ab93a9 --- /dev/null +++ b/test/config/test/public.test.ts @@ -0,0 +1,42 @@ +import { resolve } from 'pathe' +import { expect, test } from 'vitest' +import { resolveConfig } from 'vitest/node' + +test('resolves the test config', async () => { + const { viteConfig, vitestConfig } = await resolveConfig() + expect(viteConfig.mode).toBe('test') + expect(vitestConfig.mode).toBe('test') + expect(vitestConfig.reporters).toEqual([['verbose', {}]]) // inherits the root config + expect(viteConfig.plugins.find(p => p.name === 'vitest')).toBeDefined() +}) + +test('applies custom options', async () => { + const { viteConfig, vitestConfig } = await resolveConfig({ + mode: 'development', + setupFiles: ['/test/setup.ts'], + }) + expect(viteConfig.mode).toBe('development') + expect(vitestConfig.mode).toBe('test') // vitest mode is "test" or "benchmark" + expect(vitestConfig.setupFiles).toEqual(['/test/setup.ts']) + expect(viteConfig.plugins.find(p => p.name === 'vitest')).toBeDefined() +}) + +test('respects root', async () => { + const configRoot = resolve(import.meta.dirname, '../fixtures/public-config') + const { viteConfig, vitestConfig } = await resolveConfig({ + root: configRoot, + }) + expect(viteConfig.configFile).toBe(resolve(configRoot, 'vitest.config.ts')) + expect(vitestConfig.name).toBe('root config') + expect(vitestConfig.reporters).toEqual([['default', {}]]) +}) + +test('respects custom config', async () => { + const config = resolve(import.meta.dirname, '../fixtures/public-config/vitest.custom.config.ts') + const { viteConfig, vitestConfig } = await resolveConfig({ + config, + }) + expect(viteConfig.configFile).toBe(config) + expect(vitestConfig.name).toBe('custom config') + expect(vitestConfig.reporters).toEqual([['default', {}]]) +}) From af73c51fa5c1fcc57c0c78c128d18c7ca4d76da9 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 16:45:21 +0100 Subject: [PATCH 58/73] docs: document `resolveConfig` --- docs/advanced/api/index.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/advanced/api/index.md b/docs/advanced/api/index.md index c64e7eda7ec7..1a65575a3b62 100644 --- a/docs/advanced/api/index.md +++ b/docs/advanced/api/index.md @@ -75,6 +75,30 @@ const vitest = await createVitest('test', { The ["Running Tests"](/advanced/guide/tests#createvitest) guide has a usage example. ::: +## resolveConfig + +```ts +function resolveConfig( + options: UserConfig = {}, + viteOverrides: ViteUserConfig = {}, +): Promise<{ + vitestConfig: ResolvedConfig + viteConfig: ResolvedViteConfig +}> +``` + +This method resolves the config with custom parameters. If no parameters are gived, the `root` will be `process.cwd()`. + +::: info +Due to how Vite's `createServer` works, Vitest has to resolve the config during the plugin's `configResolve` hook. Therefore, this method is not actually used internally and is exposed exclusively as a public API. + +If you pass down the config to the `startVitest` or `createVitest` APIs, Vitest will still resolve the config again. +::: + +::: warning +The `resolveConfig` doesn't resolve the `workspace`. To resolve workspace configs, Vitest needs an established Vite server. +::: + ## parseCLI ```ts From e09c25c831aa7f5e84d13c2b65802bd87a5faac2 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 16:50:05 +0100 Subject: [PATCH 59/73] docs: document WorkspaceSpec type migration --- docs/advanced/api/index.md | 4 ++++ docs/guide/migration.md | 4 ++++ test/config/fixtures/bail/vitest.config.ts | 6 +++--- test/reporters/tests/task-parser.test.ts | 6 +++--- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/advanced/api/index.md b/docs/advanced/api/index.md index 1a65575a3b62..cb727392dd30 100644 --- a/docs/advanced/api/index.md +++ b/docs/advanced/api/index.md @@ -1,3 +1,7 @@ +--- +title: Advanced API +--- + # Getting Started ::: warning diff --git a/docs/guide/migration.md b/docs/guide/migration.md index 0faaae8d7bfd..cfed94061c6e 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -42,6 +42,10 @@ import { If you are using `getCurrentSuite().custom()`, the `type` of the returned task is now is equal to `'test'`. The `Custom` type will be removed in Vitest 4. +### The `WorkspaceSpec` Type is No Longer Used API {#the-workspacespec-type-is-no-longer-used} + +In the public API this type was used in custom [sequencers](/config/#sequence-sequencer) before. Please, migrate to [`TestSpecification`](/advanced/api/test-specification) instead. + ### `onTestFinished` and `onTestFailed` Now Receive a Context The [`onTestFinished`](/api/#ontestfinished) and [`onTestFailed`](/api/#ontestfailed) hooks previously received a test result as the first argument. Now, they receive a test context, like `beforeEach` and `afterEach`. diff --git a/test/config/fixtures/bail/vitest.config.ts b/test/config/fixtures/bail/vitest.config.ts index 43f27f8a1aca..f117ebe338a0 100644 --- a/test/config/fixtures/bail/vitest.config.ts +++ b/test/config/fixtures/bail/vitest.config.ts @@ -1,8 +1,8 @@ import { defineConfig } from 'vitest/config' -import type { WorkspaceSpec } from 'vitest/node' +import type { TestSpecification } from 'vitest/node' class TestNameSequencer { - async sort(files: WorkspaceSpec[]): Promise { + async sort(files: TestSpecification[]): Promise { return [...files].sort(([, filenameA], [, filenameB]) => { if (filenameA > filenameB) return 1 @@ -14,7 +14,7 @@ class TestNameSequencer { }) } - public async shard(files: WorkspaceSpec[]): Promise { + public async shard(files: TestSpecification[]): Promise { return files } } diff --git a/test/reporters/tests/task-parser.test.ts b/test/reporters/tests/task-parser.test.ts index 51d7954e3f22..22779ecb377b 100644 --- a/test/reporters/tests/task-parser.test.ts +++ b/test/reporters/tests/task-parser.test.ts @@ -1,5 +1,5 @@ import type { File, Test } from '@vitest/runner' -import type { WorkspaceSpec } from 'vitest/node' +import type { TestSpecification } from 'vitest/node' import type { Reporter } from 'vitest/reporters' import type { HookOptions } from '../../../packages/vitest/src/node/reporters/task-parser' import { expect, test } from 'vitest' @@ -132,7 +132,7 @@ class TaskReporter extends TaskParser implements Reporter { } class Sorter { - sort(files: WorkspaceSpec[]) { + sort(files: TestSpecification[]) { return files.sort((a, b) => { const idA = Number.parseInt( a.moduleId.match(/example-(\d*)\.test\.ts/)![1], @@ -151,7 +151,7 @@ class Sorter { }) } - shard(files: WorkspaceSpec[]) { + shard(files: TestSpecification[]) { return files } } From 104b80f6ea421c30ded028ef48a5d64b69e4249c Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 16:50:13 +0100 Subject: [PATCH 60/73] test: fix CI test fail --- test/config/test/public.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/config/test/public.test.ts b/test/config/test/public.test.ts index e72048ab93a9..a080cf71c6ad 100644 --- a/test/config/test/public.test.ts +++ b/test/config/test/public.test.ts @@ -22,6 +22,7 @@ test('applies custom options', async () => { }) test('respects root', async () => { + process.env.GITHUB_ACTIONS = 'false' const configRoot = resolve(import.meta.dirname, '../fixtures/public-config') const { viteConfig, vitestConfig } = await resolveConfig({ root: configRoot, @@ -32,6 +33,7 @@ test('respects root', async () => { }) test('respects custom config', async () => { + process.env.GITHUB_ACTIONS = 'false' const config = resolve(import.meta.dirname, '../fixtures/public-config/vitest.custom.config.ts') const { viteConfig, vitestConfig } = await resolveConfig({ config, From 19fd6f22aab8a20d1fca15bbf72f95ab7c4ce121 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 18:15:30 +0100 Subject: [PATCH 61/73] fix: throw if --project doesn't exist, cleanup --- packages/vitest/src/node/cli/cac.ts | 2 +- packages/vitest/src/node/core.ts | 85 +++++++++++++++++++++++------ test/config/test/failures.test.ts | 10 ++++ 3 files changed, 79 insertions(+), 18 deletions(-) diff --git a/packages/vitest/src/node/cli/cac.ts b/packages/vitest/src/node/cli/cac.ts index 2660b8af2e3c..b0a8de330fb4 100644 --- a/packages/vitest/src/node/cli/cac.ts +++ b/packages/vitest/src/node/cli/cac.ts @@ -323,7 +323,7 @@ async function collect(mode: VitestRunMode, cliFilters: string[], options: CliOp processCollected(ctx, tests, options) } else { - const files = await ctx.listFiles(cliFilters.map(normalize)) + const files = await ctx.getRelevantTestSpecifications(cliFilters.map(normalize)) outputFileList(files, options) } diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index fcae75632f6e..c9f9ca7afa22 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -79,6 +79,7 @@ export class Vitest { */ public projects: TestProject[] = [] + /** @internal */ configOverride: Partial = {} /** @internal */ coverageProvider: CoverageProvider | null | undefined /** @internal */ filenamePattern?: string[] /** @internal */ runningPromise?: Promise @@ -88,7 +89,6 @@ export class Vitest { /** @internal */ resolvedProjects: TestProject[] = [] /** @internal */ _browserLastPort = defaultBrowserPort /** @internal */ _options: UserConfig = {} - /** @internal */ configOverride: Partial = {} /** @internal */ reporters: Reporter[] = undefined! /** @internal */ vitenode: ViteNodeServer = undefined! /** @internal */ runner: ViteNodeRunner = undefined! @@ -125,12 +125,12 @@ export class Vitest { private _onUserTestsRerun: OnTestsRerunHandler[] = [] private _onFilterWatchedSpecification: ((spec: TestSpecification) => boolean)[] = [] - /** @deprecated will be removed in 3.0, use `vitest.watcher` */ + /** @deprecated will be removed in 4.0, use `onFilterWatchedSpecification` instead */ public get invalidates() { return this.watcher.invalidates } - /** @deprecated will be removed in 3.0, use `vitest.watcher` */ + /** @deprecated will be removed in 4.0, use `onFilterWatchedSpecification` instead */ public get changedTests() { return this.watcher.changedTests } @@ -269,6 +269,9 @@ export class Vitest { this.projects = this.projects.filter(p => filters.some(pattern => pattern.test(p.name)), ) + if (!this.projects.length) { + throw new Error(`No projects matched the filter "${toArray(resolved.project).join('", "')}".`) + } } if (!this.coreWorkspaceProject) { this.coreWorkspaceProject = TestProject._createBasicProject(this) @@ -430,14 +433,14 @@ export class Vitest { } /** - * Merge reports from multiple runs located in the `--merge-reports` directory. + * Merge reports from multiple runs located in the specified directory (`--merge-reports` by default). */ - public async mergeReports(): Promise { + public async mergeReports(directory?: string): Promise { if (this.reporters.some(r => r instanceof BlobReporter)) { throw new Error('Cannot merge reports when `--reporter=blob` is used. Remove blob reporter from the config first.') } - const { files, errors, coverages } = await readBlobs(this.version, this.config.mergeReports, this.projects) + const { files, errors, coverages } = await readBlobs(this.version, directory || this.config.mergeReports, this.projects) await this.report('onInit', this) await this.report('onPathsCollected', files.flatMap(f => f.filepath)) @@ -509,11 +512,16 @@ export class Vitest { return this.collectTests(files) } + /** @deprecated use `getRelevantTestSpecifications` instead */ + public listFiles(filters?: string[]): Promise { + return this.getRelevantTestSpecifications(filters) + } + /** * Returns the list of test files that match the config and filters. * @param filters String filters to match the test files */ - listFiles(filters?: string[]): Promise { + getRelevantTestSpecifications(filters?: string[]): Promise { return this.specifications.getRelevantTestSpecifications(filters) } @@ -575,7 +583,7 @@ export class Vitest { /** * Initialize reporters and the coverage provider. This method doesn't run any tests. - * If the `--watch` flag is provided, Vitest will still run those tests. + * If the `--watch` flag is provided, Vitest will still run changed tests even if this method was not called. */ async init(): Promise { this._onClose = [] @@ -890,10 +898,7 @@ export class Vitest { await this.rerunFiles(this.state.getFailedFilepaths(), 'rerun failed', false) } - /** - * Run tests, and update snapshots for failed tests. - * @param files Files to update snapshot for. If not provided, all failed files and unchecked files will be updated. - */ + /** @internal */ async updateSnapshot(files?: string[]): Promise { // default to failed files files = files || [ @@ -901,20 +906,58 @@ export class Vitest { ...this.snapshot.summary.uncheckedKeysByFile.map(s => s.filePath), ] + this.enableSnapshotUpdate() + + try { + await this.rerunFiles(files, 'update snapshot', false) + } + finally { + this.resetSnapshotUpdate() + } + } + + /** + * Enable the mode that allows updating snapshots when running tests. + * This method doesn't run any tests. + * + * Every test that runs after this method is called will update snapshots. + * To disable the mode, call `resetSnapshotUpdate`. + */ + public enableSnapshotUpdate(): void { this.configOverride.snapshotOptions = { updateSnapshot: 'all', // environment is resolved inside a worker thread snapshotEnvironment: null as any, } + } - try { - await this.rerunFiles(files, 'update snapshot', false) + /** + * Disable the mode that allows updating snapshots when running tests. + */ + public resetSnapshotUpdate(): void { + delete this.configOverride.snapshotOptions + } + + /** + * Set the global test name pattern to a regexp. + * This method doesn't run any tests. + */ + public setGlobalTestNamePattern(pattern: string | RegExp): void { + if (pattern instanceof RegExp) { + this.configOverride.testNamePattern = pattern } - finally { - delete this.configOverride.snapshotOptions + else { + this.configOverride.testNamePattern = pattern ? new RegExp(pattern) : undefined } } + /** + * Resets the global test name pattern. This method doesn't run any tests. + */ + public resetGlobalTestNamePattern(): void { + this.configOverride.testNamePattern = undefined + } + private _rerunTimer: any // we can't use a single `triggerId` yet because vscode extension relies on this private async scheduleRerun(triggerId: string[]): Promise { @@ -1073,7 +1116,7 @@ export class Vitest { * Closes all projects and exit the process * @param force If true, the process will exit immediately after closing the projects. */ - async exit(force = false): Promise { + public async exit(force = false): Promise { setTimeout(() => { this.report('onProcessTimeout').then(() => { console.warn(`close timed out after ${this.config.teardownTimeout}ms`) @@ -1133,6 +1176,14 @@ export class Vitest { return this.globTestSpecifications(filters) } + /** @deprecated filter by `this.projects` yourself */ + public getModuleProjects(filepath: string) { + return this.projects.filter((project) => { + return project.getModulesByFilepath(filepath).size + // TODO: reevaluate || project.browser?.moduleGraph.getModulesByFile(id)?.size + }) + } + /** * Should the server be kept running after the tests are done. */ diff --git a/test/config/test/failures.test.ts b/test/config/test/failures.test.ts index 1d98a7a1a673..407ace135c85 100644 --- a/test/config/test/failures.test.ts +++ b/test/config/test/failures.test.ts @@ -288,3 +288,13 @@ test('maxConcurrency 0 prints a warning', async () => { expect(ctx?.config.maxConcurrency).toBe(5) expect(stderr).toMatch('The option "maxConcurrency" cannot be set to 0. Using default value 5 instead.') }) + +test('non existing project name will throw', async () => { + const { stderr } = await runVitest({ project: 'non-existing-project' }) + expect(stderr).toMatch('No projects matched the filter "non-existing-project".') +}) + +test('non existing project name array will throw', async () => { + const { stderr } = await runVitest({ project: ['non-existing-project', 'also-non-existing'] }) + expect(stderr).toMatch('No projects matched the filter "non-existing-project", "also-non-existing".') +}) From a6c17a66cc63a729980f3b457317715ea6bc39a5 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 18:15:34 +0100 Subject: [PATCH 62/73] docs: finish docs --- docs/advanced/api/test-project.md | 7 ++ docs/advanced/api/vitest.md | 184 +++++++++++++++++++++++++++++- 2 files changed, 188 insertions(+), 3 deletions(-) diff --git a/docs/advanced/api/test-project.md b/docs/advanced/api/test-project.md index 69169491fbbc..2f23964d5757 100644 --- a/docs/advanced/api/test-project.md +++ b/docs/advanced/api/test-project.md @@ -215,6 +215,13 @@ function globTestFiles(filters?: string[]): { Globs all test files. This function returns an object with regular tests and typecheck tests. +This method accepts `filters`. Filters can only a part of the file path, unlike in other methods on the [`Vitest`](/advanced/api/vitest) instance: + +```js +project.globTestFiles(['foo']) // ✅ +project.globTestFiles(['basic/foo.js:10']) // ❌ +``` + ::: tip Vitest uses [fast-glob](https://www.npmjs.com/package/fast-glob) to find test files. `test.dir`, `test.root`, `root` or `process.cwd()` define the `cwd` option. diff --git a/docs/advanced/api/vitest.md b/docs/advanced/api/vitest.md index 6b2607e5b8fe..b93d6179c822 100644 --- a/docs/advanced/api/vitest.md +++ b/docs/advanced/api/vitest.md @@ -10,6 +10,49 @@ Vitest instance requires the current test mode. It can be either: - `test` when running runtime tests - `benchmark` when running benchmarks experimental +::: details New in Vitest 3 +Vitest 3 is one step closer to stabilising the public API. To achieve that, we deprecated and removed some of the previously public methods on the `Vitest` class. These APIs were made private: + +- `configOverride` (use [`setGlobalTestNamePattern`](#setglobaltestnamepattern) or [`enableSnapshotUpdate`](#enablesnapshotupdate)) +- `coverageProvider` +- `filenamePattern` +- `runningPromise` +- `closingPromise` +- `isCancelling` +- `coreWorkspaceProject` +- `resolvedProjects` +- `_browserLastPort` +- `_options` +- `reporters` +- `vitenode` +- `runner` +- `pool` +- `setServer` +- `_initBrowserServers` +- `rerunTask` +- `changeProjectName` +- `changeNamePattern` +- `changeFilenamePattern` +- `rerunFailed` +- `updateSnapshot` +- `_createRootProject` (renamed to `_ensureRootProject`, but still private) +- `filterTestsBySource` (this was moved to the new internal `vitest.specifications` instance) +- `runFiles` (use [`runTestSpecifications`](#runtestspecifications) instead) +- `onAfterSetServer` + +These APIs were deprecated: +- `invalidates` +- `changedTests` (use [`onFilterWatchedSpecification`](#onfilterwatchedspecification) instead) +- `server` (use [`vite`](#vite) instead) +- `getProjectsByTestFile` (use [`getModuleSpecifications`](#getmodulespecifications) instead) +- `getFileWorkspaceSpecs` (use [`getModuleSpecifications`](#getmodulespecifications) instead) +- `getModuleProjects` (filter by [`this.projects`](#projects) yourself) +- `updateLastChanged` (renamed to [`invalidateFile`](#invalidatefile)) +- `globTestSpecs` (use [`globTestSpecifications`](#globtestspecifications) instead) +- `globTestFiles` (use [`globTestSpecifications`](#globtestspecifications) instead) +- `listFile` (use [`getRelevantTestSpecifications`](#getrelevanttestspecifications) instead) +::: + ## mode ### test @@ -65,13 +108,19 @@ You can get the latest summary of snapshots via the `vitest.snapshot.summay` pro Cache manager that stores information about latest test results and test file stats. In Vitest itself this is only used by the default sequencer to sort tests. +## projects + +An array of [test projects](/advanced/api/test-project) that belong to the user's workspace. If the user did not specify a custom workspace, the workspace will only have a [root project](#getrootproject). + +Vitest will ensure that there is always at least one project in the workspace. If the user specifies a non-existent `--project` name, Vitest will throw an error. + ## getRootProject ```ts function getRootProject(): TestProject ``` -This returns the root test project. The root project generally doesn't run any tests and is not included in `vitest.projects` unless the user explicitly includes the root config in their workspace. +This returns the root test project. The root project generally doesn't run any tests and is not included in `vitest.projects` unless the user explicitly includes the root config in their workspace, or the workspace is not defined at all. The primary goal of the root project is to setup the global config. In fact, `rootProject.config` references `rootProject.globalConfig` and `vitest.config` directly: @@ -162,12 +211,81 @@ const specifications = await vitest.globTestSpecifications(['my-filter']) console.log(specifications) ``` +## getRelevantTestSpecifications + +```ts +function getRelevantTestSpecifications( + filters?: string[] +): Promise +``` + +This method resolves every test specification by calling [`project.globTestFiles`](/advanced/api/test-project#globtestfiles). It accepts string filters to match the test files - these are the same filters that [CLI supports](/guide/filtering#cli). If `--changed` flag was specified, the list will be filtered to include only files that changed. `getRelevantTestSpecifications` doesn't run any test files. + +::: warning +This method can be slow because it needs to filter `--changed` flags. Do not use it if you just need a list of test files. + +- If you need to get the list of specifications for known test files, use [`getModuleSpecifications`](#getmodulespecifications) instead. +- If you need to get the list of all possible test files, use [`globTestSpecifications`](#globtestspecifications). +::: + ## mergeReports + +```ts +function mergeReports(directory?: string): Promise +``` + +Merge reports from multiple runs located in the specified directory (`--merge-reports` if not specified). This value can also be set on `config.mergeReports` (by default, it will read `.vitest-reports` folder). + +Note that the `directory` will always be resolved relative to the working directory. + +This method is called automatically by [`startVitest`](/advanced/guide/tests) if `config.mergeReports` is set. + ## collect -## listFiles + +```ts +function collect(filters?: string[]): Promise +``` + +Execute test files without running test callbacks. `collect` returns unhandled errors and an array of [test modules](/advanced/api/test-module). It accepts string filters to match the test files - these are the same filters that [CLI supports](/guide/filtering#cli). + +This method resolves tests specifications based on the config `include`, `exclude`, `includeSource` values. Read more at [`project.globTestFiles`](/advanced/api/test-project#globtestfiles). If `--changed` flag was specified, the list will be filtered to include only files that changed. + +::: warning +Note that Vitest doesn't use static analysis to collect tests. Vitest will run every test file in isolation, just like it runs regular tests. + +This makes this method very slow, unless you disable isolation before collecting tests. +::: + ## start + +```ts +function start(filters?: string[]): Promise +``` + +Initialize reporters, the coverage provider, and run tests. This method accepts string filters to match the test files - these are the same filters that [CLI supports](/guide/filtering#cli). + +::: warning +This method should not be called if [`vitest.init()`](#init) is also invoked. Use [`runTestSpecifications`](#runtestspecifications) or [`rerunTestSpecifications`](#reruntestspecifications) instead if you need to run tests after Vitest was inititalised. +::: + +This method is called automatically by [`startVitest`](/advanced/guide/tests) if `config.mergeReports` and `config.standalone` are not set. + ## init +```ts +function init(): Promise +``` + +Initialize reporters and the coverage provider. This method doesn't run any tests. If the `--watch` flag is provided, Vitest will still run changed tests even if this method was not called. + +Internally, this method is called only if [`--standalone`](/guide/cli#standalone) flag is enabled. + +::: warning +This method should not be called if [`vitest.start()`](#start) is also invoked. +::: + +This method is called automatically by [`startVitest`](/advanced/guide/tests) if `config.standalone` is set. + ## getModuleSpecifications ```ts @@ -217,6 +335,23 @@ function runTestSpecifications( This method emits `reporter.onWatcherRerun` and `onTestsRerun` events, then it runs tests with [`runTestSpecifications`](#runtestspecifications). If there were no errors in the main process, it will emit `reporter.onWatcherStart` event. ## collectTests + +```ts +function collectTests( + specifications: TestSpecification[] +): Promise +``` + +Execute test files without running test callbacks. `collectTests` returns unhandled errors and an array of [test modules](/advanced/api/test-module). + +This method works exactly the same as [`collect`](#collect), but you need to provide test specifications yourself. + +::: warning +Note that Vitest doesn't use static analysis to collect tests. Vitest will run every test file in isolation, just like it runs regular tests. + +This makes this method very slow, unless you disable isolation before collecting tests. +::: + ## cancelCurrentRun ```ts @@ -225,7 +360,50 @@ function cancelCurrentRun(reason: CancelReason): Promise This method will gracefully cancel all ongoing tests. It will wait for started tests to finish running and will not run tests that were scheduled to run but haven't started yet. -## updateSnapshot +## setGlobalTestNamePattern + +```ts +function setGlobalTestNamePattern(pattern: string | RegExp): void +``` + +This methods overrides the global [test name pattern](/config/#testnamepattern). + +::: warning +This method doesn't start running any tests. To run tests with updated pattern, call [`runTestSpecifications`](#runtestspecifications). +::: + +## resetGlobalTestNamePattern + +```ts +function resetGlobalTestNamePattern(): void +``` + +This methods resets the [test name pattern](/config/#testnamepattern). It means Vitest won't skip any tests now. + +::: warning +This method doesn't start running any tests. To run tests without a pattern, call [`runTestSpecifications`](#runtestspecifications). +::: + +## enableSnapshotUpdate + +```ts +function enableSnapshotUpdate(): void +``` + +Enable the mode that allows updating snapshots when running tests. Every test that runs after this method is called will update snapshots. To disable the mode, call [`resetSnapshotUpdate`](#resetsnapshotupdate). + +::: warning +This method doesn't start running any tests. To update snapshots, run tests with [`runTestSpecifications`](#runtestspecifications). +::: + +## resetSnapshotUpdate + +```ts +function resetSnapshotUpdate(): void +``` + +Disable the mode that allows updating snapshots when running tests. This method doesn't start running any tests. + ## invalidateFile ```ts From de8fa28a085c5f8f3ce6a2af0f523c045017ddc1 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 18:45:45 +0100 Subject: [PATCH 63/73] chore: automatically mark the test file as cached --- packages/browser/src/node/plugin.ts | 2 +- packages/vitest/src/node/core.ts | 2 ++ packages/vitest/src/node/project.ts | 18 ++++++++++++------ packages/vitest/src/node/specifications.ts | 4 ++-- packages/vitest/src/node/watcher.ts | 5 ++--- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/browser/src/node/plugin.ts b/packages/browser/src/node/plugin.ts index f52a5b0b0015..2d6c85d9bb1f 100644 --- a/packages/browser/src/node/plugin.ts +++ b/packages/browser/src/node/plugin.ts @@ -394,7 +394,7 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { { name: 'vitest:browser:in-source-tests', transform(code, id) { - if (!project.isTestFile(id) || !code.includes('import.meta.vitest')) { + if (!project.isCachedTestFile(id) || !code.includes('import.meta.vitest')) { return } const s = new MagicString(code, { filename: cleanUrl(id) }) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index c9f9ca7afa22..f45df1904463 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -618,6 +618,8 @@ export class Vitest { /** * Get test specifications assosiated with the given module. If module is not a test file, an empty array is returned. + * + * **Note:** this method relies on a cache generated by `globTestSpecifications`. If the file was not processed yet, use `project.matchesGlobPattern` instead. * @param moduleId The module ID to get test specifications for. */ public getModuleSpecifications(moduleId: string): TestSpecification[] { diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts index 64363a7aaf8d..864a3ed1bed4 100644 --- a/packages/vitest/src/node/project.ts +++ b/packages/vitest/src/node/project.ts @@ -375,8 +375,7 @@ export class TestProject { return isBrowserEnabled(this.config) } - /** @internal */ - _markTestFile(testPath: string): void { + private markTestFile(testPath: string): void { this.testFilesList?.push(testPath) } @@ -384,7 +383,7 @@ export class TestProject { * Returns if the file is a test file. Requires `.globTestFiles()` to be called first. * @internal */ - isTestFile(testPath: string): boolean { + isCachedTestFile(testPath: string): boolean { return !!this.testFilesList && this.testFilesList.includes(testPath) } @@ -392,7 +391,7 @@ export class TestProject { * Returns if the file is a typecheck test file. Requires `.globTestFiles()` to be called first. * @internal */ - isTypecheckFile(testPath: string): boolean { + isCachedTypecheckFile(testPath: string): boolean { return !!this.typecheckFilesList && this.typecheckFilesList.includes(testPath) } @@ -417,14 +416,18 @@ export class TestProject { } /** - * Test if a file matches the test globs. This does the actual glob matching unlike `isTestFile`. + * Test if a file matches the test globs. This does the actual glob matching if the test is not cached, unlike `isCachedTestFile`. */ public matchesTestGlob(moduleId: string, source?: () => string): boolean { + if (this.isCachedTestFile(moduleId)) { + return true + } const relativeId = relative(this.config.dir || this.config.root, moduleId) if (mm.isMatch(relativeId, this.config.exclude)) { return false } if (mm.isMatch(relativeId, this.config.include)) { + this.markTestFile(moduleId) return true } if ( @@ -432,7 +435,10 @@ export class TestProject { && mm.isMatch(relativeId, this.config.includeSource) ) { const code = source?.() || readFileSync(moduleId, 'utf-8') - return this.isInSourceTestCode(code) + if (this.isInSourceTestCode(code)) { + this.markTestFile(moduleId) + return true + } } return false } diff --git a/packages/vitest/src/node/specifications.ts b/packages/vitest/src/node/specifications.ts index e6b03db7ec2a..346300718006 100644 --- a/packages/vitest/src/node/specifications.ts +++ b/packages/vitest/src/node/specifications.ts @@ -21,10 +21,10 @@ export class VitestSpecifications { const specs: TestSpecification[] = [] for (const project of this.vitest.projects) { - if (project.isTestFile(moduleId)) { + if (project.isCachedTestFile(moduleId)) { specs.push(project.createSpecification(moduleId)) } - if (project.isTypecheckFile(moduleId)) { + if (project.isCachedTypecheckFile(moduleId)) { specs.push(project.createSpecification(moduleId, [], 'typescript')) } } diff --git a/packages/vitest/src/node/watcher.ts b/packages/vitest/src/node/watcher.ts index 79c40fc00896..5f420ec877db 100644 --- a/packages/vitest/src/node/watcher.ts +++ b/packages/vitest/src/node/watcher.ts @@ -87,7 +87,6 @@ export class VitestWatcher { this.vitest.projects.forEach((project) => { if (project.matchesTestGlob(id, () => (_fileContent = readFileSync(id, 'utf-8')))) { matchingProjects.push(project) - project._markTestFile(id) } }) @@ -124,7 +123,7 @@ export class VitestWatcher { if (!projects.length) { // if there are no modules it's possible that server was restarted // we don't have information about importers anymore, so let's check if the file is a test file at least - if (this.vitest.state.filesMap.has(filepath) || this.vitest.projects.some(project => project.isTestFile(filepath))) { + if (this.vitest.state.filesMap.has(filepath) || this.vitest.projects.some(project => project.isCachedTestFile(filepath))) { this.changedTests.add(filepath) return true } @@ -143,7 +142,7 @@ export class VitestWatcher { this.invalidates.add(filepath) // one of test files that we already run, or one of test files that we can run - if (this.vitest.state.filesMap.has(filepath) || project.isTestFile(filepath)) { + if (this.vitest.state.filesMap.has(filepath) || project.isCachedTestFile(filepath)) { this.changedTests.add(filepath) files.push(filepath) continue From 90dad97ed930463e558f30f181ca16e47c07dd2d Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 6 Dec 2024 18:46:01 +0100 Subject: [PATCH 64/73] docs: add an example with watcher to guide --- docs/advanced/guide/tests.md | 59 ++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/docs/advanced/guide/tests.md b/docs/advanced/guide/tests.md index 9028c42c0160..a59dfd1bf87a 100644 --- a/docs/advanced/guide/tests.md +++ b/docs/advanced/guide/tests.md @@ -70,3 +70,62 @@ finally { await vitest.close() } ``` + +If you intend to keep the `Vitest` instance, make sure to at least call [`init`](/advanced/api/vitest#init). This will initialise reporters and the coverage provider, but won't run any tests. It is also recommended to enable the `watch` mode even if you don't intend to use the Vitest watcher, but want to keep the instance running. Vitest relies on this flag for some of its features to work correctly in a continous process. + +After reporters are initialised, use [`runTestSpecifications`](/advanced/api/vitest#runtestspecifications) or [`rerunTestSpecifications`](/advanced/api/vitest#reruntestspecifications) to run tests if manual run is required: + +```ts +watcher.on('change', async (file) => { + const specifications = vitest.getModuleSpecifications(file) + if (specifications.length) { + vitest.invalidateFile(file) + // you can use runTestSpecifications if "reporter.onWatcher*" hooks + // should not be invoked + await vitest.rerunTestSpecifications(specifications) + } +}) +``` + +::: warning +The example above shows a potential usecase if you disable the default watcher behaviour. By default, Vitest already reruns tests if files change. + +Also note that `getModuleSpecifications` will not resolve test files unless they were already processed by `globTestSpecifications`. If the file was just created, use `project.matchesGlobPattern` instead: + +```ts +watcher.on('add', async (file) => { + const specifications = [] + for (const project of vitest.projects) { + if (project.matchesGlobPattern(file)) { + specifications.push(project.createSpecification(file)) + } + } + + if (specifications.length) { + await vitest.rerunTestSpecifications(specifications) + } +}) +``` +::: + +In cases where you need to disable the watcher, you can pass down `server.watch: null` since Vite 5.3 or `server.watch: { ignored: ['*/*'] }` to a Vite config: + +```ts +await createVitest( + 'test', + {}, + { + plugins: [ + { + name: 'stop-watcher', + async configureServer(server) { + await server.watcher.close() + } + } + ], + server: { + watch: null, + }, + } +) +``` From eed0e46ff72510411f1fe4eee8f957e6b9a02aa9 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 9 Dec 2024 09:46:55 +0100 Subject: [PATCH 65/73] chore: review --- docs/advanced/api/index.md | 23 ++++++++++++++++++++++- docs/advanced/api/test-project.md | 2 +- docs/advanced/api/vitest.md | 4 ++-- packages/vitest/src/node/core.ts | 2 +- packages/vitest/src/node/watcher.ts | 8 ++++---- 5 files changed, 30 insertions(+), 9 deletions(-) diff --git a/docs/advanced/api/index.md b/docs/advanced/api/index.md index cb727392dd30..1f85b52e83b8 100644 --- a/docs/advanced/api/index.md +++ b/docs/advanced/api/index.md @@ -8,6 +8,8 @@ title: Advanced API This guide lists advanced APIs to run tests via a Node.js script. If you just want to [run tests](/guide/), you probably don't need this. It is primarily used by library authors. ::: +You can import any method from the `vitest/node` entry-point. + ## startVitest ```ts @@ -91,7 +93,24 @@ function resolveConfig( }> ``` -This method resolves the config with custom parameters. If no parameters are gived, the `root` will be `process.cwd()`. +This method resolves the config with custom parameters. If no parameters are given, the `root` will be `process.cwd()`. + +```ts +import { resolveConfig } from 'vitest/node' + +// vitestConfig only has resolved "test" properties +const { vitestConfig, viteConfig } = await resolveConfig({ + mode: 'custom', + configFile: false, + resolve: { + conditions: ['custom'] + }, + test: { + setupFiles: ['/my-setup-file.js'], + pool: 'threads', + }, +}) +``` ::: info Due to how Vite's `createServer` works, Vitest has to resolve the config during the plugin's `configResolve` hook. Therefore, this method is not actually used internally and is exposed exclusively as a public API. @@ -101,6 +120,8 @@ If you pass down the config to the `startVitest` or `createVitest` APIs, Vitest ::: warning The `resolveConfig` doesn't resolve the `workspace`. To resolve workspace configs, Vitest needs an established Vite server. + +Also note that `viteConfig.test` will not be fully resolved. If you need Vitest config, use `vitestConfig` instead. ::: ## parseCLI diff --git a/docs/advanced/api/test-project.md b/docs/advanced/api/test-project.md index 2f23964d5757..fbdaf85c1ad3 100644 --- a/docs/advanced/api/test-project.md +++ b/docs/advanced/api/test-project.md @@ -68,7 +68,7 @@ const config: SerializedConfig = vitest.projects[0].serializedConfig The `serializedConfig` property is a getter. Every time it's accessed Vitest serializes the config again in case it was changed. This also means that it always returns a different reference: ```ts -project.serializedConfig === project.serializedConfig // false +project.serializedConfig === project.serializedConfig // ❌ ``` ::: diff --git a/docs/advanced/api/vitest.md b/docs/advanced/api/vitest.md index b93d6179c822..58246b2a0170 100644 --- a/docs/advanced/api/vitest.md +++ b/docs/advanced/api/vitest.md @@ -89,7 +89,7 @@ This is a global [`ViteDevServer`](https://vite.dev/guide/api-javascript#vitedev Public `state` is an experimental API (except `vitest.state.getReportedEntity`). Breaking changes might not follow SemVer, please pin Vitest's version when using it. ::: -Global state stores information about the current tests. It uses the same API from `@vitest/runner` by default, but we recommend using the [Reported API](/advanced/reporters#reported-tasks) instead by calling `state.getReportedEntity()` on the `@vitest/runner` API: +Global state stores information about the current tests. It uses the same API from `@vitest/runner` by default, but we recommend using the [Reported Tasks API](/advanced/reporters#reported-tasks) instead by calling `state.getReportedEntity()` on the `@vitest/runner` API: ```ts const task = vitest.state.idMap.get(taskId) // old API @@ -248,7 +248,7 @@ function collect(filters?: string[]): Promise Execute test files without running test callbacks. `collect` returns unhandled errors and an array of [test modules](/advanced/api/test-module). It accepts string filters to match the test files - these are the same filters that [CLI supports](/guide/filtering#cli). -This method resolves tests specifications based on the config `include`, `exclude`, `includeSource` values. Read more at [`project.globTestFiles`](/advanced/api/test-project#globtestfiles). If `--changed` flag was specified, the list will be filtered to include only files that changed. +This method resolves tests specifications based on the config `include`, `exclude`, and `includeSource` values. Read more at [`project.globTestFiles`](/advanced/api/test-project#globtestfiles). If `--changed` flag was specified, the list will be filtered to include only files that changed. ::: warning Note that Vitest doesn't use static analysis to collect tests. Vitest will run every test file in isolation, just like it runs regular tests. diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index f45df1904463..3b7f018a62ea 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -1215,7 +1215,7 @@ export class Vitest { } /** - * Register a handler that will be called when the tests are reruning. + * Register a handler that will be called when the tests are rerunning. */ onTestsRerun(fn: OnTestsRerunHandler): void { this._onUserTestsRerun.push(fn) diff --git a/packages/vitest/src/node/watcher.ts b/packages/vitest/src/node/watcher.ts index 5f420ec877db..4d721e91c283 100644 --- a/packages/vitest/src/node/watcher.ts +++ b/packages/vitest/src/node/watcher.ts @@ -81,11 +81,11 @@ export class VitestWatcher { private onAdd = (id: string): void => { id = slash(id) this.vitest.invalidateFile(id) - let _fileContent: string | undefined + let fileContent: string | undefined const matchingProjects: TestProject[] = [] this.vitest.projects.forEach((project) => { - if (project.matchesTestGlob(id, () => (_fileContent = readFileSync(id, 'utf-8')))) { + if (project.matchesTestGlob(id, () => (fileContent ??= readFileSync(id, 'utf-8')))) { matchingProjects.push(project) } }) @@ -155,8 +155,8 @@ export class VitestWatcher { return } - const heedsRerun = this.handleFileChanged(i.file) - if (heedsRerun) { + const needsRerun = this.handleFileChanged(i.file) + if (needsRerun) { rerun = true } }) From f8a6e9bd6b2f2819b5c977dfbda75af0507be4be Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 9 Dec 2024 09:56:44 +0100 Subject: [PATCH 66/73] docs: expose the selector string --- packages/browser/context.d.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/browser/context.d.ts b/packages/browser/context.d.ts index c332b1df9de9..a2c3eea6cadc 100644 --- a/packages/browser/context.d.ts +++ b/packages/browser/context.d.ts @@ -302,6 +302,22 @@ interface LocatorSelectors { } export interface Locator extends LocatorSelectors { + /** + * Selector string that will be used to locate the element by the browser provider. + * You can use this string in the commands API: + * ```ts + * // playwright + * function test({ selector, iframe }) { + * await iframe.locator(selector).click() + * } + * // webdriverio + * function test({ selector, browser }) { + * await browser.$(selector).click() + * } + * ``` + */ + selector: string + /** * Click on an element. You can use the options to set the cursor position. * @see {@link https://vitest.dev/guide/browser/interactivity-api#userevent-click} From 491fc3c8f9836bbda979f978ca32e219ce7230b3 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 9 Dec 2024 10:10:03 +0100 Subject: [PATCH 67/73] docs: locator's selector --- docs/guide/browser/locators.md | 32 ++++++++++++++++++++++++++++++++ packages/browser/context.d.ts | 3 ++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/docs/guide/browser/locators.md b/docs/guide/browser/locators.md index b5a91ab83996..f517c46d14d5 100644 --- a/docs/guide/browser/locators.md +++ b/docs/guide/browser/locators.md @@ -708,3 +708,35 @@ This method returns an array of new locators that match the selector. Internally, this method calls `.elements` and wraps every element using [`page.elementLocator`](/guide/browser/context#page). - [See `locator.elements()`](#elements) + +## Properties + +### selector + +The `selector` is a string that will be used to locate the element by the browser provider. Playwright will use a `playwright` locator syntax while `preview` and `webdriverio` will use CSS. + +::: danger +You should not use this string in your test code. The `selector` string should only be used when working with the Commands API: + +```ts [commands.ts] +import type { BrowserCommand } from '@vitest/browser' + +const test: BrowserCommand = function test(context, selector) { + // playwright + await context.iframe.locator(selector).click() + // webdriverio + await context.browser.$(selector).click() +} +``` + +```ts [example.test.ts] +import { test } from 'vitest' +import { commands, page } from '@vitest/browser/context' + +test('works correctly', async () => { + await commands.test(page.getByText('Hello').selector) // ✅ + // vitest will automatically unwrap it to a string + await commands.test(page.getByText('Hello')) // ✅ +}) +``` +::: diff --git a/packages/browser/context.d.ts b/packages/browser/context.d.ts index a2c3eea6cadc..866caee4a7e2 100644 --- a/packages/browser/context.d.ts +++ b/packages/browser/context.d.ts @@ -315,8 +315,9 @@ export interface Locator extends LocatorSelectors { * await browser.$(selector).click() * } * ``` + * @see {@link https://vitest.dev/guide/browser/locators#selector} */ - selector: string + readonly selector: string /** * Click on an element. You can use the options to set the cursor position. From 92f0f15511bad87a7e855f12e87338d959427a6f Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 9 Dec 2024 10:10:11 +0100 Subject: [PATCH 68/73] feat: unwrap locator into a string --- packages/browser/src/client/tester/locators/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/browser/src/client/tester/locators/index.ts b/packages/browser/src/client/tester/locators/index.ts index f87a94fc945b..665ff43d616b 100644 --- a/packages/browser/src/client/tester/locators/index.ts +++ b/packages/browser/src/client/tester/locators/index.ts @@ -185,6 +185,14 @@ export abstract class Locator { return this.elements().map(element => this.elementLocator(element)) } + public toString(): string { + return this.selector + } + + public toJSON(): string { + return this.selector + } + private get state(): BrowserRunnerState { return getBrowserState() } From 085bf5bb7da2f60ce2156e0c69b7338f411881c2 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 9 Dec 2024 10:10:50 +0100 Subject: [PATCH 69/73] docs: fix import --- docs/guide/browser/locators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/browser/locators.md b/docs/guide/browser/locators.md index f517c46d14d5..be8ed5e5cf7e 100644 --- a/docs/guide/browser/locators.md +++ b/docs/guide/browser/locators.md @@ -719,7 +719,7 @@ The `selector` is a string that will be used to locate the element by the browse You should not use this string in your test code. The `selector` string should only be used when working with the Commands API: ```ts [commands.ts] -import type { BrowserCommand } from '@vitest/browser' +import type { BrowserCommand } from 'vitest/node' const test: BrowserCommand = function test(context, selector) { // playwright From c5cbaa0d40df4ff3f25ce56dec57cea9eade320a Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 10 Dec 2024 10:45:35 +0100 Subject: [PATCH 70/73] docs: remove old `start` method --- docs/advanced/api/vitest.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/docs/advanced/api/vitest.md b/docs/advanced/api/vitest.md index 58246b2a0170..54bfc825a268 100644 --- a/docs/advanced/api/vitest.md +++ b/docs/advanced/api/vitest.md @@ -63,14 +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. -## start - -```ts -function start(filters: string[]): Promise -``` - -You can start running tests or benchmarks with `start` method. You can pass an array of strings to filter test files. - ## config The root (or global) config. If workspace feature is enabled, projects will reference this as `globalConfig`. From c9e14c2fcf5cc34ee5862e49a25b270ee627c322 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 10 Dec 2024 10:48:41 +0100 Subject: [PATCH 71/73] chore: fix deps --- docs/package.json | 4 +-- pnpm-lock.yaml | 86 +++++++++++++++++++++++------------------------ 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/docs/package.json b/docs/package.json index 85273cf5bfa5..a77a7a57632d 100644 --- a/docs/package.json +++ b/docs/package.json @@ -20,8 +20,8 @@ "devDependencies": { "@iconify-json/carbon": "^1.2.4", "@iconify-json/logos": "^1.2.3", - "@shikijs/transformers": "^1.24.0", - "@shikijs/vitepress-twoslash": "^1.24.1", + "@shikijs/transformers": "^1.24.2", + "@shikijs/vitepress-twoslash": "^1.24.2", "@unocss/reset": "^0.65.1", "@vite-pwa/assets-generator": "^0.2.6", "@vite-pwa/vitepress": "^0.5.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 89d2f2ac5907..e3ee66946cff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -141,11 +141,11 @@ importers: specifier: ^1.2.3 version: 1.2.3 '@shikijs/transformers': - specifier: ^1.24.0 - version: 1.24.0 + specifier: ^1.24.2 + version: 1.24.2 '@shikijs/vitepress-twoslash': - specifier: ^1.24.1 - version: 1.24.1(typescript@5.7.2) + specifier: ^1.24.2 + version: 1.24.2(typescript@5.7.2) '@unocss/reset': specifier: ^0.65.1 version: 0.65.1 @@ -3376,35 +3376,35 @@ packages: '@shikijs/core@1.22.2': resolution: {integrity: sha512-bvIQcd8BEeR1yFvOYv6HDiyta2FFVePbzeowf5pPS1avczrPK+cjmaxxh0nx5QzbON7+Sv0sQfQVciO7bN72sg==} - '@shikijs/core@1.24.1': - resolution: {integrity: sha512-3q/9oarMVcLqJ+NQOdKL40dJVq/UKCsiWXz3QRQPBglHqa8dDJ0p6TuMuk2gHphy5FZcvFtg4UHBgpW0JtZ8+A==} + '@shikijs/core@1.24.2': + resolution: {integrity: sha512-BpbNUSKIwbKrRRA+BQj0BEWSw+8kOPKDJevWeSE/xIqGX7K0xrCZQ9kK0nnEQyrzsUoka1l81ZtJ2mGaCA32HQ==} '@shikijs/engine-javascript@1.22.2': resolution: {integrity: sha512-iOvql09ql6m+3d1vtvP8fLCVCK7BQD1pJFmHIECsujB0V32BJ0Ab6hxk1ewVSMFA58FI0pR2Had9BKZdyQrxTw==} - '@shikijs/engine-javascript@1.24.1': - resolution: {integrity: sha512-lNgUSHYDYaQ6daj4lJJqcY2Ru9LgHwpFoposJkRVRPh21Yg4kaPFRhzaWoSg3PliwcDOpDuMy3xsmQaJp201Fg==} + '@shikijs/engine-javascript@1.24.2': + resolution: {integrity: sha512-EqsmYBJdLEwEiO4H+oExz34a5GhhnVp+jH9Q/XjPjmBPc6TE/x4/gD0X3i0EbkKKNqXYHHJTJUpOLRQNkEzS9Q==} '@shikijs/engine-oniguruma@1.22.2': resolution: {integrity: sha512-GIZPAGzQOy56mGvWMoZRPggn0dTlBf1gutV5TdceLCZlFNqWmuc7u+CzD0Gd9vQUTgLbrt0KLzz6FNprqYAxlA==} - '@shikijs/engine-oniguruma@1.24.1': - resolution: {integrity: sha512-KdrTIBIONWd+Xs61eh8HdIpfigtrseat9dpARvaOe2x0g/FNTbwbkGr3y92VSOVD1XotzEskh3v/nCzyWjkf7g==} + '@shikijs/engine-oniguruma@1.24.2': + resolution: {integrity: sha512-ZN6k//aDNWRJs1uKB12pturKHh7GejKugowOFGAuG7TxDRLod1Bd5JhpOikOiFqPmKjKEPtEA6mRCf7q3ulDyQ==} - '@shikijs/transformers@1.24.0': - resolution: {integrity: sha512-Qf/hby+PRPkoHncjYnJf5svK1aCsOUtQhuLzKPnmeXJtuUZCmbH0pTpdNtXe9tgln/RHlyRJnv7q46HHS1sO0Q==} + '@shikijs/transformers@1.24.2': + resolution: {integrity: sha512-cIwn8YSwO3bsWKJ+pezcXY1Vq0BVwvuLes1TZSC5+Awi6Tsfqhf3vBahOIqZK1rraMKOti2VEAEF/95oXMig1w==} - '@shikijs/twoslash@1.24.1': - resolution: {integrity: sha512-TbXYtUREusATSCAWLw5dSwmc54Ga9wYF1gTfrOTEQJB3iFejtjA6VFZSpIGnmnQemVr4NNBTK6+4yxcFIZXD7A==} + '@shikijs/twoslash@1.24.2': + resolution: {integrity: sha512-zcwYUNdSQDKquF1t+XrtoXM+lx9rCldAkZnT+e5fULKlLT6F8/F9fwICGhBm9lWp5/U4NptH+YcJUdvFOR0SRg==} '@shikijs/types@1.22.2': resolution: {integrity: sha512-NCWDa6LGZqTuzjsGfXOBWfjS/fDIbDdmVDug+7ykVe1IKT4c1gakrvlfFYp5NhAXH/lyqLM8wsAPo5wNy73Feg==} - '@shikijs/types@1.24.1': - resolution: {integrity: sha512-ZwZFbShFY/APfKNt3s9Gv8rhTm29GodSKsOW66X6N+HGsZuaHalE1VUEX4fv93UXHTZTLjb3uxn63F96RhGfXw==} + '@shikijs/types@1.24.2': + resolution: {integrity: sha512-bdeWZiDtajGLG9BudI0AHet0b6e7FbR0EsE4jpGaI0YwHm/XJunI9+3uZnzFtX65gsyJ6ngCIWUfA4NWRPnBkQ==} - '@shikijs/vitepress-twoslash@1.24.1': - resolution: {integrity: sha512-85xpDj8fr0Gl4TJG+Q3F7+FAoPv9RO+ZwdU49fqqW1beYPPcJecvvCeb928fRhziD7k9KSkkiaOav1eif0WIig==} + '@shikijs/vitepress-twoslash@1.24.2': + resolution: {integrity: sha512-twOKyYay+ra3xBxbQhMIBM9Y3ZzZg18NAv529AL+r3p2kbDm7Lh623C9eSDsfZvWT9xCEZzaI6DEACT4YUPSuA==} '@shikijs/vscode-textmate@9.3.0': resolution: {integrity: sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==} @@ -8342,8 +8342,8 @@ packages: shiki@1.22.2: resolution: {integrity: sha512-3IZau0NdGKXhH2bBlUk4w1IHNxPh6A5B2sUpyY+8utLu2j/h1QpFkAaUA1bAMxOWWGtTWcAh531vnS4NJKS/lA==} - shiki@1.24.1: - resolution: {integrity: sha512-/qByWMg05+POb63c/OvnrU17FcCUa34WU4F6FCrd/mjDPEDPl8YUNRkRMbo8l3iYMLydfCgxi1r37JFoSw8A4A==} + shiki@1.24.2: + resolution: {integrity: sha512-TR1fi6mkRrzW+SKT5G6uKuc32Dj2EEa7Kj0k8kGqiBINb+C1TiflVOiT9ta6GqOJtC4fraxO5SLUaKBcSY38Fg==} side-channel@1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} @@ -11738,11 +11738,11 @@ snapshots: '@types/hast': 3.0.4 hast-util-to-html: 9.0.3 - '@shikijs/core@1.24.1': + '@shikijs/core@1.24.2': dependencies: - '@shikijs/engine-javascript': 1.24.1 - '@shikijs/engine-oniguruma': 1.24.1 - '@shikijs/types': 1.24.1 + '@shikijs/engine-javascript': 1.24.2 + '@shikijs/engine-oniguruma': 1.24.2 + '@shikijs/types': 1.24.2 '@shikijs/vscode-textmate': 9.3.0 '@types/hast': 3.0.4 hast-util-to-html: 9.0.3 @@ -11753,9 +11753,9 @@ snapshots: '@shikijs/vscode-textmate': 9.3.0 oniguruma-to-js: 0.4.3 - '@shikijs/engine-javascript@1.24.1': + '@shikijs/engine-javascript@1.24.2': dependencies: - '@shikijs/types': 1.24.1 + '@shikijs/types': 1.24.2 '@shikijs/vscode-textmate': 9.3.0 oniguruma-to-es: 0.7.0 @@ -11764,19 +11764,19 @@ snapshots: '@shikijs/types': 1.22.2 '@shikijs/vscode-textmate': 9.3.0 - '@shikijs/engine-oniguruma@1.24.1': + '@shikijs/engine-oniguruma@1.24.2': dependencies: - '@shikijs/types': 1.24.1 + '@shikijs/types': 1.24.2 '@shikijs/vscode-textmate': 9.3.0 - '@shikijs/transformers@1.24.0': + '@shikijs/transformers@1.24.2': dependencies: - shiki: 1.24.0 + shiki: 1.24.2 - '@shikijs/twoslash@1.24.1(typescript@5.7.2)': + '@shikijs/twoslash@1.24.2(typescript@5.7.2)': dependencies: - '@shikijs/core': 1.24.1 - '@shikijs/types': 1.24.1 + '@shikijs/core': 1.24.2 + '@shikijs/types': 1.24.2 twoslash: 0.2.12(typescript@5.7.2) transitivePeerDependencies: - supports-color @@ -11787,19 +11787,19 @@ snapshots: '@shikijs/vscode-textmate': 9.3.0 '@types/hast': 3.0.4 - '@shikijs/types@1.24.1': + '@shikijs/types@1.24.2': dependencies: '@shikijs/vscode-textmate': 9.3.0 '@types/hast': 3.0.4 - '@shikijs/vitepress-twoslash@1.24.1(typescript@5.7.2)': + '@shikijs/vitepress-twoslash@1.24.2(typescript@5.7.2)': dependencies: - '@shikijs/twoslash': 1.24.1(typescript@5.7.2) + '@shikijs/twoslash': 1.24.2(typescript@5.7.2) floating-vue: 5.2.2(vue@3.5.13(typescript@5.7.2)) mdast-util-from-markdown: 2.0.2 mdast-util-gfm: 3.0.0 mdast-util-to-hast: 13.2.0 - shiki: 1.24.1 + shiki: 1.24.2 twoslash: 0.2.12(typescript@5.7.2) twoslash-vue: 0.2.12(typescript@5.7.2) vue: 3.5.13(typescript@5.7.2) @@ -17809,12 +17809,12 @@ snapshots: '@shikijs/vscode-textmate': 9.3.0 '@types/hast': 3.0.4 - shiki@1.24.1: + shiki@1.24.2: dependencies: - '@shikijs/core': 1.24.1 - '@shikijs/engine-javascript': 1.24.1 - '@shikijs/engine-oniguruma': 1.24.1 - '@shikijs/types': 1.24.1 + '@shikijs/core': 1.24.2 + '@shikijs/engine-javascript': 1.24.2 + '@shikijs/engine-oniguruma': 1.24.2 + '@shikijs/types': 1.24.2 '@shikijs/vscode-textmate': 9.3.0 '@types/hast': 3.0.4 @@ -18872,7 +18872,7 @@ snapshots: '@docsearch/js': 3.6.2(@algolia/client-search@4.20.0)(@types/react@18.2.79)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.9.0) '@iconify-json/simple-icons': 1.2.11 '@shikijs/core': 1.22.2 - '@shikijs/transformers': 1.24.0 + '@shikijs/transformers': 1.24.2 '@shikijs/types': 1.22.2 '@types/markdown-it': 14.1.2 '@vitejs/plugin-vue': 5.2.1(vite@5.4.0(@types/node@22.10.1)(terser@5.36.0))(vue@3.5.12(typescript@5.7.2)) From 23602f048bf9ee05aac1537bed51503479ec9d3b Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 10 Dec 2024 10:54:03 +0100 Subject: [PATCH 72/73] chore: lint --- test/coverage-test/test/isolation.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/coverage-test/test/isolation.test.ts b/test/coverage-test/test/isolation.test.ts index 7a84b419b461..8c8cd3c89896 100644 --- a/test/coverage-test/test/isolation.test.ts +++ b/test/coverage-test/test/isolation.test.ts @@ -1,4 +1,4 @@ -import type { WorkspaceSpec } from 'vitest/node' +import type { TestSpecification } from 'vitest/node' import { expect, test } from 'vitest' import { readCoverageMap, runVitest } from '../utils' @@ -55,7 +55,7 @@ for (const isolate of [true, false]) { } class Sorter { - sort(files: WorkspaceSpec[]) { + sort(files: TestSpecification[]) { return files.sort((a) => { if (a.moduleId.includes('isolation-1')) { return -1 @@ -64,7 +64,7 @@ class Sorter { }) } - shard(files: WorkspaceSpec[]) { + shard(files: TestSpecification[]) { return files } } From 2ad4d6fa839f296fa68c829f8a75178da395237d Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 10 Dec 2024 11:12:25 +0100 Subject: [PATCH 73/73] docs: correction --- docs/advanced/api/vitest.md | 2 +- packages/vitest/src/node/cli/cli-config.ts | 2 +- packages/vitest/src/node/core.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/advanced/api/vitest.md b/docs/advanced/api/vitest.md index 54bfc825a268..cb8582ba970f 100644 --- a/docs/advanced/api/vitest.md +++ b/docs/advanced/api/vitest.md @@ -226,7 +226,7 @@ This method can be slow because it needs to filter `--changed` flags. Do not use function mergeReports(directory?: string): Promise ``` -Merge reports from multiple runs located in the specified directory (`--merge-reports` if not specified). This value can also be set on `config.mergeReports` (by default, it will read `.vitest-reports` folder). +Merge reports from multiple runs located in the specified directory (value from `--merge-reports` if not specified). This value can also be set on `config.mergeReports` (by default, it will read `.vitest-reports` folder). Note that the `directory` will always be resolved relative to the working directory. diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index 48a27ad7578a..57f7f0186bc2 100644 --- a/packages/vitest/src/node/cli/cli-config.ts +++ b/packages/vitest/src/node/cli/cli-config.ts @@ -802,7 +802,7 @@ export const cliOptionsConfig: VitestCLIOptions = { }, mergeReports: { description: - 'Paths to blob reports directory. If this options is used, Vitest won\'t run any tests, it will only report previously recorded tests', + 'Path to a blob reports directory. If this options is used, Vitest won\'t run any tests, it will only report previously recorded tests', argument: '[path]', transform(value) { if (!value || typeof value === 'boolean') { diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 3b7f018a62ea..927c2005efaf 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -433,7 +433,7 @@ export class Vitest { } /** - * Merge reports from multiple runs located in the specified directory (`--merge-reports` by default). + * Merge reports from multiple runs located in the specified directory (value from `--merge-reports` if not specified). */ public async mergeReports(directory?: string): Promise { if (this.reporters.some(r => r instanceof BlobReporter)) {