diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 388181400ce4..246443e63853 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())) { @@ -146,7 +148,7 @@ export default ({ mode }: { mode: string }) => { items: [ { text: 'Advanced API', - link: '/advanced/api', + link: '/advanced/api/', activeMatch: '^/advanced/', }, { @@ -243,7 +245,15 @@ export default ({ mode }: { mode: string }) => { }, ], }, - footer(), + { + items: [ + ...footer(), + { + text: 'Node API Reference', + link: '/advanced/api/', + }, + ], + }, ], '/advanced': [ { @@ -251,8 +261,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 +330,9 @@ export default ({ mode }: { mode: string }) => { }, ], }, - footer(), + { + items: footer(), + }, ], '/team': [], '/': [ @@ -308,7 +358,7 @@ export default ({ mode }: { mode: string }) => { link: '/guide/browser', }, { - text: 'Advanced API', + text: 'Node API Reference', link: '/advanced/api', }, { @@ -325,19 +375,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[] { @@ -423,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/.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.md b/docs/advanced/api.md deleted file mode 100644 index fafde8716ff2..000000000000 --- a/docs/advanced/api.md +++ /dev/null @@ -1,346 +0,0 @@ ---- -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: - -```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. - -### `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. -::: - -::: 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) -} -``` -::: - -## TestProject 3.0.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`. 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. - -### 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()`. - -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. - -### 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 -``` - -### 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. 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/index.md b/docs/advanced/api/index.md new file mode 100644 index 000000000000..1f85b52e83b8 --- /dev/null +++ b/docs/advanced/api/index.md @@ -0,0 +1,151 @@ +--- +title: Advanced API +--- + +# 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. +::: + +You can import any method from the `vitest/node` entry-point. + +## 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/api/test-module) 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. +::: + +## 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 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. + +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. + +Also note that `viteConfig.test` will not be fully resolved. If you need Vitest config, use `vitestConfig` instead. +::: + +## 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..3f5046685f89 --- /dev/null +++ b/docs/advanced/api/test-case.md @@ -0,0 +1,281 @@ +# TestCase + +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 +} +``` + +::: 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. + +## 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' + +// [!code word:'the validation works correctly'] +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 +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', () => { + // ... + }) +}) +``` + +## 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 3: + +```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 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', () => { + // ... +}) +``` + +## 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 +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 {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 skipped during collection, not finished yet or was just collected. + +If the test was skipped, the return value will be `TestResultSkipped`: + +```ts +interface TestResultSkipped { + /** + * The test was skipped with `skip` or `todo` flag. + * You can see which one was used in the `options.mode` option. + */ + state: 'skipped' + /** + * Skipped tests have no errors. + */ + errors: undefined + /** + * A custom note passed down to `ctx.skip(note)`. + */ + note: string | undefined +} +``` + +::: tip +If the test was skipped because another test has `only` flag, the `options.mode` will be equal to `skip`. +::: + +If the test failed, the return value will be `TestResultFailed`: + +```ts +interface TestResultFailed { + /** + * The test failed to execute. + */ + state: 'failed' + /** + * Errors that were thrown during the test execution. + */ + errors: TestError[] +} +``` + +If the test passed, the retunr value will be `TestResultPassed`: + +```ts +interface TestResultPassed { + /** + * The test passed successfully. + */ + state: 'passed' + /** + * Errors that were thrown during the test execution. + */ + errors: TestError[] | undefined +} +``` + +::: 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`. + */ + 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..974f37dbd11d --- /dev/null +++ b/docs/advanced/api/test-collection.md @@ -0,0 +1,93 @@ +# TestCollection + +`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 child of module.children) { + console.log(child.type, child.name) +} +``` +::: + +## size + +The number of tests and suites in the collection. + +::: warning +This number includes only tests and suites at the top-level, it doesn't include nested suites and tests. +::: + +## at + +```ts +function at(index: number): TestCase | TestSuite | undefined +``` + +Returns the test or suite at a specific index. This method accepts negative indexes. + +## 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 new file mode 100644 index 000000000000..d3f69f0c4bb7 --- /dev/null +++ b/docs/advanced/api/test-module.md @@ -0,0 +1,74 @@ +# 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 +if (task.type === 'module') { + task // TestModule +} +``` + +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. + +## 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. + */ + 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..fbdaf85c1ad3 --- /dev/null +++ b/docs/advanced/api/test-project.md @@ -0,0 +1,315 @@ +--- +title: TestProject +--- + +# TestProject 3.0.0 {#testproject} + +- **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. +::: + +## 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 // ❌ +``` +::: + +## 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.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. +::: + +## 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[], +): 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` 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' +import { resolve } from 'node:path/posix' + +const vitest = await createVitest('test') +const project = vitest.projects[0] +const specification = project.createSpecification( + resolve('./example.test.ts'), + [20, 40], // optional test lines +) +await vitest.runTestSpecifications([specification]) +``` + +::: warning +`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. +::: + +## isRootProject + +```ts +function isRootProject(): boolean +``` + +Checks if the current project is the root project. You can also get the root project by calling [`vitest.getRootProject()`](#getrootproject). + +## 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. + +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. + +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 +``` + +## 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 +function onTestsRerun(cb: OnTestsRerunHandler): void +``` + +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) => { + console.log(specs) +}) +``` + +## isBrowserEnabled + +```ts +function isBrowserEnabled(): boolean +``` + +Returns `true` if this project runs tests in the browser. + +## 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..3fefba0c8954 --- /dev/null +++ b/docs/advanced/api/test-specification.md @@ -0,0 +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). diff --git a/docs/advanced/api/test-suite.md b/docs/advanced/api/test-suite.md new file mode 100644 index 000000000000..0a7ddb4d13e4 --- /dev/null +++ b/docs/advanced/api/test-suite.md @@ -0,0 +1,193 @@ +# 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 +} +``` + +::: 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. + +## 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 3: + +```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 +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. +::: + +## 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 +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/advanced/api/vitest.md b/docs/advanced/api/vitest.md new file mode 100644 index 000000000000..cb8582ba970f --- /dev/null +++ b/docs/advanced/api/vitest.md @@ -0,0 +1,513 @@ +--- +outline: deep +title: Vitest API +--- + +# Vitest + +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 + +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 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. + +## 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 experimental + +::: warning +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 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 +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. + +## 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, 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: + +```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.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: + +```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`](/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 + +```ts +function getProvidedContext(): ProvidedContext +``` + +This returns the root context object. This is a shorthand for `vitest.getRootProject().getProvidedContext`. + +## getProjectByName + +```ts +function getProjectByName(name: string): TestProject +``` + +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 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 + +```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 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 +const specifications = await vitest.globTestSpecifications(['my-filter']) +// [TestSpecification{ moduleId: '/tests/my-filter.test.ts' }] +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 (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. + +This method is called automatically by [`startVitest`](/advanced/guide/tests) if `config.mergeReports` is set. + +## collect + +```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`, 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. + +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 +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 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 + +```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 + +```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 +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. + +## 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 +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. +::: + +## 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 +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. diff --git a/docs/advanced/guide/tests.md b/docs/advanced/guide/tests.md index ce768f7815f3..a59dfd1bf87a 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,15 +57,75 @@ 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() } ``` + +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, + }, + } +) +``` diff --git a/docs/advanced/pool.md b/docs/advanced/pool.md index 36c5a6c9cdd7..35b0cfeeb717 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 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 49586526003c..494c1fba1a11 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)`: @@ -67,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) @@ -85,300 +88,6 @@ class MyReporter implements Reporter { } } ``` -::: - -### TestCase - -`TestCase` represents a single test. - -```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 - /** - * 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 - /** - * 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' - /** - * Task instance. - * @experimental Public task API is experimental and does not follow semver. - */ - readonly task: RunnerTestSuite - /** - * 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 -} -``` - -### TestModule - -`TestModule` represents a single file that contains suites and tests. - -```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. - */ - 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..0e4e2be53b38 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. @@ -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. */ @@ -179,14 +216,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 */ @@ -242,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' @@ -270,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', () => { @@ -297,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/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/docs/guide/browser/locators.md b/docs/guide/browser/locators.md index b5a91ab83996..be8ed5e5cf7e 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/node' + +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/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` diff --git a/docs/guide/cli.md b/docs/guide/cli.md index b4ac323c2187..062ddcd27d1c 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 3, 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 # ❌ ``` ::: diff --git a/docs/guide/migration.md b/docs/guide/migration.md index ea1398229a4e..cfed94061c6e 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 API {#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 { @@ -42,11 +42,21 @@ 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`. -## 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 +338,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/docs/package.json b/docs/package.json index 9949cae14cdd..a77a7a57632d 100644 --- a/docs/package.json +++ b/docs/package.json @@ -20,7 +20,8 @@ "devDependencies": { "@iconify-json/carbon": "^1.2.4", "@iconify-json/logos": "^1.2.3", - "@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/eslint.config.js b/eslint.config.js index fca823e74f11..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', ], }, { @@ -107,6 +109,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/browser/context.d.ts b/packages/browser/context.d.ts index c332b1df9de9..866caee4a7e2 100644 --- a/packages/browser/context.d.ts +++ b/packages/browser/context.d.ts @@ -302,6 +302,23 @@ 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() + * } + * ``` + * @see {@link https://vitest.dev/guide/browser/locators#selector} + */ + readonly 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} 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() } 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/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/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/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/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 44c1baf3b189..bb3fa918ee8f 100644 --- a/packages/runner/src/run.ts +++ b/packages/runner/src/run.ts @@ -1,8 +1,7 @@ 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, 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') { @@ -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..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' /** @@ -39,7 +38,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 } @@ -140,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 db53a597d28b..1c593cb744ae 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[] /** @@ -117,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[] /** @@ -208,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 } @@ -217,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 } /** @@ -232,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 @@ -317,7 +320,7 @@ interface TestForFunction { // test.for([[1, 2], [3, 4, 5]]) (cases: ReadonlyArray): TestForFunctionReturn< T, - ExtendedContext & ExtraContext + TestContext & ExtraContext > // test.for` @@ -327,7 +330,7 @@ interface TestForFunction { // ` (strings: TemplateStringsArray, ...values: any[]): TestForFunctionReturn< any, - ExtendedContext & ExtraContext + TestContext & ExtraContext > } @@ -460,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 @@ -522,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 } @@ -559,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 { @@ -594,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 */ @@ -622,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/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/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/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 ea085f4376ef..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) @@ -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, @@ -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/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/cac.ts b/packages/vitest/src/node/cli/cac.ts index d1c8e78b103d..b0a8de330fb4 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 } { @@ -247,11 +247,14 @@ async function benchmark(cliFilters: string[], options: CliOptions): Promise filter.includes(':'))) { + argv.includeTaskLocation ??= true + } return argv } @@ -264,9 +267,9 @@ 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() + const ctx = await startVitest(mode, cliFilters.map(normalize), normalizeCliOptions(cliFilters, options)) + if (!ctx.shouldKeepServer()) { + await ctx.exit() } } catch (e) { @@ -302,12 +305,12 @@ 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, }) 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') @@ -320,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/cli/cli-api.ts b/packages/vitest/src/node/cli/cli-api.ts index a0fb736c518c..4272c30064ad 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,14 @@ 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 || [] - errors.forEach((error) => { + suite.errors().forEach((error) => { errorsPrinted = true ctx.logger.printError(error, { - project: ctx.getProjectByName(suite.file.projectName), + project: suite.project, }) }) }) @@ -192,7 +190,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 +198,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 +209,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 +222,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 +232,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 +244,62 @@ 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 | TestModule) => void) { + modules.forEach((testModule) => { + callback(testModule) + for (const suite of testModule.children.allSuites()) { + callback(suite) } }) } -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(testModules: TestModule[]) { + const results: string[] = [] + + testModules.forEach((testModule) => { + for (const test of testModule.children.allTests()) { + if (test.skipped()) { + continue } - return name - }) - }).flat() + const fullName = `${test.module.task.name} > ${test.fullName}` + results.push( + (test.project.name ? `[${test.project.name}] ` : '') + fullName, + ) + } + }) + + return results } const envPackageNames: Record< diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index 8aa7118b6206..57f7f0186bc2 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: { @@ -799,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') { @@ -839,7 +842,6 @@ export const cliOptionsConfig: VitestCLIOptions = { poolMatchGlobs: null, deps: null, name: null, - includeTaskLocation: null, snapshotEnvironment: null, compare: null, outputJson: null, 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/core.ts b/packages/vitest/src/node/core.ts index 2da9761b8760..927c2005efaf 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -1,21 +1,20 @@ 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' -import type { RunnerTask, RunnerTestSuite } from '../public' 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' import type { CoverageProvider } from './types/coverage' import type { Reporter } from './types/reporter' -import { existsSync, promises as fs, readFileSync } from 'node:fs' -import { resolve } from 'node:path' +import type { TestRunResult } from './types/tests' +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' @@ -26,16 +25,17 @@ 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 { 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 { VitestWatcher } from './watcher' import { resolveWorkspace } from './workspace/resolveWorkspace' const WATCHER_DEBOUNCE = 100 @@ -48,76 +48,148 @@ export interface VitestOptions { } export class Vitest { + /** + * Current Vitest version. + * @example '2.0.0' + */ public readonly version = version static readonly version = version - - config: ResolvedConfig = undefined! - configOverride: Partial = {} - - server: ViteDevServer = undefined! - state: StateManager = undefined! - snapshot: SnapshotManager = undefined! - cache: VitestCache = undefined! - reporters: Reporter[] = undefined! - coverageProvider: CoverageProvider | null | undefined - logger: Logger - pool: ProcessPool | undefined - - vitenode: ViteNodeServer = undefined! - - invalidates: Set = new Set() - changedTests: Set = new Set() - watchedTests: Set = new Set() - filenamePattern?: string[] - runningPromise?: Promise - closingPromise?: Promise - isCancelling = false - - isFirstRun = true - restartsCount = 0 - runner: ViteNodeRunner = undefined! - - public packageInstaller: VitestPackageInstaller - - /** TODO: rename to `_coreRootProject` */ - /** @internal */ - public coreWorkspaceProject!: TestProject - - /** @private */ - public resolvedProjects: TestProject[] = [] + /** + * 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 + /** + * 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[] = [] - public distPath = distDir - - private _cachedSpecs = new Map() + /** @internal */ configOverride: Partial = {} + /** @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 */ reporters: Reporter[] = undefined! + /** @internal */ vitenode: ViteNodeServer = undefined! + /** @internal */ runner: ViteNodeRunner = undefined! + + private isFirstRun = true + private restartsCount = 0 + + private readonly specifications: VitestSpecifications + private readonly watcher: VitestWatcher + private pool: ProcessPool | undefined + private _config?: ResolvedConfig + private _vite?: ViteDevServer + private _state?: StateManager + private _cache?: VitestCache + private _snapshot?: SnapshotManager private _workspaceConfigPath?: string - /** @deprecated use `_cachedSpecs` */ - projectTestFiles = this._cachedSpecs - - /** @internal */ - public _browserLastPort = defaultBrowserPort - - /** @internal */ - public _options: UserConfig = {} - constructor( public readonly mode: VitestRunMode, options: VitestOptions = {}, ) { 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[] = [] - private _onClose: (() => Awaited)[] = [] + private _onClose: (() => Awaitable)[] = [] private _onSetServer: OnServerRestartHandler[] = [] - private _onCancelListeners: ((reason: CancelReason) => Promise | void)[] = [] + private _onCancelListeners: ((reason: CancelReason) => Awaitable)[] = [] private _onUserTestsRerun: OnTestsRerunHandler[] = [] + private _onFilterWatchedSpecification: ((spec: TestSpecification) => boolean)[] = [] + + /** @deprecated will be removed in 4.0, use `onFilterWatchedSpecification` instead */ + public get invalidates() { + return this.watcher.invalidates + } + + /** @deprecated will be removed in 4.0, use `onFilterWatchedSpecification` instead */ + public get changedTests() { + return this.watcher.changedTests + } + + /** + * The global config. + */ + get config(): ResolvedConfig { + assert(this._config, 'config') + return this._config + } + + /** @deprecated use `vitest.vite` instead */ + get server(): ViteDevServer { + return this._vite! + } + + /** + * Global Vite's dev server instance. + */ + get vite(): ViteDevServer { + 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 sort tests. + */ + get cache(): VitestCache { + assert(this._cache, 'cache') + return this._cache + } - 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?.() + this.watcher.unregisterWatcher() clearTimeout(this._rerunTimer) this.restartsCount += 1 this._browserLastPort = defaultBrowserPort @@ -129,19 +201,19 @@ 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.state = new StateManager() - this.cache = new VitestCache(this.version) - this.snapshot = new SnapshotManager({ ...resolved.snapshotOptions }) + this._vite = server + this._config = resolved + this._state = new StateManager() + this._cache = new VitestCache(this.version) + this._snapshot = new SnapshotManager({ ...resolved.snapshotOptions }) if (this.config.watch) { - this.registerWatcher() + this.watcher.registerWatcher() } this.vitenode = new ViteNodeServer(server, this.config.server) @@ -183,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() @@ -201,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) @@ -210,22 +281,45 @@ 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())) } - public provide(key: T, value: ProvidedContext[T]) { - this.getRootTestProject().provide(key, value) + /** + * 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.getRootProject().provide(key, value) } /** - * @internal + * Get global provided context. */ - _createRootProject() { + public getProvidedContext(): ProvidedContext { + return this.getRootProject().getProvidedContext() + } + + /** @internal */ + _ensureRootProject(): TestProject { + if (this.coreWorkspaceProject) { + return this.coreWorkspaceProject + } this.coreWorkspaceProject = TestProject._createBasicProject(this) return this.coreWorkspaceProject } - public getRootTestProject(): TestProject { + /** @deprecated use `getRootProject` instead */ + public getCoreWorkspaceProject(): TestProject { + return this.getRootProject() + } + + /** + * Return project that has the root (or "global") config. + */ + 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.`) } @@ -238,15 +332,25 @@ 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 = '') { - return this.projects.find(p => p.name === name) + public getProjectByName(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 + } + + /** + * 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 { @@ -254,8 +358,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) @@ -271,7 +375,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, @@ -286,12 +390,12 @@ export class Vitest { this._workspaceConfigPath = workspaceConfigPath if (!workspaceConfigPath) { - return [this._createRootProject()] + 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.`) @@ -305,7 +409,15 @@ export class Vitest { ) } - private async initCoverageProvider() { + /** + * 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 } @@ -320,19 +432,22 @@ export class Vitest { return this.coverageProvider } - async mergeReports() { + /** + * 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)) { 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)) 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) @@ -373,41 +488,52 @@ 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 { + testModules: this.state.getTestModules(), + unhandledErrors: this.state.getUnhandledErrors(), + } } - async collect(filters?: string[]) { + async collect(filters?: string[]): Promise { this._onClose = [] - const files = await this.filterTestsBySource( - await this.globTestFiles(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 { testModules: [], unhandledErrors: [] } } - await this.collectFiles(files) - - return { - tests: this.state.getFiles(), - errors: this.state.getUnhandledErrors(), - } + return this.collectTests(files) } - async listFiles(filters?: string[]) { - const files = await this.filterTestsBySource( - await this.globTestFiles(filters), - ) + /** @deprecated use `getRelevantTestSpecifications` instead */ + public listFiles(filters?: string[]): Promise { + return this.getRelevantTestSpecifications(filters) + } - return files + /** + * Returns the list of test files that match the config and filters. + * @param filters String filters to match the test files + */ + getRelevantTestSpecifications(filters?: string[]): Promise { + return this.specifications.getRelevantTestSpecifications(filters) } - async start(filters?: string[]) { + /** + * 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[]): Promise { this._onClose = [] try { @@ -419,9 +545,7 @@ export class Vitest { } this.filenamePattern = filters && filters?.length > 0 ? filters : undefined - const files = await this.filterTestsBySource( - await this.globTestFiles(filters), - ) + const files = await this.specifications.getRelevantTestSpecifications(filters) // if run with --changed, don't exit if no tests are found if (!files.length) { @@ -438,19 +562,30 @@ export class Vitest { } } + let testModules: TestRunResult = { + testModules: [], + unhandledErrors: [], + } + 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 changed tests even if this method was not called. + */ + async init(): Promise { this._onClose = [] try { @@ -462,128 +597,71 @@ 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()) { - 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 + /** + * @deprecated remove when vscode extension supports "getModuleSpecifications" + */ + getProjectsByTestFile(file: string): WorkspaceSpec[] { + return this.getModuleSpecifications(file) as WorkspaceSpec[] } - async filterTestsBySource(specs: WorkspaceSpec[]) { - 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 [filepath, deps] of testGraphs) { - // if deps or the test itself were changed - if (related.some(path => path === filepath[1] || deps.has(path))) { - runningTests.push(filepath) - } - } - - return runningTests + /** @deprecated */ + getFileWorkspaceSpecs(file: string): WorkspaceSpec[] { + return this.getModuleSpecifications(file) as WorkspaceSpec[] } /** - * @deprecated remove when vscode extension supports "getFileWorkspaceSpecs" + * 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. */ - getProjectsByTestFile(file: string) { - return this.getFileWorkspaceSpecs(file) as WorkspaceSpec[] + public getModuleSpecifications(moduleId: string): TestSpecification[] { + return this.specifications.getModuleSpecifications(moduleId) } - getFileWorkspaceSpecs(file: string) { - const _cached = this._cachedSpecs.get(file) - if (_cached) { - return _cached - } + /** + * 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) + } - const specs: TestSpecification[] = [] - for (const project of this.projects) { - if (project.isTestFile(file)) { - specs.push(project.createSpecification(file)) - } - if (project.isTypecheckFile(file)) { - specs.push(project.createSpecification(file, 'typescript')) - } - } - specs.forEach(spec => this.ensureSpecCached(spec)) - return specs + /** + * 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) } - async initializeGlobalSetup(paths: TestSpecification[]) { - 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)), + ]) + const result = await this.runTestSpecifications(specifications, allTestsRun) + + await this.report('onWatcherStart', this.state.getFiles(files)) + return result } - async runFiles(specs: TestSpecification[], allTestsRun: boolean) { + private async runFiles(specs: TestSpecification[], allTestsRun: boolean): Promise { const filepaths = specs.map(spec => spec.moduleId) this.state.collectPaths(filepaths) @@ -602,8 +680,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() @@ -614,7 +692,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') @@ -628,6 +706,11 @@ export class Vitest { this.cache.results.updateResults(files) await this.cache.results.writeToCache() + + return { + testModules: this.state.getTestModules(), + unhandledErrors: this.state.getUnhandledErrors(), + } } finally { // can be duplicate files if different projects are using the same file @@ -635,7 +718,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,8 +735,12 @@ export class Vitest { return await this.runningPromise } - async collectFiles(specs: WorkspaceSpec[]) { - const filepaths = specs.map(spec => spec.moduleId) + /** + * Collect tests in specified modules. Vitest will run the files to collect tests. + * @param specifications A list of specifications to run. + */ + public async collectTests(specifications: TestSpecification[]): Promise { + const filepaths = specifications.map(spec => spec.moduleId) this.state.collectPaths(filepaths) // previous run @@ -667,15 +754,15 @@ 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() - await this.initializeGlobalSetup(specs) + await this.initializeGlobalSetup(specifications) try { - await this.pool.collectTests(specs, invalidates) + await this.pool.collectTests(specifications, invalidates) } catch (err) { this.state.catchError(err, 'Unhandled Error') @@ -688,6 +775,11 @@ export class Vitest { if (hasFailed(files)) { process.exitCode = 1 } + + return { + testModules: this.state.getTestModules(), + unhandledErrors: this.state.getUnhandledErrors(), + } })() .finally(() => { this.runningPromise = undefined @@ -700,39 +792,53 @@ 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.getRootProject() + if (!projects.has(coreProject)) { + projects.add(coreProject) + } + for (const project of projects) { + await project._initializeGlobalSetup() + } + } + + /** @internal */ + async rerunFiles(files: string[] = this.state.getFilepaths(), trigger?: string, allTestsRun = true, resetTestNamePattern = false): Promise { if (resetTestNamePattern) { this.configOverride.testNamePattern = undefined } if (this.filenamePattern) { - const filteredFiles = await this.globTestFiles(this.filenamePattern) - files = files.filter(file => filteredFiles.some(f => f[1] === file)) + const filteredFiles = await this.globTestSpecifications(this.filenamePattern) + files = files.filter(file => filteredFiles.some(f => f.moduleId === file)) } + const specifications = files.flatMap(file => this.getModuleSpecifications(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) { + /** @internal */ + async rerunTask(id: string): Promise { const task = this.state.idMap.get(id) if (!task) { throw new Error(`Task ${id} was not found`) @@ -740,11 +846,12 @@ 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', ) } - async changeProjectName(pattern: string) { + /** @internal */ + async changeProjectName(pattern: string): Promise { if (pattern === '') { delete this.configOverride.project } @@ -753,11 +860,12 @@ 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 === '') } - 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 @@ -778,7 +886,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' @@ -786,33 +895,74 @@ export class Vitest { await this.rerunFiles(files, trigger, pattern === '') } - async rerunFailed() { + /** @internal */ + async rerunFailed(): Promise { await this.rerunFiles(this.state.getFailedFilepaths(), 'rerun failed', false) } - async updateSnapshot(files?: string[]) { + /** @internal */ + async updateSnapshot(files?: string[]): Promise { // default to failed files files = files || [ ...this.state.getFailedFilepaths(), ...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 - private async scheduleRerun(triggerId: string[]) { + // 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) await this.runningPromise @@ -824,17 +974,8 @@ 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() + if (this.watcher.changedTests.size === 0) { + this.watcher.invalidates.clear() return } @@ -846,11 +987,11 @@ 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.globTestFiles(this.filenamePattern) - files = files.filter(file => filteredFiles.some(f => f[1] === file)) + const filteredFiles = await this.globTestSpecifications(this.filenamePattern) + 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) { @@ -858,42 +999,35 @@ 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(', ') + // 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(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. + * Invalidate a file in all projects. */ - public watchTests(tests: string[]) { - this.watchedTests = new Set( - tests.map(test => slash(test)), - ) - } - - 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,146 +1036,19 @@ export class Vitest { }) } - onChange = (id: string) => { - id = slash(id) - this.logger.clearHighlightCache(id) - this.updateLastChanged(id) - const needsRerun = this.handleFileChanged(id) - if (needsRerun.length) { - this.scheduleRerun(needsRerun) - } + /** @deprecated use `invalidateFile` */ + updateLastChanged(filepath: string): void { + this.invalidateFile(filepath) } - onUnlink = (id: string) => { - 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) - } - } - - onAdd = async (id: string) => { - id = slash(id) - this.updateLastChanged(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) - } - } - } - - checkUnhandledErrors(errors: unknown[]) { + /** @internal */ + public _checkUnhandledErrors(errors: unknown[]): void { if (errors.length && !this.config.dangerouslyIgnoreUnhandledErrors) { process.exitCode = 1 } } - private unregisterWatcher = noop - private registerWatcher() { - 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.getModuleProjects(filepath) - 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.getModulesByFilepath(filepath) - if (!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) { + private async reportCoverage(coverage: unknown, allTestsRun: boolean): Promise { if (this.state.getCountOfFailedTests() > 0) { await this.coverageProvider?.onTestFailure?.() @@ -1061,11 +1068,15 @@ 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,8 +1087,8 @@ 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)) { - closePromises.push(this.coreWorkspaceProject.close().then(() => this.server = undefined as any)) + if (this.coreWorkspaceProject && !this.resolvedProjects.includes(this.coreWorkspaceProject)) { + closePromises.push(this.coreWorkspaceProject.close().then(() => this._vite = undefined as any)) } if (this.pool) { @@ -1104,16 +1115,17 @@ 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) { + public async exit(force = false): Promise { setTimeout(() => { this.report('onProcessTimeout').then(() => { console.warn(`close timed out after ${this.config.teardownTimeout}ms`) 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') @@ -1121,7 +1133,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 +1150,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,103 +1158,91 @@ 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(new Set(specifications.map(spec => spec.moduleId))) } + /** + * @deprecated use `globTestSpecifications` instead + */ public async globTestSpecs(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.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.ensureSpecCached(spec) - files.push(spec) - }) - typecheckTestFiles.forEach((file) => { - const loc = testLocations[file] - testLocHasMatch[file] = true - - const spec = project.createSpecification(file, 'typescript', loc) - this.ensureSpecCached(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.globTestSpecifications(filters) } /** - * @deprecated use `globTestSpecs` instead + * @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] - 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) - } + /** @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 + }) } - // The server needs to be running for communication - shouldKeepServer() { + /** + * Should the server be kept running after the tests are done. + */ + shouldKeepServer(): boolean { return !!this.config?.watch } - onServerRestart(fn: OnServerRestartHandler) { + /** + * Register a handler that will be called when the server is restarted due to a config change. + */ + onServerRestart(fn: OnServerRestartHandler): void { 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 is cancelled with `vitest.cancelCurrentRun`. + */ + onCancel(fn: (reason: CancelReason) => Awaitable): void { this._onCancelListeners.push(fn) } - onClose(fn: () => void) { + /** + * Register a handler that will be called when the server is closed. + */ + onClose(fn: () => Awaitable): void { this._onClose.push(fn) } + /** + * Register a handler that will be called when the tests are rerunning. + */ onTestsRerun(fn: OnTestsRerunHandler): void { 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): void { + 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}\`.`) + } +} + +export type OnServerRestartHandler = (reason?: string) => Promise | void +export type OnTestsRerunHandler = (testFiles: TestSpecification[]) => Promise | void diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index 74d9aabdc97c..66119740c772 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, @@ -258,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 @@ -269,7 +273,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) } 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/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..7cb70a5c3317 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 @@ -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 }) @@ -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: [], @@ -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/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/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/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/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/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 bbdbbf7c00cb..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[] = [], ) { @@ -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/project.ts b/packages/vitest/src/node/project.ts index fce4f51b954b..864a3ed1bed4 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' @@ -62,9 +62,10 @@ export class TestProject { */ public readonly tmpDir = join(tmpdir(), nanoid()) - vitenode!: ViteNodeServer - runner!: ViteNodeRunner - typechecker?: Typechecker + /** @internal */ vitenode!: ViteNodeServer + /** @internal */ typechecker?: Typechecker + + private runner!: ViteNodeRunner private closingPromise: Promise | undefined @@ -122,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, } } @@ -133,14 +134,15 @@ export class TestProject { */ public createSpecification( moduleId: string, + locations?: number[] | undefined, + /** @internal */ pool?: string, - testLocations?: number[] | undefined, ): TestSpecification { return new TestSpecification( this, moduleId, pool || getFilePoolName(this, moduleId), - testLocations, + locations, ) } @@ -195,7 +197,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 */ @@ -373,8 +375,7 @@ export class TestProject { return isBrowserEnabled(this.config) } - /** @internal */ - _markTestFile(testPath: string): void { + private markTestFile(testPath: string): void { this.testFilesList?.push(testPath) } @@ -382,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) } @@ -390,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) } @@ -415,29 +416,36 @@ 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(filepath: string, source?: string): boolean { - const relativeId = relative(this.config.dir || this.config.root, filepath) + 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 ( this.config.includeSource?.length && mm.isMatch(relativeId, this.config.includeSource) ) { - const code = source || readFileSync(filepath, 'utf-8') - return this.isInSourceTestCode(code) + const code = source?.() || readFileSync(moduleId, 'utf-8') + if (this.isInSourceTestCode(code)) { + this.markTestFile(moduleId) + return true + } } return false } /** @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 { @@ -521,6 +529,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 || '' diff --git a/packages/vitest/src/node/reporters/base.ts b/packages/vitest/src/node/reporters/base.ts index 71df7bf40885..e4c9d42abb2d 100644 --- a/packages/vitest/src/node/reporters/base.ts +++ b/packages/vitest/src/node/reporters/base.ts @@ -268,9 +268,9 @@ export abstract class BaseReporter implements Reporter { write('\n') } - const project = log.taskId - ? this.ctx.getProjectByTaskId(log.taskId) - : this.ctx.getRootTestProject() + const project = task + ? this.ctx.getProjectByName(task.file.projectName || '') + : this.ctx.getRootProject() const stack = log.browser ? (project.browser?.parseStacktrace(log.origin) || []) @@ -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..cfa35f50a4c0 100644 --- a/packages/vitest/src/node/reporters/github-actions.ts +++ b/packages/vitest/src/node/reporters/github-actions.ts @@ -23,14 +23,14 @@ export class GithubActionsReporter implements Reporter { }>() for (const error of errors) { projectErrors.push({ - project: this.ctx.getRootTestProject(), + project: this.ctx.getRootProject(), title: 'Unhandled error', error, }) } 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/reported-tasks.ts b/packages/vitest/src/node/reporters/reported-tasks.ts index cd918e7bfbd4..fa881accdb99 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 @@ -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 @@ -32,6 +32,7 @@ class ReportedTaskImplementation { */ public readonly location: { line: number; column: number } | undefined + /** @internal */ protected constructor( task: RunnerTask, project: TestProject, @@ -42,8 +43,18 @@ 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 */ static register(task: RunnerTask, project: TestProject) { const state = new this(task, project) as TestCase | TestSuite | TestModule @@ -55,6 +66,7 @@ class ReportedTaskImplementation { export class TestCase extends ReportedTaskImplementation { #fullName: string | undefined + /** @internal */ declare public readonly task: RunnerTestCase public readonly type = 'test' @@ -78,6 +90,7 @@ export class TestCase extends ReportedTaskImplementation { */ public readonly parent: TestSuite | TestModule + /** @internal */ protected constructor(task: RunnerTestCase, project: TestProject) { super(task, project) @@ -109,7 +122,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 @@ -141,12 +154,11 @@ export class TestCase extends ReportedTaskImplementation { } /** - * Checks if the test did not fail the suite. - * If the test is not finished yet or was skipped, it will return `true`. + * Checks if the test was skipped during collection or dynamically with `ctx.skip()`. */ - public ok(): boolean { - const result = this.result() - return !result || result.state !== 'failed' + public skipped(): boolean { + const mode = this.task.result?.state || this.task.mode + return mode === 'skip' || mode === 'todo' } /** @@ -190,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) { @@ -287,6 +299,7 @@ class TestCollection { export type { TestCollection } abstract class SuiteImplementation extends ReportedTaskImplementation { + /** @internal */ declare public readonly task: RunnerTestSuite | RunnerTestFile /** @@ -294,15 +307,32 @@ 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) } + + /** + * 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. + */ + public errors(): TestError[] { + return (this.task.result?.errors as TestError[] | undefined) || [] + } } export class TestSuite extends SuiteImplementation { #fullName: string | undefined + /** @internal */ declare public readonly task: RunnerTestSuite public readonly type = 'suite' @@ -326,6 +356,7 @@ export class TestSuite extends SuiteImplementation { */ public readonly options: TaskOptions + /** @internal */ protected constructor(task: RunnerTestSuite, project: TestProject) { super(task, project) @@ -341,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 `>`. */ @@ -358,6 +395,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' @@ -369,11 +407,23 @@ export class TestModule extends SuiteImplementation { */ public readonly moduleId: string + /** @internal */ protected constructor(task: RunnerTestFile, project: TestProject) { super(task, project) 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`. @@ -444,8 +494,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' /** @@ -453,7 +503,7 @@ export interface TestResultSkipped { */ errors: undefined /** - * A custom note. + * A custom note passed down to `ctx.skip(note)`. */ note: string | undefined } @@ -516,6 +566,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/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/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 { diff --git a/packages/vitest/src/node/spec.ts b/packages/vitest/src/node/spec.ts index 9c74a8ca87d1..8ae14ec121dc 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 4, the project will only support a single pool and this property will be removed. + */ public readonly pool: Pool - /** @private */ - public readonly testLocations: number[] | undefined - // public readonly location: WorkspaceSpecLocation | undefined + /** + * Line numbers of the test locations to run. + */ + public readonly testLines: number[] | undefined constructor( project: TestProject, moduleId: string, pool: Pool, - testLocations?: number[] | undefined, - // location?: WorkspaceSpecLocation | undefined, + testLines?: 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.testLines = testLines } toJSON(): SerializedTestSpecification { @@ -47,7 +56,7 @@ export class TestSpecification { root: this.project.config.root, }, this.moduleId, - { pool: this.pool }, + { pool: this.pool, testLines: this.testLines }, ] } @@ -61,8 +70,3 @@ export class TestSpecification { yield this.pool } } - -// interface WorkspaceSpecLocation { -// start: number -// end: number -// } diff --git a/packages/vitest/src/node/specifications.ts b/packages/vitest/src/node/specifications.ts new file mode 100644 index 000000000000..346300718006 --- /dev/null +++ b/packages/vitest/src/node/specifications.ts @@ -0,0 +1,199 @@ +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 { join, relative, resolve } from 'pathe' +import { isWindows } from '../utils/env' +import { groupFilters, parseFilter } from './cli/filter' +import { GitNotFoundError, IncludeTaskLocationDisabledError, LocationFilterFileNotFoundError } from './errors' + +export class VitestSpecifications { + private readonly _cachedSpecs = new Map() + + 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.isCachedTestFile(moduleId)) { + specs.push(project.createSpecification(moduleId)) + } + if (project.isCachedTypecheckFile(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), + ) + } + + 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 testLines = 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 lines = testLines[file] + testLocHasMatch[file] = true + + const spec = project.createSpecification(file, lines) + this.ensureSpecificationCached(spec) + files.push(spec) + }) + typecheckTestFiles.forEach((file) => { + const lines = testLines[file] + testLocHasMatch[file] = true + + const spec = project.createSpecification(file, lines, 'typescript') + this.ensureSpecificationCached(spec) + files.push(spec) + }) + })) + + Object.entries(testLines).forEach(([filepath, loc]) => { + if (loc.length !== 0 && !testLocHasMatch[filepath]) { + throw new LocationFilterFileNotFoundError( + relative(dir, filepath), + ) + } + }) + + return files + } + + 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) + } + + public ensureSpecificationCached(spec: TestSpecification): TestSpecification[] { + const file = spec.moduleId + const specs = this._cachedSpecs.get(file) || [] + 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 + } + + 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: TestSpecification[] = [] + + 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 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) + } + })) + } + + await addImports(spec.project, spec.moduleId) + deps.delete(spec.moduleId) + + return deps + } +} diff --git a/packages/vitest/src/node/stdin.ts b/packages/vitest/src/node/stdin.ts index 705980b1ecb3..291cbe13119e 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 @@ -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) @@ -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..9b05e1f97ee0 --- /dev/null +++ b/packages/vitest/src/node/types/tests.ts @@ -0,0 +1,6 @@ +import type { TestModule } from '../reporters' + +export interface TestRunResult { + testModules: TestModule[] + unhandledErrors: unknown[] +} diff --git a/packages/vitest/src/node/watcher.ts b/packages/vitest/src/node/watcher.ts new file mode 100644 index 000000000000..4d721e91c283 --- /dev/null +++ b/packages/vitest/src/node/watcher.ts @@ -0,0 +1,172 @@ +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 { + /** + * 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 readonly _onRerun: ((file: string) => void)[] = [] + + 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 + } + + 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 { + this._onRerun.forEach(cb => cb(file)) + } + + private onChange = (id: string): void => { + id = slash(id) + this.vitest.logger.clearHighlightCache(id) + this.vitest.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.vitest.invalidateFile(id) + let fileContent: string | undefined + + const matchingProjects: TestProject[] = [] + this.vitest.projects.forEach((project) => { + if (project.matchesTestGlob(id, () => (fileContent ??= readFileSync(id, 'utf-8')))) { + matchingProjects.push(project) + } + }) + + 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.isCachedTestFile(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.isCachedTestFile(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 needsRerun = this.handleFileChanged(i.file) + if (needsRerun) { + rerun = true + } + }) + } + + if (rerun) { + files.push(filepath) + } + } + + return !!files.length + } +} 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, })) 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 edb086a81e57..b85293dda07b 100644 --- a/packages/vitest/src/public/node.ts +++ b/packages/vitest/src/public/node.ts @@ -6,14 +6,20 @@ 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' +export { resolveApiServerConfig } from '../node/config/resolveConfig' +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' 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' @@ -25,18 +31,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' @@ -44,7 +51,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' @@ -94,7 +101,6 @@ export type { VitestEnvironment, VitestRunMode, } from '../node/types/config' - export type { BaseCoverageOptions, CoverageIstanbulOptions, @@ -107,6 +113,8 @@ export type { ReportContext, ResolvedCoverageOptions, } from '../node/types/coverage' + +export type { TestRunResult } from '../node/types/tests' /** * @deprecated Use `TestModule` instead */ @@ -130,13 +138,10 @@ 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' + export { esbuildVersion, isFileServingAllowed, 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/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/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/runtime/types/utils.ts b/packages/vitest/src/runtime/types/utils.ts index 073327421d9f..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 }, + options: { pool: string; testLines?: number[] | undefined }, ] diff --git a/packages/vitest/src/types/general.ts b/packages/vitest/src/types/general.ts index 2d46006f0da4..a423417a1d88 100644 --- a/packages/vitest/src/types/general.ts +++ b/packages/vitest/src/types/general.ts @@ -45,6 +45,4 @@ export interface ModuleGraphData { inlined: string[] } -export type OnServerRestartHandler = (reason?: string) => Promise | void -export type OnTestsRerunHandler = (testFiles: string[]) => 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 } 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 b343d154eff6..750fe32181f5 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,13 +27,10 @@ function getTransformMode( } export async function groupFilesByEnv( - files: Array, + 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, testLines }) => { 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: testLines, }, project, environment, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6652bd3a173..e3ee66946cff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -140,9 +140,12 @@ importers: '@iconify-json/logos': specifier: ^1.2.3 version: 1.2.3 + '@shikijs/transformers': + 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 @@ -3373,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.22.2': - resolution: {integrity: sha512-8f78OiBa6pZDoZ53lYTmuvpFPlWtevn23bzG+azpPVvZg7ITax57o/K3TC91eYL3OMJOO0onPbgnQyZjRos8XQ==} + '@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==} @@ -8339,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==} @@ -11735,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 @@ -11750,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 @@ -11761,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.22.2': + '@shikijs/transformers@1.24.2': dependencies: - shiki: 1.22.2 + 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 @@ -11784,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) @@ -17806,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 @@ -18869,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.22.2 + '@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)) 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!') }, } } 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/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', diff --git a/test/cli/test/reported-tasks.test.ts b/test/cli/test/reported-tasks.test.ts index 15b0b768ef84..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 @@ -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()] 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/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/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".') +}) diff --git a/test/config/test/public.test.ts b/test/config/test/public.test.ts new file mode 100644 index 000000000000..a080cf71c6ad --- /dev/null +++ b/test/config/test/public.test.ts @@ -0,0 +1,44 @@ +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 () => { + process.env.GITHUB_ACTIONS = 'false' + 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 () => { + process.env.GITHUB_ACTIONS = 'false' + 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', {}]]) +}) 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/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 } } 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, }) diff --git a/test/reporters/src/context.ts b/test/reporters/src/context.ts index 26484c995c89..0804dcbb9e1c 100644 --- a/test/reporters/src/context.ts +++ b/test/reporters/src/context.ts @@ -34,11 +34,13 @@ 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, } + // @ts-expect-error logger is readonly context.logger = { ctx: context as Vitest, log: (text: string) => output += `${text}\n`, 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 } } 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 }) } 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/**" + ] +}