Skip to content

Commit

Permalink
Merge branch 'main' into fix-re-mock-instance-method
Browse files Browse the repository at this point in the history
  • Loading branch information
hi-ogawa authored Dec 2, 2023
2 parents a5c3a06 + ceca93d commit e4bad94
Show file tree
Hide file tree
Showing 16 changed files with 202 additions and 71 deletions.
3 changes: 3 additions & 0 deletions docs/.vitepress/contributors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ const plainTeamMembers: CoreTeam[] = [
avatar: contributorsAvatars.AriPerkkio,
name: 'Ari Perkkiö',
github: 'AriPerkkio',
mastodon: 'https://elk.zone/m.webtoo.ls/@AriPerkkio',
twitter: 'ari_perkkio',
sponsor: 'https://github.com/sponsors/AriPerkkio',
title: 'A fullstack developer, working',
desc: 'Core team member of Vitest',
org: 'Cloudamite',
Expand Down
24 changes: 22 additions & 2 deletions docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -598,15 +598,35 @@ import { describe, test } from 'vitest'
const isDev = process.env.NODE_ENV === 'development'
describe.skipIf(isDev)('prod only test', () => {
// this test only runs in production
describe.skipIf(isDev)('prod only test suite', () => {
// this test suite only runs in production
})
```

::: warning
You cannot use this syntax when using Vitest as [type checker](/guide/testing-types).
:::

### describe.runIf

- **Type:** `(condition: any) => void`

Opposite of [describe.skipIf](#describe-skipif).

```ts
import { assert, test } from 'vitest'
const isDev = process.env.NODE_ENV === 'development'
describe.runIf(isDev)('dev only test suite', () => {
// this test suite only runs in development
})
```

::: warning
You cannot use this syntax, when using Vitest as [type checker](/guide/testing-types).
:::

### describe.only

- **Type:** `(name: string | Function, fn: TestFunction, options?: number | TestOptions) => void`
Expand Down
2 changes: 1 addition & 1 deletion docs/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1326,7 +1326,7 @@ Watermarks for statements, lines, branches and functions. See [istanbul document
#### coverage.processingConcurrency

- **Type:** `boolean`
- **Default:** `Math.min(20, os.cpu().length)`
- **Default:** `Math.min(20, os.cpus().length)`
- **Available for providers:** `'v8' | 'istanbul'`
- **CLI:** `--coverage.processingConcurrency=<number>`

Expand Down
56 changes: 56 additions & 0 deletions docs/guide/mocking.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,62 @@ The following principles apply
* All objects will be deeply cloned
* All instances of classes and their prototypes will be deeply cloned

### Mocking Pitfalls

Beware that it is not possible to mock calls to methods that are called inside other methods of the same file. For example, in this code:

```ts
export function foo() {
return 'foo'
}

export function foobar() {
return `${foo()}bar`
}
```

It is not possible to mock the `foo` method from the outside because it is referenced directly. So this code will have no effect on the `foo` call inside `foobar` (but it will affect the `foo` call in other modules):

```ts
import { vi } from 'vitest'
import * as mod from './foobar.js'

// this will only affect "foo" outside of the original module
vi.spyOn(mod, 'foo')
vi.mock('./foobar.js', async (importOriginal) => {
return {
...await importOriginal(),
// this will only affect "foo" outside of the original module
foo: () => 'mocked'
}
})
```

You can confirm this behaviour by providing the implementation to the `foobar` method directly:

```ts
// foobar.test.js
import * as mod from './foobar.js'

vi.spyOn(mod, 'foo')

// exported foo references mocked method
mod.foobar(mod.foo)
```

```ts
// foobar.js
export function foo() {
return 'foo'
}

export function foobar(injectedFoo) {
return injectedFoo !== foo // false
}
```

This is the intended behaviour. It is usually a sign of bad code when mocking is involved in such a manner. Consider refactoring your code into multiple files or improving your application architecture by using techniques such as [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection).

### Example

```js
Expand Down
2 changes: 1 addition & 1 deletion netlify.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build.environment]
NODE_VERSION = "18"
NODE_VERSION = "20"
# don't need playwright for docs build
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1"

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"rollup-plugin-esbuild": "^6.1.0",
"rollup-plugin-license": "^3.2.0",
"simple-git-hooks": "^2.9.0",
"tsx": "^4.1.1",
"tsx": "^4.6.1",
"typescript": "^5.2.2",
"vite": "^5.0.0-beta.19",
"vitest": "workspace:*"
Expand Down
59 changes: 51 additions & 8 deletions packages/browser/src/client/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const browserHashMap = new Map<string, [test: boolean, timestamp: string]>()

const url = new URL(location.href)
const testId = url.searchParams.get('id') || 'unknown'
const reloadTries = Number(url.searchParams.get('reloadTries') || '0')

function getQueryPaths() {
return url.searchParams.getAll('path')
Expand Down Expand Up @@ -62,21 +63,51 @@ function on(event: string, listener: (...args: any[]) => void) {
return () => window.removeEventListener(event, listener)
}

// we can't import "processError" yet because error might've been thrown before the module was loaded
async function defaultErrorReport(type: string, unhandledError: any) {
const error = {
function serializeError(unhandledError: any) {
return {
...unhandledError,
name: unhandledError.name,
message: unhandledError.message,
stack: unhandledError.stack,
stack: String(unhandledError.stack),
}
}

// we can't import "processError" yet because error might've been thrown before the module was loaded
async function defaultErrorReport(type: string, unhandledError: any) {
const error = serializeError(unhandledError)
if (testId !== 'no-isolate')
error.VITEST_TEST_PATH = testId
await client.rpc.onUnhandledError(error, type)
await client.rpc.onDone(testId)
}

const stopErrorHandler = on('error', e => defaultErrorReport('Error', e.error))
function catchWindowErrors(cb: (e: ErrorEvent) => void) {
let userErrorListenerCount = 0
function throwUnhandlerError(e: ErrorEvent) {
if (userErrorListenerCount === 0 && e.error != null)
cb(e)
else
console.error(e.error)
}
const addEventListener = window.addEventListener.bind(window)
const removeEventListener = window.removeEventListener.bind(window)
window.addEventListener('error', throwUnhandlerError)
window.addEventListener = function (...args: Parameters<typeof addEventListener>) {
if (args[0] === 'error')
userErrorListenerCount++
return addEventListener.apply(this, args)
}
window.removeEventListener = function (...args: Parameters<typeof removeEventListener>) {
if (args[0] === 'error' && userErrorListenerCount)
userErrorListenerCount--
return removeEventListener.apply(this, args)
}
return function clearErrorHandlers() {
window.removeEventListener('error', throwUnhandlerError)
}
}

const stopErrorHandler = catchWindowErrors(e => defaultErrorReport('Error', e.error))
const stopRejectionHandler = on('unhandledrejection', e => defaultErrorReport('Unhandled Rejection', e.reason))

let runningTests = false
Expand All @@ -100,15 +131,27 @@ ws.addEventListener('open', async () => {
const { getSafeTimers } = await importId('vitest/utils') as typeof import('vitest/utils')
safeRpc = createSafeRpc(client, getSafeTimers)
}
catch (err) {
location.reload()
catch (err: any) {
if (reloadTries >= 10) {
const error = serializeError(new Error('Vitest failed to load "vitest/utils" after 10 retries.'))
error.cause = serializeError(err)

await client.rpc.onUnhandledError(error, 'Reload Error')
await client.rpc.onDone(testId)
return
}

const tries = reloadTries + 1
const newUrl = new URL(location.href)
newUrl.searchParams.set('reloadTries', String(tries))
location.href = newUrl.href
return
}

stopErrorHandler()
stopRejectionHandler()

on('error', event => reportUnexpectedError(safeRpc, 'Error', event.error))
catchWindowErrors(event => reportUnexpectedError(safeRpc, 'Error', event.error))
on('unhandledrejection', event => reportUnexpectedError(safeRpc, 'Unhandled Rejection', event.reason))

// @ts-expect-error untyped global for internal use
Expand Down
13 changes: 8 additions & 5 deletions packages/browser/src/client/runner.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { File, TaskResultPack, Test } from '@vitest/runner'
import type { File, TaskResultPack, Test, VitestRunner } from '@vitest/runner'
import type { ResolvedConfig } from 'vitest'
import { rpc } from './rpc'

Expand All @@ -11,7 +11,10 @@ interface CoverageHandler {
takeCoverage: () => Promise<unknown>
}

export function createBrowserRunner(original: any, coverageModule: CoverageHandler | null) {
export function createBrowserRunner(
original: { new(config: ResolvedConfig): VitestRunner },
coverageModule: CoverageHandler | null,
): { new(options: BrowserRunnerOptions): VitestRunner } {
return class BrowserTestRunner extends original {
public config: ResolvedConfig
hashMap = new Map<string, [test: boolean, timstamp: string]>()
Expand All @@ -23,7 +26,7 @@ export function createBrowserRunner(original: any, coverageModule: CoverageHandl
}

async onAfterRunTask(task: Test) {
await super.onAfterRunTest?.(task)
await super.onAfterRunTask?.(task)
task.result?.errors?.forEach((error) => {
console.error(error.message)
})
Expand All @@ -39,8 +42,8 @@ export function createBrowserRunner(original: any, coverageModule: CoverageHandl
}
}

async onAfterRunFiles() {
await super.onAfterRun?.()
async onAfterRunFiles(files: File[]) {
await super.onAfterRunFiles?.(files)
const coverage = await coverageModule?.takeCoverage?.()

if (coverage) {
Expand Down
20 changes: 1 addition & 19 deletions packages/browser/src/client/utils.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,5 @@
// it's possible that import was not optimized yet
async function tryImport(id: string, tries = 20): Promise<any> {
try {
return await import(id)
}
catch (cause) {
if (tries <= 0) {
location.reload()
throw new Error(`Failed to import ${id}.`, { cause })
}

await new Promise(resolve => setTimeout(resolve, 0))
return await tryImport(id, tries - 1)
}
}

export async function importId(id: string) {
const name = `/@id/${id}`
// TODO: this import _should_ always work, but sometimes it doesn't
// this is a workaround until we can properly debug it - maybe server is not ready?
// @ts-expect-error mocking vitest apis
return __vi_wrap_module__(tryImport(name))
return __vi_wrap_module__(import(name))
}
3 changes: 3 additions & 0 deletions packages/expect/src/jest-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ function eq(
if (a instanceof Error && b instanceof Error)
return a.message === b.message

if (a instanceof URL && b instanceof URL)
return a.href === b.href

if (Object.is(a, b))
return true

Expand Down
5 changes: 4 additions & 1 deletion packages/vitest/src/integrations/browser/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ export async function createBrowserServer(project: WorkspaceProject, configFile:
// browser never runs in middleware mode
server.middlewareMode = false

config.server = server
config.server = {
...config.server,
...server,
}
config.server.fs ??= {}
config.server.fs.allow = config.server.fs.allow || []
config.server.fs.allow.push(
Expand Down
11 changes: 9 additions & 2 deletions packages/vitest/src/node/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ViteNodeRunner } from 'vite-node/client'
import { SnapshotManager } from '@vitest/snapshot/manager'
import type { CancelReason, File } from '@vitest/runner'
import { ViteNodeServer } from 'vite-node/server'
import type { ArgumentsType, CoverageProvider, OnServerRestartHandler, Reporter, ResolvedConfig, UserConfig, UserWorkspaceConfig, VitestRunMode } from '../types'
import type { ArgumentsType, Awaitable, CoverageProvider, OnServerRestartHandler, Reporter, ResolvedConfig, UserConfig, UserWorkspaceConfig, VitestRunMode } from '../types'
import { hasFailed, noop, slash, toArray } from '../utils'
import { getCoverageProvider } from '../integrations/coverage'
import type { BrowserProvider } from '../types/browser'
Expand Down Expand Up @@ -76,6 +76,7 @@ export class Vitest {
private _onClose: (() => Awaited<unknown>)[] = []
private _onSetServer: OnServerRestartHandler[] = []
private _onCancelListeners: ((reason: CancelReason) => Promise<void> | void)[] = []
private _poolClosePromise?: Awaitable<void>

async setServer(options: UserConfig, server: ViteDevServer, cliOptions: UserConfig) {
this.unregisterWatcher?.()
Expand Down Expand Up @@ -357,6 +358,10 @@ export class Vitest {
await this.runFiles(files)
}

// In run mode close pool as early as possible
if (!this.config.watch && this.pool?.close)
this._poolClosePromise = this.pool.close()

await this.reportCoverage(true)

if (this.config.watch)
Expand Down Expand Up @@ -780,7 +785,9 @@ export class Vitest {

if (this.pool) {
closePromises.push((async () => {
await this.pool?.close?.()
await (this._poolClosePromise || this.pool?.close?.())

this._poolClosePromise = undefined
this.pool = undefined
})())
}
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/runtime/mocker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ export class VitestMocker {
return {
...actual,
// your mocked methods
},
}
})`)}\n`,
)
}
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/types/coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ export interface BaseCoverageOptions {

/**
* Concurrency limit used when processing the coverage results.
* Defaults to `Math.min(20, os.cpu().length)`
* Defaults to `Math.min(20, os.cpus().length)`
*/
processingConcurrency?: number
}
Expand Down
Loading

0 comments on commit e4bad94

Please sign in to comment.