diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 000000000000..9204d25b1488
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,19 @@
+### Description
+
+
+
+
+
+### Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
+- [ ] It's really useful if your PR references an issue where it is discussed ahead of time. If the feature is substantial or introduces breaking changes without a discussion, PR might be closed.
+- [ ] Ideally, include a test that fails without this PR but passes with it.
+- [ ] Please, don't make changes to `pnpm-lock.yaml` unless you introduce a new test example.
+
+### Tests
+- [ ] Run the tests with `pnpm test:ci`.
+
+### Documentation
+- [ ] If you introduce new functionality, document it. You can run documentation with `pnpm run docs` command.
+
+### Changesets
+- [ ] Changes in changelog are generated from PR name. Please, make sure that it explains your changes in an understandable manner. Please, prefix changeset messages with `feat:`, `fix:`, `perf:`, `docs:`, or `chore:`.
diff --git a/docs/api/index.md b/docs/api/index.md
index e5611ced246e..5b08b91ffeaf 100644
--- a/docs/api/index.md
+++ b/docs/api/index.md
@@ -559,7 +559,7 @@ When you use `test` or `bench` in the top level of file, they are collected as p
In some cases, you might run suites multiple times with different environments, and some of the suites might be environment-specific. Instead of wrapping the suite with `if`, you can use `describe.skipIf` to skip the suite whenever the condition is truthy.
```ts
- import { assert, test } from 'vitest'
+ import { describe, test } from 'vitest'
const isDev = process.env.NODE_ENV === 'development'
diff --git a/docs/config/index.md b/docs/config/index.md
index 30e83dacbc33..73131c2c866c 100644
--- a/docs/config/index.md
+++ b/docs/config/index.md
@@ -133,10 +133,13 @@ You will not be able to edit your `node_modules` code for debugging, since the c
#### deps.external
- **Type:** `(string | RegExp)[]`
-- **Default:** `['**/node_modules/**']`
+- **Default:** `[/\/node_modules\//]`
Externalize means that Vite will bypass the package to native Node. Externalized dependencies will not be applied Vite's transformers and resolvers, so they do not support HMR on reload. Typically, packages under `node_modules` are externalized.
+When using strings they need to be paths inside your [`deps.moduleDirectories`](/config/#deps-moduledirectories). For example `external: ['module/folder']` with the default `moduleDirectories` option will externalize `node_modules/module/folder`.
+Regular expressions on the other hand are matched against the whole path.
+
#### deps.inline
- **Type:** `(string | RegExp)[] | true`
@@ -483,7 +486,7 @@ Custom reporters for output. Reporters can be [a Reporter instance](https://gith
- `'basic'` - give a reporter like default reporter in ci
- `'verbose'` - keep the full task tree visible
- `'dot'` - show each task as a single dot
- - `'junit'` - JUnit XML reporter (you can configure `testsuites` tag name with `VITEST_JUNIT_SUITE_NAME` environmental variable)
+ - `'junit'` - JUnit XML reporter (you can configure `testsuites` tag name with `VITEST_JUNIT_SUITE_NAME` environmental variable, and `classname` tag property with `VITEST_JUNIT_CLASSNAME`)
- `'json'` - give a simple JSON summary
- `'html'` - outputs HTML report based on [`@vitest/ui`](/guide/ui)
- `'hanging-process'` - displays a list of hanging processes, if Vitest cannot exit process safely. This might be a heavy operation, enable it only if Vitest consistently cannot exit process
@@ -720,6 +723,9 @@ List of files included in coverage as glob patterns
'dist/**',
'packages/*/test?(s)/**',
'**/*.d.ts',
+ '**/virtual:*',
+ '**/__x00__*',
+ '**/\x00*',
'cypress/**',
'test?(s)/**',
'test?(-*).?(c|m)[jt]s?(x)',
diff --git a/docs/guide/index.md b/docs/guide/index.md
index c466396b3c64..88666454766a 100644
--- a/docs/guide/index.md
+++ b/docs/guide/index.md
@@ -38,6 +38,51 @@ It is recommended that you install a copy of `vitest` in your `package.json`, us
The `npx` command will execute the command either from a local `node_modules/.bin` installing any packages needed in order for the command to run. By default, npx will check whether command exists in $PATH, or in the local project binaries, and execute that. If command is not found, it will be installed prior to execution.
+## Writing Tests
+
+As an example, we will write a simple test that verifies the output of a function that adds two numbers.
+
+``` js
+// sum.js
+export function sum(a, b) {
+ return a + b
+}
+```
+
+``` js
+// sum.test.js
+import { expect, test } from 'vitest'
+import { sum } from './sum'
+
+test('adds 1 + 2 to equal 3', () => {
+ expect(sum(1, 2)).toBe(3)
+})
+```
+
+Next, in order to execute the test, add the following section to your `package.json`:
+
+```json
+{
+ "scripts": {
+ "test": "vitest"
+ }
+}
+```
+
+Finally, run `npm run test`, `yarn test`, or `pnpm test`, depending on your package manager, and Vitest will print this message:
+
+```log
+✓ sum.test.js (1)
+ ✓ adds 1 + 2 to equal 3
+
+Test Files 1 passed (1)
+ Tests 1 passed (1)
+ Start at 02:15:44
+ Duration 311ms (transform 23ms, setup 0ms, collect 16ms, tests 2ms, environment 0ms, prepare 106ms)
+```
+
+Learn more about the usage of Vitest, see the [API](https://vitest.dev/api/) section.
+
## Configuring Vitest
One of the main advantages of Vitest is its unified configuration with Vite. If present, `vitest` will read your root `vite.config.ts` to match with the plugins and setup as your Vite app. For example, your Vite [resolve.alias](https://vitejs.dev/config/shared-options.html#resolve-alias) and [plugins](https://vitejs.dev/guide/using-plugins.html) configuration will work out-of-the-box. If you want a different configuration during testing, you can:
@@ -46,7 +91,9 @@ One of the main advantages of Vitest is its unified configuration with Vite. If
- Pass `--config` option to CLI, e.g. `vitest --config ./path/to/vitest.config.ts`
- Use `process.env.VITEST` or `mode` property on `defineConfig` (will be set to `test` if not overridden) to conditionally apply different configuration in `vite.config.ts`
-To configure `vitest` itself, add `test` property in your Vite config. You'll also need to add a reference to Vitest types using a [triple slash command](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html#-reference-types-) at the top of your config file, if you are importing `defineConfig` from `vite` itself.
+Vitest supports the same extensions for your configuration file as Vite does: `.js`, `.mjs`, `.cjs`, `.ts`, `.cts`, `.mts`. Vitest does not support `.json` extension.
+
+If you are not using Vite as your build tool, you can configure Vitest using the `test` property in your config file:
```ts
import { defineConfig } from 'vitest/config'
@@ -58,8 +105,52 @@ export default defineConfig({
})
```
+::: tip
+Even if you do not use Vite yourself, Vitest relies heavily on it for its transformation pipeline. For that reason, you can also configure any property described in [Vite documentation](https://vitejs.dev/config/).
+:::
+
+If you are already using Vite, add `test` property in your Vite config. You'll also need to add a reference to Vitest types using a [triple slash command](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html#-reference-types-) at the top of your config file.
+
+```ts
+///
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ test: {
+ // ...
+ },
+})
+```
+
See the list of config options in the [Config Reference](../config/)
+::: warning
+If you decide to have two separate config files for Vite and Vitest, make sure to define the same Vite options in your Vitest config file since it will override your Vite file, not extend it. You can also use `mergeConfig` method from `vite` or `vitest/config` entries to merge Vite config with Vitest config:
+
+:::code-group
+```ts [vitest.config.mjs]
+import { defineConfig, mergeConfig } from 'vitest/config'
+import viteConfig from './vite.config.mjs'
+
+export default mergeConfig(viteConfig, defineConfig({
+ test: {
+ // ...
+ }
+}))
+```
+
+```ts [vite.config.mjs]
+import { defineConfig } from 'vite'
+import Vue from '@vitejs/plugin-vue'
+
+export default defineConfig({
+ plugins: [Vue()],
+})
+```
+
+But we recommend to use the same file for both Vite and Vitest instead of creating two separate files.
+:::
+
## Workspaces Support
Run different project configurations inside the same project with [Vitest Workspaces](/guide/workspace). You can define a list of files and folders that define your workspace in `vitest.workspace` file. The file supports `js`/`ts`/`json` extensions. This feature works great with monorepo setups.
diff --git a/packages/coverage-istanbul/package.json b/packages/coverage-istanbul/package.json
index 9b3fa86fe879..fda2fc4cc84c 100644
--- a/packages/coverage-istanbul/package.json
+++ b/packages/coverage-istanbul/package.json
@@ -46,8 +46,8 @@
},
"dependencies": {
"istanbul-lib-coverage": "^3.2.0",
- "istanbul-lib-instrument": "^5.2.1",
- "istanbul-lib-report": "^3.0.0",
+ "istanbul-lib-instrument": "^6.0.0",
+ "istanbul-lib-report": "^3.0.1",
"istanbul-lib-source-maps": "^4.0.1",
"istanbul-reports": "^3.1.5",
"test-exclude": "^6.0.0"
diff --git a/packages/coverage-v8/package.json b/packages/coverage-v8/package.json
index e5259740e4e8..c4ecf8dca36b 100644
--- a/packages/coverage-v8/package.json
+++ b/packages/coverage-v8/package.json
@@ -48,7 +48,7 @@
"@ampproject/remapping": "^2.2.1",
"@bcoe/v8-coverage": "^0.2.3",
"istanbul-lib-coverage": "^3.2.0",
- "istanbul-lib-report": "^3.0.0",
+ "istanbul-lib-report": "^3.0.1",
"istanbul-lib-source-maps": "^4.0.1",
"istanbul-reports": "^3.1.5",
"magic-string": "^0.30.1",
diff --git a/packages/coverage-v8/src/provider.ts b/packages/coverage-v8/src/provider.ts
index b0f230fcf261..7733007804e3 100644
--- a/packages/coverage-v8/src/provider.ts
+++ b/packages/coverage-v8/src/provider.ts
@@ -13,7 +13,8 @@ import remapping from '@ampproject/remapping'
import { normalize, resolve } from 'pathe'
import c from 'picocolors'
import { provider } from 'std-env'
-import type { EncodedSourceMap } from 'vite-node'
+import { cleanUrl } from 'vite-node/utils'
+import type { EncodedSourceMap, FetchResult } from 'vite-node'
import { coverageConfigDefaults, defaultExclude, defaultInclude } from 'vitest/config'
import { BaseCoverageProvider } from 'vitest/coverage'
import type { AfterSuiteRunMeta, CoverageProvider, CoverageV8Options, ReportContext, ResolvedCoverageOptions } from 'vitest'
@@ -36,6 +37,7 @@ interface TestExclude {
}
type Options = ResolvedCoverageOptions<'v8'>
+type TransformResults = Map
// TODO: vite-node should export this
const WRAPPER_LENGTH = 185
@@ -99,18 +101,19 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
if (provider === 'stackblitz')
this.ctx.logger.log(c.blue(' % ') + c.yellow('@vitest/coverage-v8 does not work on Stackblitz. Report will be empty.'))
+ const transformResults = normalizeTransformResults(this.ctx.projects.map(project => project.vitenode.fetchCache))
const merged = mergeProcessCovs(this.coverages)
const scriptCoverages = merged.result.filter(result => this.testExclude.shouldInstrument(fileURLToPath(result.url)))
if (this.options.all && allTestsRun) {
const coveredFiles = Array.from(scriptCoverages.map(r => r.url))
- const untestedFiles = await this.getUntestedFiles(coveredFiles)
+ const untestedFiles = await this.getUntestedFiles(coveredFiles, transformResults)
scriptCoverages.push(...untestedFiles)
}
const converted = await Promise.all(scriptCoverages.map(async ({ url, functions }) => {
- const sources = await this.getSources(url, functions)
+ const sources = await this.getSources(url, transformResults, functions)
// If no source map was found from vite-node we can assume this file was not run in the wrapper
const wrapperLength = sources.sourceMap ? WRAPPER_LENGTH : 0
@@ -177,14 +180,14 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
}
}
- private async getUntestedFiles(testedFiles: string[]): Promise {
+ private async getUntestedFiles(testedFiles: string[], transformResults: TransformResults): Promise {
const includedFiles = await this.testExclude.glob(this.ctx.config.root)
const uncoveredFiles = includedFiles
.map(file => pathToFileURL(resolve(this.ctx.config.root, file)))
.filter(file => !testedFiles.includes(file.href))
return await Promise.all(uncoveredFiles.map(async (uncoveredFile) => {
- const { source } = await this.getSources(uncoveredFile.href)
+ const { source } = await this.getSources(uncoveredFile.href, transformResults)
return {
url: uncoveredFile.href,
@@ -204,16 +207,14 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
}))
}
- private async getSources(url: string, functions: Profiler.FunctionCoverage[] = []): Promise<{
+ private async getSources(url: string, transformResults: TransformResults, functions: Profiler.FunctionCoverage[] = []): Promise<{
source: string
originalSource?: string
sourceMap?: { sourcemap: EncodedSourceMap }
}> {
const filePath = normalize(fileURLToPath(url))
- const transformResult = this.ctx.projects
- .map(project => project.vitenode.fetchCache.get(filePath)?.result)
- .filter(Boolean)
- .shift()
+
+ const transformResult = transformResults.get(filePath)
const map = transformResult?.map
const code = transformResult?.code
@@ -277,3 +278,18 @@ function findLongestFunctionLength(functions: Profiler.FunctionCoverage[]) {
return Math.max(previous, maxEndOffset)
}, 0)
}
+
+function normalizeTransformResults(fetchCaches: Map[]) {
+ const normalized: TransformResults = new Map()
+
+ for (const fetchCache of fetchCaches) {
+ for (const [key, value] of fetchCache.entries()) {
+ const cleanEntry = cleanUrl(key)
+
+ if (!normalized.has(cleanEntry))
+ normalized.set(cleanEntry, value.result)
+ }
+ }
+
+ return normalized
+}
diff --git a/packages/vite-node/package.json b/packages/vite-node/package.json
index 25e3dc0f0cce..f0f31f22ef74 100644
--- a/packages/vite-node/package.json
+++ b/packages/vite-node/package.json
@@ -45,7 +45,8 @@
"types": "./dist/source-map.d.ts",
"require": "./dist/source-map.cjs",
"import": "./dist/source-map.mjs"
- }
+ },
+ "./*": "./*"
},
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
diff --git a/packages/vite-node/src/cli.ts b/packages/vite-node/src/cli.ts
index ef8e607e47f0..39794c21dee9 100644
--- a/packages/vite-node/src/cli.ts
+++ b/packages/vite-node/src/cli.ts
@@ -12,21 +12,31 @@ import { installSourcemapsSupport } from './source-map'
const cli = cac('vite-node')
cli
- .version(version)
.option('-r, --root ', 'Use specified root directory')
.option('-c, --config ', 'Use specified config file')
.option('-m, --mode ', 'Set env mode')
.option('-w, --watch', 'Restart on file changes, similar to "nodemon"')
.option('--script', 'Use vite-node as a script runner')
.option('--options ', 'Use specified Vite server options')
- .help()
+ .option('-v, --version', 'Output the version number')
+ .option('-h, --help', 'Display help for command')
cli
.command('[...files]')
.allowUnknownOptions()
.action(run)
-cli.parse()
+cli.parse(process.argv, { run: false })
+
+if (cli.args.length === 0) {
+ cli.runMatchedCommand()
+}
+else {
+ const i = cli.rawArgs.indexOf(cli.args[0]) + 1
+ const scriptArgs = cli.rawArgs.slice(i).filter(it => it !== '--')
+ const executeArgs = [...cli.rawArgs.slice(0, i), '--', ...scriptArgs]
+ cli.parse(executeArgs)
+}
export interface CliOptions {
root?: string
@@ -35,6 +45,8 @@ export interface CliOptions {
mode?: string
watch?: boolean
options?: ViteNodeServerOptionsCLI
+ version?: boolean
+ help?: boolean
'--'?: string[]
}
@@ -48,9 +60,18 @@ async function run(files: string[], options: CliOptions = {}) {
process.argv = [...process.argv.slice(0, 2), ...(options['--'] || [])]
}
+ if (options.version) {
+ cli.version(version)
+ cli.outputVersion()
+ process.exit(0)
+ }
+ if (options.help) {
+ cli.version(version).outputHelp()
+ process.exit(0)
+ }
if (!files.length) {
console.error(c.red('No files specified.'))
- cli.outputHelp()
+ cli.version(version).outputHelp()
process.exit(1)
}
@@ -63,6 +84,9 @@ async function run(files: string[], options: CliOptions = {}) {
configFile: options.config,
root: options.root,
mode: options.mode,
+ server: {
+ hmr: !!options.watch,
+ },
plugins: [
options.watch && viteNodeHmrPlugin(),
],
diff --git a/packages/vite-node/src/client.ts b/packages/vite-node/src/client.ts
index f930bf83f07d..d2814e122f00 100644
--- a/packages/vite-node/src/client.ts
+++ b/packages/vite-node/src/client.ts
@@ -32,17 +32,26 @@ const clientStub = {
if (typeof document === 'undefined')
return
- const element = document.getElementById(id)
- if (element)
- element.remove()
+ const element = document.querySelector(`[data-vite-dev-id="${id}"]`)
+ if (element) {
+ element.textContent = css
+ return
+ }
const head = document.querySelector('head')
const style = document.createElement('style')
style.setAttribute('type', 'text/css')
- style.id = id
- style.innerHTML = css
+ style.setAttribute('data-vite-dev-id', id)
+ style.textContent = css
head?.appendChild(style)
},
+ removeStyle(id: string) {
+ if (typeof document === 'undefined')
+ return
+ const sheet = document.querySelector(`[data-vite-dev-id="${id}"]`)
+ if (sheet)
+ document.head.removeChild(sheet)
+ },
}
export const DEFAULT_REQUEST_STUBS: Record = {
@@ -372,7 +381,7 @@ export class ViteNodeRunner {
Object.defineProperty(meta, 'hot', {
enumerable: true,
get: () => {
- hotContext ||= this.options.createHotContext?.(this, `/@fs/${fsPath}`)
+ hotContext ||= this.options.createHotContext?.(this, moduleId)
return hotContext
},
set: (value) => {
diff --git a/packages/vite-node/src/hmr/emitter.ts b/packages/vite-node/src/hmr/emitter.ts
index 1faa87fdedfb..3a814d29885e 100644
--- a/packages/vite-node/src/hmr/emitter.ts
+++ b/packages/vite-node/src/hmr/emitter.ts
@@ -30,6 +30,20 @@ export function viteNodeHmrPlugin(): Plugin {
return {
name: 'vite-node:hmr',
+ config() {
+ // chokidar fsevents is unstable on macos when emitting "ready" event
+ if (process.platform === 'darwin' && process.env.VITE_TEST_WATCHER_DEBUG) {
+ return {
+ server: {
+ watch: {
+ useFsEvents: false,
+ usePolling: false,
+ },
+ },
+ }
+ }
+ },
+
configureServer(server) {
const _send = server.ws.send
server.emitter = emitter
@@ -37,6 +51,12 @@ export function viteNodeHmrPlugin(): Plugin {
_send(payload)
emitter.emit('message', payload)
}
+ if (process.env.VITE_TEST_WATCHER_DEBUG) {
+ server.watcher.on('ready', () => {
+ // eslint-disable-next-line no-console
+ console.log('[debug] watcher is ready')
+ })
+ }
},
}
}
diff --git a/packages/vite-node/src/hmr/hmr.ts b/packages/vite-node/src/hmr/hmr.ts
index 4346b2c35573..e6c77677b3d2 100644
--- a/packages/vite-node/src/hmr/hmr.ts
+++ b/packages/vite-node/src/hmr/hmr.ts
@@ -6,8 +6,13 @@ import c from 'picocolors'
import createDebug from 'debug'
import type { ViteNodeRunner } from '../client'
import type { HotContext } from '../types'
+import { normalizeRequestId } from '../utils'
import type { HMREmitter } from './emitter'
+export type ModuleNamespace = Record & {
+ [Symbol.toStringTag]: 'Module'
+}
+
const debugHmr = createDebug('vite-node:hmr')
export type InferCustomEventPayload =
@@ -21,7 +26,7 @@ export interface HotModule {
export interface HotCallback {
// the dependencies must be fetchable paths
deps: string[]
- fn: (modules: object[]) => void
+ fn: (modules: (ModuleNamespace | undefined)[]) => void
}
interface CacheData {
@@ -77,16 +82,16 @@ export async function reload(runner: ViteNodeRunner, files: string[]) {
return Promise.all(files.map(file => runner.executeId(file)))
}
-function notifyListeners(
+async function notifyListeners(
runner: ViteNodeRunner,
event: T,
data: InferCustomEventPayload,
-): void
-function notifyListeners(runner: ViteNodeRunner, event: string, data: any): void {
+): Promise
+async function notifyListeners(runner: ViteNodeRunner, event: string, data: any): Promise {
const maps = getCache(runner)
const cbs = maps.customListenersMap.get(event)
if (cbs)
- cbs.forEach(cb => cb(data))
+ await Promise.all(cbs.map(cb => cb(data)))
}
async function queueUpdate(runner: ViteNodeRunner, p: Promise<(() => void) | undefined>) {
@@ -103,6 +108,9 @@ async function queueUpdate(runner: ViteNodeRunner, p: Promise<(() => void) | und
}
async function fetchUpdate(runner: ViteNodeRunner, { path, acceptedPath }: Update) {
+ path = normalizeRequestId(path)
+ acceptedPath = normalizeRequestId(acceptedPath)
+
const maps = getCache(runner)
const mod = maps.hotModulesMap.get(path)
@@ -113,48 +121,29 @@ async function fetchUpdate(runner: ViteNodeRunner, { path, acceptedPath }: Updat
return
}
- const moduleMap = new Map()
const isSelfUpdate = path === acceptedPath
-
- // make sure we only import each dep once
- const modulesToUpdate = new Set()
- if (isSelfUpdate) {
- // self update - only update self
- modulesToUpdate.add(path)
- }
- else {
- // dep update
- for (const { deps } of mod.callbacks) {
- deps.forEach((dep) => {
- if (acceptedPath === dep)
- modulesToUpdate.add(dep)
- })
- }
- }
+ let fetchedModule: ModuleNamespace | undefined
// determine the qualified callbacks before we re-import the modules
- const qualifiedCallbacks = mod.callbacks.filter(({ deps }) => {
- return deps.some(dep => modulesToUpdate.has(dep))
- })
-
- await Promise.all(
- Array.from(modulesToUpdate).map(async (dep) => {
- const disposer = maps.disposeMap.get(dep)
- if (disposer)
- await disposer(maps.dataMap.get(dep))
- try {
- const newMod = await reload(runner, [dep])
- moduleMap.set(dep, newMod)
- }
- catch (e: any) {
- warnFailedFetch(e, dep)
- }
- }),
+ const qualifiedCallbacks = mod.callbacks.filter(({ deps }) =>
+ deps.includes(acceptedPath),
)
+ if (isSelfUpdate || qualifiedCallbacks.length > 0) {
+ const disposer = maps.disposeMap.get(acceptedPath)
+ if (disposer)
+ await disposer(maps.dataMap.get(acceptedPath))
+ try {
+ [fetchedModule] = await reload(runner, [acceptedPath])
+ }
+ catch (e: any) {
+ warnFailedFetch(e, acceptedPath)
+ }
+ }
+
return () => {
for (const { deps, fn } of qualifiedCallbacks)
- fn(deps.map(dep => moduleMap.get(dep)))
+ fn(deps.map(dep => (dep === acceptedPath ? fetchedModule : undefined)))
const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}`
console.log(`${c.cyan('[vite-node]')} hot updated: ${loggedPath}`)
@@ -179,28 +168,27 @@ export async function handleMessage(runner: ViteNodeRunner, emitter: HMREmitter,
sendMessageBuffer(runner, emitter)
break
case 'update':
- notifyListeners(runner, 'vite:beforeUpdate', payload)
- if (maps.isFirstUpdate) {
- reload(runner, files)
- maps.isFirstUpdate = true
- }
- payload.updates.forEach((update) => {
- if (update.type === 'js-update') {
- queueUpdate(runner, fetchUpdate(runner, update))
- }
- else {
- // css-update
- console.error(`${c.cyan('[vite-node]')} no support css hmr.}`)
- }
- })
+ await notifyListeners(runner, 'vite:beforeUpdate', payload)
+ await Promise.all(payload.updates.map((update) => {
+ if (update.type === 'js-update')
+ return queueUpdate(runner, fetchUpdate(runner, update))
+
+ // css-update
+ console.error(`${c.cyan('[vite-node]')} no support css hmr.}`)
+ return null
+ }))
+ await notifyListeners(runner, 'vite:afterUpdate', payload)
break
case 'full-reload':
- notifyListeners(runner, 'vite:beforeFullReload', payload)
+ await notifyListeners(runner, 'vite:beforeFullReload', payload)
maps.customListenersMap.delete('vite:beforeFullReload')
- reload(runner, files)
+ await reload(runner, files)
+ break
+ case 'custom':
+ await notifyListeners(runner, payload.event, payload.data)
break
case 'prune':
- notifyListeners(runner, 'vite:beforePrune', payload)
+ await notifyListeners(runner, 'vite:beforePrune', payload)
payload.paths.forEach((path) => {
const fn = maps.pruneMap.get(path)
if (fn)
@@ -208,7 +196,7 @@ export async function handleMessage(runner: ViteNodeRunner, emitter: HMREmitter,
})
break
case 'error': {
- notifyListeners(runner, 'vite:error', payload)
+ await notifyListeners(runner, 'vite:error', payload)
const err = payload.err
console.error(`${c.cyan('[vite-node]')} Internal Server Error\n${err.message}\n${err.stack}`)
break
diff --git a/packages/vite-node/src/server.ts b/packages/vite-node/src/server.ts
index e1eb6f5a4563..cad850034238 100644
--- a/packages/vite-node/src/server.ts
+++ b/packages/vite-node/src/server.ts
@@ -1,6 +1,6 @@
import { performance } from 'node:perf_hooks'
import { existsSync } from 'node:fs'
-import { join, relative, resolve } from 'pathe'
+import { join, normalize, relative, resolve } from 'pathe'
import type { TransformResult, ViteDevServer } from 'vite'
import createDebug from 'debug'
import type { EncodedSourceMap } from '@jridgewell/trace-mapping'
@@ -72,6 +72,18 @@ export class ViteNodeServer {
const customModuleDirectories = envValue?.split(',')
if (customModuleDirectories)
options.deps.moduleDirectories.push(...customModuleDirectories)
+
+ options.deps.moduleDirectories = options.deps.moduleDirectories.map((dir) => {
+ if (!dir.startsWith('/'))
+ dir = `/${dir}`
+ if (!dir.endsWith('/'))
+ dir += '/'
+ return normalize(dir)
+ })
+
+ // always add node_modules as a module directory
+ if (!options.deps.moduleDirectories.includes('/node_modules/'))
+ options.deps.moduleDirectories.push('/node_modules/')
}
shouldExternalize(id: string) {
diff --git a/packages/vite-node/src/utils.ts b/packages/vite-node/src/utils.ts
index 78acb5a4b887..1f2b76362cc3 100644
--- a/packages/vite-node/src/utils.ts
+++ b/packages/vite-node/src/utils.ts
@@ -6,6 +6,15 @@ import type { Arrayable, Nullable } from './types'
export const isWindows = process.platform === 'win32'
+const drive = isWindows ? process.cwd()[0] : null
+const driveOpposite = drive
+ ? (drive === drive.toUpperCase()
+ ? drive.toLowerCase()
+ : drive.toUpperCase())
+ : null
+const driveRegexp = drive ? new RegExp(`(?:^|/@fs/)${drive}(\:[\\/])`) : null
+const driveOppositeRegext = driveOpposite ? new RegExp(`(?:^|/@fs/)${driveOpposite}(\:[\\/])`) : null
+
export function slash(str: string) {
return str.replace(/\\/g, '/')
}
@@ -16,6 +25,10 @@ export function normalizeRequestId(id: string, base?: string): string {
if (base && id.startsWith(base))
id = `/${id.slice(base.length)}`
+ // keep drive the same as in process cwd
+ if (driveRegexp && !driveRegexp?.test(id) && driveOppositeRegext?.test(id))
+ id = id.replace(driveOppositeRegext, `${drive}$1`)
+
return id
.replace(/^\/@id\/__x00__/, '\0') // virtual modules start with `\0`
.replace(/^\/@id\//, '')
diff --git a/packages/vitest/src/config.ts b/packages/vitest/src/config.ts
index 0afb83cccd86..1fdd469cd2bf 100644
--- a/packages/vitest/src/config.ts
+++ b/packages/vitest/src/config.ts
@@ -10,13 +10,19 @@ export { configDefaults, defaultInclude, defaultExclude, coverageConfigDefaults
export { mergeConfig } from 'vite'
export type { ConfigEnv, ViteUserConfig as UserConfig }
+export type UserConfigFnObject = (env: ConfigEnv) => ViteUserConfig
+export type UserConfigFnPromise = (env: ConfigEnv) => Promise
export type UserConfigFn = (env: ConfigEnv) => ViteUserConfig | Promise
-export type UserConfigExport = ViteUserConfig | Promise | UserConfigFn
+export type UserConfigExport = ViteUserConfig | Promise | UserConfigFnObject | UserConfigFnPromise | UserConfigFn
export type UserProjectConfigFn = (env: ConfigEnv) => UserWorkspaceConfig | Promise
export type UserProjectConfigExport = UserWorkspaceConfig | Promise | UserProjectConfigFn
-export function defineConfig(config: UserConfigExport) {
+export function defineConfig(config: ViteUserConfig): ViteUserConfig
+export function defineConfig(config: Promise): Promise
+export function defineConfig(config: UserConfigFnObject): UserConfigFnObject
+export function defineConfig(config: UserConfigExport): UserConfigExport
+export function defineConfig(config: UserConfigExport): UserConfigExport {
return config
}
diff --git a/packages/vitest/src/defaults.ts b/packages/vitest/src/defaults.ts
index 10040a9486dc..2f80f9e85321 100644
--- a/packages/vitest/src/defaults.ts
+++ b/packages/vitest/src/defaults.ts
@@ -15,6 +15,9 @@ const defaultCoverageExcludes = [
'dist/**',
'packages/*/test?(s)/**',
'**/*.d.ts',
+ '**/virtual:*',
+ '**/__x00__*',
+ '**/\x00*',
'cypress/**',
'test?(s)/**',
'test?(-*).?(c|m)[jt]s?(x)',
diff --git a/packages/vitest/src/integrations/env/utils.ts b/packages/vitest/src/integrations/env/utils.ts
index 230f1f2bbd55..640a2be9f2a4 100644
--- a/packages/vitest/src/integrations/env/utils.ts
+++ b/packages/vitest/src/integrations/env/utils.ts
@@ -5,6 +5,7 @@ const skipKeys = [
'self',
'top',
'parent',
+ 'URL',
]
export function getWindowKeys(global: any, win: any) {
diff --git a/packages/vitest/src/node/config.ts b/packages/vitest/src/node/config.ts
index 947bdfc7d895..087076ac6375 100644
--- a/packages/vitest/src/node/config.ts
+++ b/packages/vitest/src/node/config.ts
@@ -140,7 +140,7 @@ export function resolveConfig(
resolved.deps.inline.push(...extraInlineDeps)
}
}
- resolved.deps.moduleDirectories ??= ['/node_modules/']
+ resolved.deps.moduleDirectories ??= []
resolved.deps.moduleDirectories = resolved.deps.moduleDirectories.map((dir) => {
if (!dir.startsWith('/'))
dir = `/${dir}`
@@ -148,6 +148,8 @@ export function resolveConfig(
dir += '/'
return normalize(dir)
})
+ if (!resolved.deps.moduleDirectories.includes('/node_modules/'))
+ resolved.deps.moduleDirectories.push('/node_modules/')
if (resolved.runner) {
resolved.runner = resolveModule(resolved.runner, { paths: [resolved.root] })
diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts
index 9a93c90e6bb7..bfde636754ed 100644
--- a/packages/vitest/src/node/core.ts
+++ b/packages/vitest/src/node/core.ts
@@ -306,19 +306,28 @@ export class Vitest {
await this.globTestFiles(filters),
)
+ // if run with --changed, don't exit if no tests are found
if (!files.length) {
- const exitCode = this.config.passWithNoTests ? 0 : 1
-
await this.reportCoverage(true)
+
this.logger.printNoTestFound(filters)
- process.exit(exitCode)
+ if (!this.config.watch || !(this.config.changed || this.config.related?.length)) {
+ const exitCode = this.config.passWithNoTests ? 0 : 1
+ process.exit(exitCode)
+ }
}
- // populate once, update cache on watch
- await this.cache.stats.populateStats(this.config.root, files)
+ // all subsequent runs will treat this as a fresh run
+ this.config.changed = false
+ this.config.related = undefined
- await this.runFiles(files)
+ if (files.length) {
+ // populate once, update cache on watch
+ await this.cache.stats.populateStats(this.config.root, files)
+
+ await this.runFiles(files)
+ }
await this.reportCoverage(true)
@@ -326,15 +335,16 @@ export class Vitest {
await this.report('onWatcherStart')
}
- private async getTestDependencies(filepath: WorkspaceSpec) {
- const deps = new Set()
-
+ private async getTestDependencies(filepath: WorkspaceSpec, deps = new Set()) {
const addImports = async ([project, filepath]: WorkspaceSpec) => {
- const transformed = await project.vitenode.transformRequest(filepath)
+ if (deps.has(filepath))
+ return
+ const mod = project.server.moduleGraph.getModuleById(filepath)
+ const transformed = mod?.ssrTransformResult || await project.vitenode.transformRequest(filepath)
if (!transformed)
return
const dependencies = [...transformed.deps || [], ...transformed.dynamicDeps || []]
- for (const dep of dependencies) {
+ await Promise.all(dependencies.map(async (dep) => {
const path = await this.server.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)) {
@@ -342,7 +352,7 @@ export class Vitest {
await addImports([project, fsPath])
}
- }
+ }))
}
await addImports(filepath)
@@ -373,7 +383,8 @@ export class Vitest {
return specs
// don't run anything if no related sources are found
- if (!related.length)
+ // if we are in watch mode, we want to process all tests
+ if (!this.config.watch && !related.length)
return []
const testGraphs = await Promise.all(
@@ -644,7 +655,7 @@ export class Vitest {
if (mm.isMatch(id, this.config.forceRerunTriggers)) {
this.state.getFilepaths().forEach(file => this.changedTests.add(file))
- return []
+ return [id]
}
const projects = this.getModuleProjects(id)
@@ -653,7 +664,8 @@ export class Vitest {
const files: string[] = []
- for (const { server, browser } of projects) {
+ for (const project of projects) {
+ const { server, browser } = project
const mod = server.moduleGraph.getModuleById(id) || browser?.moduleGraph.getModuleById(id)
if (!mod) {
// files with `?v=` query from the browser
@@ -675,7 +687,8 @@ export class Vitest {
this.invalidates.add(id)
- if (this.state.filesMap.has(id)) {
+ // one of test files that we already run, or one of test files that we can run
+ if (this.state.filesMap.has(id) || project.isTestFile(id)) {
this.changedTests.add(id)
files.push(id)
continue
diff --git a/packages/vitest/src/node/logger.ts b/packages/vitest/src/node/logger.ts
index c509b516918b..76ee78cb8264 100644
--- a/packages/vitest/src/node/logger.ts
+++ b/packages/vitest/src/node/logger.ts
@@ -15,6 +15,12 @@ interface ErrorOptions {
project?: WorkspaceProject
}
+const ESC = '\x1B['
+const ERASE_DOWN = `${ESC}J`
+const ERASE_SCROLLBACK = `${ESC}3J`
+const CURSOR_TO_START = `${ESC}1;1H`
+const CLEAR_SCREEN = '\x1Bc'
+
export class Logger {
outputStream = process.stdout
errorStream = process.stderr
@@ -50,7 +56,7 @@ export class Logger {
return
}
- this.console.log(`\x1Bc${message}`)
+ this.console.log(`${ERASE_SCROLLBACK}${CLEAR_SCREEN}${message}`)
}
clearScreen(message: string, force = false) {
@@ -70,9 +76,7 @@ export class Logger {
const log = this._clearScreenPending
this._clearScreenPending = undefined
- // equivalent to ansi-escapes:
- // stdout.write(ansiEscapes.cursorTo(0, 0) + ansiEscapes.eraseDown + log)
- this.console.log(`\u001B[1;1H\u001B[J${log}`)
+ this.console.log(`${CURSOR_TO_START}${ERASE_DOWN}${log}`)
}
printError(err: unknown, options: ErrorOptions = {}) {
@@ -98,10 +102,15 @@ export class Logger {
if (config.watchExclude)
this.console.error(c.dim('watch exclude: ') + c.yellow(config.watchExclude.join(comma)))
- if (config.passWithNoTests)
- this.log(`No ${config.mode} files found, exiting with code 0\n`)
- else
- this.error(c.red(`\nNo ${config.mode} files found, exiting with code 1`))
+ if (config.watch && (config.changed || config.related?.length)) {
+ this.log(`No affected ${config.mode} files found\n`)
+ }
+ else {
+ if (config.passWithNoTests)
+ this.log(`No ${config.mode} files found, exiting with code 0\n`)
+ else
+ this.error(c.red(`\nNo ${config.mode} files found, exiting with code 1`))
+ }
}
printBanner() {
diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts
index ca080eab5b9b..3d282b759705 100644
--- a/packages/vitest/src/node/plugins/index.ts
+++ b/packages/vitest/src/node/plugins/index.ts
@@ -90,6 +90,12 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('t
},
}
+ // chokidar fsevents is unstable on macos when emitting "ready" event
+ if (process.platform === 'darwin' && process.env.VITE_TEST_WATCHER_DEBUG) {
+ config.server!.watch!.useFsEvents = false
+ config.server!.watch!.usePolling = false
+ }
+
const classNameStrategy = (typeof testConfig.css !== 'boolean' && testConfig.css?.modules?.classNameStrategy) || 'stable'
if (classNameStrategy !== 'scoped') {
@@ -154,6 +160,12 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('t
}
},
async configureServer(server) {
+ if (options.watch && process.env.VITE_TEST_WATCHER_DEBUG) {
+ server.watcher.on('ready', () => {
+ // eslint-disable-next-line no-console
+ console.log('[debug] watcher is ready')
+ })
+ }
try {
await ctx.setServer(options, server, userConfig)
if (options.api && options.watch)
diff --git a/packages/vitest/src/node/reporters/junit.ts b/packages/vitest/src/node/reporters/junit.ts
index c1dc5bbf844c..c8b4e0460434 100644
--- a/packages/vitest/src/node/reporters/junit.ts
+++ b/packages/vitest/src/node/reporters/junit.ts
@@ -168,7 +168,7 @@ export class JUnitReporter implements Reporter {
async writeTasks(tasks: Task[], filename: string): Promise {
for (const task of tasks) {
await this.writeElement('testcase', {
- classname: filename,
+ classname: process.env.VITEST_JUNIT_CLASSNAME ?? filename,
name: task.name,
time: getDuration(task),
}, async () => {
diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts
index 44b2cb913157..120c8f20ccee 100644
--- a/packages/vitest/src/node/workspace.ts
+++ b/packages/vitest/src/node/workspace.ts
@@ -73,6 +73,8 @@ export class WorkspaceProject {
closingPromise: Promise | undefined
browserProvider: BrowserProvider | undefined
+ testFilesList: string[] = []
+
constructor(
public path: string | number,
public ctx: Vitest,
@@ -132,9 +134,15 @@ export class WorkspaceProject {
}))
}
+ this.testFilesList = testFiles
+
return testFiles
}
+ isTestFile(id: string) {
+ return this.testFilesList.includes(id)
+ }
+
async globFiles(include: string[], exclude: string[], cwd: string) {
const globOptions: fg.Options = {
absolute: true,
diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts
index c6cc51bd0f34..b785382e858d 100644
--- a/packages/vitest/src/types/config.ts
+++ b/packages/vitest/src/types/config.ts
@@ -333,7 +333,7 @@ export interface InlineConfig {
/**
* Default timeout to wait for close when Vitest shuts down, in milliseconds
*
- * @default 1000
+ * @default 10000
*/
teardownTimeout?: number
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9d827779c008..d3867039dee1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -929,11 +929,11 @@ importers:
specifier: ^3.2.0
version: 3.2.0
istanbul-lib-instrument:
- specifier: ^5.2.1
- version: 5.2.1
+ specifier: ^6.0.0
+ version: 6.0.0
istanbul-lib-report:
- specifier: ^3.0.0
- version: 3.0.0
+ specifier: ^3.0.1
+ version: 3.0.1
istanbul-lib-source-maps:
specifier: ^4.0.1
version: 4.0.1
@@ -978,8 +978,8 @@ importers:
specifier: ^3.2.0
version: 3.2.0
istanbul-lib-report:
- specifier: ^3.0.0
- version: 3.0.0
+ specifier: ^3.0.1
+ version: 3.0.1
istanbul-lib-source-maps:
specifier: ^4.0.1
version: 4.0.1
@@ -1847,6 +1847,9 @@ importers:
vite:
specifier: ^4.3.9
version: 4.3.9(@types/node@18.16.19)
+ vite-node:
+ specifier: workspace:*
+ version: link:../../packages/vite-node
vitest:
specifier: workspace:*
version: link:../../packages/vitest
@@ -5939,7 +5942,7 @@ packages:
graceful-fs: 4.2.10
istanbul-lib-coverage: 3.2.0
istanbul-lib-instrument: 5.2.1
- istanbul-lib-report: 3.0.0
+ istanbul-lib-report: 3.0.1
istanbul-lib-source-maps: 4.0.1
istanbul-reports: 3.1.5
jest-haste-map: 27.5.1
@@ -6559,7 +6562,7 @@ packages:
resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==}
dependencies:
'@gar/promisify': 1.1.3
- semver: 7.5.2
+ semver: 7.5.4
dev: true
/@npmcli/move-file@1.1.2:
@@ -9178,7 +9181,7 @@ packages:
debug: 4.3.4(supports-color@8.1.1)
globby: 11.1.0
is-glob: 4.0.3
- semver: 7.5.2
+ semver: 7.5.4
tsutils: 3.21.0(typescript@5.1.6)
typescript: 5.1.6
transitivePeerDependencies:
@@ -9199,7 +9202,7 @@ packages:
'@typescript-eslint/typescript-estree': 5.60.0(typescript@5.1.6)
eslint: 8.44.0
eslint-scope: 5.1.1
- semver: 7.5.2
+ semver: 7.5.4
transitivePeerDependencies:
- supports-color
- typescript
@@ -11585,7 +11588,7 @@ packages:
/builtins@5.0.1:
resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==}
dependencies:
- semver: 7.5.2
+ semver: 7.5.4
dev: true
/bumpp@9.1.1:
@@ -11658,7 +11661,7 @@ packages:
find-up: 5.0.0
foreground-child: 2.0.0
istanbul-lib-coverage: 3.2.0
- istanbul-lib-report: 3.0.0
+ istanbul-lib-report: 3.0.1
istanbul-reports: 3.1.5
rimraf: 3.0.2
test-exclude: 6.0.0
@@ -14442,7 +14445,7 @@ packages:
optionator: 0.9.1
progress: 2.0.3
regexpp: 3.2.0
- semver: 7.5.2
+ semver: 7.5.4
strip-ansi: 6.0.1
strip-json-comments: 3.1.1
text-table: 0.2.0
@@ -15289,7 +15292,7 @@ packages:
memfs: 3.4.7
minimatch: 3.1.2
schema-utils: 2.7.0
- semver: 7.5.2
+ semver: 7.5.4
tapable: 1.1.3
typescript: 4.8.4
webpack: 4.46.0
@@ -17007,13 +17010,27 @@ packages:
semver: 6.3.0
transitivePeerDependencies:
- supports-color
+ dev: true
- /istanbul-lib-report@3.0.0:
- resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==}
- engines: {node: '>=8'}
+ /istanbul-lib-instrument@6.0.0:
+ resolution: {integrity: sha512-x58orMzEVfzPUKqlbLd1hXCnySCxKdDKa6Rjg97CwuLLRI4g3FHTdnExu1OqffVFay6zeMW+T6/DowFLndWnIw==}
+ engines: {node: '>=10'}
dependencies:
+ '@babel/core': 7.22.5
+ '@babel/parser': 7.22.5
+ '@istanbuljs/schema': 0.1.3
istanbul-lib-coverage: 3.2.0
- make-dir: 3.1.0
+ semver: 7.5.4
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /istanbul-lib-report@3.0.1:
+ resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
+ engines: {node: '>=10'}
+ dependencies:
+ istanbul-lib-coverage: 3.2.0
+ make-dir: 4.0.0
supports-color: 7.2.0
/istanbul-lib-source-maps@4.0.1:
@@ -17031,7 +17048,7 @@ packages:
engines: {node: '>=8'}
dependencies:
html-escaper: 2.0.2
- istanbul-lib-report: 3.0.0
+ istanbul-lib-report: 3.0.1
/iterate-iterator@1.0.2:
resolution: {integrity: sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw==}
@@ -17553,7 +17570,7 @@ packages:
jest-util: 27.5.1
natural-compare: 1.4.0
pretty-format: 27.5.1
- semver: 7.5.2
+ semver: 7.5.4
transitivePeerDependencies:
- supports-color
dev: true
@@ -18540,6 +18557,13 @@ packages:
engines: {node: '>=8'}
dependencies:
semver: 6.3.0
+ dev: true
+
+ /make-dir@4.0.0:
+ resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
+ engines: {node: '>=10'}
+ dependencies:
+ semver: 7.5.4
/make-error@1.3.6:
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
@@ -19257,7 +19281,7 @@ packages:
resolution: {integrity: sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ==}
engines: {node: '>=10'}
dependencies:
- semver: 7.5.2
+ semver: 7.5.4
dev: true
/node-addon-api@6.1.0:
@@ -19360,7 +19384,7 @@ packages:
dependencies:
hosted-git-info: 4.1.0
is-core-module: 2.12.1
- semver: 7.5.2
+ semver: 7.5.4
validate-npm-package-license: 3.0.4
dev: true
@@ -20375,7 +20399,7 @@ packages:
loader-utils: 2.0.2
postcss: 7.0.39
schema-utils: 3.1.1
- semver: 7.5.2
+ semver: 7.5.4
webpack: 4.46.0
dev: true
@@ -22004,6 +22028,13 @@ packages:
lru-cache: 6.0.0
dev: true
+ /semver@7.5.4:
+ resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==}
+ engines: {node: '>=10'}
+ hasBin: true
+ dependencies:
+ lru-cache: 6.0.0
+
/send@0.18.0:
resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
engines: {node: '>= 0.8.0'}
@@ -24686,7 +24717,7 @@ packages:
espree: 9.5.2
esquery: 1.5.0
lodash: 4.17.21
- semver: 7.5.2
+ semver: 7.5.4
transitivePeerDependencies:
- supports-color
dev: true
diff --git a/test/core/src/esm/esm.js b/test/core/src/esm/esm.js
new file mode 100644
index 000000000000..ae67ddada465
--- /dev/null
+++ b/test/core/src/esm/esm.js
@@ -0,0 +1 @@
+export const test = 1
diff --git a/test/core/src/esm/package.json b/test/core/src/esm/package.json
new file mode 100644
index 000000000000..3dbc1ca591c0
--- /dev/null
+++ b/test/core/src/esm/package.json
@@ -0,0 +1,3 @@
+{
+ "type": "module"
+}
diff --git a/test/core/test/imports.test.ts b/test/core/test/imports.test.ts
index 75fc6ec8e984..a6d2970cec73 100644
--- a/test/core/test/imports.test.ts
+++ b/test/core/test/imports.test.ts
@@ -1,6 +1,7 @@
import { mkdir, writeFile } from 'node:fs/promises'
+import { fileURLToPath } from 'node:url'
import { resolve } from 'pathe'
-import { describe, expect, test } from 'vitest'
+import { describe, expect, test, vi } from 'vitest'
import { dynamicRelativeImport } from '../src/relative-import'
// @ts-expect-error module is not typed
@@ -130,3 +131,34 @@ describe('importing special files from node_modules', async () => {
expect(mod.default).toBe('/src/node_modules/file.mp3')
})
})
+
+describe.runIf(process.platform === 'win32')('importing files with different drive casing', async () => {
+ test('importing a local file with different drive casing works', async () => {
+ const path = new URL('./../src/timeout', import.meta.url)
+ const filepath = fileURLToPath(path)
+ const drive = filepath[0].toLowerCase()
+ const upperDrive = drive.toUpperCase()
+ const lowercasePath = filepath.replace(`${upperDrive}:`, `${drive}:`)
+ const uppercasePath = filepath.replace(`${drive}:`, `${upperDrive}:`)
+ expect(lowercasePath).not.toBe(uppercasePath)
+ const mod1 = await import(lowercasePath)
+ const mod2 = await import(uppercasePath)
+ const mod3 = await import('./../src/timeout')
+ expect(mod1).toBe(mod2)
+ expect(mod1).toBe(mod3)
+ })
+
+ test('importing an external file with different drive casing works', async () => {
+ const path = new URL('./../src/esm/esm.js', import.meta.url)
+ const filepath = fileURLToPath(path)
+ const drive = filepath[0].toLowerCase()
+ const upperDrive = drive.toUpperCase()
+ const lowercasePath = filepath.replace(`${upperDrive}:`, `${drive}:`)
+ const uppercasePath = filepath.replace(`${drive}:`, `${upperDrive}:`)
+ expect(lowercasePath).not.toBe(uppercasePath)
+ const mod1 = await import(lowercasePath)
+ vi.resetModules() // since they reference the same global ESM cache, it should not matter
+ const mod2 = await import(uppercasePath)
+ expect(mod1).toBe(mod2)
+ })
+})
diff --git a/test/core/vitest.config.ts b/test/core/vitest.config.ts
index d2252dd7b2dc..837aed1e3085 100644
--- a/test/core/vitest.config.ts
+++ b/test/core/vitest.config.ts
@@ -66,7 +66,7 @@ export default defineConfig({
seed: 101,
},
deps: {
- external: ['tinyspy', /src\/external/],
+ external: ['tinyspy', /src\/external/, /esm\/esm/],
inline: ['inline-lib'],
moduleDirectories: ['node_modules', 'projects', 'packages'],
},
diff --git a/test/coverage-test/coverage-report-tests/__snapshots__/v8.report.test.ts.snap b/test/coverage-test/coverage-report-tests/__snapshots__/v8.report.test.ts.snap
index d561f7561b8b..1d002a4ef9c3 100644
--- a/test/coverage-test/coverage-report-tests/__snapshots__/v8.report.test.ts.snap
+++ b/test/coverage-test/coverage-report-tests/__snapshots__/v8.report.test.ts.snap
@@ -704,12 +704,11 @@ exports[`v8 json report 1`] = `
"0": 1,
"1": 1,
"2": 1,
- "3": 1,
},
"statementMap": {
"0": {
"end": {
- "column": 50,
+ "column": 38,
"line": 1,
},
"start": {
@@ -719,7 +718,7 @@ exports[`v8 json report 1`] = `
},
"1": {
"end": {
- "column": 38,
+ "column": 0,
"line": 2,
},
"start": {
@@ -729,7 +728,7 @@ exports[`v8 json report 1`] = `
},
"2": {
"end": {
- "column": 0,
+ "column": 21,
"line": 3,
},
"start": {
@@ -737,16 +736,6 @@ exports[`v8 json report 1`] = `
"line": 3,
},
},
- "3": {
- "end": {
- "column": 39,
- "line": 4,
- },
- "start": {
- "column": 0,
- "line": 4,
- },
- },
},
},
"/src/Defined.vue": {
diff --git a/test/coverage-test/coverage-report-tests/generic.report.test.ts b/test/coverage-test/coverage-report-tests/generic.report.test.ts
index c9b62ddce129..350c23c3677e 100644
--- a/test/coverage-test/coverage-report-tests/generic.report.test.ts
+++ b/test/coverage-test/coverage-report-tests/generic.report.test.ts
@@ -101,3 +101,19 @@ test('coverage provider does not conflict with built-in reporter\'s outputFile',
expect(files).toContain('junit.xml')
})
+
+test('virtual files should be excluded', () => {
+ const files = fs.readdirSync(resolve('./coverage'))
+ const srcFiles = fs.readdirSync(resolve('./coverage/src'))
+
+ for (const file of [...files, ...srcFiles]) {
+ expect(file).not.toContain('virtual:')
+
+ // Vitest in node
+ expect(file).not.toContain('__x00__')
+ expect(file).not.toContain('\0')
+
+ // Vitest browser
+ expect(file).not.toContain('\x00')
+ }
+})
diff --git a/test/coverage-test/src/Counter/index.ts b/test/coverage-test/src/Counter/index.ts
index 378476d78d22..65b75829d50c 100644
--- a/test/coverage-test/src/Counter/index.ts
+++ b/test/coverage-test/src/Counter/index.ts
@@ -1,4 +1,3 @@
-import CounterComponent from './Counter.component'
import CounterVue from './Counter.vue'
-export { CounterComponent, CounterVue }
+export { CounterVue }
diff --git a/test/coverage-test/test/coverage.test.ts b/test/coverage-test/test/coverage.test.ts
index 6d3e20f399a3..fcf719945215 100644
--- a/test/coverage-test/test/coverage.test.ts
+++ b/test/coverage-test/test/coverage.test.ts
@@ -1,9 +1,16 @@
import { expect, test } from 'vitest'
+
+// @ts-expect-error -- untyped virtual file provided by custom plugin
+import virtualFile1 from 'virtual:vitest-custom-virtual-file-1'
+
import { implicitElse } from '../src/implicitElse'
import { useImportEnv } from '../src/importEnv'
import { second } from '../src/function-count'
import { runDynamicFileCJS, runDynamicFileESM } from '../src/dynamic-files'
+// @ts-expect-error -- untyped virtual file provided by custom plugin
+import virtualFile2 from '\0vitest-custom-virtual-file-2'
+
// Browser mode crashes with dynamic files. Enable this when browser mode works.
// To keep istanbul report consistent between browser and node, skip dynamic tests when istanbul is used.
const skipDynamicFiles = globalThis.process?.env.COVERAGE_PROVIDER === 'istanbul' || !globalThis.process?.env.COVERAGE_PROVIDER
@@ -40,3 +47,8 @@ test.skipIf(skipDynamicFiles)('run dynamic ESM file', async () => {
test.skipIf(skipDynamicFiles)('run dynamic CJS file', async () => {
await runDynamicFileCJS()
})
+
+test('virtual file imports', () => {
+ expect(virtualFile1).toBe('This file should be excluded from coverage report #1')
+ expect(virtualFile2).toBe('This file should be excluded from coverage report #2')
+})
diff --git a/test/coverage-test/vitest.config.ts b/test/coverage-test/vitest.config.ts
index 0eb103425292..258ea5e8bf7b 100644
--- a/test/coverage-test/vitest.config.ts
+++ b/test/coverage-test/vitest.config.ts
@@ -7,6 +7,33 @@ const provider = process.argv[1 + process.argv.indexOf('--provider')]
export default defineConfig({
plugins: [
vue(),
+ {
+ // Simulates Vite's virtual files: https://vitejs.dev/guide/api-plugin.html#virtual-modules-convention
+ name: 'vitest-custom-virtual-files',
+ resolveId(id) {
+ if (id === 'virtual:vitest-custom-virtual-file-1')
+ return 'src/virtual:vitest-custom-virtual-file-1.ts'
+
+ if (id === '\0vitest-custom-virtual-file-2')
+ return 'src/\0vitest-custom-virtual-file-2.ts'
+ },
+ load(id) {
+ if (id === 'src/virtual:vitest-custom-virtual-file-1.ts') {
+ return `
+ const virtualFile = "This file should be excluded from coverage report #1"
+ export default virtualFile;
+ `
+ }
+
+ // Vitest browser resolves this as "\x00", Node as "__x00__"
+ if (id === 'src/__x00__vitest-custom-virtual-file-2.ts' || id === 'src/\x00vitest-custom-virtual-file-2.ts') {
+ return `
+ const virtualFile = "This file should be excluded from coverage report #2"
+ export default virtualFile;
+ `
+ }
+ },
+ },
],
define: {
MY_CONSTANT: '"my constant"',
diff --git a/test/reporters/tests/default.test.ts b/test/reporters/tests/default.test.ts
index 7a77f45316a4..1957aeab94bd 100644
--- a/test/reporters/tests/default.test.ts
+++ b/test/reporters/tests/default.test.ts
@@ -32,6 +32,8 @@ describe('default reporter', async () => {
test('rerun should undo', async () => {
const vitest = await run([], true, '-t', 'passed')
+ vitest.resetOutput()
+
// one file
vitest.write('p')
await vitest.waitForStdout('Input filename pattern')
diff --git a/test/test-utils/index.ts b/test/test-utils/index.ts
index 99ab08ece2e9..7764e0b61112 100644
--- a/test/test-utils/index.ts
+++ b/test/test-utils/index.ts
@@ -1,5 +1,6 @@
import { Console } from 'node:console'
import { Writable } from 'node:stream'
+import fs from 'node:fs'
import { type UserConfig, type VitestRunMode, afterEach } from 'vitest'
import type { Vitest } from 'vitest/node'
import { startVitest } from 'vitest/node'
@@ -86,7 +87,7 @@ function captureLogs() {
}
}
-export async function runVitestCli(_options?: Options | string, ...args: string[]) {
+export async function runCli(command: string, _options?: Options | string, ...args: string[]) {
let options = _options
if (typeof _options === 'string') {
@@ -94,12 +95,12 @@ export async function runVitestCli(_options?: Options | string, ...args: string[
options = undefined
}
- const subprocess = execa('vitest', args, options as Options)
+ const subprocess = execa(command, args, options as Options)
let setDone: (value?: unknown) => void
const isDone = new Promise(resolve => (setDone = resolve))
- const vitest = {
+ const cli = {
stdout: '',
stderr: '',
stdoutListeners: [] as (() => void)[],
@@ -110,12 +111,15 @@ export async function runVitestCli(_options?: Options | string, ...args: string[
subprocess.stdin!.write(text)
},
waitForStdout(expected: string) {
+ const error = new Error('Timeout error')
+ Error.captureStackTrace(error, this.waitForStdout)
return new Promise((resolve, reject) => {
if (this.stdout.includes(expected))
return resolve()
const timeout = setTimeout(() => {
- reject(new Error(`Timeout when waiting for output "${expected}".\nReceived:\n${this.stdout}. \nStderr:\n${this.stderr}`))
+ error.message = `Timeout when waiting for output "${expected}".\nReceived:\n${this.stdout} \nStderr:\n${this.stderr}`
+ reject(error)
}, process.env.CI ? 20_000 : 4_000)
const listener = () => {
@@ -131,12 +135,15 @@ export async function runVitestCli(_options?: Options | string, ...args: string[
})
},
waitForStderr(expected: string) {
+ const error = new Error('Timeout')
+ Error.captureStackTrace(error, this.waitForStderr)
return new Promise((resolve, reject) => {
if (this.stderr.includes(expected))
return resolve()
const timeout = setTimeout(() => {
- reject(new Error(`Timeout when waiting for error "${expected}".\nReceived:\n${this.stderr}`))
+ error.message = `Timeout when waiting for error "${expected}".\nReceived:\n${this.stderr}\nStdout:\n${this.stdout}`
+ reject(error)
}, process.env.CI ? 20_000 : 4_000)
const listener = () => {
@@ -158,13 +165,13 @@ export async function runVitestCli(_options?: Options | string, ...args: string[
}
subprocess.stdout!.on('data', (data) => {
- vitest.stdout += stripAnsi(data.toString())
- vitest.stdoutListeners.forEach(fn => fn())
+ cli.stdout += stripAnsi(data.toString())
+ cli.stdoutListeners.forEach(fn => fn())
})
subprocess.stderr!.on('data', (data) => {
- vitest.stderr += stripAnsi(data.toString())
- vitest.stderrListeners.forEach(fn => fn())
+ cli.stderr += stripAnsi(data.toString())
+ cli.stderrListeners.forEach(fn => fn())
})
subprocess.on('exit', () => setDone())
@@ -174,16 +181,54 @@ export async function runVitestCli(_options?: Options | string, ...args: string[
if (subprocess.exitCode === null)
subprocess.kill()
- await vitest.isDone
+ await cli.isDone
})
- if (args.includes('--watch')) { // Wait for initial test run to complete
- await vitest.waitForStdout('Waiting for file changes')
- vitest.resetOutput()
+ if (args.includes('--watch')) {
+ if (command === 'vitest') // Wait for initial test run to complete
+ await cli.waitForStdout('Waiting for file changes')
+ // make sure watcher is ready
+ await cli.waitForStdout('[debug] watcher is ready')
+ cli.stdout = cli.stdout.replace('[debug] watcher is ready\n', '')
}
else {
- await vitest.isDone
+ await cli.isDone
}
- return vitest
+ return cli
+}
+
+export async function runVitestCli(_options?: Options | string, ...args: string[]) {
+ process.env.VITE_TEST_WATCHER_DEBUG = 'true'
+ return runCli('vitest', _options, ...args)
+}
+
+export async function runViteNodeCli(_options?: Options | string, ...args: string[]) {
+ process.env.VITE_TEST_WATCHER_DEBUG = 'true'
+ return runCli('vite-node', _options, ...args)
+}
+
+const originalFiles = new Map()
+const createdFiles = new Set()
+afterEach(() => {
+ originalFiles.forEach((content, file) => {
+ fs.writeFileSync(file, content, 'utf-8')
+ })
+ createdFiles.forEach((file) => {
+ fs.unlinkSync(file)
+ })
+ originalFiles.clear()
+ createdFiles.clear()
+})
+
+export function createFile(file: string, content: string) {
+ createdFiles.add(file)
+ fs.writeFileSync(file, content, 'utf-8')
+}
+
+export function editFile(file: string, callback: (content: string) => string) {
+ const content = fs.readFileSync(file, 'utf-8')
+ if (!originalFiles.has(file))
+ originalFiles.set(file, content)
+ fs.writeFileSync(file, callback(content), 'utf-8')
}
diff --git a/test/test-utils/package.json b/test/test-utils/package.json
index a9b39f620758..8005128685c5 100644
--- a/test/test-utils/package.json
+++ b/test/test-utils/package.json
@@ -8,6 +8,7 @@
"execa": "^7.1.1",
"strip-ansi": "^7.0.1",
"vite": "latest",
+ "vite-node": "workspace:*",
"vitest": "workspace:*"
}
}
diff --git a/test/vite-node/src/cli-parse-args.js b/test/vite-node/src/cli-parse-args.js
new file mode 100644
index 000000000000..afaf1236eae6
--- /dev/null
+++ b/test/vite-node/src/cli-parse-args.js
@@ -0,0 +1,2 @@
+// eslint-disable-next-line no-console
+console.log(process.argv)
diff --git a/test/vite-node/src/hmr-script.js b/test/vite-node/src/hmr-script.js
new file mode 100644
index 000000000000..16b5afa83080
--- /dev/null
+++ b/test/vite-node/src/hmr-script.js
@@ -0,0 +1,7 @@
+console.error('Hello!')
+
+if (import.meta.hot) {
+ import.meta.hot.accept(() => {
+ console.error('Accept')
+ })
+}
diff --git a/test/vite-node/src/watched.js b/test/vite-node/src/watched.js
new file mode 100644
index 000000000000..54046b60bfa1
--- /dev/null
+++ b/test/vite-node/src/watched.js
@@ -0,0 +1,2 @@
+// eslint-disable-next-line no-console
+console.log('test 1')
diff --git a/test/vite-node/test/circular.test.ts b/test/vite-node/test/circular.test.ts
index 297c7dd93cda..e17152ff00bf 100644
--- a/test/vite-node/test/circular.test.ts
+++ b/test/vite-node/test/circular.test.ts
@@ -1,17 +1,15 @@
import { expect, test } from 'vitest'
-import { execa } from 'execa'
import { resolve } from 'pathe'
-
-const cliPath = resolve(__dirname, '../../../packages/vite-node/src/cli.ts')
+import { runViteNodeCli } from '../../test-utils'
test('circular 1', async () => {
const entryPath = resolve(__dirname, '../src/circular1/index.ts')
- const result = await execa('npx', ['esno', cliPath, entryPath], { reject: true })
- expect(result.stdout).toMatchInlineSnapshot('"A Bindex index"')
+ const cli = await runViteNodeCli(entryPath)
+ expect(cli.stdout).toContain('A Bindex index')
}, 60_000)
test('circular 2', async () => {
const entryPath = resolve(__dirname, '../src/circular2/index.ts')
- const result = await execa('npx', ['esno', cliPath, entryPath], { reject: true })
- expect(result.stdout).toMatchInlineSnapshot('"ac b"')
+ const cli = await runViteNodeCli(entryPath)
+ expect(cli.stdout).toContain('ac b')
}, 60_000)
diff --git a/test/vite-node/test/cli.test.ts b/test/vite-node/test/cli.test.ts
new file mode 100644
index 000000000000..08625c318794
--- /dev/null
+++ b/test/vite-node/test/cli.test.ts
@@ -0,0 +1,48 @@
+import { resolve } from 'pathe'
+import pkg from 'vite-node/package.json'
+import { expect, it } from 'vitest'
+import { editFile, runViteNodeCli } from '../../test-utils'
+
+const entryPath = resolve(__dirname, '../src/cli-parse-args.js')
+
+const version = (pkg as any).version
+
+const parseResult = (s: string) => JSON.parse(s.replaceAll('\'', '"'))
+
+it('basic', async () => {
+ const cli = await runViteNodeCli(entryPath)
+ expect(cli.stdout).toContain('node')
+ expect(parseResult(cli.stdout)).toHaveLength(2)
+})
+
+it('--help', async () => {
+ const cli1 = await runViteNodeCli('--help', entryPath)
+ expect(cli1.stdout).toContain('Usage:')
+ const cli2 = await runViteNodeCli('-h', entryPath)
+ expect(cli2.stdout).toContain('Usage:')
+})
+
+it('--version', async () => {
+ const cli1 = await runViteNodeCli('--version', entryPath)
+ expect(cli1.stdout).toContain(`vite-node/${version}`)
+ const cli2 = await runViteNodeCli('-v', entryPath)
+ expect(cli2.stdout).toContain(`vite-node/${version}`)
+})
+
+it('script args', async () => {
+ const cli1 = await runViteNodeCli(entryPath, '--version', '--help')
+ expect(parseResult(cli1.stdout)).include('--version').include('--help')
+})
+
+it('script args in -- after', async () => {
+ const cli1 = await runViteNodeCli(entryPath, '--', '--version', '--help')
+ expect(parseResult(cli1.stdout)).include('--version').include('--help')
+})
+
+it('correctly runs --watch', async () => {
+ const entryPath = resolve(__dirname, '../src/watched.js')
+ const cli = await runViteNodeCli('--watch', entryPath)
+ await cli.waitForStdout('test 1')
+ editFile(entryPath, c => c.replace('test 1', 'test 2'))
+ await cli.waitForStdout('test 2')
+})
diff --git a/test/vite-node/test/hmr.test.ts b/test/vite-node/test/hmr.test.ts
new file mode 100644
index 000000000000..0cfdca19b36a
--- /dev/null
+++ b/test/vite-node/test/hmr.test.ts
@@ -0,0 +1,17 @@
+import { test } from 'vitest'
+import { resolve } from 'pathe'
+import { editFile, runViteNodeCli } from '../../test-utils'
+
+test('hmr.accept works correctly', async () => {
+ const scriptFile = resolve(__dirname, '../src/hmr-script.js')
+
+ const viteNode = await runViteNodeCli('--watch', scriptFile)
+
+ await viteNode.waitForStderr('Hello!')
+
+ editFile(scriptFile, content => content.replace('Hello!', 'Hello world!'))
+
+ await viteNode.waitForStderr('Hello world!')
+ await viteNode.waitForStderr('Accept')
+ await viteNode.waitForStdout(`[vite-node] hot updated: ${scriptFile}`)
+})
diff --git a/test/vite-node/test/self-export.test.ts b/test/vite-node/test/self-export.test.ts
index 53cfc47dfff4..e4b7cc51dd62 100644
--- a/test/vite-node/test/self-export.test.ts
+++ b/test/vite-node/test/self-export.test.ts
@@ -1,7 +1,7 @@
import { expect, it } from 'vitest'
-import { execa } from 'execa'
import { resolve } from 'pathe'
import ansiEscapes, { HelloWorld } from '../src/self-export'
+import { runViteNodeCli } from '../../test-utils'
it('should export self', () => {
expect(ansiEscapes.HelloWorld).eq(HelloWorld)
@@ -9,16 +9,14 @@ it('should export self', () => {
expect(HelloWorld).eq(1)
})
-const cliPath = resolve(__dirname, '../../../packages/vite-node/src/cli.ts')
-
it('example 1', async () => {
const entryPath = resolve(__dirname, '../src/self-export-example1.ts')
- const result = await execa('npx', ['esno', cliPath, entryPath], { reject: true })
- expect(result.stdout).includes('Function')
+ const cli = await runViteNodeCli(entryPath)
+ await cli.waitForStdout('Function')
}, 60_000)
it('example 2', async () => {
const entryPath = resolve(__dirname, '../src/self-export-example2.ts')
- const result = await execa('npx', ['esno', cliPath, entryPath], { reject: true })
- expect(result.stdout).includes('HelloWorld: 1').includes('default')
+ const cli = await runViteNodeCli(entryPath)
+ await cli.waitForStdout('HelloWorld: 1')
}, 60_000)
diff --git a/test/vite-node/vitest.config.ts b/test/vite-node/vitest.config.ts
index 1515dfeb73a4..3212242e1663 100644
--- a/test/vite-node/vitest.config.ts
+++ b/test/vite-node/vitest.config.ts
@@ -3,5 +3,6 @@ import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
clearMocks: true,
+ testTimeout: process.env.CI ? 120_000 : 5_000,
},
})
diff --git a/test/watch/fixtures/force-watch/trigger.js b/test/watch/fixtures/force-watch/trigger.js
new file mode 100644
index 000000000000..2e2b68e75b83
--- /dev/null
+++ b/test/watch/fixtures/force-watch/trigger.js
@@ -0,0 +1 @@
+export const trigger = false
diff --git a/test/watch/fixtures/vitest.config.ts b/test/watch/fixtures/vitest.config.ts
index 7d418e5cca77..623e5e267490 100644
--- a/test/watch/fixtures/vitest.config.ts
+++ b/test/watch/fixtures/vitest.config.ts
@@ -10,5 +10,9 @@ export default defineConfig({
// This configuration is edited by tests
reporters: 'verbose',
+
+ forceRerunTriggers: [
+ '**/force-watch/**',
+ ],
},
})
diff --git a/test/watch/test/file-watching.test.ts b/test/watch/test/file-watching.test.ts
index 877e61346b59..c79754985307 100644
--- a/test/watch/test/file-watching.test.ts
+++ b/test/watch/test/file-watching.test.ts
@@ -1,7 +1,7 @@
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { afterEach, describe, expect, test } from 'vitest'
-import { runVitestCli } from '../../test-utils'
+import * as testUtils from '../../test-utils'
const sourceFile = 'fixtures/math.ts'
const sourceFileContent = readFileSync(sourceFile, 'utf-8')
@@ -12,9 +12,19 @@ const testFileContent = readFileSync(testFile, 'utf-8')
const configFile = 'fixtures/vitest.config.ts'
const configFileContent = readFileSync(configFile, 'utf-8')
+const forceTriggerFile = 'fixtures/force-watch/trigger.js'
+const forceTriggerFileContent = readFileSync(forceTriggerFile, 'utf-8')
+
const cliArgs = ['--root', 'fixtures', '--watch']
const cleanups: (() => void)[] = []
+async function runVitestCli(...args: string[]) {
+ const vitest = await testUtils.runVitestCli(...args)
+ if (args.includes('--watch'))
+ vitest.resetOutput()
+ return vitest
+}
+
function editFile(fileContent: string) {
return `// Modified by file-watching.test.ts
${fileContent}
@@ -26,6 +36,7 @@ afterEach(() => {
writeFileSync(sourceFile, sourceFileContent, 'utf8')
writeFileSync(testFile, testFileContent, 'utf8')
writeFileSync(configFile, configFileContent, 'utf8')
+ writeFileSync(forceTriggerFile, forceTriggerFileContent, 'utf8')
cleanups.splice(0).forEach(cleanup => cleanup())
})
@@ -39,6 +50,18 @@ test('editing source file triggers re-run', async () => {
await vitest.waitForStdout('1 passed')
})
+test('editing force rerun trigger reruns all tests', async () => {
+ const vitest = await runVitestCli(...cliArgs)
+
+ writeFileSync(forceTriggerFile, editFile(forceTriggerFileContent), 'utf8')
+
+ await vitest.waitForStdout('Waiting for file changes...')
+ await vitest.waitForStdout('RERUN ../force-watch/trigger.js')
+ await vitest.waitForStdout('example.test.ts')
+ await vitest.waitForStdout('math.test.ts')
+ await vitest.waitForStdout('2 passed')
+})
+
test('editing test file triggers re-run', async () => {
const vitest = await runVitestCli(...cliArgs)
diff --git a/test/watch/test/related.test.ts b/test/watch/test/related.test.ts
new file mode 100644
index 000000000000..654e4c533c32
--- /dev/null
+++ b/test/watch/test/related.test.ts
@@ -0,0 +1,22 @@
+import { test } from 'vitest'
+import { resolve } from 'pathe'
+import { editFile, runVitestCli } from '../../test-utils'
+
+const cliArgs = ['--root', 'fixtures', '--watch', '--changed']
+
+test('when nothing is changed, run nothing but keep watching', async () => {
+ const vitest = await runVitestCli(...cliArgs)
+
+ await vitest.waitForStdout('No affected test files found')
+ await vitest.waitForStdout('Waiting for file changes...')
+
+ editFile(resolve(__dirname, '../fixtures/math.ts'), content => `${content}\n\n`)
+
+ await vitest.waitForStdout('RERUN ../math.ts')
+ await vitest.waitForStdout('1 passed')
+
+ editFile(resolve(__dirname, '../fixtures/math.test.ts'), content => `${content}\n\n`)
+
+ await vitest.waitForStdout('RERUN ../math.test.ts')
+ await vitest.waitForStdout('1 passed')
+})
diff --git a/test/watch/test/stdin.test.ts b/test/watch/test/stdin.test.ts
index af91599ec4f2..8de44d7b0fb6 100644
--- a/test/watch/test/stdin.test.ts
+++ b/test/watch/test/stdin.test.ts
@@ -1,7 +1,14 @@
import { rmSync, writeFileSync } from 'node:fs'
import { afterEach, expect, test } from 'vitest'
-import { runVitestCli } from '../../test-utils'
+import * as testUtils from '../../test-utils'
+
+async function runVitestCli(...args: string[]) {
+ const vitest = await testUtils.runVitestCli(...args)
+ if (args.includes('--watch'))
+ vitest.resetOutput()
+ return vitest
+}
const cliArgs = ['--root', 'fixtures', '--watch']
const cleanups: (() => void)[] = []
diff --git a/test/watch/test/stdout.test.ts b/test/watch/test/stdout.test.ts
index 6110bf26a850..5104a3632416 100644
--- a/test/watch/test/stdout.test.ts
+++ b/test/watch/test/stdout.test.ts
@@ -12,6 +12,7 @@ afterEach(() => {
test('console.log is visible on test re-run', async () => {
const vitest = await runVitestCli('--root', 'fixtures', '--watch')
+ vitest.resetOutput()
const testCase = `
test('test with logging', () => {
console.log('First')
diff --git a/test/watch/test/workspaces.test.ts b/test/watch/test/workspaces.test.ts
index 285734037ff8..18aeac8e5fa7 100644
--- a/test/watch/test/workspaces.test.ts
+++ b/test/watch/test/workspaces.test.ts
@@ -26,8 +26,8 @@ test("dynamic test case", () => {
})
`
-function startVitest() {
- return runVitestCli(
+async function startVitest() {
+ const vitest = await runVitestCli(
{ cwd: root, env: { TEST_WATCH: 'true' } },
'--root',
root,
@@ -36,6 +36,8 @@ function startVitest() {
'--watch',
'--no-coverage',
)
+ vitest.resetOutput()
+ return vitest
}
afterEach(() => {
diff --git a/test/watch/vitest.config.ts b/test/watch/vitest.config.ts
index f5944ddb461d..069ca3858ee6 100644
--- a/test/watch/vitest.config.ts
+++ b/test/watch/vitest.config.ts
@@ -9,7 +9,7 @@ export default defineConfig({
},
// For Windows CI mostly
- testTimeout: process.env.CI ? 30_000 : 10_000,
+ testTimeout: process.env.CI ? 60_000 : 10_000,
// Test cases may have side effects, e.g. files under fixtures/ are modified on the fly to trigger file watchers
singleThread: true,