From 251893b41caaff26b9c3fb59cb79216bec9d149f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=A0=20/=20green?= Date: Wed, 13 Nov 2024 19:54:04 +0900 Subject: [PATCH 01/18] chore: set `resolve.mainFields` and `resolve.conditions` for SSR environment (#6896) --- packages/vitest/src/node/plugins/index.ts | 12 ++++++++++++ packages/vitest/src/node/plugins/workspace.ts | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index 5b687e27e421..b24a0e06fce8 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -113,6 +113,18 @@ export async function VitestPlugin( // https://github.com/vitejs/vite/pull/16453 emptyOutDir: false, }, + // eslint-disable-next-line ts/ban-ts-comment + // @ts-ignore Vite 6 compat + environments: { + ssr: { + resolve: { + // by default Vite resolves `module` field, which not always a native ESM module + // setting this option can bypass that and fallback to cjs version + mainFields: [], + conditions: ['node'], + }, + }, + }, test: { poolOptions: { threads: { diff --git a/packages/vitest/src/node/plugins/workspace.ts b/packages/vitest/src/node/plugins/workspace.ts index bdffdbe16117..8f4e3c9027e0 100644 --- a/packages/vitest/src/node/plugins/workspace.ts +++ b/packages/vitest/src/node/plugins/workspace.ts @@ -96,6 +96,18 @@ export function WorkspaceVitestPlugin( ), }, }, + // eslint-disable-next-line ts/ban-ts-comment + // @ts-ignore Vite 6 compat + environments: { + ssr: { + resolve: { + // by default Vite resolves `module` field, which not always a native ESM module + // setting this option can bypass that and fallback to cjs version + mainFields: [], + conditions: ['node'], + }, + }, + }, test: { name, }, From 9a0c93d7876d84a975efa8c8ffca0d42bb9d5782 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 13 Nov 2024 11:54:53 +0100 Subject: [PATCH 02/18] fix(browser): stop the browser rpc when the pool is closed (#6858) --- packages/browser/src/node/pool.ts | 14 ++++++++++++-- packages/vitest/src/node/core.ts | 3 ++- packages/vitest/src/node/types/browser.ts | 1 + 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/browser/src/node/pool.ts b/packages/browser/src/node/pool.ts index c0bc9f0f8828..bd7399a42dee 100644 --- a/packages/browser/src/node/pool.ts +++ b/packages/browser/src/node/pool.ts @@ -86,8 +86,13 @@ export function createBrowserPool(ctx: Vitest): ProcessPool { [...files.map(f => relative(project.config.root, f))].join(', '), ) const promise = waitForTests(method, contextId, project, files) - promises.push(promise) - orchestrator.createTesters(files) + const tester = orchestrator.createTesters(files).catch((error) => { + if (error instanceof Error && error.message.startsWith('[birpc] rpc is closed')) { + return + } + return Promise.reject(error) + }) + promises.push(promise, tester) } else { const contextId = crypto.randomUUID() @@ -156,6 +161,11 @@ export function createBrowserPool(ctx: Vitest): ProcessPool { async close() { await Promise.all([...providers].map(provider => provider.close())) providers.clear() + ctx.resolvedProjects.forEach((project) => { + project.browser?.state.orchestrators.forEach((orchestrator) => { + orchestrator.$close() + }) + }) }, runTests: files => runWorkspaceTests('run', files), collectTests: files => runWorkspaceTests('collect', files), diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index d1eb91d4c6f2..0d885bd58fe9 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -77,7 +77,8 @@ export class Vitest { private coreWorkspaceProject!: WorkspaceProject - private resolvedProjects: WorkspaceProject[] = [] + /** @private */ + public resolvedProjects: WorkspaceProject[] = [] public projects: WorkspaceProject[] = [] public distPath = distDir diff --git a/packages/vitest/src/node/types/browser.ts b/packages/vitest/src/node/types/browser.ts index 3d607c6dae47..243fdfcbc614 100644 --- a/packages/vitest/src/node/types/browser.ts +++ b/packages/vitest/src/node/types/browser.ts @@ -194,6 +194,7 @@ export interface BrowserServerStateContext { export interface BrowserOrchestrator { createTesters: (files: string[]) => Promise onCancel: (reason: CancelReason) => Promise + $close: () => void } export interface BrowserServerState { From 93b67c24b1e8297f230d54d953a5c537aa7b9f75 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 13 Nov 2024 13:16:24 +0100 Subject: [PATCH 03/18] fix: throw an error and a warning if `.poll`, `.element`, `.rejects`/`.resolves`, and `locator.*` weren't awaited (#6877) --- docs/api/expect.md | 6 +- docs/guide/browser/locators.md | 2 + packages/browser/src/client/tester/context.ts | 80 +++++++++-------- .../src/client/tester/expect-element.ts | 6 +- .../src/client/tester/locators/index.ts | 6 +- .../src/client/tester/locators/preview.ts | 20 ++--- packages/browser/src/client/tester/logger.ts | 6 +- packages/browser/src/client/tester/runner.ts | 13 ++- packages/browser/src/client/utils.ts | 34 ++++++++ packages/expect/src/jest-expect.ts | 18 +++- packages/expect/src/utils.ts | 56 +++++++++++- packages/runner/src/run.ts | 42 ++++----- packages/utils/src/source-map.ts | 10 +++ packages/vitest/src/integrations/chai/poll.ts | 36 +++++++- .../vitest/src/integrations/snapshot/chai.ts | 18 +++- packages/vitest/src/runtime/worker.ts | 4 + packages/vitest/src/types/worker.ts | 1 + test/browser/specs/runner.test.ts | 15 ++-- test/browser/test/failing.test.ts | 11 +++ test/browser/test/userEvent.test.ts | 9 +- .../fixtures/fails/poll-no-awaited.test.ts | 20 +++++ .../cli/test/__snapshots__/fails.test.ts.snap | 9 ++ test/cli/test/fails.test.ts | 86 ++++++++++++++++++- test/test-utils/index.ts | 8 +- 24 files changed, 417 insertions(+), 99 deletions(-) create mode 100644 test/cli/fixtures/fails/poll-no-awaited.test.ts diff --git a/docs/api/expect.md b/docs/api/expect.md index 1103ee6a15cb..0e12270e177b 100644 --- a/docs/api/expect.md +++ b/docs/api/expect.md @@ -82,7 +82,7 @@ test('element exists', async () => { ``` ::: warning -`expect.poll` makes every assertion asynchronous, so do not forget to await it otherwise you might get unhandled promise rejections. +`expect.poll` makes every assertion asynchronous, so you need to await it. Since Vitest 2.2, if you forget to await it, the test will fail with a warning to do so. `expect.poll` doesn't work with several matchers: @@ -1185,6 +1185,8 @@ test('buyApples returns new stock id', async () => { :::warning If the assertion is not awaited, then you will have a false-positive test that will pass every time. To make sure that assertions are actually called, you may use [`expect.assertions(number)`](#expect-assertions). + +Since Vitest 2.2, if a method is not awaited, Vitest will show a warning at the end of the test. In Vitest 3, the test will be marked as "failed" if the assertion is not awaited. ::: ## rejects @@ -1214,6 +1216,8 @@ test('buyApples throws an error when no id provided', async () => { :::warning If the assertion is not awaited, then you will have a false-positive test that will pass every time. To make sure that assertions were actually called, you can use [`expect.assertions(number)`](#expect-assertions). + +Since Vitest 2.2, if a method is not awaited, Vitest will show a warning at the end of the test. In Vitest 3, the test will be marked as "failed" if the assertion is not awaited. ::: ## expect.assertions diff --git a/docs/guide/browser/locators.md b/docs/guide/browser/locators.md index 9c6e50f8d191..eaf3a59be5b5 100644 --- a/docs/guide/browser/locators.md +++ b/docs/guide/browser/locators.md @@ -389,6 +389,8 @@ It is recommended to use this only after the other locators don't work for your ## Methods +All methods are asynchronous and must be awaited. Since Vitest 2.2, tests will fail if a method is not awaited. + ### click ```ts diff --git a/packages/browser/src/client/tester/context.ts b/packages/browser/src/client/tester/context.ts index 5cf0e294f2d9..d648f50aa7ea 100644 --- a/packages/browser/src/client/tester/context.ts +++ b/packages/browser/src/client/tester/context.ts @@ -11,7 +11,7 @@ import type { UserEventTabOptions, UserEventTypeOptions, } from '../../../context' -import { convertElementToCssSelector, getBrowserState, getWorkerState } from '../utils' +import { convertElementToCssSelector, ensureAwaited, getBrowserState, getWorkerState } from '../utils' // this file should not import anything directly, only types and utils @@ -40,12 +40,14 @@ export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent return createUserEvent(__tl_user_event_base__, options) }, async cleanup() { - if (typeof __tl_user_event_base__ !== 'undefined') { - __tl_user_event__ = __tl_user_event_base__?.setup(options ?? {}) - return - } - await triggerCommand('__vitest_cleanup', keyboard) - keyboard.unreleased = [] + return ensureAwaited(async () => { + if (typeof __tl_user_event_base__ !== 'undefined') { + __tl_user_event__ = __tl_user_event_base__?.setup(options ?? {}) + return + } + await triggerCommand('__vitest_cleanup', keyboard) + keyboard.unreleased = [] + }) }, click(element: Element | Locator, options: UserEventClickOptions = {}) { return convertToLocator(element).click(processClickOptions(options)) @@ -84,39 +86,45 @@ export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent // testing-library user-event async type(element: Element | Locator, text: string, options: UserEventTypeOptions = {}) { - if (typeof __tl_user_event__ !== 'undefined') { - return __tl_user_event__.type( - element instanceof Element ? element : element.element(), + return ensureAwaited(async () => { + if (typeof __tl_user_event__ !== 'undefined') { + return __tl_user_event__.type( + element instanceof Element ? element : element.element(), + text, + options, + ) + } + + const selector = convertToSelector(element) + const { unreleased } = await triggerCommand<{ unreleased: string[] }>( + '__vitest_type', + selector, text, - options, + { ...options, unreleased: keyboard.unreleased }, ) - } - - const selector = convertToSelector(element) - const { unreleased } = await triggerCommand<{ unreleased: string[] }>( - '__vitest_type', - selector, - text, - { ...options, unreleased: keyboard.unreleased }, - ) - keyboard.unreleased = unreleased + keyboard.unreleased = unreleased + }) }, tab(options: UserEventTabOptions = {}) { - if (typeof __tl_user_event__ !== 'undefined') { - return __tl_user_event__.tab(options) - } - return triggerCommand('__vitest_tab', options) + return ensureAwaited(() => { + if (typeof __tl_user_event__ !== 'undefined') { + return __tl_user_event__.tab(options) + } + return triggerCommand('__vitest_tab', options) + }) }, async keyboard(text: string) { - if (typeof __tl_user_event__ !== 'undefined') { - return __tl_user_event__.keyboard(text) - } - const { unreleased } = await triggerCommand<{ unreleased: string[] }>( - '__vitest_keyboard', - text, - keyboard, - ) - keyboard.unreleased = unreleased + return ensureAwaited(async () => { + if (typeof __tl_user_event__ !== 'undefined') { + return __tl_user_event__.keyboard(text) + } + const { unreleased } = await triggerCommand<{ unreleased: string[] }>( + '__vitest_keyboard', + text, + keyboard, + ) + keyboard.unreleased = unreleased + }) }, } } @@ -167,12 +175,12 @@ export const page: BrowserPage = { const name = options.path || `${taskName.replace(/[^a-z0-9]/gi, '-')}-${number}.png` - return triggerCommand('__vitest_screenshot', name, { + return ensureAwaited(() => triggerCommand('__vitest_screenshot', name, { ...options, element: options.element ? convertToSelector(options.element) : undefined, - }) + })) }, getByRole() { throw new Error('Method "getByRole" is not implemented in the current provider.') diff --git a/packages/browser/src/client/tester/expect-element.ts b/packages/browser/src/client/tester/expect-element.ts index 58a2a5a6cffb..7782853142d7 100644 --- a/packages/browser/src/client/tester/expect-element.ts +++ b/packages/browser/src/client/tester/expect-element.ts @@ -14,8 +14,10 @@ export async function setupExpectDom() { if (elementOrLocator instanceof Element || elementOrLocator == null) { return elementOrLocator } - const isNot = chai.util.flag(this, 'negate') - const name = chai.util.flag(this, '_name') + chai.util.flag(this, '_poll.element', true) + + const isNot = chai.util.flag(this, 'negate') as boolean + const name = chai.util.flag(this, '_name') as string // special case for `toBeInTheDocument` matcher if (isNot && name === 'toBeInTheDocument') { return elementOrLocator.query() diff --git a/packages/browser/src/client/tester/locators/index.ts b/packages/browser/src/client/tester/locators/index.ts index dd3cdea61ff1..f87a94fc945b 100644 --- a/packages/browser/src/client/tester/locators/index.ts +++ b/packages/browser/src/client/tester/locators/index.ts @@ -22,7 +22,7 @@ import { Ivya, type ParsedSelector, } from 'ivya' -import { getBrowserState, getWorkerState } from '../../utils' +import { ensureAwaited, getBrowserState, getWorkerState } from '../../utils' import { getElementError } from '../public-utils' // we prefer using playwright locators because they are more powerful and support Shadow DOM @@ -202,11 +202,11 @@ export abstract class Locator { || this.worker.current?.file?.filepath || undefined - return this.rpc.triggerCommand( + return ensureAwaited(() => this.rpc.triggerCommand( this.state.contextId, command, filepath, args, - ) + )) } } diff --git a/packages/browser/src/client/tester/locators/preview.ts b/packages/browser/src/client/tester/locators/preview.ts index beb03c178b7f..0e966a8557c7 100644 --- a/packages/browser/src/client/tester/locators/preview.ts +++ b/packages/browser/src/client/tester/locators/preview.ts @@ -9,7 +9,7 @@ import { getByTextSelector, getByTitleSelector, } from 'ivya' -import { convertElementToCssSelector } from '../../utils' +import { convertElementToCssSelector, ensureAwaited } from '../../utils' import { getElementError } from '../public-utils' import { Locator, selectorEngine } from './index' @@ -58,28 +58,28 @@ class PreviewLocator extends Locator { } click(): Promise { - return userEvent.click(this.element()) + return ensureAwaited(() => userEvent.click(this.element())) } dblClick(): Promise { - return userEvent.dblClick(this.element()) + return ensureAwaited(() => userEvent.dblClick(this.element())) } tripleClick(): Promise { - return userEvent.tripleClick(this.element()) + return ensureAwaited(() => userEvent.tripleClick(this.element())) } hover(): Promise { - return userEvent.hover(this.element()) + return ensureAwaited(() => userEvent.hover(this.element())) } unhover(): Promise { - return userEvent.unhover(this.element()) + return ensureAwaited(() => userEvent.unhover(this.element())) } async fill(text: string): Promise { await this.clear() - return userEvent.type(this.element(), text) + return ensureAwaited(() => userEvent.type(this.element(), text)) } async upload(file: string | string[] | File | File[]): Promise { @@ -100,7 +100,7 @@ class PreviewLocator extends Locator { return fileInstance }) const uploadFiles = await Promise.all(uploadPromise) - return userEvent.upload(this.element() as HTMLElement, uploadFiles) + return ensureAwaited(() => userEvent.upload(this.element() as HTMLElement, uploadFiles)) } selectOptions(options_: string | string[] | HTMLElement | HTMLElement[] | Locator | Locator[]): Promise { @@ -110,7 +110,7 @@ class PreviewLocator extends Locator { } return option }) - return userEvent.selectOptions(this.element(), options as string[] | HTMLElement[]) + return ensureAwaited(() => userEvent.selectOptions(this.element(), options as string[] | HTMLElement[])) } async dropTo(): Promise { @@ -118,7 +118,7 @@ class PreviewLocator extends Locator { } clear(): Promise { - return userEvent.clear(this.element()) + return ensureAwaited(() => userEvent.clear(this.element())) } async screenshot(): Promise { diff --git a/packages/browser/src/client/tester/logger.ts b/packages/browser/src/client/tester/logger.ts index 30a5149cfd1d..e2e5219cc204 100644 --- a/packages/browser/src/client/tester/logger.ts +++ b/packages/browser/src/client/tester/logger.ts @@ -41,10 +41,8 @@ export function setupConsoleLogSpy() { trace(...args) const content = processLog(args) const error = new Error('$$Trace') - const stack = (error.stack || '') - .split('\n') - .slice(error.stack?.includes('$$Trace') ? 2 : 1) - .join('\n') + const processor = (globalThis as any).__vitest_worker__?.onFilterStackTrace || ((s: string) => s || '') + const stack = processor(error.stack || '') sendLog('stderr', `${content}\n${stack}`, true) } diff --git a/packages/browser/src/client/tester/runner.ts b/packages/browser/src/client/tester/runner.ts index bf1505ced25c..40ff4501b2c5 100644 --- a/packages/browser/src/client/tester/runner.ts +++ b/packages/browser/src/client/tester/runner.ts @@ -7,7 +7,8 @@ import { page, userEvent } from '@vitest/browser/context' import { loadDiffConfig, loadSnapshotSerializers, takeCoverageInsideWorker } from 'vitest/browser' import { NodeBenchmarkRunner, VitestTestRunner } from 'vitest/runners' import { originalPositionFor, TraceMap } from 'vitest/utils' -import { executor } from '../utils' +import { createStackString, parseStacktrace } from '../../../../utils/src/source-map' +import { executor, getWorkerState } from '../utils' import { rpc } from './rpc' import { VitestBrowserSnapshotEnvironment } from './snapshot' @@ -29,7 +30,7 @@ export function createBrowserRunner( mocker: VitestBrowserClientMocker, state: WorkerGlobalState, coverageModule: CoverageHandler | null, -): { new (options: BrowserRunnerOptions): VitestRunner } { +): { new (options: BrowserRunnerOptions): VitestRunner & { sourceMapCache: Map } } { return class BrowserTestRunner extends runnerClass implements VitestRunner { public config: SerializedConfig hashMap = browserHashMap @@ -171,6 +172,14 @@ export async function initiateRunner( ]) runner.config.diffOptions = diffOptions cachedRunner = runner + getWorkerState().onFilterStackTrace = (stack: string) => { + const stacks = parseStacktrace(stack, { + getSourceMap(file) { + return runner.sourceMapCache.get(file) + }, + }) + return createStackString(stacks) + } return runner } diff --git a/packages/browser/src/client/utils.ts b/packages/browser/src/client/utils.ts index 168704878cb3..147b7c3dc344 100644 --- a/packages/browser/src/client/utils.ts +++ b/packages/browser/src/client/utils.ts @@ -25,6 +25,40 @@ export function getConfig(): SerializedConfig { return getBrowserState().config } +export function ensureAwaited(promise: () => Promise): Promise { + const test = getWorkerState().current + if (!test || test.type !== 'test') { + return promise() + } + let awaited = false + const sourceError = new Error('STACK_TRACE_ERROR') + test.onFinished ??= [] + test.onFinished.push(() => { + if (!awaited) { + const error = new Error( + `The call was not awaited. This method is asynchronous and must be awaited; otherwise, the call will not start to avoid unhandled rejections.`, + ) + error.stack = sourceError.stack?.replace(sourceError.message, error.message) + throw error + } + }) + // don't even start the promise if it's not awaited to not cause any unhanded promise rejections + let promiseResult: Promise | undefined + return { + then(onFulfilled, onRejected) { + awaited = true + return (promiseResult ||= promise()).then(onFulfilled, onRejected) + }, + catch(onRejected) { + return (promiseResult ||= promise()).catch(onRejected) + }, + finally(onFinally) { + return (promiseResult ||= promise()).finally(onFinally) + }, + [Symbol.toStringTag]: 'Promise', + } satisfies Promise +} + export interface BrowserRunnerState { files: string[] runningFiles: string[] diff --git a/packages/expect/src/jest-expect.ts b/packages/expect/src/jest-expect.ts index c8cb304ce193..72615f364de1 100644 --- a/packages/expect/src/jest-expect.ts +++ b/packages/expect/src/jest-expect.ts @@ -22,7 +22,7 @@ import { subsetEquality, typeEquality, } from './jest-utils' -import { recordAsyncExpect, wrapAssertion } from './utils' +import { createAssertionMessage, recordAsyncExpect, wrapAssertion } from './utils' // polyfill globals because expect can be used in node environment declare class Node { @@ -983,6 +983,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { } return (...args: any[]) => { + utils.flag(this, '_name', key) const promise = obj.then( (value: any) => { utils.flag(this, 'object', value) @@ -1004,7 +1005,12 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { }, ) - return recordAsyncExpect(test, promise) + return recordAsyncExpect( + test, + promise, + createAssertionMessage(utils, this, !!args.length), + error, + ) } }, }) @@ -1045,6 +1051,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { } return (...args: any[]) => { + utils.flag(this, '_name', key) const promise = wrapper.then( (value: any) => { const _error = new AssertionError( @@ -1069,7 +1076,12 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { }, ) - return recordAsyncExpect(test, promise) + return recordAsyncExpect( + test, + promise, + createAssertionMessage(utils, this, !!args.length), + error, + ) } }, }) diff --git a/packages/expect/src/utils.ts b/packages/expect/src/utils.ts index a043cd9cd9cb..9d5c44be9173 100644 --- a/packages/expect/src/utils.ts +++ b/packages/expect/src/utils.ts @@ -2,14 +2,32 @@ import type { Test } from '@vitest/runner/types' import type { Assertion } from './types' import { processError } from '@vitest/utils/error' +export function createAssertionMessage( + util: Chai.ChaiUtils, + assertion: Assertion, + hasArgs: boolean, +) { + const not = util.flag(assertion, 'negate') ? 'not.' : '' + const name = `${util.flag(assertion, '_name')}(${hasArgs ? 'expected' : ''})` + const promiseName = util.flag(assertion, 'promise') + const promise = promiseName ? `.${promiseName}` : '' + return `expect(actual)${promise}.${not}${name}` +} + export function recordAsyncExpect( - test: any, - promise: Promise | PromiseLike, + _test: any, + promise: Promise, + assertion: string, + error: Error, ) { + const test = _test as Test | undefined // record promise for test, that resolves before test ends if (test && promise instanceof Promise) { // if promise is explicitly awaited, remove it from the list promise = promise.finally(() => { + if (!test.promises) { + return + } const index = test.promises.indexOf(promise) if (index !== -1) { test.promises.splice(index, 1) @@ -21,6 +39,35 @@ export function recordAsyncExpect( test.promises = [] } test.promises.push(promise) + + let resolved = false + test.onFinished ??= [] + test.onFinished.push(() => { + if (!resolved) { + const processor = (globalThis as any).__vitest_worker__?.onFilterStackTrace || ((s: string) => s || '') + const stack = processor(error.stack) + console.warn([ + `Promise returned by \`${assertion}\` was not awaited. `, + 'Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. ', + 'Please remember to await the assertion.\n', + stack, + ].join('')) + } + }) + + return { + then(onFullfilled, onRejected) { + resolved = true + return promise.then(onFullfilled, onRejected) + }, + catch(onRejected) { + return promise.catch(onRejected) + }, + finally(onFinally) { + return promise.finally(onFinally) + }, + [Symbol.toStringTag]: 'Promise', + } satisfies Promise } return promise @@ -32,7 +79,10 @@ export function wrapAssertion( fn: (this: Chai.AssertionStatic & Assertion, ...args: any[]) => void, ) { return function (this: Chai.AssertionStatic & Assertion, ...args: any[]) { - utils.flag(this, '_name', name) + // private + if (name !== 'withTest') { + utils.flag(this, '_name', name) + } if (!utils.flag(this, 'soft')) { return fn.apply(this, args) diff --git a/packages/runner/src/run.ts b/packages/runner/src/run.ts index bb3c73105255..0d9cbeeb14b9 100644 --- a/packages/runner/src/run.ts +++ b/packages/runner/src/run.ts @@ -61,7 +61,8 @@ function getSuiteHooks( return hooks } -async function callTaskHooks( +async function callTestHooks( + runner: VitestRunner, task: Task, hooks: ((result: TaskResult) => Awaitable)[], sequence: SequenceHooks, @@ -71,11 +72,21 @@ async function callTaskHooks( } if (sequence === 'parallel') { - await Promise.all(hooks.map(fn => fn(task.result!))) + try { + await Promise.all(hooks.map(fn => fn(task.result!))) + } + catch (e) { + failTask(task.result!, e, runner.config.diffOptions) + } } else { for (const fn of hooks) { - await fn(task.result!) + try { + await fn(task.result!) + } + catch (e) { + failTask(task.result!, e, runner.config.diffOptions) + } } } } @@ -271,24 +282,15 @@ export async function runTest(test: Test | Custom, runner: VitestRunner): Promis failTask(test.result, e, runner.config.diffOptions) } - try { - await callTaskHooks(test, test.onFinished || [], 'stack') - } - catch (e) { - failTask(test.result, e, runner.config.diffOptions) - } + await callTestHooks(runner, test, test.onFinished || [], 'stack') if (test.result.state === 'fail') { - try { - await callTaskHooks( - test, - test.onFailed || [], - runner.config.sequence.hooks, - ) - } - catch (e) { - failTask(test.result, e, runner.config.diffOptions) - } + await callTestHooks( + runner, + test, + test.onFailed || [], + runner.config.sequence.hooks, + ) } delete test.onFailed @@ -331,7 +333,7 @@ export async function runTest(test: Test | Custom, runner: VitestRunner): Promis updateTask(test, runner) } -function failTask(result: TaskResult, err: unknown, diffOptions?: DiffOptions) { +function failTask(result: TaskResult, err: unknown, diffOptions: DiffOptions | undefined) { if (err instanceof PendingError) { result.state = 'skip' return diff --git a/packages/utils/src/source-map.ts b/packages/utils/src/source-map.ts index 21b20a1605a8..0ca0382536ce 100644 --- a/packages/utils/src/source-map.ts +++ b/packages/utils/src/source-map.ts @@ -179,6 +179,16 @@ export function parseSingleV8Stack(raw: string): ParsedStack | null { } } +export function createStackString(stacks: ParsedStack[]): string { + return stacks.map((stack) => { + const line = `${stack.file}:${stack.line}:${stack.column}` + if (stack.method) { + return ` at ${stack.method}(${line})` + } + return ` at ${line}` + }).join('\n') +} + export function parseStacktrace( stack: string, options: StackTraceParserOptions = {}, diff --git a/packages/vitest/src/integrations/chai/poll.ts b/packages/vitest/src/integrations/chai/poll.ts index 1a7176021f60..4ee87549758a 100644 --- a/packages/vitest/src/integrations/chai/poll.ts +++ b/packages/vitest/src/integrations/chai/poll.ts @@ -1,4 +1,5 @@ import type { Assertion, ExpectStatic } from '@vitest/expect' +import type { Test } from '@vitest/runner' import { getSafeTimers } from '@vitest/utils' import * as chai from 'chai' import { getWorkerState } from '../../runtime/utils' @@ -39,6 +40,10 @@ export function createExpectPoll(expect: ExpectStatic): ExpectStatic['poll'] { poll: true, }) as Assertion fn = fn.bind(assertion) + const test = chai.util.flag(assertion, 'vitest-test') as Test | undefined + if (!test) { + throw new Error('expect.poll() must be called inside a test') + } const proxy: any = new Proxy(assertion, { get(target, key, receiver) { const assertionFunction = Reflect.get(target, key, receiver) @@ -59,7 +64,7 @@ export function createExpectPoll(expect: ExpectStatic): ExpectStatic['poll'] { return function (this: any, ...args: any[]) { const STACK_TRACE_ERROR = new Error('STACK_TRACE_ERROR') - return new Promise((resolve, reject) => { + const promise = () => new Promise((resolve, reject) => { let intervalId: any let lastError: any const { setTimeout, clearTimeout } = getSafeTimers() @@ -90,6 +95,35 @@ export function createExpectPoll(expect: ExpectStatic): ExpectStatic['poll'] { } check() }) + let awaited = false + test.onFinished ??= [] + test.onFinished.push(() => { + if (!awaited) { + const negated = chai.util.flag(assertion, 'negate') ? 'not.' : '' + const name = chai.util.flag(assertion, '_poll.element') ? 'element(locator)' : 'poll(assertion)' + const assertionString = `expect.${name}.${negated}${String(key)}()` + const error = new Error( + `${assertionString} was not awaited. This assertion is asynchronous and must be awaited; otherwise, it is not executed to avoid unhandled rejections:\n\nawait ${assertionString}\n`, + ) + throw copyStackTrace(error, STACK_TRACE_ERROR) + } + }) + let resultPromise: Promise | undefined + // only .then is enough to check awaited, but we type this as `Promise` in global types + // so let's follow it + return { + then(onFulfilled, onRejected) { + awaited = true + return (resultPromise ||= promise()).then(onFulfilled, onRejected) + }, + catch(onRejected) { + return (resultPromise ||= promise()).catch(onRejected) + }, + finally(onFinally) { + return (resultPromise ||= promise()).finally(onFinally) + }, + [Symbol.toStringTag]: 'Promise', + } satisfies Promise } }, }) diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index 3837d2b88d20..f0eff3d94adc 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -1,4 +1,4 @@ -import type { ChaiPlugin } from '@vitest/expect' +import type { Assertion, ChaiPlugin } from '@vitest/expect' import type { Test } from '@vitest/runner' import { equals, iterableEquality, subsetEquality } from '@vitest/expect' import { getNames } from '@vitest/runner/utils' @@ -7,7 +7,7 @@ import { SnapshotClient, stripSnapshotIndentation, } from '@vitest/snapshot' -import { recordAsyncExpect } from '../../../../expect/src/utils' +import { createAssertionMessage, recordAsyncExpect } from '../../../../expect/src/utils' let _client: SnapshotClient @@ -64,6 +64,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { properties?: object, message?: string, ) { + utils.flag(this, '_name', key) const isNot = utils.flag(this, 'negate') if (isNot) { throw new Error(`${key} cannot be used with "not"`) @@ -90,11 +91,13 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { utils.addMethod( chai.Assertion.prototype, 'toMatchFileSnapshot', - function (this: Record, file: string, message?: string) { + function (this: Assertion, file: string, message?: string) { + utils.flag(this, '_name', 'toMatchFileSnapshot') const isNot = utils.flag(this, 'negate') if (isNot) { throw new Error('toMatchFileSnapshot cannot be used with "not"') } + const error = new Error('resolves') const expected = utils.flag(this, 'object') const test = utils.flag(this, 'vitest-test') as Test const errorMessage = utils.flag(this, 'message') @@ -110,7 +113,12 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { ...getTestNames(test), }) - return recordAsyncExpect(test, promise) + return recordAsyncExpect( + test, + promise, + createAssertionMessage(utils, this, true), + error, + ) }, ) @@ -123,6 +131,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { inlineSnapshot?: string, message?: string, ) { + utils.flag(this, '_name', 'toMatchInlineSnapshot') const isNot = utils.flag(this, 'negate') if (isNot) { throw new Error('toMatchInlineSnapshot cannot be used with "not"') @@ -162,6 +171,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { chai.Assertion.prototype, 'toThrowErrorMatchingSnapshot', function (this: Record, message?: string) { + utils.flag(this, '_name', 'toThrowErrorMatchingSnapshot') const isNot = utils.flag(this, 'negate') if (isNot) { throw new Error( diff --git a/packages/vitest/src/runtime/worker.ts b/packages/vitest/src/runtime/worker.ts index 00897b4f0a53..4ccfa155e408 100644 --- a/packages/vitest/src/runtime/worker.ts +++ b/packages/vitest/src/runtime/worker.ts @@ -1,6 +1,7 @@ import type { ContextRPC, WorkerGlobalState } from '../types/worker' import type { VitestWorker } from './workers/types' import { pathToFileURL } from 'node:url' +import { createStackString, parseStacktrace } from '@vitest/utils/source-map' import { workerId as poolId } from 'tinypool' import { ModuleCacheMap } from 'vite-node/client' import { loadEnvironment } from '../integrations/env/loader' @@ -90,6 +91,9 @@ async function execute(method: 'run' | 'collect', ctx: ContextRPC) { }, rpc, providedContext: ctx.providedContext, + onFilterStackTrace(stack) { + return createStackString(parseStacktrace(stack)) + }, } satisfies WorkerGlobalState const methodName = method === 'collect' ? 'collectTests' : 'runTests' diff --git a/packages/vitest/src/types/worker.ts b/packages/vitest/src/types/worker.ts index b6dbf9c18032..2c8b9a0fc0c5 100644 --- a/packages/vitest/src/types/worker.ts +++ b/packages/vitest/src/types/worker.ts @@ -47,4 +47,5 @@ export interface WorkerGlobalState { environment: number prepare: number } + onFilterStackTrace?: (trace: string) => string } diff --git a/test/browser/specs/runner.test.ts b/test/browser/specs/runner.test.ts index 15820327d6b2..64b8a94fc632 100644 --- a/test/browser/specs/runner.test.ts +++ b/test/browser/specs/runner.test.ts @@ -101,8 +101,8 @@ log with a stack error with a stack ❯ test/logs.test.ts:59:10 `.trim()) - // console.trace doens't add additional stack trace - expect(stderr).not.toMatch('test/logs.test.ts:60:10') + // console.trace processes the stack trace correctly + expect(stderr).toMatch('test/logs.test.ts:60:10') }) test.runIf(browser === 'webkit')(`logs have stack traces in safari`, () => { @@ -115,16 +115,21 @@ log with a stack error with a stack ❯ test/logs.test.ts:59:16 `.trim()) - // console.trace doens't add additional stack trace - expect(stderr).not.toMatch('test/logs.test.ts:60:16') + // console.trace processes the stack trace correctly + expect(stderr).toMatch('test/logs.test.ts:60:16') }) test(`stack trace points to correct file in every browser`, () => { // dependeing on the browser it references either `.toBe()` or `expect()` - expect(stderr).toMatch(/test\/failing.test.ts:5:(12|17)/) + expect(stderr).toMatch(/test\/failing.test.ts:10:(12|17)/) // column is 18 in safari, 8 in others expect(stderr).toMatch(/throwError src\/error.ts:8:(18|8)/) + + expect(stderr).toContain('The call was not awaited. This method is asynchronous and must be awaited; otherwise, the call will not start to avoid unhandled rejections.') + expect(stderr).toMatch(/test\/failing.test.ts:18:(27|36)/) + expect(stderr).toMatch(/test\/failing.test.ts:19:(27|33)/) + expect(stderr).toMatch(/test\/failing.test.ts:20:(27|39)/) }) test('popup apis should log a warning', () => { diff --git a/test/browser/test/failing.test.ts b/test/browser/test/failing.test.ts index 14cb207fe897..96495127452e 100644 --- a/test/browser/test/failing.test.ts +++ b/test/browser/test/failing.test.ts @@ -1,6 +1,11 @@ +import { page } from '@vitest/browser/context' import { expect, it } from 'vitest' import { throwError } from '../src/error' +document.body.innerHTML = ` + +` + it('correctly fails and prints a diff', () => { expect(1).toBe(2) }) @@ -8,3 +13,9 @@ it('correctly fails and prints a diff', () => { it('correctly print error in another file', () => { throwError() }) + +it('several locator methods are not awaited', () => { + page.getByRole('button').dblClick() + page.getByRole('button').click() + page.getByRole('button').tripleClick() +}) diff --git a/test/browser/test/userEvent.test.ts b/test/browser/test/userEvent.test.ts index 10900dd1a3cf..2b81643bfcf8 100644 --- a/test/browser/test/userEvent.test.ts +++ b/test/browser/test/userEvent.test.ts @@ -11,9 +11,14 @@ const userEvent = _uE.setup() describe('userEvent.click', () => { test('correctly clicks a button', async () => { + const wrapper = document.createElement('div') + wrapper.style.height = '100px' + wrapper.style.width = '200px' + wrapper.style.backgroundColor = 'red' + wrapper.style.display = 'flex' + wrapper.style.justifyContent = 'center' + wrapper.style.alignItems = 'center' const button = document.createElement('button') - button.style.height = '100px' - button.style.width = '200px' button.textContent = 'Click me' document.body.appendChild(button) const onClick = vi.fn() diff --git a/test/cli/fixtures/fails/poll-no-awaited.test.ts b/test/cli/fixtures/fails/poll-no-awaited.test.ts new file mode 100644 index 000000000000..b6eeab4f8d3d --- /dev/null +++ b/test/cli/fixtures/fails/poll-no-awaited.test.ts @@ -0,0 +1,20 @@ +import { expect, test } from 'vitest'; + +test('poll is not awaited once', () => { + expect.poll(() => 2).toBe(2) +}) + +test('poll is not awaited several times', () => { + expect.poll(() => 3).toBe(3) + expect.poll(() => 'string').not.toBe('correct') +}) + +test('poll is not awaited but there is an async assertion afterwards', async () => { + expect.poll(() => 4).toBe(4) + await expect(new Promise((r) => setTimeout(() => r(3), 50))).resolves.toBe(3) +}) + +test('poll is not awaited but there is an error afterwards', async () => { + expect.poll(() => 4).toBe(4) + expect(3).toBe(4) +}) diff --git a/test/cli/test/__snapshots__/fails.test.ts.snap b/test/cli/test/__snapshots__/fails.test.ts.snap index 2b27922890ed..8b6c55f29a16 100644 --- a/test/cli/test/__snapshots__/fails.test.ts.snap +++ b/test/cli/test/__snapshots__/fails.test.ts.snap @@ -60,6 +60,15 @@ exports[`should fail no-assertions.test.ts > no-assertions.test.ts 1`] = `"Error exports[`should fail node-browser-context.test.ts > node-browser-context.test.ts 1`] = `"Error: @vitest/browser/context can be imported only inside the Browser Mode. Your test is running in forks pool. Make sure your regular tests are excluded from the "test.include" glob pattern."`; +exports[`should fail poll-no-awaited.test.ts > poll-no-awaited.test.ts 1`] = ` +"Error: expect.poll(assertion).toBe() was not awaited. This assertion is asynchronous and must be awaited; otherwise, it is not executed to avoid unhandled rejections: +AssertionError: expected 3 to be 4 // Object.is equality +Error: expect.poll(assertion).toBe() was not awaited. This assertion is asynchronous and must be awaited; otherwise, it is not executed to avoid unhandled rejections: +Error: expect.poll(assertion).toBe() was not awaited. This assertion is asynchronous and must be awaited; otherwise, it is not executed to avoid unhandled rejections: +Error: expect.poll(assertion).not.toBe() was not awaited. This assertion is asynchronous and must be awaited; otherwise, it is not executed to avoid unhandled rejections: +Error: expect.poll(assertion).toBe() was not awaited. This assertion is asynchronous and must be awaited; otherwise, it is not executed to avoid unhandled rejections:" +`; + exports[`should fail primitive-error.test.ts > primitive-error.test.ts 1`] = `"Unknown Error: 42"`; exports[`should fail snapshot-with-not.test.ts > snapshot-with-not.test.ts 1`] = ` diff --git a/test/cli/test/fails.test.ts b/test/cli/test/fails.test.ts index b9e7e3343bea..037fc6c3c86f 100644 --- a/test/cli/test/fails.test.ts +++ b/test/cli/test/fails.test.ts @@ -1,8 +1,9 @@ +import type { TestCase } from 'vitest/node' import { resolve } from 'pathe' + import { glob } from 'tinyglobby' import { expect, it } from 'vitest' - -import { runVitest } from '../../test-utils' +import { runInlineTests, runVitest, ts } from '../../test-utils' const root = resolve(__dirname, '../fixtures/fails') const files = await glob(['**/*.test.ts'], { cwd: root, dot: true, expandDirectories: false }) @@ -50,3 +51,84 @@ it('should not report coverage when "coverag.reportOnFailure" has default value expect(stdout).not.toMatch('Coverage report from istanbul') }) + +it('prints a warning if the assertion is not awaited', async () => { + const { stderr, results, root } = await runInlineTests({ + 'base.test.js': ts` + import { expect, test } from 'vitest'; + + test('single not awaited', () => { + expect(Promise.resolve(1)).resolves.toBe(1) + }) + + test('several not awaited', () => { + expect(Promise.resolve(1)).resolves.toBe(1) + expect(Promise.reject(1)).rejects.toBe(1) + }) + + test('not awaited and failed', () => { + expect(Promise.resolve(1)).resolves.toBe(1) + expect(1).toBe(2) + }) + + test('toMatchSnapshot not awaited', () => { + expect(1).toMatchFileSnapshot('./snapshot.txt') + }) + `, + }) + expect(results[0].children.size).toEqual(4) + const failedTest = results[0].children.at(2) as TestCase + expect(failedTest.result()).toEqual({ + state: 'failed', + errors: [ + expect.objectContaining({ + message: expect.stringContaining('expected 1 to be 2'), + }), + ], + }) + const warnings: string[] = [] + const lines = stderr.split('\n') + lines.forEach((line, index) => { + if (line.includes('Promise returned by')) { + warnings.push(lines.slice(index, index + 2).join('\n').replace(`${root}/`, '/')) + } + }) + expect(warnings).toMatchInlineSnapshot(` + [ + "Promise returned by \`expect(actual).resolves.toBe(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. Please remember to await the assertion. + at /base.test.js:5:33", + "Promise returned by \`expect(actual).rejects.toBe(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. Please remember to await the assertion. + at /base.test.js:10:32", + "Promise returned by \`expect(actual).resolves.toBe(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. Please remember to await the assertion. + at /base.test.js:9:33", + "Promise returned by \`expect(actual).resolves.toBe(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. Please remember to await the assertion. + at /base.test.js:14:33", + "Promise returned by \`expect(actual).toMatchFileSnapshot(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. Please remember to await the assertion. + at /base.test.js:19:17", + ] + `) +}) + +it('prints a warning if the assertion is not awaited in the browser mode', async () => { + const { stderr } = await runInlineTests({ + './vitest.config.js': { + test: { + browser: { + enabled: true, + name: 'chromium', + provider: 'playwright', + headless: true, + }, + }, + }, + 'base.test.js': ts` + import { expect, test } from 'vitest'; + + test('single not awaited', () => { + expect(Promise.resolve(1)).resolves.toBe(1) + }) + `, + }) + expect(stderr).toContain('Promise returned by \`expect(actual).resolves.toBe(expected)\` was not awaited') + expect(stderr).toContain('base.test.js:5:33') +}) diff --git a/test/test-utils/index.ts b/test/test-utils/index.ts index eebe9535e8fe..d63bb4429b12 100644 --- a/test/test-utils/index.ts +++ b/test/test-utils/index.ts @@ -1,7 +1,7 @@ import type { Options } from 'tinyexec' import type { UserConfig as ViteUserConfig } from 'vite' import type { WorkspaceProjectConfiguration } from 'vitest/config' -import type { UserConfig, Vitest, VitestRunMode } from 'vitest/node' +import type { TestModule, UserConfig, Vitest, VitestRunMode } from 'vitest/node' import { webcrypto as crypto } from 'node:crypto' import fs from 'node:fs' import { Readable, Writable } from 'node:stream' @@ -291,6 +291,12 @@ export async function runInlineTests( }) return { fs, + root, ...vitest, + get results() { + return (vitest.ctx?.state.getFiles() || []).map(file => vitest.ctx?.state.getReportedEntity(file) as TestModule) + }, } } + +export const ts = String.raw From 32be0af7534ca8dd563cc613ab246b9842bf31c1 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 13 Nov 2024 15:28:46 +0100 Subject: [PATCH 04/18] chore: update browser readme --- packages/browser/README.md | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/packages/browser/README.md b/packages/browser/README.md index 79fe11524a56..1e5eac4cf290 100644 --- a/packages/browser/README.md +++ b/packages/browser/README.md @@ -1,33 +1,19 @@ # @vitest/browser -Browser runner for Vitest. +[Browser runner](https://vitest.dev/guide/browser/) for Vitest. > ⚠️ This package is **experimental**. While this package will be released along with other packages, it will not follow SemVer for breaking changes until we mark it as ready. -## Progress - -Current Status: **Working in progress** - -- [x] Init package and integration -- [x] Stub node packages for Vitest runtime -- [x] Works in development mode -- [x] Better log in terminal -- [x] Fulfill tests (using Browser only APIs, Vue and React components) -- [ ] Show progress and error on the browser page -- [x] Headless mode in CI -- [x] Docs - -Related PRs - -- [#1302](https://github.com/vitest-dev/vitest/pull/1302) - ## Development Setup At project root: ```bash -pnpm dev - cd test/browser -pnpm vitest --browser +# runs relevant tests for the browser mode +# useful to confirm everything works fine +pnpm test +# runs tests as the browser mode +# useful during development +pnpm test-fixtures ``` From 417bdb423d7a33d9c8be5b2d0d7027a2c38ded43 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 13 Nov 2024 16:03:43 +0100 Subject: [PATCH 05/18] fix(browser): init browsers eagerly when tests are running (#6876) --- docs/guide/browser/index.md | 2 + packages/browser/src/node/pool.ts | 1 + .../browser/src/node/providers/preview.ts | 15 +++---- packages/vitest/src/node/core.ts | 12 ++---- packages/vitest/src/node/logger.ts | 41 +++++++++++-------- packages/vitest/src/node/stdin.ts | 9 ++++ packages/vitest/src/node/workspace.ts | 18 ++++---- test/browser/specs/runner.test.ts | 5 +-- test/browser/specs/server-url.test.ts | 14 ++++--- .../fixtures/browser-multiple/basic.test.js | 3 ++ .../browser-multiple/vitest.workspace.ts | 6 ++- test/cli/test/browser-multiple.test.ts | 9 ++-- test/config/test/browser-html.test.ts | 13 ++---- 13 files changed, 83 insertions(+), 65 deletions(-) create mode 100644 test/cli/fixtures/browser-multiple/basic.test.js diff --git a/docs/guide/browser/index.md b/docs/guide/browser/index.md index 14dc37749b7f..87d5ca5d398e 100644 --- a/docs/guide/browser/index.md +++ b/docs/guide/browser/index.md @@ -107,6 +107,8 @@ export default defineConfig({ ::: info Vitest assigns port `63315` to avoid conflicts with the development server, allowing you to run both in parallel. You can change that with the [`browser.api`](/config/#browser-api) option. + +Since Vitest 2.1.5, CLI no longer prints the Vite URL automcatically. You can press "b" to print the URL when running in watch mode. ::: If you have not used Vite before, make sure you have your framework's plugin installed and specified in the config. Some frameworks might require extra configuration to work - check their Vite related documentation to be sure. diff --git a/packages/browser/src/node/pool.ts b/packages/browser/src/node/pool.ts index bd7399a42dee..9079535bee75 100644 --- a/packages/browser/src/node/pool.ts +++ b/packages/browser/src/node/pool.ts @@ -131,6 +131,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool { if (isCancelled) { break } + await project.initBrowserProvider() await executeTests(method, project, files) } diff --git a/packages/browser/src/node/providers/preview.ts b/packages/browser/src/node/providers/preview.ts index 8693f661d6fa..a8e8829a7cfd 100644 --- a/packages/browser/src/node/providers/preview.ts +++ b/packages/browser/src/node/providers/preview.ts @@ -3,7 +3,7 @@ import type { BrowserProvider, WorkspaceProject } from 'vitest/node' export class PreviewBrowserProvider implements BrowserProvider { public name = 'preview' as const public supportsParallelism: boolean = false - private ctx!: WorkspaceProject + private project!: WorkspaceProject private open = false getSupportedBrowsers() { @@ -19,25 +19,26 @@ export class PreviewBrowserProvider implements BrowserProvider { return {} } - async initialize(ctx: WorkspaceProject) { - this.ctx = ctx + async initialize(project: WorkspaceProject) { + this.project = project this.open = false - if (ctx.config.browser.headless) { + if (project.config.browser.headless) { throw new Error( 'You\'ve enabled headless mode for "preview" provider but it doesn\'t support it. Use "playwright" or "webdriverio" instead: https://vitest.dev/guide/browser/#configuration', ) } + project.ctx.logger.printBrowserBanner(project) } async openPage(_contextId: string, url: string) { this.open = true - if (!this.ctx.browser) { + if (!this.project.browser) { throw new Error('Browser is not initialized') } - const options = this.ctx.browser.vite.config.server + const options = this.project.browser.vite.config.server const _open = options.open options.open = url - this.ctx.browser.vite.openBrowser() + this.project.browser.vite.openBrowser() options.open = _open } diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 0d885bd58fe9..74fcceda3874 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -303,10 +303,6 @@ export class Vitest { return this.coverageProvider } - private async initBrowserProviders() { - return Promise.all(this.projects.map(w => w.initBrowserProvider())) - } - async mergeReports() { 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.') @@ -369,8 +365,6 @@ export class Vitest { async collect(filters?: string[]) { this._onClose = [] - await this.initBrowserProviders() - const files = await this.filterTestsBySource( await this.globTestFiles(filters), ) @@ -402,7 +396,6 @@ export class Vitest { try { await this.initCoverageProvider() await this.coverageProvider?.clean(this.config.coverage.clean) - await this.initBrowserProviders() } finally { await this.report('onInit', this) @@ -445,7 +438,6 @@ export class Vitest { try { await this.initCoverageProvider() await this.coverageProvider?.clean(this.config.coverage.clean) - await this.initBrowserProviders() } finally { await this.report('onInit', this) @@ -693,6 +685,10 @@ export class Vitest { await Promise.all(this._onCancelListeners.splice(0).map(listener => listener(reason))) } + async initBrowserServers() { + await Promise.all(this.projects.map(p => p.initBrowserServer())) + } + async rerunFiles(files: string[] = this.state.getFilepaths(), trigger?: string, allTestsRun = true) { if (this.filenamePattern) { const filteredFiles = await this.globTestFiles([this.filenamePattern]) diff --git a/packages/vitest/src/node/logger.ts b/packages/vitest/src/node/logger.ts index 7ac51abb63bb..5254bf6b3d4e 100644 --- a/packages/vitest/src/node/logger.ts +++ b/packages/vitest/src/node/logger.ts @@ -12,7 +12,7 @@ import { createLogUpdate } from 'log-update' import c from 'tinyrainbow' import { highlightCode } from '../utils/colors' import { printError } from './error' -import { divider, withLabel } from './reporters/renderers/utils' +import { divider, formatProjectName, withLabel } from './reporters/renderers/utils' import { RandomSequencer } from './sequencers/RandomSequencer' export interface ErrorOptions { @@ -217,21 +217,6 @@ export class Logger { this.log(PAD + c.gray(`Running tests with seed "${this.ctx.config.sequence.seed}"`)) } - this.ctx.projects.forEach((project) => { - if (!project.browser) { - return - } - const name = project.getName() - const output = project.isCore() ? '' : ` [${name}]` - - const resolvedUrls = project.browser.vite.resolvedUrls - const origin = resolvedUrls?.local[0] ?? resolvedUrls?.network[0] - const provider = project.browser.provider.name - const providerString = provider === 'preview' ? '' : ` by ${provider}` - - this.log(PAD + c.dim(c.green(`${output} Browser runner started${providerString} at ${new URL('/', origin)}`))) - }) - if (this.ctx.config.ui) { const host = this.ctx.config.api?.host || 'localhost' const port = this.ctx.server.config.server.port @@ -260,6 +245,30 @@ export class Logger { } } + printBrowserBanner(project: WorkspaceProject) { + if (!project.browser) { + return + } + + const resolvedUrls = project.browser.vite.resolvedUrls + const origin = resolvedUrls?.local[0] ?? resolvedUrls?.network[0] + if (!origin) { + return + } + + const name = project.getName() + const output = project.isCore() + ? '' + : formatProjectName(name) + const provider = project.browser.provider.name + const providerString = provider === 'preview' ? '' : ` by ${c.reset(c.bold(provider))}` + this.log( + c.dim( + `${output}Browser runner started${providerString} ${c.dim('at')} ${c.blue(new URL('/', origin))}\n`, + ), + ) + } + printUnhandledErrors(errors: unknown[]) { const errorMessage = c.red( c.bold( diff --git a/packages/vitest/src/node/stdin.ts b/packages/vitest/src/node/stdin.ts index 889439595e1e..705980b1ecb3 100644 --- a/packages/vitest/src/node/stdin.ts +++ b/packages/vitest/src/node/stdin.ts @@ -18,6 +18,7 @@ const keys = [ ['p', 'filter by a filename'], ['t', 'filter by a test name regex pattern'], ['w', 'filter by a project name'], + ['b', 'start the browser server if not started yet'], ['q', 'quit'], ] const cancelKeys = ['space', 'c', 'h', ...keys.map(key => key[0]).flat()] @@ -120,6 +121,14 @@ export function registerConsoleShortcuts( if (name === 'p') { return inputFilePattern() } + if (name === 'b') { + await ctx.initBrowserServers() + ctx.projects.forEach((project) => { + ctx.logger.log() + ctx.logger.printBrowserBanner(project) + }) + return null + } } async function keypressHandler(str: string, key: any) { diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index d60ba846853c..76e87a144f63 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -358,16 +358,15 @@ export class WorkspaceProject { return testFiles } - async initBrowserServer(configFile: string | undefined) { - if (!this.isBrowserEnabled()) { + async initBrowserServer() { + if (!this.isBrowserEnabled() || this.browser) { return } await this.ctx.packageInstaller.ensureInstalled('@vitest/browser', this.config.root, this.ctx.version) const { createBrowserServer, distRoot } = await import('@vitest/browser') - await this.browser?.close() const browser = await createBrowserServer( this, - configFile, + this.server.config.configFile, [ ...MocksPlugins({ filter(id) { @@ -408,9 +407,7 @@ export class WorkspaceProject { } static async createCoreProject(ctx: Vitest) { - const project = WorkspaceProject.createBasicProject(ctx) - await project.initBrowserServer(ctx.server.config.configFile) - return project + return WorkspaceProject.createBasicProject(ctx) } async setServer(options: UserConfig, server: ViteDevServer) { @@ -449,8 +446,6 @@ export class WorkspaceProject { return node.resolveId(id, importer) }, }) - - await this.initBrowserServer(this.server.config.configFile) } isBrowserEnabled(): boolean { @@ -495,9 +490,12 @@ export class WorkspaceProject { } async initBrowserProvider() { - if (!this.isBrowserEnabled()) { + if (!this.isBrowserEnabled() || this.browser?.provider) { return } + if (!this.browser) { + await this.initBrowserServer() + } await this.browser?.initBrowserProvider() } } diff --git a/test/browser/specs/runner.test.ts b/test/browser/specs/runner.test.ts index 64b8a94fc632..545ad88116d9 100644 --- a/test/browser/specs/runner.test.ts +++ b/test/browser/specs/runner.test.ts @@ -1,7 +1,6 @@ import { readFile } from 'node:fs/promises' import { beforeAll, describe, expect, onTestFailed, test } from 'vitest' -import { defaultBrowserPort } from 'vitest/config' -import { browser, provider, runBrowserTests } from './utils' +import { browser, runBrowserTests } from './utils' describe('running browser tests', async () => { let stderr: string @@ -29,8 +28,6 @@ describe('running browser tests', async () => { console.error(stderr) }) - expect(stdout).toContain(`Browser runner started by ${provider} at http://localhost:${defaultBrowserPort}/`) - expect(browserResultJson.testResults).toHaveLength(19) expect(passedTests).toHaveLength(17) expect(failedTests).toHaveLength(2) diff --git a/test/browser/specs/server-url.test.ts b/test/browser/specs/server-url.test.ts index 84f544cf5bd7..153cbd1cba55 100644 --- a/test/browser/specs/server-url.test.ts +++ b/test/browser/specs/server-url.test.ts @@ -1,24 +1,28 @@ import { afterEach, expect, test } from 'vitest' -import { provider, runBrowserTests } from './utils' +import { runBrowserTests } from './utils' afterEach(() => { delete process.env.TEST_HTTPS }) test('server-url http', async () => { - const { stdout, stderr } = await runBrowserTests({ + const { stderr, ctx } = await runBrowserTests({ root: './fixtures/server-url', + watch: true, // otherwise the browser is closed before we can get the url }) + const url = ctx?.projects[0].browser?.vite.resolvedUrls?.local[0] expect(stderr).toBe('') - expect(stdout).toContain(`Browser runner started by ${provider} at http://localhost:51133/`) + expect(url).toBe('http://localhost:51133/') }) test('server-url https', async () => { process.env.TEST_HTTPS = '1' - const { stdout, stderr } = await runBrowserTests({ + const { stdout, stderr, ctx } = await runBrowserTests({ root: './fixtures/server-url', + watch: true, // otherwise the browser is closed before we can get the url }) expect(stderr).toBe('') - expect(stdout).toContain(`Browser runner started by ${provider} at https://localhost:51122/`) + const url = ctx?.projects[0].browser?.vite.resolvedUrls?.local[0] + expect(url).toBe('https://localhost:51122/') expect(stdout).toContain('Test Files 1 passed') }) diff --git a/test/cli/fixtures/browser-multiple/basic.test.js b/test/cli/fixtures/browser-multiple/basic.test.js new file mode 100644 index 000000000000..5679012438a1 --- /dev/null +++ b/test/cli/fixtures/browser-multiple/basic.test.js @@ -0,0 +1,3 @@ +import { test } from 'vitest'; + +test('passes') diff --git a/test/cli/fixtures/browser-multiple/vitest.workspace.ts b/test/cli/fixtures/browser-multiple/vitest.workspace.ts index 4bd2b203b471..8ed483f8e423 100644 --- a/test/cli/fixtures/browser-multiple/vitest.workspace.ts +++ b/test/cli/fixtures/browser-multiple/vitest.workspace.ts @@ -6,7 +6,8 @@ export default defineWorkspace([ cacheDir: resolve(import.meta.dirname, 'basic-1'), test: { name: 'basic-1', - include: ['none'], + dir: import.meta.dirname, + include: ['./basic.test.js'], browser: { enabled: true, name: 'chromium', @@ -19,7 +20,8 @@ export default defineWorkspace([ cacheDir: resolve(import.meta.dirname, 'basic-2'), test: { name: 'basic-2', - include: ['none'], + dir: import.meta.dirname, + include: ['./basic.test.js'], browser: { enabled: true, name: 'chromium', diff --git a/test/cli/test/browser-multiple.test.ts b/test/cli/test/browser-multiple.test.ts index a26ca1263253..3da5e1bf439e 100644 --- a/test/cli/test/browser-multiple.test.ts +++ b/test/cli/test/browser-multiple.test.ts @@ -8,15 +8,16 @@ it('automatically assigns the port', async () => { const workspace = resolve(import.meta.dirname, '../fixtures/browser-multiple/vitest.workspace.ts') const spy = vi.spyOn(console, 'log') onTestFinished(() => spy.mockRestore()) - const { stderr, stdout } = await runVitest({ + const { stderr, ctx } = await runVitest({ root, workspace, dir: root, - watch: false, + watch: true, }) + const urls = ctx?.projects.map(p => p.browser?.vite.resolvedUrls?.local[0]) expect(spy).not.toHaveBeenCalled() expect(stderr).not.toContain('is in use, trying another one...') - expect(stdout).toContain('Browser runner started by playwright at http://localhost:63315/') - expect(stdout).toContain('Browser runner started by playwright at http://localhost:63316/') + expect(urls).toContain('http://localhost:63315/') + expect(urls).toContain('http://localhost:63316/') }) diff --git a/test/config/test/browser-html.test.ts b/test/config/test/browser-html.test.ts index 58812f856c07..19abfc738cdc 100644 --- a/test/config/test/browser-html.test.ts +++ b/test/config/test/browser-html.test.ts @@ -5,22 +5,20 @@ import { runVitest } from '../../test-utils' const root = resolve(import.meta.dirname, '../fixtures/browser-custom-html') test('throws an error with non-existing path', async () => { - const { stderr, thrown } = await runVitest({ + const { stderr } = await runVitest({ root, config: './vitest.config.non-existing.ts', }, [], 'test', {}, { fails: true }) - expect(thrown).toBe(true) expect(stderr).toContain(`Tester HTML file "${resolve(root, './some-non-existing-path')}" doesn't exist.`) }) test('throws an error and exits if there is an error in the html file hook', async () => { - const { stderr, stdout, exitCode } = await runVitest({ + const { stderr, exitCode } = await runVitest({ root, config: './vitest.config.error-hook.ts', }) - expect(stderr).toContain('expected error in transformIndexHtml') - // error happens when browser is opened - expect(stdout).toContain('Browser runner started by playwright') + expect(stderr).toContain('Error: expected error in transformIndexHtml') + expect(stderr).toContain('[vite] Internal server error: expected error in transformIndexHtml') expect(exitCode).toBe(1) }) @@ -31,7 +29,6 @@ test('allows correct custom html', async () => { reporters: ['basic'], }) expect(stderr).toBe('') - expect(stdout).toContain('Browser runner started by playwright') expect(stdout).toContain('✓ browser-basic.test.ts') expect(exitCode).toBe(0) }) @@ -43,7 +40,6 @@ test('allows custom transformIndexHtml with custom html file', async () => { reporters: ['basic'], }) expect(stderr).toBe('') - expect(stdout).toContain('Browser runner started by playwright') expect(stdout).toContain('✓ browser-custom.test.ts') expect(exitCode).toBe(0) }) @@ -55,7 +51,6 @@ test('allows custom transformIndexHtml without custom html file', async () => { reporters: ['basic'], }) expect(stderr).toBe('') - expect(stdout).toContain('Browser runner started by playwright') expect(stdout).toContain('✓ browser-custom.test.ts') expect(exitCode).toBe(0) }) From 9e7c99f6b8f379bbad182cf7bc53e009e64b1dbb Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 13 Nov 2024 16:05:10 +0100 Subject: [PATCH 06/18] docs: typo --- docs/guide/browser/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/browser/index.md b/docs/guide/browser/index.md index 87d5ca5d398e..2d1d6b708737 100644 --- a/docs/guide/browser/index.md +++ b/docs/guide/browser/index.md @@ -108,7 +108,7 @@ export default defineConfig({ ::: info Vitest assigns port `63315` to avoid conflicts with the development server, allowing you to run both in parallel. You can change that with the [`browser.api`](/config/#browser-api) option. -Since Vitest 2.1.5, CLI no longer prints the Vite URL automcatically. You can press "b" to print the URL when running in watch mode. +Since Vitest 2.1.5, the CLI no longer prints the Vite URL automatically. You can press "b" to print the URL when running in watch mode. ::: If you have not used Vite before, make sure you have your framework's plugin installed and specified in the config. Some frameworks might require extra configuration to work - check their Vite related documentation to be sure. From 32f23b98b4b1a3708360debc03b6719f5226db87 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 13 Nov 2024 16:05:30 +0100 Subject: [PATCH 07/18] chore: release v2.1.5 --- package.json | 2 +- packages/browser/package.json | 2 +- packages/coverage-istanbul/package.json | 2 +- packages/coverage-v8/package.json | 2 +- packages/expect/package.json | 2 +- packages/mocker/package.json | 2 +- packages/pretty-format/package.json | 2 +- packages/runner/package.json | 2 +- packages/snapshot/package.json | 2 +- packages/spy/package.json | 2 +- packages/ui/package.json | 2 +- packages/utils/package.json | 2 +- packages/vite-node/package.json | 2 +- packages/vitest/package.json | 2 +- packages/web-worker/package.json | 2 +- packages/ws-client/package.json | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index dcedef52d1e1..9f63b997c17b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/monorepo", "type": "module", - "version": "2.1.4", + "version": "2.1.5", "private": true, "packageManager": "pnpm@9.12.3", "description": "Next generation testing framework powered by Vite", diff --git a/packages/browser/package.json b/packages/browser/package.json index 56a0fb672b08..1dcfebcd44c0 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/browser", "type": "module", - "version": "2.1.4", + "version": "2.1.5", "description": "Browser running for Vitest", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/coverage-istanbul/package.json b/packages/coverage-istanbul/package.json index 62a98e50892b..b1a1f4dd0815 100644 --- a/packages/coverage-istanbul/package.json +++ b/packages/coverage-istanbul/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/coverage-istanbul", "type": "module", - "version": "2.1.4", + "version": "2.1.5", "description": "Istanbul coverage provider for Vitest", "author": "Anthony Fu ", "license": "MIT", diff --git a/packages/coverage-v8/package.json b/packages/coverage-v8/package.json index da199285d492..93dd7c4c2a7b 100644 --- a/packages/coverage-v8/package.json +++ b/packages/coverage-v8/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/coverage-v8", "type": "module", - "version": "2.1.4", + "version": "2.1.5", "description": "V8 coverage provider for Vitest", "author": "Anthony Fu ", "license": "MIT", diff --git a/packages/expect/package.json b/packages/expect/package.json index 1b86acb831e1..d435e52a10c1 100644 --- a/packages/expect/package.json +++ b/packages/expect/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/expect", "type": "module", - "version": "2.1.4", + "version": "2.1.5", "description": "Jest's expect matchers as a Chai plugin", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/mocker/package.json b/packages/mocker/package.json index 34f7f42af073..d80a2f618903 100644 --- a/packages/mocker/package.json +++ b/packages/mocker/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/mocker", "type": "module", - "version": "2.1.4", + "version": "2.1.5", "description": "Vitest module mocker implementation", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/pretty-format/package.json b/packages/pretty-format/package.json index 030ec8c5b6e5..9a9deb37934b 100644 --- a/packages/pretty-format/package.json +++ b/packages/pretty-format/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/pretty-format", "type": "module", - "version": "2.1.4", + "version": "2.1.5", "description": "Fork of pretty-format with support for ESM", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/runner/package.json b/packages/runner/package.json index ff8c251c9638..060c39af447c 100644 --- a/packages/runner/package.json +++ b/packages/runner/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/runner", "type": "module", - "version": "2.1.4", + "version": "2.1.5", "description": "Vitest test runner", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/snapshot/package.json b/packages/snapshot/package.json index 7b2e4bfb8831..048d5f53627b 100644 --- a/packages/snapshot/package.json +++ b/packages/snapshot/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/snapshot", "type": "module", - "version": "2.1.4", + "version": "2.1.5", "description": "Vitest snapshot manager", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/spy/package.json b/packages/spy/package.json index d1c86cc809aa..e8a2b4fa2b00 100644 --- a/packages/spy/package.json +++ b/packages/spy/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/spy", "type": "module", - "version": "2.1.4", + "version": "2.1.5", "description": "Lightweight Jest compatible spy implementation", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/ui/package.json b/packages/ui/package.json index 765c33bbca3c..a2ec2b133413 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/ui", "type": "module", - "version": "2.1.4", + "version": "2.1.5", "description": "UI for Vitest", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/utils/package.json b/packages/utils/package.json index 359999a604db..e4625ca44bd5 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/utils", "type": "module", - "version": "2.1.4", + "version": "2.1.5", "description": "Shared Vitest utility functions", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/vite-node/package.json b/packages/vite-node/package.json index 1188ac72bf59..39ec50c53052 100644 --- a/packages/vite-node/package.json +++ b/packages/vite-node/package.json @@ -1,7 +1,7 @@ { "name": "vite-node", "type": "module", - "version": "2.1.4", + "version": "2.1.5", "description": "Vite as Node.js runtime", "author": "Anthony Fu ", "license": "MIT", diff --git a/packages/vitest/package.json b/packages/vitest/package.json index 49f5a06e5a8b..2cd118d71d90 100644 --- a/packages/vitest/package.json +++ b/packages/vitest/package.json @@ -1,7 +1,7 @@ { "name": "vitest", "type": "module", - "version": "2.1.4", + "version": "2.1.5", "description": "Next generation testing framework powered by Vite", "author": "Anthony Fu ", "license": "MIT", diff --git a/packages/web-worker/package.json b/packages/web-worker/package.json index d6e8da419fc8..e971e1fb0a02 100644 --- a/packages/web-worker/package.json +++ b/packages/web-worker/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/web-worker", "type": "module", - "version": "2.1.4", + "version": "2.1.5", "description": "Web Worker support for testing in Vitest", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/ws-client/package.json b/packages/ws-client/package.json index 4037aa5fb837..fa905377ab6c 100644 --- a/packages/ws-client/package.json +++ b/packages/ws-client/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/ws-client", "type": "module", - "version": "2.1.4", + "version": "2.1.5", "description": "WebSocket client wrapper for communicating with Vitest", "author": "Anthony Fu ", "license": "MIT", From 391860f79b58be0882afff7b0548f578c40d0ee6 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 14 Nov 2024 00:59:31 +0900 Subject: [PATCH 08/18] feat: support inline `diff` options and support `printBasicPrototype` (#6740) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Vladimir Co-authored-by: Michał Grzegorzewski <4864089+spamshaker@users.noreply.github.com> --- docs/config/index.md | 58 +++++++-- packages/browser/src/node/plugin.ts | 2 +- packages/utils/src/diff/index.ts | 5 +- .../utils/src/diff/normalizeDiffOptions.ts | 1 + packages/utils/src/diff/types.ts | 18 +++ packages/vitest/src/node/cli/cli-config.ts | 53 ++++++++- .../vitest/src/node/config/resolveConfig.ts | 2 +- .../vitest/src/node/config/serializeConfig.ts | 1 + packages/vitest/src/node/types/config.ts | 5 +- packages/vitest/src/runtime/config.ts | 3 +- packages/vitest/src/runtime/setup-common.ts | 3 + test/config/fixtures/diff/basic.test.ts | 20 ++++ test/config/fixtures/diff/vite.config.ts | 10 ++ .../test/__snapshots__/diff.test.ts.snap | 111 ++++++++++++++++++ test/config/test/diff.test.ts | 19 +++ test/config/test/failures.test.ts | 4 +- 16 files changed, 295 insertions(+), 20 deletions(-) create mode 100644 test/config/fixtures/diff/basic.test.ts create mode 100644 test/config/fixtures/diff/vite.config.ts create mode 100644 test/config/test/__snapshots__/diff.test.ts.snap create mode 100644 test/config/test/diff.test.ts diff --git a/docs/config/index.md b/docs/config/index.md index 1fc760dc40bc..a0f9152d09d2 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -2284,22 +2284,32 @@ export default defineConfig({ ### diff - **Type:** `string` -- **CLI:** `--diff=` +- **CLI:** `--diff=` -Path to a diff config that will be used to generate diff interface. Useful if you want to customize diff display. +`DiffOptions` object or a path to a module which exports `DiffOptions`. Useful if you want to customize diff display. + +For example, as a config object: :::code-group -```ts [vitest.diff.ts] -import type { DiffOptions } from 'vitest' -import c from 'tinyrainbow' +```ts [vitest.config.js] +import { defineConfig } from 'vitest/config' +import c from 'picocolors' -export default { - aIndicator: c.bold('--'), - bIndicator: c.bold('++'), - omitAnnotationLines: true, -} satisfies DiffOptions +export default defineConfig({ + test: { + diff: { + aIndicator: c.bold('--'), + bIndicator: c.bold('++'), + omitAnnotationLines: true, + } + } +}) ``` +::: +Or as a module: + +:::code-group ```ts [vitest.config.js] import { defineConfig } from 'vitest/config' @@ -2309,12 +2319,32 @@ export default defineConfig({ } }) ``` + +```ts [vitest.diff.ts] +import type { DiffOptions } from 'vitest' +import c from 'picocolors' + +export default { + aIndicator: c.bold('--'), + bIndicator: c.bold('++'), + omitAnnotationLines: true, +} satisfies DiffOptions +``` ::: +#### diff.expand + +- **Type**: `boolean` +- **Default**: `true` +- **CLI:** `--diff.expand=false` + +Expand all common lines. + #### diff.truncateThreshold - **Type**: `number` - **Default**: `0` +- **CLI:** `--diff.truncateThreshold=` The maximum length of diff result to be displayed. Diffs above this threshold will be truncated. Truncation won't take effect with default value 0. @@ -2323,6 +2353,7 @@ Truncation won't take effect with default value 0. - **Type**: `string` - **Default**: `'... Diff result is truncated'` +- **CLI:** `--diff.truncateAnnotation=` Annotation that is output at the end of diff result if it's truncated. @@ -2333,6 +2364,13 @@ Annotation that is output at the end of diff result if it's truncated. Color of truncate annotation, default is output with no color. +#### diff.printBasicPrototype + +- **Type**: `boolean` +- **Default**: `true` + +Print basic prototype `Object` and `Array` in diff output + ### fakeTimers - **Type:** `FakeTimerInstallOpts` diff --git a/packages/browser/src/node/plugin.ts b/packages/browser/src/node/plugin.ts index 14c216cfa1ff..e1d049d7c702 100644 --- a/packages/browser/src/node/plugin.ts +++ b/packages/browser/src/node/plugin.ts @@ -230,7 +230,7 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { 'msw/browser', ] - if (project.config.diff) { + if (typeof project.config.diff === 'string') { entries.push(project.config.diff) } diff --git a/packages/utils/src/diff/index.ts b/packages/utils/src/diff/index.ts index fcb94a9f07b6..d9445dc47dc0 100644 --- a/packages/utils/src/diff/index.ts +++ b/packages/utils/src/diff/index.ts @@ -23,7 +23,7 @@ import { getType } from './getType' import { normalizeDiffOptions } from './normalizeDiffOptions' import { diffStringsRaw, diffStringsUnified } from './printDiffs' -export type { DiffOptions, DiffOptionsColor } from './types' +export type { DiffOptions, DiffOptionsColor, SerializedDiffOptions } from './types' export { diffLinesRaw, diffLinesUnified, diffLinesUnified2 } export { diffStringsRaw, diffStringsUnified } @@ -180,11 +180,12 @@ function getFormatOptions( formatOptions: PrettyFormatOptions, options?: DiffOptions, ): PrettyFormatOptions { - const { compareKeys } = normalizeDiffOptions(options) + const { compareKeys, printBasicPrototype } = normalizeDiffOptions(options) return { ...formatOptions, compareKeys, + printBasicPrototype, } } diff --git a/packages/utils/src/diff/normalizeDiffOptions.ts b/packages/utils/src/diff/normalizeDiffOptions.ts index a7d9be7134ae..f2940a38758c 100644 --- a/packages/utils/src/diff/normalizeDiffOptions.ts +++ b/packages/utils/src/diff/normalizeDiffOptions.ts @@ -34,6 +34,7 @@ function getDefaultOptions(): DiffOptionsNormalized { includeChangeCounts: false, omitAnnotationLines: false, patchColor: c.yellow, + printBasicPrototype: true, truncateThreshold: DIFF_TRUNCATE_THRESHOLD_DEFAULT, truncateAnnotation: '... Diff result is truncated', truncateAnnotationColor: noColor, diff --git a/packages/utils/src/diff/types.ts b/packages/utils/src/diff/types.ts index 951fcdbb27c1..211ab033f12c 100644 --- a/packages/utils/src/diff/types.ts +++ b/packages/utils/src/diff/types.ts @@ -26,12 +26,29 @@ export interface DiffOptions { includeChangeCounts?: boolean omitAnnotationLines?: boolean patchColor?: DiffOptionsColor + printBasicPrototype?: boolean compareKeys?: CompareKeys truncateThreshold?: number truncateAnnotation?: string truncateAnnotationColor?: DiffOptionsColor } +export interface SerializedDiffOptions { + aAnnotation?: string + aIndicator?: string + bAnnotation?: string + bIndicator?: string + commonIndicator?: string + contextLines?: number + emptyFirstOrLastLinePlaceholder?: string + expand?: boolean + includeChangeCounts?: boolean + omitAnnotationLines?: boolean + printBasicPrototype?: boolean + truncateThreshold?: number + truncateAnnotation?: string +} + export interface DiffOptionsNormalized { aAnnotation: string aColor: DiffOptionsColor @@ -51,6 +68,7 @@ export interface DiffOptionsNormalized { includeChangeCounts: boolean omitAnnotationLines: boolean patchColor: DiffOptionsColor + printBasicPrototype: boolean truncateThreshold: number truncateAnnotation: string truncateAnnotationColor: DiffOptionsColor diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index 638ec22d508a..3d4f9e31961f 100644 --- a/packages/vitest/src/node/cli/cli-config.ts +++ b/packages/vitest/src/node/cli/cli-config.ts @@ -599,9 +599,58 @@ export const cliOptionsConfig: VitestCLIOptions = { }, diff: { description: - 'Path to a diff config that will be used to generate diff interface', + 'DiffOptions object or a path to a module which exports DiffOptions object', argument: '', - normalize: true, + subcommands: { + aAnnotation: { + description: 'Annotation for expected lines (default: `Expected`)', + argument: '', + }, + aIndicator: { + description: 'Indicator for expected lines (default: `-`)', + argument: '', + }, + bAnnotation: { + description: 'Annotation for received lines (default: `Received`)', + argument: '', + }, + bIndicator: { + description: 'Indicator for received lines (default: `+`)', + argument: '', + }, + commonIndicator: { + description: 'Indicator for common lines (default: ` `)', + argument: '', + }, + contextLines: { + description: 'Number of lines of context to show around each change (default: `5`)', + argument: '', + }, + emptyFirstOrLastLinePlaceholder: { + description: 'Placeholder for an empty first or last line (default: `""`)', + argument: '', + }, + expand: { + description: 'Expand all common lines (default: `true`)', + }, + includeChangeCounts: { + description: 'Include comparison counts in diff output (default: `false`)', + }, + omitAnnotationLines: { + description: 'Omit annotation lines from the output (default: `false`)', + }, + printBasicPrototype: { + description: 'Print basic prototype Object and Array (default: `true`)', + }, + truncateThreshold: { + description: 'Number of lines to show before and after each change (default: `0`)', + argument: '', + }, + truncateAnnotation: { + description: 'Annotation for truncated lines (default: `... Diff result is truncated`)', + argument: '', + }, + }, }, exclude: { description: 'Additional file globs to be excluded from test', diff --git a/packages/vitest/src/node/config/resolveConfig.ts b/packages/vitest/src/node/config/resolveConfig.ts index 6246282707a1..b83426ad761c 100644 --- a/packages/vitest/src/node/config/resolveConfig.ts +++ b/packages/vitest/src/node/config/resolveConfig.ts @@ -578,7 +578,7 @@ export function resolveConfig( } } - if (resolved.diff) { + if (typeof resolved.diff === 'string') { resolved.diff = resolvePath(resolved.diff, resolved.root) resolved.forceRerunTriggers.push(resolved.diff) } diff --git a/packages/vitest/src/node/config/serializeConfig.ts b/packages/vitest/src/node/config/serializeConfig.ts index 249f4bbdaa1a..234e67a3be34 100644 --- a/packages/vitest/src/node/config/serializeConfig.ts +++ b/packages/vitest/src/node/config/serializeConfig.ts @@ -37,6 +37,7 @@ export function serializeConfig( pool: config.pool, expect: config.expect, snapshotSerializers: config.snapshotSerializers, + // TODO: non serializable function? diff: config.diff, retry: config.retry, disableConsoleIntercept: config.disableConsoleIntercept, diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts index ab9b327d7264..1eae8e3c1bb8 100644 --- a/packages/vitest/src/node/types/config.ts +++ b/packages/vitest/src/node/types/config.ts @@ -2,6 +2,7 @@ import type { FakeTimerInstallOpts } from '@sinonjs/fake-timers' import type { PrettyFormatOptions } from '@vitest/pretty-format' import type { SequenceHooks, SequenceSetupFiles } from '@vitest/runner' import type { SnapshotStateOptions } from '@vitest/snapshot' +import type { SerializedDiffOptions } from '@vitest/utils/diff' import type { AliasOptions, ConfigEnv, DepOptimizationConfig, ServerOptions, UserConfig as ViteUserConfig } from 'vite' import type { ViteNodeServerOptions } from 'vite-node' import type { ChaiConfig } from '../../integrations/chai/config' @@ -563,7 +564,7 @@ export interface InlineConfig { /** * Path to a module which has a default export of diff config. */ - diff?: string + diff?: string | SerializedDiffOptions /** * Paths to snapshot serializer modules. @@ -979,7 +980,7 @@ export interface ResolvedConfig mode: VitestRunMode base?: string - diff?: string + diff?: string | SerializedDiffOptions bail?: number setupFiles: string[] diff --git a/packages/vitest/src/runtime/config.ts b/packages/vitest/src/runtime/config.ts index 922bb6f80e24..ef9b8d0b4f56 100644 --- a/packages/vitest/src/runtime/config.ts +++ b/packages/vitest/src/runtime/config.ts @@ -3,6 +3,7 @@ import type { PrettyFormatOptions } from '@vitest/pretty-format' import type { SequenceHooks, SequenceSetupFiles } from '@vitest/runner' import type { SnapshotUpdateState } from '@vitest/snapshot' import type { SnapshotEnvironment } from '@vitest/snapshot/environment' +import type { SerializedDiffOptions } from '@vitest/utils/diff' /** * Config that tests have access to. @@ -98,7 +99,7 @@ export interface SerializedConfig { showDiff?: boolean truncateThreshold?: number } | undefined - diff: string | undefined + diff: string | SerializedDiffOptions | undefined retry: number includeTaskLocation: boolean | undefined inspect: boolean | string | undefined diff --git a/packages/vitest/src/runtime/setup-common.ts b/packages/vitest/src/runtime/setup-common.ts index cdd6cbb63bd5..033816637cbd 100644 --- a/packages/vitest/src/runtime/setup-common.ts +++ b/packages/vitest/src/runtime/setup-common.ts @@ -47,6 +47,9 @@ export async function loadDiffConfig( config: SerializedConfig, executor: VitestExecutor, ) { + if (typeof config.diff === 'object') { + return config.diff + } if (typeof config.diff !== 'string') { return } diff --git a/test/config/fixtures/diff/basic.test.ts b/test/config/fixtures/diff/basic.test.ts new file mode 100644 index 000000000000..af61b0814634 --- /dev/null +++ b/test/config/fixtures/diff/basic.test.ts @@ -0,0 +1,20 @@ +import { expect, test } from 'vitest' + +test('large diff', () => { + const x = [...Array(30)].map((_, i) => i); + const y = [...x]; + y[0] = 1000; + y[15] = 2000; + y[29] = 3000; + expect(x).toEqual(y) +}) + +test("printBasicPrototype", () => { + expect({ + obj: { k: "foo" }, + arr: [1, 2] + }).toEqual({ + obj: { k: "bar" }, + arr: [1, 3] + }); +}) diff --git a/test/config/fixtures/diff/vite.config.ts b/test/config/fixtures/diff/vite.config.ts new file mode 100644 index 000000000000..2d94b473bfba --- /dev/null +++ b/test/config/fixtures/diff/vite.config.ts @@ -0,0 +1,10 @@ +import {defineConfig} from 'vitest/config' + +export default defineConfig({ + test: { + diff: { + // expand: false, + // printBasicPrototype: false, + } + } +}) diff --git a/test/config/test/__snapshots__/diff.test.ts.snap b/test/config/test/__snapshots__/diff.test.ts.snap new file mode 100644 index 000000000000..5ab25703726a --- /dev/null +++ b/test/config/test/__snapshots__/diff.test.ts.snap @@ -0,0 +1,111 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`inline diff options: { expand: false, printBasicPrototype: false } 1`] = ` +[ + "- Expected ++ Received + +@@ -1,7 +1,7 @@ + [ +- 1000, ++ 0, + 1, + 2, + 3, + 4, + 5, +@@ -12,11 +12,11 @@ + 10, + 11, + 12, + 13, + 14, +- 2000, ++ 15, + 16, + 17, + 18, + 19, + 20, +@@ -26,7 +26,7 @@ + 24, + 25, + 26, + 27, + 28, +- 3000, ++ 29, + ]", + "- Expected ++ Received + + { + "arr": [ + 1, +- 3, ++ 2, + ], + "obj": { +- "k": "bar", ++ "k": "foo", + }, + }", +] +`; + +exports[`inline diff options: undefined 1`] = ` +[ + "- Expected ++ Received + + Array [ +- 1000, ++ 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, +- 2000, ++ 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, +- 3000, ++ 29, + ]", + "- Expected ++ Received + + Object { + "arr": Array [ + 1, +- 3, ++ 2, + ], + "obj": Object { +- "k": "bar", ++ "k": "foo", + }, + }", +] +`; diff --git a/test/config/test/diff.test.ts b/test/config/test/diff.test.ts new file mode 100644 index 000000000000..059e1d0cacc5 --- /dev/null +++ b/test/config/test/diff.test.ts @@ -0,0 +1,19 @@ +import { stripVTControlCharacters } from 'node:util' +import { expect, test } from 'vitest' +import { runVitest } from '../../test-utils' + +test.for([ + [undefined], + [{ expand: false, printBasicPrototype: false }], +])(`inline diff options: %o`, async ([options]) => { + const { ctx } = await runVitest({ + root: './fixtures/diff', + diff: options, + }) + const errors = ctx!.state.getFiles().flatMap(f => + f.tasks.flatMap(t => t.result?.errors ?? []), + ) + expect( + errors.map(e => e.diff && stripVTControlCharacters(e.diff)), + ).matchSnapshot() +}) diff --git a/test/config/test/failures.test.ts b/test/config/test/failures.test.ts index 591a2bd2175e..1d98a7a1a673 100644 --- a/test/config/test/failures.test.ts +++ b/test/config/test/failures.test.ts @@ -247,7 +247,9 @@ test('coverage.autoUpdate cannot update thresholds when configuration file doesn }) test('boolean flag 100 should not crash CLI', async () => { - const { stderr } = await runVitestCli('--coverage.enabled', '--coverage.thresholds.100') + let { stderr } = await runVitestCli('--coverage.enabled', '--coverage.thresholds.100') + // non-zero coverage shows up, which is non-deterministic, so strip it. + stderr = stderr.replace(/\([0-9.]+%\) does/g, '(0%) does') expect(stderr).toMatch('ERROR: Coverage for lines (0%) does not meet global threshold (100%)') expect(stderr).toMatch('ERROR: Coverage for functions (0%) does not meet global threshold (100%)') From ff66206ae844b965e0c42f1017c22a96aea98f56 Mon Sep 17 00:00:00 2001 From: Jacob Erdman <56931564+jacoberdman2147@users.noreply.github.com> Date: Wed, 13 Nov 2024 08:01:01 -0800 Subject: [PATCH 09/18] feat(expect): add `toHaveBeenCalledExactlyOnceWith` expect matcher (#6894) Co-authored-by: Vladimir --- docs/api/expect.md | 24 +++++++++++ packages/expect/src/jest-expect.ts | 21 ++++++++++ packages/expect/src/types.ts | 9 +++++ test/core/test/jest-expect.test.ts | 64 ++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+) diff --git a/docs/api/expect.md b/docs/api/expect.md index 0e12270e177b..a511cbb36ee5 100644 --- a/docs/api/expect.md +++ b/docs/api/expect.md @@ -876,6 +876,30 @@ test('spy function', () => { }) ``` +## toHaveBeenCalledExactlyOnceWith 2.2.0 {#tohavebeencalledexactlyoncewith} + +- **Type**: `(...args: any[]) => Awaitable` + +This assertion checks if a function was called exactly once and with certain parameters. Requires a spy function to be passed to `expect`. + +```ts +import { expect, test, vi } from 'vitest' + +const market = { + buy(subject: string, amount: number) { + // ... + }, +} + +test('spy function', () => { + const buySpy = vi.spyOn(market, 'buy') + + market.buy('apples', 10) + + expect(buySpy).toHaveBeenCalledExactlyOnceWith('apples', 10) +}) +``` + ## toHaveBeenLastCalledWith - **Type**: `(...args: any[]) => Awaitable` diff --git a/packages/expect/src/jest-expect.ts b/packages/expect/src/jest-expect.ts index 72615f364de1..6616e6f9d23a 100644 --- a/packages/expect/src/jest-expect.ts +++ b/packages/expect/src/jest-expect.ts @@ -595,6 +595,27 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { throw new AssertionError(formatCalls(spy, msg, args)) } }) + def('toHaveBeenCalledExactlyOnceWith', function (...args) { + const spy = getSpy(this) + const spyName = spy.getMockName() + const callCount = spy.mock.calls.length + const hasCallWithArgs = spy.mock.calls.some(callArg => + jestEquals(callArg, args, [...customTesters, iterableEquality]), + ) + const pass = hasCallWithArgs && callCount === 1 + const isNot = utils.flag(this, 'negate') as boolean + + const msg = utils.getMessage(this, [ + pass, + `expected "${spyName}" to be called once with arguments: #{exp}`, + `expected "${spyName}" to not be called once with arguments: #{exp}`, + args, + ]) + + if ((pass && isNot) || (!pass && !isNot)) { + throw new AssertionError(formatCalls(spy, msg, args)) + } + }) def( ['toHaveBeenNthCalledWith', 'nthCalledWith'], function (times: number, ...args: any[]) { diff --git a/packages/expect/src/types.ts b/packages/expect/src/types.ts index c1f033437980..e53b059e555e 100644 --- a/packages/expect/src/types.ts +++ b/packages/expect/src/types.ts @@ -635,6 +635,15 @@ export interface Assertion */ toHaveBeenCalledOnce: () => void + /** + * Ensure that a mock function is called with specific arguments and called + * exactly once. + * + * @example + * expect(mockFunc).toHaveBeenCalledExactlyOnceWith('arg1', 42); + */ + toHaveBeenCalledExactlyOnceWith: (...args: E) => void + /** * Checks that a value satisfies a custom matcher function. * diff --git a/test/core/test/jest-expect.test.ts b/test/core/test/jest-expect.test.ts index 4d6bb3a0e8e7..79ffc4874b39 100644 --- a/test/core/test/jest-expect.test.ts +++ b/test/core/test/jest-expect.test.ts @@ -634,6 +634,70 @@ describe('toHaveBeenCalledWith', () => { }) }) +describe('toHaveBeenCalledExactlyOnceWith', () => { + describe('negated', () => { + it('fails if called', () => { + const mock = vi.fn() + mock(3) + + expect(() => { + expect(mock).not.toHaveBeenCalledExactlyOnceWith(3) + }).toThrow(/^expected "spy" to not be called once with arguments: \[ 3 \][^e]/) + }) + + it('passes if called multiple times with args', () => { + const mock = vi.fn() + mock(3) + mock(3) + + expect(mock).not.toHaveBeenCalledExactlyOnceWith(3) + }) + + it('passes if not called', () => { + const mock = vi.fn() + expect(mock).not.toHaveBeenCalledExactlyOnceWith(3) + }) + + it('passes if called with a different argument', () => { + const mock = vi.fn() + mock(4) + + expect(mock).not.toHaveBeenCalledExactlyOnceWith(3) + }) + }) + + it('fails if not called or called too many times', () => { + const mock = vi.fn() + + expect(() => { + expect(mock).toHaveBeenCalledExactlyOnceWith(3) + }).toThrow(/^expected "spy" to be called once with arguments: \[ 3 \][^e]/) + + mock(3) + mock(3) + + expect(() => { + expect(mock).toHaveBeenCalledExactlyOnceWith(3) + }).toThrow(/^expected "spy" to be called once with arguments: \[ 3 \][^e]/) + }) + + it('fails if called with wrong args', () => { + const mock = vi.fn() + mock(4) + + expect(() => { + expect(mock).toHaveBeenCalledExactlyOnceWith(3) + }).toThrow(/^expected "spy" to be called once with arguments: \[ 3 \][^e]/) + }) + + it('passes if called exactly once with args', () => { + const mock = vi.fn() + mock(3) + + expect(mock).toHaveBeenCalledExactlyOnceWith(3) + }) +}) + describe('async expect', () => { it('resolves', async () => { await expect((async () => 'true')()).resolves.toBe('true') From 9c8f7e3e687a775dbe998bfdac5cb33baf01f086 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 13 Nov 2024 17:01:17 +0100 Subject: [PATCH 10/18] feat(vitest): include `coverageMap` in json report (#6606) --- docs/guide/reporters.md | 9 +++++++-- packages/vitest/src/node/reporters/json.ts | 10 ++++++---- test/workspaces/globalTest.ts | 1 + 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/guide/reporters.md b/docs/guide/reporters.md index 03050696eaff..4d3785fd1e73 100644 --- a/docs/guide/reporters.md +++ b/docs/guide/reporters.md @@ -263,7 +263,7 @@ export default defineConfig({ ### JSON Reporter -Outputs a report of the test results in JSON format. Can either be printed to the terminal or written to a file using the [`outputFile`](/config/#outputfile) configuration option. +Generates a report of the test results in a JSON format compatible with Jest's `--json` option. Can either be printed to the terminal or written to a file using the [`outputFile`](/config/#outputfile) configuration option. :::code-group ```bash [CLI] @@ -322,10 +322,15 @@ Example of a JSON report: "message": "", "name": "/root-directory/__tests__/test-file-1.test.ts" } - ] + ], + "coverageMap": {} } ``` +::: info +Since Vitest 2.2, the JSON reporter includes coverage information in `coverageMap` if coverage is enabled. +::: + ### HTML Reporter Generates an HTML file to view test results through an interactive [GUI](/guide/ui). After the file has been generated, Vitest will keep a local development server running and provide a link to view the report in a browser. diff --git a/packages/vitest/src/node/reporters/json.ts b/packages/vitest/src/node/reporters/json.ts index 4b292344cb75..1b46c62bd2ab 100644 --- a/packages/vitest/src/node/reporters/json.ts +++ b/packages/vitest/src/node/reporters/json.ts @@ -1,5 +1,6 @@ import type { File, Suite, TaskMeta, TaskState } from '@vitest/runner' import type { SnapshotSummary } from '@vitest/snapshot' +import type { CoverageMap } from 'istanbul-lib-coverage' import type { Vitest } from '../core' import type { Reporter } from '../types/reporter' import { existsSync, promises as fs } from 'node:fs' @@ -63,7 +64,7 @@ export interface JsonTestResults { success: boolean testResults: Array snapshot: SnapshotSummary - // coverageMap?: CoverageMap | null | undefined + coverageMap?: CoverageMap | null | undefined // numRuntimeErrorTestSuites: number // wasInterrupted: boolean } @@ -86,7 +87,7 @@ export class JsonReporter implements Reporter { this.start = Date.now() } - protected async logTasks(files: File[]) { + protected async logTasks(files: File[], coverageMap?: CoverageMap | null) { const suites = getSuites(files) const numTotalTestSuites = suites.length const tests = getTests(files) @@ -188,13 +189,14 @@ export class JsonReporter implements Reporter { startTime: this.start, success, testResults, + coverageMap, } await this.writeReport(JSON.stringify(result)) } - async onFinished(files = this.ctx.state.getFiles()) { - await this.logTasks(files) + async onFinished(files = this.ctx.state.getFiles(), _errors: unknown[] = [], coverageMap?: unknown) { + await this.logTasks(files, coverageMap as CoverageMap) } /** diff --git a/test/workspaces/globalTest.ts b/test/workspaces/globalTest.ts index d642baa0c546..0069891a748d 100644 --- a/test/workspaces/globalTest.ts +++ b/test/workspaces/globalTest.ts @@ -37,6 +37,7 @@ export async function teardown() { assert.equal(results.numTotalTestSuites, 28) assert.equal(results.numTotalTests, 31) assert.equal(results.numPassedTests, 31) + assert.ok(results.coverageMap) const shared = results.testResults.filter((r: any) => r.name.includes('space_shared/test.spec.ts')) From e26e066c7759d43c3b819e3bb9d3c905e7b6c459 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 13 Nov 2024 17:01:32 +0100 Subject: [PATCH 11/18] feat(vitest): add `onTestsRerun` method to global setup context (#6803) --- docs/config/index.md | 12 ++++++ packages/vitest/src/node/core.ts | 18 ++++++-- packages/vitest/src/node/globalSetup.ts | 3 +- packages/vitest/src/node/workspace.ts | 1 + packages/vitest/src/public/node.ts | 5 ++- packages/vitest/src/types/general.ts | 2 +- test/watch/fixtures/global-setup.ts | 15 +++++++ test/watch/fixtures/vitest.config.ts | 4 ++ test/watch/test/global-setup-rerun.test.ts | 50 ++++++++++++++++++++++ 9 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 test/watch/fixtures/global-setup.ts create mode 100644 test/watch/test/global-setup-rerun.test.ts diff --git a/docs/config/index.md b/docs/config/index.md index a0f9152d09d2..7a441d0687f0 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -1089,6 +1089,18 @@ inject('wsPort') === 3000 ``` ::: +Since Vitest 2.2.0, you can define a custom callback function to be called when Vitest reruns tests. If the function is asynchronous, the runner will wait for it to complete before executing the tests. + +```ts +import type { GlobalSetupContext } from 'vitest/node' + +export default function setup({ onTestsRerun }: GlobalSetupContext) { + onTestsRerun(async () => { + await restartDb() + }) +} +``` + ### forceRerunTriggers - **Type**: `string[]` diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 74fcceda3874..f10eb087b8a0 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -3,7 +3,7 @@ import type { Writable } from 'node:stream' import type { ViteDevServer } from 'vite' import type { defineWorkspace } from 'vitest/config' import type { SerializedCoverageConfig } from '../runtime/config' -import type { ArgumentsType, OnServerRestartHandler, ProvidedContext, UserConsoleLog } from '../types/general' +import type { ArgumentsType, OnServerRestartHandler, OnTestsRerunHandler, ProvidedContext, UserConsoleLog } from '../types/general' import type { ProcessPool, WorkspaceSpec } from './pool' import type { TestSpecification } from './spec' import type { ResolvedConfig, UserConfig, VitestRunMode } from './types/config' @@ -104,6 +104,7 @@ export class Vitest { private _onClose: (() => Awaited)[] = [] private _onSetServer: OnServerRestartHandler[] = [] private _onCancelListeners: ((reason: CancelReason) => Promise | void)[] = [] + private _onUserTestsRerun: OnTestsRerunHandler[] = [] async setServer(options: UserConfig, server: ViteDevServer, cliOptions: UserConfig) { this.unregisterWatcher?.() @@ -119,6 +120,7 @@ export class Vitest { this.coverageProvider = undefined this.runningPromise = undefined this._cachedSpecs.clear() + this._onUserTestsRerun = [] const resolved = resolveConfig(this.mode, options, server.config, this.logger) @@ -695,7 +697,10 @@ export class Vitest { files = files.filter(file => filteredFiles.some(f => f[1] === file)) } - await this.report('onWatcherRerun', files, trigger) + await Promise.all([ + this.report('onWatcherRerun', files, trigger), + ...this._onUserTestsRerun.map(fn => fn(files)), + ]) await this.runFiles(files.flatMap(file => this.getProjectsByTestFile(file)), allTestsRun) await this.report('onWatcherStart', this.state.getFiles(files)) @@ -817,7 +822,10 @@ export class Vitest { const triggerIds = new Set(triggerId.map(id => relative(this.config.root, id))) const triggerLabel = Array.from(triggerIds).join(', ') - await this.report('onWatcherRerun', files, triggerLabel) + await Promise.all([ + this.report('onWatcherRerun', files, triggerLabel), + ...this._onUserTestsRerun.map(fn => fn(files)), + ]) await this.runFiles(files.flatMap(file => this.getProjectsByTestFile(file)), false) @@ -1154,4 +1162,8 @@ export class Vitest { onClose(fn: () => void) { this._onClose.push(fn) } + + onTestsRerun(fn: OnTestsRerunHandler): void { + this._onUserTestsRerun.push(fn) + } } diff --git a/packages/vitest/src/node/globalSetup.ts b/packages/vitest/src/node/globalSetup.ts index caf1d4e0820f..33d17d8aadfe 100644 --- a/packages/vitest/src/node/globalSetup.ts +++ b/packages/vitest/src/node/globalSetup.ts @@ -1,5 +1,5 @@ import type { ViteNodeRunner } from 'vite-node/client' -import type { ProvidedContext } from '../types/general' +import type { OnTestsRerunHandler, ProvidedContext } from '../types/general' import type { ResolvedConfig } from './types/config' import { toArray } from '@vitest/utils' @@ -9,6 +9,7 @@ export interface GlobalSetupContext { key: T, value: ProvidedContext[T] ) => void + onTestsRerun: (cb: OnTestsRerunHandler) => void } export interface GlobalSetupFile { diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index 76e87a144f63..fea8e499818f 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -170,6 +170,7 @@ export class WorkspaceProject { const teardown = await globalSetupFile.setup?.({ provide: (key, value) => this.provide(key, value), config: this.config, + onTestsRerun: cb => this.ctx.onTestsRerun(cb), }) if (teardown == null || !!globalSetupFile.teardown) { continue diff --git a/packages/vitest/src/public/node.ts b/packages/vitest/src/public/node.ts index ce7b67697811..e46a3911134e 100644 --- a/packages/vitest/src/public/node.ts +++ b/packages/vitest/src/public/node.ts @@ -126,7 +126,10 @@ export type { TscErrorInfo as TypeCheckErrorInfo, } from '../typecheck/types' -export type { OnServerRestartHandler } from '../types/general' +export type { + OnServerRestartHandler, + OnTestsRerunHandler, +} from '../types/general' export { createDebugger } from '../utils/debugger' diff --git a/packages/vitest/src/types/general.ts b/packages/vitest/src/types/general.ts index 99cba19ca50b..2d46006f0da4 100644 --- a/packages/vitest/src/types/general.ts +++ b/packages/vitest/src/types/general.ts @@ -46,5 +46,5 @@ export interface ModuleGraphData { } export type OnServerRestartHandler = (reason?: string) => Promise | void - +export type OnTestsRerunHandler = (testFiles: string[]) => Promise | void export interface ProvidedContext {} diff --git a/test/watch/fixtures/global-setup.ts b/test/watch/fixtures/global-setup.ts new file mode 100644 index 000000000000..b86537e952dc --- /dev/null +++ b/test/watch/fixtures/global-setup.ts @@ -0,0 +1,15 @@ +import { GlobalSetupContext } from 'vitest/node'; + +const calls: string[] = []; + +(globalThis as any).__CALLS = calls + +export default ({ onTestsRerun }: GlobalSetupContext) => { + calls.push('start') + onTestsRerun(() => { + calls.push('rerun') + }) + return () => { + calls.push('end') + } +} diff --git a/test/watch/fixtures/vitest.config.ts b/test/watch/fixtures/vitest.config.ts index 184e5719a9d6..864dd0acabc5 100644 --- a/test/watch/fixtures/vitest.config.ts +++ b/test/watch/fixtures/vitest.config.ts @@ -18,5 +18,9 @@ export default defineConfig({ forceRerunTriggers: [ '**/force-watch/**', ], + + globalSetup: process.env.TEST_GLOBAL_SETUP + ? './global-setup.ts' + : undefined, }, }) diff --git a/test/watch/test/global-setup-rerun.test.ts b/test/watch/test/global-setup-rerun.test.ts new file mode 100644 index 000000000000..f386157fadf2 --- /dev/null +++ b/test/watch/test/global-setup-rerun.test.ts @@ -0,0 +1,50 @@ +import { expect, test } from 'vitest' +import { editFile, runVitest } from '../../test-utils' + +const testFile = 'fixtures/math.test.ts' + +test('global setup calls hooks correctly when file changes', async () => { + process.env.TEST_GLOBAL_SETUP = 'true' + const { vitest, ctx } = await runVitest({ + root: 'fixtures', + watch: true, + include: ['math.test.ts'], + }) + + await vitest.waitForStdout('Waiting for file changes') + + const calls = (globalThis as any).__CALLS as string[] + expect(calls).toEqual(['start']) + + editFile(testFile, testFileContent => `${testFileContent}\n\n`) + + await vitest.waitForStdout('RERUN') + expect(calls).toEqual(['start', 'rerun']) + + await ctx?.close() + + expect(calls).toEqual(['start', 'rerun', 'end']) +}) + +test('global setup calls hooks correctly with a manual rerun', async () => { + process.env.TEST_GLOBAL_SETUP = 'true' + const { vitest, ctx } = await runVitest({ + root: 'fixtures', + watch: true, + include: ['math.test.ts'], + }) + + await vitest.waitForStdout('Waiting for file changes') + + const calls = (globalThis as any).__CALLS as string[] + expect(calls).toEqual(['start']) + + vitest.write('r') + + await vitest.waitForStdout('RERUN') + expect(calls).toEqual(['start', 'rerun']) + + await ctx?.close() + + expect(calls).toEqual(['start', 'rerun', 'end']) +}) From 8d179afcc75973717e59deb7768b7bc03b2fcdc7 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 13 Nov 2024 17:02:47 +0100 Subject: [PATCH 12/18] fix(browser): improve source maps when `vi.mock` is present (#6810) --- packages/mocker/src/node/hoistMocksPlugin.ts | 228 ++++++------ test/core/test/injector-mock.test.ts | 365 +++++++++++-------- test/public-mocker/test/mocker.test.ts | 2 +- test/public-mocker/vite.config.ts | 2 +- 4 files changed, 331 insertions(+), 266 deletions(-) diff --git a/packages/mocker/src/node/hoistMocksPlugin.ts b/packages/mocker/src/node/hoistMocksPlugin.ts index 5515b3b1f09b..04a4ce867d9f 100644 --- a/packages/mocker/src/node/hoistMocksPlugin.ts +++ b/packages/mocker/src/node/hoistMocksPlugin.ts @@ -6,23 +6,23 @@ import type { Expression, Identifier, ImportDeclaration, - ImportExpression, VariableDeclaration, } from 'estree' import type { SourceMap } from 'magic-string' +import type { RollupAstNode } from 'rollup' import type { Plugin, Rollup } from 'vite' import type { Node, Positioned } from './esmWalker' import { findNodeAround } from 'acorn-walk' import MagicString from 'magic-string' import { createFilter } from 'vite' -import { esmWalker, getArbitraryModuleIdentifier } from './esmWalker' +import { esmWalker } from './esmWalker' interface HoistMocksOptions { /** * List of modules that should always be imported before compiler hints. - * @default ['vitest'] + * @default 'vitest' */ - hoistedModules?: string[] + hoistedModule?: string /** * @default ["vi", "vitest"] */ @@ -106,11 +106,14 @@ function isIdentifier(node: any): node is Positioned { return node.type === 'Identifier' } -function getBetterEnd(code: string, node: Node) { +function getNodeTail(code: string, node: Node) { let end = node.end if (code[node.end] === ';') { end += 1 } + if (code[node.end] === '\n') { + return end + 1 + } if (code[node.end + 1] === '\n') { end += 1 } @@ -160,48 +163,43 @@ export function hoistMocks( dynamicImportMockMethodNames = ['mock', 'unmock', 'doMock', 'doUnmock'], hoistedMethodNames = ['hoisted'], utilsObjectNames = ['vi', 'vitest'], - hoistedModules = ['vitest'], + hoistedModule = 'vitest', } = options - const hoistIndex = code.match(hashbangRE)?.[0].length ?? 0 + // hoist at the start of the file, after the hashbang + let hoistIndex = hashbangRE.exec(code)?.[0].length ?? 0 let hoistedModuleImported = false let uid = 0 const idToImportMap = new Map() + const imports: { + node: RollupAstNode + id: string + }[] = [] + // this will transform import statements into dynamic ones, if there are imports // it will keep the import as is, if we don't need to mock anything // in browser environment it will wrap the module value with "vitest_wrap_module" function // that returns a proxy to the module so that named exports can be mocked - const transformImportDeclaration = (node: ImportDeclaration) => { - const source = node.source.value as string - - const importId = `__vi_import_${uid++}__` - const hasSpecifiers = node.specifiers.length > 0 - const code = hasSpecifiers - ? `const ${importId} = await import('${source}')\n` - : `await import('${source}')\n` - return { - code, - id: importId, - } - } - - function defineImport(node: Positioned) { + function defineImport( + importNode: ImportDeclaration & { + start: number + end: number + }, + ) { + const source = importNode.source.value as string // always hoist vitest import to top of the file, so // "vi" helpers can access it - if (hoistedModules.includes(node.source.value as string)) { + if (hoistedModule === source) { hoistedModuleImported = true return } + const importId = `__vi_import_${uid++}__` + imports.push({ id: importId, node: importNode }) - const declaration = transformImportDeclaration(node) - if (!declaration) { - return null - } - s.appendLeft(hoistIndex, declaration.code) - return declaration.id + return importId } // 1. check all import statements and record id -> importName map @@ -214,13 +212,20 @@ export function hoistMocks( if (!importId) { continue } - s.remove(node.start, getBetterEnd(code, node)) for (const spec of node.specifiers) { if (spec.type === 'ImportSpecifier') { - idToImportMap.set( - spec.local.name, - `${importId}.${getArbitraryModuleIdentifier(spec.imported)}`, - ) + if (spec.imported.type === 'Identifier') { + idToImportMap.set( + spec.local.name, + `${importId}.${spec.imported.name}`, + ) + } + else { + idToImportMap.set( + spec.local.name, + `${importId}[${JSON.stringify(spec.imported.value as string)}]`, + ) + } } else if (spec.type === 'ImportDefaultSpecifier') { idToImportMap.set(spec.local.name, `${importId}.default`) @@ -235,7 +240,7 @@ export function hoistMocks( const declaredConst = new Set() const hoistedNodes: Positioned< - CallExpression | VariableDeclaration | AwaitExpression + CallExpression | VariableDeclaration | AwaitExpression >[] = [] function createSyntaxError(node: Positioned, message: string) { @@ -300,6 +305,8 @@ export function hoistMocks( } } + const usedUtilityExports = new Set() + esmWalker(ast, { onIdentifier(id, info, parentStack) { const binding = idToImportMap.get(id.name) @@ -333,6 +340,7 @@ export function hoistMocks( && isIdentifier(node.callee.property) ) { const methodName = node.callee.property.name + usedUtilityExports.add(node.callee.object.name) if (hoistableMockMethodNames.includes(methodName)) { const method = `${node.callee.object.name}.${methodName}` @@ -347,6 +355,35 @@ export function hoistMocks( `Cannot export the result of "${method}". Remove export declaration because "${method}" doesn\'t return anything.`, ) } + // rewrite vi.mock(import('..')) into vi.mock('..') + if ( + node.type === 'CallExpression' + && node.callee.type === 'MemberExpression' + && dynamicImportMockMethodNames.includes((node.callee.property as Identifier).name) + ) { + const moduleInfo = node.arguments[0] as Positioned + // vi.mock(import('./path')) -> vi.mock('./path') + if (moduleInfo.type === 'ImportExpression') { + const source = moduleInfo.source as Positioned + s.overwrite( + moduleInfo.start, + moduleInfo.end, + s.slice(source.start, source.end), + ) + } + // vi.mock(await import('./path')) -> vi.mock('./path') + if ( + moduleInfo.type === 'AwaitExpression' + && moduleInfo.argument.type === 'ImportExpression' + ) { + const source = moduleInfo.argument.source as Positioned + s.overwrite( + moduleInfo.start, + moduleInfo.end, + s.slice(source.start, source.end), + ) + } + } hoistedNodes.push(node) } // vi.doMock(import('./path')) -> vi.doMock('./path') @@ -394,9 +431,8 @@ export function hoistMocks( 'AwaitExpression', )?.node as Positioned | undefined // hoist "await vi.hoisted(async () => {})" or "vi.hoisted(() => {})" - hoistedNodes.push( - awaitedExpression?.argument === node ? awaitedExpression : node, - ) + const moveNode = awaitedExpression?.argument === node ? awaitedExpression : node + hoistedNodes.push(moveNode) } } } @@ -446,24 +482,6 @@ export function hoistMocks( ) } - function rewriteMockDynamicImport( - nodeCode: string, - moduleInfo: Positioned, - expressionStart: number, - expressionEnd: number, - mockStart: number, - ) { - const source = moduleInfo.source as Positioned - const importPath = s.slice(source.start, source.end) - const nodeCodeStart = expressionStart - mockStart - const nodeCodeEnd = expressionEnd - mockStart - return ( - nodeCode.slice(0, nodeCodeStart) - + importPath - + nodeCode.slice(nodeCodeEnd) - ) - } - // validate hoistedNodes doesn't have nodes inside other nodes for (let i = 0; i < hoistedNodes.length; i++) { const node = hoistedNodes[i] @@ -479,61 +497,55 @@ export function hoistMocks( } } - // Wait for imports to be hoisted and then hoist the mocks - const hoistedCode = hoistedNodes - .map((node) => { - const end = getBetterEnd(code, node) - /** - * In the following case, we need to change the `user` to user: __vi_import_x__.user - * So we should get the latest code from `s`. - * - * import user from './user' - * vi.mock('./mock.js', () => ({ getSession: vi.fn().mockImplementation(() => ({ user })) })) - */ - let nodeCode = s.slice(node.start, end) - - // rewrite vi.mock(import('..')) into vi.mock('..') - if ( - node.type === 'CallExpression' - && node.callee.type === 'MemberExpression' - && dynamicImportMockMethodNames.includes((node.callee.property as Identifier).name) - ) { - const moduleInfo = node.arguments[0] as Positioned - // vi.mock(import('./path')) -> vi.mock('./path') - if (moduleInfo.type === 'ImportExpression') { - nodeCode = rewriteMockDynamicImport( - nodeCode, - moduleInfo, - moduleInfo.start, - moduleInfo.end, - node.start, - ) - } - // vi.mock(await import('./path')) -> vi.mock('./path') - if ( - moduleInfo.type === 'AwaitExpression' - && moduleInfo.argument.type === 'ImportExpression' - ) { - nodeCode = rewriteMockDynamicImport( - nodeCode, - moduleInfo.argument as Positioned, - moduleInfo.start, - moduleInfo.end, - node.start, - ) - } - } + // hoist vi.mock/vi.hoisted + for (const node of hoistedNodes) { + const end = getNodeTail(code, node) + if (hoistIndex === end) { + hoistIndex = end + } + // don't hoist into itself if it's already at the top + else if (hoistIndex !== node.start) { + s.move(node.start, end, hoistIndex) + } + } - s.remove(node.start, end) - return `${nodeCode}${nodeCode.endsWith('\n') ? '' : '\n'}` - }) - .join('') + // hoist actual dynamic imports last so they are inserted after all hoisted mocks + for (const { node: importNode, id: importId } of imports) { + const source = importNode.source.value as string - if (hoistedCode || hoistedModuleImported) { - s.prepend( - (!hoistedModuleImported && hoistedCode ? API_NOT_FOUND_CHECK(utilsObjectNames) : '') - + hoistedCode, + s.update( + importNode.start, + importNode.end, + `const ${importId} = await import(${JSON.stringify( + source, + )});\n`, ) + + if (importNode.start === hoistIndex) { + // no need to hoist, but update hoistIndex to keep the order + hoistIndex = importNode.end + } + else { + // There will be an error if the module is called before it is imported, + // so the module import statement is hoisted to the top + s.move(importNode.start, importNode.end, hoistIndex) + } + } + + if (!hoistedModuleImported && hoistedNodes.length) { + const utilityImports = [...usedUtilityExports] + // "vi" or "vitest" is imported from a module other than "vitest" + if (utilityImports.some(name => idToImportMap.has(name))) { + s.prepend(API_NOT_FOUND_CHECK(utilityImports)) + } + // if "vi" or "vitest" are not imported at all, import them + else if (utilityImports.length) { + s.prepend( + `import { ${[...usedUtilityExports].join(', ')} } from ${JSON.stringify( + hoistedModule, + )}\n`, + ) + } } return { diff --git a/test/core/test/injector-mock.test.ts b/test/core/test/injector-mock.test.ts index 80f8b2d35b77..b0b9fd4ae4af 100644 --- a/test/core/test/injector-mock.test.ts +++ b/test/core/test/injector-mock.test.ts @@ -19,7 +19,7 @@ const hoistMocksOptions: HoistMocksPluginOptions = { }, } -async function hoistSimple(code: string, url = '') { +function hoistSimple(code: string, url = '') { return hoistMocks(code, url, parse, hoistMocksOptions) } @@ -33,7 +33,7 @@ test('hoists mock, unmock, hoisted', () => { vi.unmock('path') vi.hoisted(() => {}) `)).toMatchInlineSnapshot(` - "if (typeof globalThis["vi"] === "undefined" && typeof globalThis["vitest"] === "undefined") { throw new Error("There are some problems in resolving the mocks API.\\nYou may encounter this issue when importing the mocks API from another module other than 'vitest'.\\nTo fix this issue you can either:\\n- import the mocks API directly from 'vitest'\\n- enable the 'globals' options") } + "import { vi } from "vitest" vi.mock('path', () => {}) vi.unmock('path') vi.hoisted(() => {})" @@ -42,21 +42,18 @@ test('hoists mock, unmock, hoisted', () => { test('always hoists import from vitest', () => { expect(hoistSimpleCode(` - import { vi } from 'vitest' - vi.mock('path', () => {}) - vi.unmock('path') - vi.hoisted(() => {}) - import { test } from 'vitest' +import { vi } from 'vitest' +vi.mock('path', () => {}) +vi.unmock('path') +vi.hoisted(() => {}) +import { test } from 'vitest' `)).toMatchInlineSnapshot(` "vi.mock('path', () => {}) vi.unmock('path') vi.hoisted(() => {}) - import { vi } from 'vitest' - - - - import { test } from 'vitest'" + import { vi } from 'vitest' + import { test } from 'vitest'" `) }) @@ -73,16 +70,13 @@ test('always hoists all imports but they are under mocks', () => { "vi.mock('path', () => {}) vi.unmock('path') vi.hoisted(() => {}) - const __vi_import_0__ = await import('./path.js') - const __vi_import_1__ = await import('./path2.js') + const __vi_import_0__ = await import("./path.js"); + const __vi_import_1__ = await import("./path2.js"); import { vi } from 'vitest' - - - - import { test } from 'vitest'" + import { test } from 'vitest'" `) }) @@ -93,7 +87,7 @@ test('correctly mocks namespaced', () => { vi.mock('../src/add', () => {}) `)).toMatchInlineSnapshot(` "vi.mock('../src/add', () => {}) - const __vi_import_0__ = await import('../src/add') + const __vi_import_0__ = await import("../src/add"); import { vi } from 'vitest'" `) @@ -107,7 +101,7 @@ test('correctly access import', () => { vi.mock('../src/add', () => {}) `)).toMatchInlineSnapshot(` "vi.mock('../src/add', () => {}) - const __vi_import_0__ = await import('../src/add') + const __vi_import_0__ = await import("../src/add"); import { vi } from 'vitest' @@ -117,14 +111,14 @@ test('correctly access import', () => { describe('transform', () => { const hoistSimpleCodeWithoutMocks = (code: string) => { - return hoistMocks(`import {vi} from "vitest";\n${code}\nvi.mock('faker');`, '/test.js', parse, hoistMocksOptions)?.code.trim() + return hoistMocks(`import {vi} from "vitest";\n${code}\nvi.mock('faker');\n`, '/test.js', parse, hoistMocksOptions)?.code.trim() } - test('default import', async () => { + test('default import', () => { expect( hoistSimpleCodeWithoutMocks(`import foo from 'vue';console.log(foo.bar)`), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; console.log(__vi_import_0__.default.bar)" `) @@ -150,8 +144,8 @@ vi.mock('./mock.js', () => ({ admin: __vi_import_1__.admin, })) })) - const __vi_import_0__ = await import('./user') - const __vi_import_1__ = await import('./admin') + const __vi_import_0__ = await import("./user"); + const __vi_import_1__ = await import("./admin"); import { vi } from 'vitest'" `) @@ -190,34 +184,34 @@ vi.mock('./mock.js', () => { `) }) - test('named import', async () => { + test('named import', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `import { ref } from 'vue';function foo() { return ref(0) }`, ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; function foo() { return __vi_import_0__.ref(0) }" `) }) - test('namespace import', async () => { + test('namespace import', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `import * as vue from 'vue';function foo() { return vue.ref(0) }`, ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; function foo() { return __vi_import_0__.ref(0) }" `) }) - test('export function declaration', async () => { - expect(await hoistSimpleCodeWithoutMocks(`export function foo() {}`)) + test('export function declaration', () => { + expect(hoistSimpleCodeWithoutMocks(`export function foo() {}`)) .toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; @@ -225,8 +219,8 @@ vi.mock('./mock.js', () => { `) }) - test('export class declaration', async () => { - expect(await hoistSimpleCodeWithoutMocks(`export class foo {}`)) + test('export class declaration', () => { + expect(hoistSimpleCodeWithoutMocks(`export class foo {}`)) .toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; @@ -234,8 +228,8 @@ vi.mock('./mock.js', () => { `) }) - test('export var declaration', async () => { - expect(await hoistSimpleCodeWithoutMocks(`export const a = 1, b = 2`)) + test('export var declaration', () => { + expect(hoistSimpleCodeWithoutMocks(`export const a = 1, b = 2`)) .toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; @@ -243,9 +237,9 @@ vi.mock('./mock.js', () => { `) }) - test('export named', async () => { + test('export named', () => { expect( - await hoistSimpleCodeWithoutMocks(`const a = 1, b = 2; export { a, b as c }`), + hoistSimpleCodeWithoutMocks(`const a = 1, b = 2; export { a, b as c }`), ).toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; @@ -253,9 +247,9 @@ vi.mock('./mock.js', () => { `) }) - test('export named from', async () => { + test('export named from', () => { expect( - await hoistSimpleCodeWithoutMocks(`export { ref, computed as c } from 'vue'`), + hoistSimpleCodeWithoutMocks(`export { ref, computed as c } from 'vue'`), ).toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; @@ -263,22 +257,22 @@ vi.mock('./mock.js', () => { `) }) - test('named exports of imported binding', async () => { + test('named exports of imported binding', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `import {createApp} from 'vue';export {createApp}`, ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; export {createApp}" `) }) - test('export * from', async () => { + test('export * from', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `export * from 'vue'\n` + `export * from 'react'`, ), ).toMatchInlineSnapshot(` @@ -289,8 +283,8 @@ vi.mock('./mock.js', () => { `) }) - test('export * as from', async () => { - expect(await hoistSimpleCodeWithoutMocks(`export * as foo from 'vue'`)) + test('export * as from', () => { + expect(hoistSimpleCodeWithoutMocks(`export * as foo from 'vue'`)) .toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; @@ -298,9 +292,9 @@ vi.mock('./mock.js', () => { `) }) - test('export default', async () => { + test('export default', () => { expect( - await hoistSimpleCodeWithoutMocks(`export default {}`), + hoistSimpleCodeWithoutMocks(`export default {}`), ).toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; @@ -308,35 +302,35 @@ vi.mock('./mock.js', () => { `) }) - test('export then import minified', async () => { + test('export then import minified', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `export * from 'vue';import {createApp} from 'vue';`, ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; export * from 'vue';" `) }) - test('hoist import to top', async () => { + test('hoist import to top', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `path.resolve('server.js');import path from 'node:path';`, ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('node:path') + const __vi_import_0__ = await import("node:path"); import {vi} from "vitest"; __vi_import_0__.default.resolve('server.js');" `) }) - test('import.meta', async () => { + test('import.meta', () => { expect( - await hoistSimpleCodeWithoutMocks(`console.log(import.meta.url)`), + hoistSimpleCodeWithoutMocks(`console.log(import.meta.url)`), ).toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; @@ -344,8 +338,8 @@ vi.mock('./mock.js', () => { `) }) - test('dynamic import', async () => { - const result = await hoistSimpleCodeWithoutMocks( + test('dynamic import', () => { + const result = hoistSimpleCodeWithoutMocks( `export const i = () => import('./foo')`, ) expect(result).toMatchInlineSnapshot(` @@ -355,115 +349,115 @@ vi.mock('./mock.js', () => { `) }) - test('do not rewrite method definition', async () => { - const result = await hoistSimpleCodeWithoutMocks( + test('do not rewrite method definition', () => { + const result = hoistSimpleCodeWithoutMocks( `import { fn } from 'vue';class A { fn() { fn() } }`, ) expect(result).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; class A { fn() { __vi_import_0__.fn() } }" `) }) - test('do not rewrite when variable is in scope', async () => { - const result = await hoistSimpleCodeWithoutMocks( + test('do not rewrite when variable is in scope', () => { + const result = hoistSimpleCodeWithoutMocks( `import { fn } from 'vue';function A(){ const fn = () => {}; return { fn }; }`, ) expect(result).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; function A(){ const fn = () => {}; return { fn }; }" `) }) // #5472 - test('do not rewrite when variable is in scope with object destructuring', async () => { - const result = await hoistSimpleCodeWithoutMocks( + test('do not rewrite when variable is in scope with object destructuring', () => { + const result = hoistSimpleCodeWithoutMocks( `import { fn } from 'vue';function A(){ let {fn, test} = {fn: 'foo', test: 'bar'}; return { fn }; }`, ) expect(result).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; function A(){ let {fn, test} = {fn: 'foo', test: 'bar'}; return { fn }; }" `) }) // #5472 - test('do not rewrite when variable is in scope with array destructuring', async () => { - const result = await hoistSimpleCodeWithoutMocks( + test('do not rewrite when variable is in scope with array destructuring', () => { + const result = hoistSimpleCodeWithoutMocks( `import { fn } from 'vue';function A(){ let [fn, test] = ['foo', 'bar']; return { fn }; }`, ) expect(result).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; function A(){ let [fn, test] = ['foo', 'bar']; return { fn }; }" `) }) // #5727 - test('rewrite variable in string interpolation in function nested arguments', async () => { - const result = await hoistSimpleCodeWithoutMocks( + test('rewrite variable in string interpolation in function nested arguments', () => { + const result = hoistSimpleCodeWithoutMocks( `import { fn } from 'vue';function A({foo = \`test\${fn}\`} = {}){ return {}; }`, ) expect(result).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; function A({foo = \`test\${__vi_import_0__.fn}\`} = {}){ return {}; }" `) }) // #6520 - test('rewrite variables in default value of destructuring params', async () => { - const result = await hoistSimpleCodeWithoutMocks( + test('rewrite variables in default value of destructuring params', () => { + const result = hoistSimpleCodeWithoutMocks( `import { fn } from 'vue';function A({foo = fn}){ return {}; }`, ) expect(result).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; function A({foo = __vi_import_0__.fn}){ return {}; }" `) }) - test('do not rewrite when function declaration is in scope', async () => { - const result = await hoistSimpleCodeWithoutMocks( + test('do not rewrite when function declaration is in scope', () => { + const result = hoistSimpleCodeWithoutMocks( `import { fn } from 'vue';function A(){ function fn() {}; return { fn }; }`, ) expect(result).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; function A(){ function fn() {}; return { fn }; }" `) }) - test('do not rewrite catch clause', async () => { - const result = await hoistSimpleCodeWithoutMocks( + test('do not rewrite catch clause', () => { + const result = hoistSimpleCodeWithoutMocks( `import {error} from './dependency';try {} catch(error) {}`, ) expect(result).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('./dependency') + const __vi_import_0__ = await import("./dependency"); import {vi} from "vitest"; try {} catch(error) {}" `) }) // #2221 - test('should declare variable for imported super class', async () => { + test('should declare variable for imported super class', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `import { Foo } from './dependency';` + `class A extends Foo {}`, ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('./dependency') + const __vi_import_0__ = await import("./dependency"); import {vi} from "vitest"; const Foo = __vi_import_0__.Foo; class A extends Foo {}" @@ -472,14 +466,14 @@ vi.mock('./mock.js', () => { // exported classes: should prepend the declaration at root level, before the // first class that uses the binding expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `import { Foo } from './dependency';` + `export default class A extends Foo {}\n` + `export class B extends Foo {}`, ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('./dependency') + const __vi_import_0__ = await import("./dependency"); import {vi} from "vitest"; const Foo = __vi_import_0__.Foo; export default class A extends Foo {} @@ -488,16 +482,16 @@ vi.mock('./mock.js', () => { }) // #4049 - test('should handle default export variants', async () => { + test('should handle default export variants', () => { // default anonymous functions - expect(await hoistSimpleCodeWithoutMocks(`export default function() {}\n`)) + expect(hoistSimpleCodeWithoutMocks(`export default function() {}\n`)) .toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; export default function() {}" `) // default anonymous class - expect(await hoistSimpleCodeWithoutMocks(`export default class {}\n`)) + expect(hoistSimpleCodeWithoutMocks(`export default class {}\n`)) .toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; @@ -505,7 +499,7 @@ vi.mock('./mock.js', () => { `) // default named functions expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `export default function foo() {}\n` + `foo.prototype = Object.prototype;`, ), @@ -517,7 +511,7 @@ vi.mock('./mock.js', () => { `) // default named classes expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `export default class A {}\n` + `export class B extends A {}`, ), ).toMatchInlineSnapshot(` @@ -528,9 +522,9 @@ vi.mock('./mock.js', () => { `) }) - test('sourcemap source', async () => { + test('sourcemap source', () => { const map = ( - (await hoistSimple( + (hoistSimple( `vi.mock(any); export const a = 1`, 'input.js', @@ -539,9 +533,9 @@ vi.mock('./mock.js', () => { expect(map?.sources).toStrictEqual(['input.js']) }) - test('overwrite bindings', async () => { + test('overwrite bindings', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `import { inject } from 'vue';` + `const a = { inject }\n` + `const b = { test: inject }\n` @@ -553,7 +547,7 @@ vi.mock('./mock.js', () => { ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; const a = { inject: __vi_import_0__.inject } const b = { test: __vi_import_0__.inject } @@ -565,9 +559,9 @@ vi.mock('./mock.js', () => { `) }) - test('Empty array pattern', async () => { + test('Empty array pattern', () => { expect( - await hoistSimpleCodeWithoutMocks(`const [, LHS, RHS] = inMatch;`), + hoistSimpleCodeWithoutMocks(`const [, LHS, RHS] = inMatch;`), ).toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; @@ -575,9 +569,9 @@ vi.mock('./mock.js', () => { `) }) - test('function argument destructure', async () => { + test('function argument destructure', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( ` import { foo, bar } from 'foo' const a = ({ _ = foo() }) => {} @@ -587,7 +581,7 @@ function c({ _ = bar() + foo() }) {} ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('foo') + const __vi_import_0__ = await import("foo"); import {vi} from "vitest"; @@ -597,9 +591,9 @@ function c({ _ = bar() + foo() }) {} `) }) - test('object destructure alias', async () => { + test('object destructure alias', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( ` import { n } from 'foo' const a = () => { @@ -610,7 +604,7 @@ const a = () => { ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('foo') + const __vi_import_0__ = await import("foo"); import {vi} from "vitest"; @@ -622,7 +616,7 @@ const a = () => { // #9585 expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( ` import { n, m } from 'foo' const foo = {} @@ -634,7 +628,7 @@ const foo = {} ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('foo') + const __vi_import_0__ = await import("foo"); import {vi} from "vitest"; @@ -646,9 +640,9 @@ const foo = {} `) }) - test('nested object destructure alias', async () => { + test('nested object destructure alias', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( ` import { remove, add, get, set, rest, objRest } from 'vue' @@ -678,10 +672,11 @@ objRest() ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; + function a() { const { o: { remove }, @@ -707,9 +702,9 @@ objRest() `) }) - test('object props and methods', async () => { + test('object props and methods', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( ` import foo from 'foo' @@ -728,10 +723,11 @@ const obj = { ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('foo') + const __vi_import_0__ = await import("foo"); import {vi} from "vitest"; + const bar = 'bar' const obj = { @@ -746,9 +742,9 @@ const obj = { `) }) - test('class props', async () => { + test('class props', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( ` import { remove, add } from 'vue' @@ -760,10 +756,11 @@ class A { ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; + const add = __vi_import_0__.add; const remove = __vi_import_0__.remove; class A { @@ -773,9 +770,9 @@ class A { `) }) - test('class methods', async () => { + test('class methods', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( ` import foo from 'foo' @@ -792,10 +789,11 @@ class A { ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('foo') + const __vi_import_0__ = await import("foo"); import {vi} from "vitest"; + const bar = 'bar' class A { @@ -808,9 +806,9 @@ class A { `) }) - test('declare scope', async () => { + test('declare scope', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( ` import { aaa, bbb, ccc, ddd } from 'vue' @@ -838,10 +836,11 @@ bbb() ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; + function foobar() { ddd() @@ -865,9 +864,9 @@ bbb() `) }) - test('continuous exports', async () => { + test('continuous exports', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( ` export function fn1() { }export function fn2() { @@ -885,7 +884,7 @@ export function fn1() { }) // https://github.com/vitest-dev/vitest/issues/1141 - test('export default expression', async () => { + test('export default expression', () => { // esbuild transform result of following TS code // export default function getRandom() { // return Math.random() @@ -896,7 +895,7 @@ export default (function getRandom() { }); `.trim() - expect(await hoistSimpleCodeWithoutMocks(code)).toMatchInlineSnapshot(` + expect(hoistSimpleCodeWithoutMocks(code)).toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; export default (function getRandom() { @@ -905,7 +904,7 @@ export default (function getRandom() { `) expect( - await hoistSimpleCodeWithoutMocks(`export default (class A {});`), + hoistSimpleCodeWithoutMocks(`export default (class A {});`), ).toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; @@ -914,18 +913,18 @@ export default (function getRandom() { }) // #8002 - test('with hashbang', async () => { + test('with hashbang', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `#!/usr/bin/env node console.log("it can parse the hashbang")`, ), ).toMatchInlineSnapshot(`undefined`) }) - test('import hoisted after hashbang', async () => { + test('import hoisted after hashbang', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `#!/usr/bin/env node console.log(foo); import foo from "foo"`, @@ -934,7 +933,7 @@ import foo from "foo"`, }) // #10289 - test('track scope by class, function, condition blocks', async () => { + test('track scope by class, function, condition blocks', () => { const code = ` import { foo, bar } from 'foobar' if (false) { @@ -962,9 +961,9 @@ export class Test { } };`.trim() - expect(await hoistSimpleCodeWithoutMocks(code)).toMatchInlineSnapshot(` + expect(hoistSimpleCodeWithoutMocks(code)).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('foobar') + const __vi_import_0__ = await import("foobar"); import {vi} from "vitest"; if (false) { @@ -995,9 +994,9 @@ export class Test { }) // #10386 - test('track var scope by function', async () => { + test('track var scope by function', () => { expect( - await hoistSimpleCodeWithoutMocks(` + hoistSimpleCodeWithoutMocks(` import { foo, bar } from 'foobar' function test() { if (true) { @@ -1007,7 +1006,7 @@ function test() { }`), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('foobar') + const __vi_import_0__ = await import("foobar"); import {vi} from "vitest"; @@ -1021,9 +1020,9 @@ function test() { }) // #11806 - test('track scope by blocks', async () => { + test('track scope by blocks', () => { expect( - await hoistSimpleCodeWithoutMocks(` + hoistSimpleCodeWithoutMocks(` import { foo, bar, baz } from 'foobar' function test() { [foo]; @@ -1036,7 +1035,7 @@ function test() { }`), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('foobar') + const __vi_import_0__ = await import("foobar"); import {vi} from "vitest"; @@ -1052,9 +1051,9 @@ function test() { `) }) - test('track scope in for loops', async () => { + test('track scope in for loops', () => { expect( - await hoistSimpleCodeWithoutMocks(` + hoistSimpleCodeWithoutMocks(` import { test } from './test.js' for (const test of tests) { @@ -1070,10 +1069,11 @@ for (const test in tests) { }`), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('./test.js') + const __vi_import_0__ = await import("./test.js"); import {vi} from "vitest"; + for (const test of tests) { console.log(test) } @@ -1088,8 +1088,8 @@ for (const test in tests) { `) }) - test('avoid binding ClassExpression', async () => { - const result = await hoistSimpleCodeWithoutMocks( + test('avoid binding ClassExpression', () => { + const result = hoistSimpleCodeWithoutMocks( ` import Foo, { Bar } from './foo'; @@ -1103,10 +1103,11 @@ const Baz = class extends Foo {} ) expect(result).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('./foo') + const __vi_import_0__ = await import("./foo"); import {vi} from "vitest"; + console.log(__vi_import_0__.default, __vi_import_0__.Bar); const obj = { foo: class Foo {}, @@ -1116,15 +1117,15 @@ const Baz = class extends Foo {} `) }) - test('import assertion attribute', async () => { + test('import assertion attribute', () => { expect( - await hoistSimpleCodeWithoutMocks(` + hoistSimpleCodeWithoutMocks(` import * as foo from './foo.json' with { type: 'json' }; import('./bar.json', { with: { type: 'json' } }); `), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('./foo.json') + const __vi_import_0__ = await import("./foo.json"); import {vi} from "vitest"; @@ -1132,12 +1133,12 @@ const Baz = class extends Foo {} `) }) - test('import and export ordering', async () => { + test('import and export ordering', () => { // Given all imported modules logs `mod ${mod}` on execution, // and `foo` is `bar`, the logging order should be: // "mod a", "mod foo", "mod b", "bar1", "bar2" expect( - await hoistSimpleCodeWithoutMocks(` + hoistSimpleCodeWithoutMocks(` console.log(foo + 1) export * from './a' import { foo } from './foo' @@ -1146,7 +1147,7 @@ console.log(foo + 2) `), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('./foo') + const __vi_import_0__ = await import("./foo"); import {vi} from "vitest"; console.log(__vi_import_0__.foo + 1) @@ -1157,7 +1158,7 @@ console.log(foo + 2) `) }) - test('handle single "await vi.hoisted"', async () => { + test('handle single "await vi.hoisted"', () => { expect( hoistSimpleCode(` import { vi } from 'vitest'; @@ -1210,7 +1211,7 @@ await vi vi.mock(await import(\`./path\`), () => {}); `), ).toMatchInlineSnapshot(` - "if (typeof globalThis["vi"] === "undefined" && typeof globalThis["vitest"] === "undefined") { throw new Error("There are some problems in resolving the mocks API.\\nYou may encounter this issue when importing the mocks API from another module other than 'vitest'.\\nTo fix this issue you can either:\\n- import the mocks API directly from 'vitest'\\n- enable the 'globals' options") } + "import { vi } from "vitest" vi.mock('./path') vi.mock(somePath) vi.mock(\`./path\`) @@ -1271,6 +1272,58 @@ test('test', async () => { })" `) }) + + test('correctly hoists when import.meta is used', () => { + expect(hoistSimpleCode(` +import { calc } from './calc' +function sum(a, b) { + return calc("+", 1, 2); +} + +if (import.meta.vitest) { + const { vi, test, expect } = import.meta.vitest + vi.mock('faker') + test('sum', () => { + expect(sum(1, 2)).toBe(3) + }) +} + `)).toMatchInlineSnapshot(` + "import { vi } from "vitest" + vi.mock('faker') + const __vi_import_0__ = await import("./calc"); + + + function sum(a, b) { + return __vi_import_0__.calc("+", 1, 2); + } + + if (import.meta.vitest) { + const { vi, test, expect } = import.meta.vitest + test('sum', () => { + expect(sum(1, 2)).toBe(3) + }) + }" + `) + }) + + test('injects an error if a utility import is imported from an external module', () => { + expect(hoistSimpleCode(` +import { expect, test, vi } from './proxy-module' +vi.mock('vite') +test('hi', () => { + expect(1 + 1).toEqual(2) +}) + `)).toMatchInlineSnapshot(` + "if (typeof globalThis["vi"] === "undefined") { throw new Error("There are some problems in resolving the mocks API.\\nYou may encounter this issue when importing the mocks API from another module other than 'vitest'.\\nTo fix this issue you can either:\\n- import the mocks API directly from 'vitest'\\n- enable the 'globals' options") } + __vi_import_0__.vi.mock('vite') + const __vi_import_0__ = await import("./proxy-module"); + + + __vi_import_0__.test('hi', () => { + __vi_import_0__.expect(1 + 1).toEqual(2) + })" + `) + }) }) describe('throws an error when nodes are incompatible', () => { diff --git a/test/public-mocker/test/mocker.test.ts b/test/public-mocker/test/mocker.test.ts index 116c7838a5e1..a646d388f8e8 100644 --- a/test/public-mocker/test/mocker.test.ts +++ b/test/public-mocker/test/mocker.test.ts @@ -54,7 +54,7 @@ async function createTestServer(config: UserConfig) { globalThisAccessor: 'Symbol.for("vitest.mocker")', hoistMocks: { utilsObjectNames: ['mocker'], - hoistedModules: ['virtual:mocker'], + hoistedModule: 'virtual:mocker', hoistableMockMethodNames: ['customMock'], dynamicImportMockMethodNames: ['customMock'], hoistedMethodNames: ['customHoisted'], diff --git a/test/public-mocker/vite.config.ts b/test/public-mocker/vite.config.ts index 043a3e3221fd..7943f178b09e 100644 --- a/test/public-mocker/vite.config.ts +++ b/test/public-mocker/vite.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ globalThisAccessor: 'Symbol.for("vitest.mocker")', hoistMocks: { utilsObjectNames: ['mocker'], - hoistedModules: ['virtual:mocker'], + hoistedModule: 'virtual:mocker', hoistableMockMethodNames: ['customMock'], dynamicImportMockMethodNames: ['customMock'], hoistedMethodNames: ['customHoisted'], From 697c35c52de6db4d8da1c853104aacecd1337d1f Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 13 Nov 2024 17:03:12 +0100 Subject: [PATCH 13/18] feat: allow a custom note when calling `ctx.skip()` dynamically (#6805) --- packages/runner/src/context.ts | 4 +-- packages/runner/src/errors.ts | 2 +- packages/runner/src/run.ts | 3 +- packages/runner/src/types/tasks.ts | 4 ++- .../node/reporters/renderers/listRenderer.ts | 3 +- .../src/node/reporters/reported-tasks.ts | 27 +++++++++++++---- packages/vitest/src/node/reporters/verbose.ts | 3 ++ packages/vitest/src/node/state.ts | 1 + test/cli/fixtures/skip-note/basic.test.ts | 5 ++++ test/cli/test/skip-note.test.ts | 30 +++++++++++++++++++ 10 files changed, 71 insertions(+), 11 deletions(-) create mode 100644 test/cli/fixtures/skip-note/basic.test.ts create mode 100644 test/cli/test/skip-note.test.ts diff --git a/packages/runner/src/context.ts b/packages/runner/src/context.ts index d18f457b968f..2bde9eb4b479 100644 --- a/packages/runner/src/context.ts +++ b/packages/runner/src/context.ts @@ -67,9 +67,9 @@ export function createTestContext( context.task = test - context.skip = () => { + context.skip = (note?: string) => { test.pending = true - throw new PendingError('test is skipped; abort execution', test) + throw new PendingError('test is skipped; abort execution', test, note) } context.onTestFailed = (fn) => { diff --git a/packages/runner/src/errors.ts b/packages/runner/src/errors.ts index 3ce264122843..2d3090a7fcc4 100644 --- a/packages/runner/src/errors.ts +++ b/packages/runner/src/errors.ts @@ -4,7 +4,7 @@ export class PendingError extends Error { public code = 'VITEST_PENDING' public taskId: string - constructor(public message: string, task: TaskBase) { + constructor(public message: string, task: TaskBase, public note: string | undefined) { super(message) this.taskId = task.id } diff --git a/packages/runner/src/run.ts b/packages/runner/src/run.ts index 0d9cbeeb14b9..b7b03e34ac1d 100644 --- a/packages/runner/src/run.ts +++ b/packages/runner/src/run.ts @@ -257,7 +257,7 @@ export async function runTest(test: Test | Custom, runner: VitestRunner): Promis // skipped with new PendingError if (test.pending || test.result?.state === 'skip') { test.mode = 'skip' - test.result = { state: 'skip' } + test.result = { state: 'skip', note: test.result?.note } updateTask(test, runner) setCurrentTest(undefined) return @@ -336,6 +336,7 @@ export async function runTest(test: Test | Custom, runner: VitestRunner): Promis function failTask(result: TaskResult, err: unknown, diffOptions: DiffOptions | undefined) { if (err instanceof PendingError) { result.state = 'skip' + result.note = err.note return } diff --git a/packages/runner/src/types/tasks.ts b/packages/runner/src/types/tasks.ts index 66f09e80eaf5..5f1f9cc0a4b2 100644 --- a/packages/runner/src/types/tasks.ts +++ b/packages/runner/src/types/tasks.ts @@ -147,6 +147,8 @@ export interface TaskResult { * `repeats` option is set. This number also contains `retryCount`. */ repeatCount?: number + /** @private */ + note?: string } /** @@ -611,7 +613,7 @@ export interface TaskContext { * Mark tests as skipped. All execution after this call will be skipped. * This function throws an error, so make sure you are not catching it accidentally. */ - skip: () => void + skip: (note?: string) => void } export type ExtendedContext = TaskContext & diff --git a/packages/vitest/src/node/reporters/renderers/listRenderer.ts b/packages/vitest/src/node/reporters/renderers/listRenderer.ts index 0e62982caee3..aff7f042935f 100644 --- a/packages/vitest/src/node/reporters/renderers/listRenderer.ts +++ b/packages/vitest/src/node/reporters/renderers/listRenderer.ts @@ -144,7 +144,8 @@ function renderTree( } if (task.mode === 'skip' || task.mode === 'todo') { - suffix += ` ${c.dim(c.gray('[skipped]'))}` + const note = task.result?.note || 'skipped' + suffix += ` ${c.dim(c.gray(`[${note}]`))}` } if ( diff --git a/packages/vitest/src/node/reporters/reported-tasks.ts b/packages/vitest/src/node/reporters/reported-tasks.ts index aa0964bf6c32..9cc4c6b3cbd2 100644 --- a/packages/vitest/src/node/reporters/reported-tasks.ts +++ b/packages/vitest/src/node/reporters/reported-tasks.ts @@ -119,14 +119,27 @@ export class TestCase extends ReportedTaskImplementation { return undefined } const state = result.state === 'fail' - ? 'failed' + ? 'failed' as const : result.state === 'pass' - ? 'passed' - : 'skipped' + ? 'passed' as const + : 'skipped' as const + if (state === 'skipped') { + return { + state, + note: result.note, + errors: undefined, + } satisfies TestResultSkipped + } + if (state === 'passed') { + return { + state, + errors: result.errors as TestError[] | undefined, + } satisfies TestResultPassed + } return { state, - errors: result.errors as TestError[] | undefined, - } as TestResult + errors: (result.errors || []) as TestError[], + } satisfies TestResultFailed } /** @@ -441,6 +454,10 @@ export interface TestResultSkipped { * Skipped tests have no errors. */ errors: undefined + /** + * A custom note. + */ + note: string | undefined } export interface TestDiagnostic { diff --git a/packages/vitest/src/node/reporters/verbose.ts b/packages/vitest/src/node/reporters/verbose.ts index dac91a52a72a..bb3bbceb5e83 100644 --- a/packages/vitest/src/node/reporters/verbose.ts +++ b/packages/vitest/src/node/reporters/verbose.ts @@ -43,6 +43,9 @@ export class VerboseReporter extends DefaultReporter { ` ${Math.floor(task.result.heap / 1024 / 1024)} MB heap used`, ) } + if (task.result?.note) { + title += c.dim(c.gray(` [${task.result.note}]`)) + } this.ctx.logger.log(title) if (task.result.state === 'fail') { task.result.errors?.forEach((error) => { diff --git a/packages/vitest/src/node/state.ts b/packages/vitest/src/node/state.ts index ec0f22158149..efeef50f87d7 100644 --- a/packages/vitest/src/node/state.ts +++ b/packages/vitest/src/node/state.ts @@ -40,6 +40,7 @@ export class StateManager { task.mode = 'skip' task.result ??= { state: 'skip' } task.result.state = 'skip' + task.result.note = _err.note } return } diff --git a/test/cli/fixtures/skip-note/basic.test.ts b/test/cli/fixtures/skip-note/basic.test.ts new file mode 100644 index 000000000000..89a5b057dfd7 --- /dev/null +++ b/test/cli/fixtures/skip-note/basic.test.ts @@ -0,0 +1,5 @@ +import { test } from 'vitest'; + +test('my skipped test', ctx => { + ctx.skip('custom message') +}) diff --git a/test/cli/test/skip-note.test.ts b/test/cli/test/skip-note.test.ts new file mode 100644 index 000000000000..2a5ca30dd82c --- /dev/null +++ b/test/cli/test/skip-note.test.ts @@ -0,0 +1,30 @@ +import type { TestCase } from 'vitest/node' +import { resolve } from 'node:path' +import { expect, test } from 'vitest' +import { runVitest } from '../../test-utils' + +const root = resolve(import.meta.dirname, '../fixtures/skip-note') + +test.for([ + { reporter: 'default', isTTY: true }, + { reporter: 'verbose', isTTY: false }, +])('can leave a note when skipping in the $reporter reporter', async ({ reporter, isTTY }) => { + const { ctx, stdout, stderr } = await runVitest({ + root, + reporters: [ + [reporter, { isTTY }], + ], + }) + + expect(stderr).toBe('') + expect(stdout).toContain('my skipped test [custom message]') + + expect(ctx).toBeDefined() + const testTask = ctx!.state.getFiles()[0].tasks[0] + const test = ctx!.state.getReportedEntity(testTask) as TestCase + const result = test.result() + expect(result).toEqual({ + state: 'skipped', + note: 'custom message', + }) +}) From 746d898651fc30aaa0d2a47af9ceeaaaa0432311 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 14 Nov 2024 01:04:34 +0900 Subject: [PATCH 14/18] feat(snapshot): provide `config` to `resolveSnapshotPath` (#6800) --- docs/config/index.md | 2 +- packages/browser/src/node/rpc.ts | 6 ++++-- packages/snapshot/src/manager.ts | 4 ++-- packages/snapshot/src/types/index.ts | 2 +- packages/vitest/src/node/pools/rpc.ts | 5 ++++- packages/vitest/src/node/types/config.ts | 11 ++++++++++- packages/vitest/src/public/node.ts | 2 ++ .../__snapshots__/project1/basic.test.ts.snap | 3 +++ .../__snapshots__/project2/basic.test.ts.snap | 3 +++ .../snapshot-path-context/basic.test.ts | 5 +++++ .../snapshot-path-context/vitest.config.ts | 15 +++++++++++++++ .../snapshot-path-context/vitest.workspace.ts | 18 ++++++++++++++++++ test/config/test/snapshot.test.ts | 19 +++++++++++++++++++ 13 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 test/config/fixtures/snapshot-path-context/__snapshots__/project1/basic.test.ts.snap create mode 100644 test/config/fixtures/snapshot-path-context/__snapshots__/project2/basic.test.ts.snap create mode 100644 test/config/fixtures/snapshot-path-context/basic.test.ts create mode 100644 test/config/fixtures/snapshot-path-context/vitest.config.ts create mode 100644 test/config/fixtures/snapshot-path-context/vitest.workspace.ts create mode 100644 test/config/test/snapshot.test.ts diff --git a/docs/config/index.md b/docs/config/index.md index 7a441d0687f0..0bab567ee59f 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -1899,7 +1899,7 @@ A list of paths to snapshot serializer modules for snapshot testing, useful if y ### resolveSnapshotPath -- **Type**: `(testPath: string, snapExtension: string) => string` +- **Type**: `(testPath: string, snapExtension: string, context: { config: SerializedConfig }) => string` - **Default**: stores snapshot files in `__snapshots__` directory Overrides default snapshot path. For example, to store snapshots next to test files: diff --git a/packages/browser/src/node/rpc.ts b/packages/browser/src/node/rpc.ts index 6d37821be88e..0147438cddb9 100644 --- a/packages/browser/src/node/rpc.ts +++ b/packages/browser/src/node/rpc.ts @@ -1,5 +1,5 @@ import type { ErrorWithDiff } from 'vitest' -import type { BrowserCommandContext } from 'vitest/node' +import type { BrowserCommandContext, ResolveSnapshotPathHandlerContext } from 'vitest/node' import type { WebSocket } from 'ws' import type { BrowserServer } from './server' import type { WebSocketBrowserEvents, WebSocketBrowserHandlers } from './types' @@ -90,7 +90,9 @@ export function setupBrowserRpc(server: BrowserServer) { return ctx.report('onUserConsoleLog', log) }, resolveSnapshotPath(testPath) { - return ctx.snapshot.resolvePath(testPath) + return ctx.snapshot.resolvePath(testPath, { + config: project.getSerializableConfig(), + }) }, resolveSnapshotRawPath(testPath, rawPath) { return ctx.snapshot.resolveRawPath(testPath, rawPath) diff --git a/packages/snapshot/src/manager.ts b/packages/snapshot/src/manager.ts index 5a4caa0029af..11ed69847aab 100644 --- a/packages/snapshot/src/manager.ts +++ b/packages/snapshot/src/manager.ts @@ -23,7 +23,7 @@ export class SnapshotManager { addSnapshotResult(this.summary, result) } - resolvePath(testPath: string): string { + resolvePath(testPath: string, context?: T): string { const resolver = this.options.resolveSnapshotPath || (() => { return join( @@ -32,7 +32,7 @@ export class SnapshotManager { ) }) - const path = resolver(testPath, this.extension) + const path = resolver(testPath, this.extension, context) return path } diff --git a/packages/snapshot/src/types/index.ts b/packages/snapshot/src/types/index.ts index d1bf8af4fac8..b5378e5a8cb6 100644 --- a/packages/snapshot/src/types/index.ts +++ b/packages/snapshot/src/types/index.ts @@ -20,7 +20,7 @@ export interface SnapshotStateOptions { snapshotEnvironment: SnapshotEnvironment expand?: boolean snapshotFormat?: PrettyFormatOptions - resolveSnapshotPath?: (path: string, extension: string) => string + resolveSnapshotPath?: (path: string, extension: string, context?: any) => string } export interface SnapshotMatchOptions { diff --git a/packages/vitest/src/node/pools/rpc.ts b/packages/vitest/src/node/pools/rpc.ts index 9018c2b9a6bd..28b65a4bcd97 100644 --- a/packages/vitest/src/node/pools/rpc.ts +++ b/packages/vitest/src/node/pools/rpc.ts @@ -1,5 +1,6 @@ import type { RawSourceMap } from 'vite-node' import type { RuntimeRPC } from '../../types/rpc' +import type { ResolveSnapshotPathHandlerContext } from '../types/config' import type { WorkspaceProject } from '../workspace' import { mkdir, writeFile } from 'node:fs/promises' import { join } from 'pathe' @@ -20,7 +21,9 @@ export function createMethodsRPC(project: WorkspaceProject, options: MethodsOpti ctx.snapshot.add(snapshot) }, resolveSnapshotPath(testPath: string) { - return ctx.snapshot.resolvePath(testPath) + return ctx.snapshot.resolvePath(testPath, { + config: project.getSerializableConfig(), + }) }, async getSourceMap(id, force) { if (force) { diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts index 1eae8e3c1bb8..fe600405ddc1 100644 --- a/packages/vitest/src/node/types/config.ts +++ b/packages/vitest/src/node/types/config.ts @@ -6,6 +6,7 @@ import type { SerializedDiffOptions } from '@vitest/utils/diff' import type { AliasOptions, ConfigEnv, DepOptimizationConfig, ServerOptions, UserConfig as ViteUserConfig } from 'vite' import type { ViteNodeServerOptions } from 'vite-node' import type { ChaiConfig } from '../../integrations/chai/config' +import type { SerializedConfig } from '../../runtime/config' import type { EnvironmentOptions } from '../../types/environment' import type { Arrayable, ErrorWithDiff, ParsedStack, ProvidedContext } from '../../types/general' import type { HappyDOMOptions } from '../../types/happy-dom-options' @@ -225,6 +226,14 @@ type ReporterWithOptions = : [Name, Partial] : [Name, Record] +export interface ResolveSnapshotPathHandlerContext { config: SerializedConfig } + +export type ResolveSnapshotPathHandler = ( + testPath: string, + snapExtension: string, + context: ResolveSnapshotPathHandlerContext +) => string + export interface InlineConfig { /** * Name of the project. Will be used to display in the reporter. @@ -574,7 +583,7 @@ export interface InlineConfig { /** * Resolve custom snapshot path */ - resolveSnapshotPath?: (path: string, extension: string) => string + resolveSnapshotPath?: ResolveSnapshotPathHandler /** * Path to a custom snapshot environment module that has a default export of `SnapshotEnvironment` object. diff --git a/packages/vitest/src/public/node.ts b/packages/vitest/src/public/node.ts index e46a3911134e..c8ba9ef7679f 100644 --- a/packages/vitest/src/public/node.ts +++ b/packages/vitest/src/public/node.ts @@ -79,6 +79,8 @@ export type { ProjectConfig, ResolvedConfig, ResolvedProjectConfig, + ResolveSnapshotPathHandler, + ResolveSnapshotPathHandlerContext, RuntimeConfig, SequenceHooks, SequenceSetupFiles, diff --git a/test/config/fixtures/snapshot-path-context/__snapshots__/project1/basic.test.ts.snap b/test/config/fixtures/snapshot-path-context/__snapshots__/project1/basic.test.ts.snap new file mode 100644 index 000000000000..ec0254df2305 --- /dev/null +++ b/test/config/fixtures/snapshot-path-context/__snapshots__/project1/basic.test.ts.snap @@ -0,0 +1,3 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`basic 1`] = `"hello"`; diff --git a/test/config/fixtures/snapshot-path-context/__snapshots__/project2/basic.test.ts.snap b/test/config/fixtures/snapshot-path-context/__snapshots__/project2/basic.test.ts.snap new file mode 100644 index 000000000000..ec0254df2305 --- /dev/null +++ b/test/config/fixtures/snapshot-path-context/__snapshots__/project2/basic.test.ts.snap @@ -0,0 +1,3 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`basic 1`] = `"hello"`; diff --git a/test/config/fixtures/snapshot-path-context/basic.test.ts b/test/config/fixtures/snapshot-path-context/basic.test.ts new file mode 100644 index 000000000000..4cb90a2abcc7 --- /dev/null +++ b/test/config/fixtures/snapshot-path-context/basic.test.ts @@ -0,0 +1,5 @@ +import { test, expect } from 'vitest' + +test('basic', () => { + expect('hello').toMatchSnapshot() +}) diff --git a/test/config/fixtures/snapshot-path-context/vitest.config.ts b/test/config/fixtures/snapshot-path-context/vitest.config.ts new file mode 100644 index 000000000000..a03d2b41cc56 --- /dev/null +++ b/test/config/fixtures/snapshot-path-context/vitest.config.ts @@ -0,0 +1,15 @@ +import { join, dirname, basename } from 'node:path'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + resolveSnapshotPath(path, extension, context) { + return join( + dirname(path), + '__snapshots__', + context.config.name ?? 'na', + basename(path) + extension + ); + }, + }, +}); diff --git a/test/config/fixtures/snapshot-path-context/vitest.workspace.ts b/test/config/fixtures/snapshot-path-context/vitest.workspace.ts new file mode 100644 index 000000000000..30764821b1e0 --- /dev/null +++ b/test/config/fixtures/snapshot-path-context/vitest.workspace.ts @@ -0,0 +1,18 @@ +import { defineWorkspace } from 'vitest/config' + +export default defineWorkspace([ + { + extends: './vitest.config.ts', + test: { + name: 'project1', + root: import.meta.dirname, + } + }, + { + extends: './vitest.config.ts', + test: { + name: 'project2', + root: import.meta.dirname, + } + } +]) diff --git a/test/config/test/snapshot.test.ts b/test/config/test/snapshot.test.ts new file mode 100644 index 000000000000..9e890cc40062 --- /dev/null +++ b/test/config/test/snapshot.test.ts @@ -0,0 +1,19 @@ +import { expect, test } from 'vitest' +import { runVitest } from '../../test-utils' + +test('resolveSnapshotPath context', async () => { + const { stderr, ctx } = await runVitest({ + root: './fixtures/snapshot-path-context', + }) + expect(stderr).toBe('') + expect( + Object.fromEntries( + ctx!.state.getFiles().map(f => [`${f.projectName}|${f.name}`, f.result?.state]), + ), + ).toMatchInlineSnapshot(` + { + "project1|basic.test.ts": "pass", + "project2|basic.test.ts": "pass", + } + `) +}) From 82f2b50863045e9884dc082a68730178970edc7a Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 13 Nov 2024 17:20:11 +0100 Subject: [PATCH 15/18] test: don't start the server in watch mode --- test/cli/test/browser-multiple.test.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/test/cli/test/browser-multiple.test.ts b/test/cli/test/browser-multiple.test.ts index 3da5e1bf439e..a98eb9f776f5 100644 --- a/test/cli/test/browser-multiple.test.ts +++ b/test/cli/test/browser-multiple.test.ts @@ -1,6 +1,7 @@ +import type { Vitest } from 'vitest/node' import { resolve } from 'pathe' -import { expect, it, onTestFinished, vi } from 'vitest' +import { expect, it, onTestFinished, vi } from 'vitest' import { runVitest } from '../../test-utils' it('automatically assigns the port', async () => { @@ -8,13 +9,24 @@ it('automatically assigns the port', async () => { const workspace = resolve(import.meta.dirname, '../fixtures/browser-multiple/vitest.workspace.ts') const spy = vi.spyOn(console, 'log') onTestFinished(() => spy.mockRestore()) - const { stderr, ctx } = await runVitest({ + let ctx: Vitest + let urls: (string | undefined)[] = [] + const { stderr } = await runVitest({ root, workspace, dir: root, - watch: true, + watch: false, + reporters: [ + { + onInit(ctx_) { + ctx = ctx_ + }, + onFinished() { + urls = ctx.projects.map(p => p.browser?.vite.resolvedUrls?.local[0]) + }, + }, + ], }) - const urls = ctx?.projects.map(p => p.browser?.vite.resolvedUrls?.local[0]) expect(spy).not.toHaveBeenCalled() expect(stderr).not.toContain('is in use, trying another one...') From 85c64e35b120f240e66d5fb9ca67fa55ed8eb74d Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 13 Nov 2024 17:20:26 +0100 Subject: [PATCH 16/18] feat(runner): test context can inject values from the config's `provide` (#6813) --- docs/guide/test-context.md | 56 ++++++++++++++++++++- packages/runner/src/fixture.ts | 14 ++++-- packages/runner/src/suite.ts | 6 ++- packages/runner/src/types/runner.ts | 4 ++ packages/vitest/src/runtime/runners/test.ts | 7 +++ test/workspaces/globalTest.ts | 6 ++- test/workspaces/space_shared/test.spec.ts | 13 +++++ test/workspaces/vitest.workspace.ts | 3 ++ 8 files changed, 101 insertions(+), 8 deletions(-) diff --git a/docs/guide/test-context.md b/docs/guide/test-context.md index ea5f483dc78d..9d0473107425 100644 --- a/docs/guide/test-context.md +++ b/docs/guide/test-context.md @@ -176,9 +176,61 @@ const test = base.extend({ ], }) -test('', () => {}) +test('works correctly') ``` +#### Default fixture + +Since Vitest 2.2, you can provide different values in different [projects](/guide/workspace). To enable this feature, pass down `{ injected: true }` to the options. If the key is not specified in the [project configuration](/config/#provide), then the default value will be used. + +:::code-group +```ts [fixtures.test.ts] +import { test as base } from 'vitest' + +const test = base.extend({ + url: [ + // default value if "url" is not defined in the config + 'default', + // mark the fixure as "injected" to allow the override + { injected: true }, + ], +}) + +test('works correctly', ({ url }) => { + // url is "/default" in "project-new" + // url is "/full" in "project-full" + // url is "/empty" in "project-empty" +}) +``` +```ts [vitest.workspace.ts] +import { defineWorkspace } from 'vitest/config' + +export default defineWorkspace([ + { + test: { + name: 'project-new', + }, + }, + { + test: { + name: 'project-full', + provide: { + url: '/full', + }, + }, + }, + { + test: { + name: 'project-empty', + provide: { + url: '/empty', + }, + }, + }, +]) +``` +::: + #### TypeScript To provide fixture types for all your custom contexts, you can pass the fixtures type as a generic. @@ -194,7 +246,7 @@ const myTest = test.extend({ archive: [] }) -myTest('', (context) => { +myTest('types are defined correctly', (context) => { expectTypeOf(context.todos).toEqualTypeOf() expectTypeOf(context.archive).toEqualTypeOf() }) diff --git a/packages/runner/src/fixture.ts b/packages/runner/src/fixture.ts index 59ea15a021fa..cd073ec911e3 100644 --- a/packages/runner/src/fixture.ts +++ b/packages/runner/src/fixture.ts @@ -5,6 +5,10 @@ import { getFixture } from './map' export interface FixtureItem extends FixtureOptions { prop: string value: any + /** + * Indicated if the injected value should be preferred over the fixture value + */ + injected?: boolean /** * Indicates whether the fixture is a function */ @@ -17,11 +21,12 @@ export interface FixtureItem extends FixtureOptions { export function mergeContextFixtures( fixtures: Record, - context: { fixtures?: FixtureItem[] } = {}, + context: { fixtures?: FixtureItem[] }, + inject: (key: string) => unknown, ): { fixtures?: FixtureItem[] } { - const fixtureOptionKeys = ['auto'] + const fixtureOptionKeys = ['auto', 'injected'] const fixtureArray: FixtureItem[] = Object.entries(fixtures).map( ([prop, value]) => { const fixtureItem = { value } as FixtureItem @@ -34,7 +39,10 @@ export function mergeContextFixtures( ) { // fixture with options Object.assign(fixtureItem, value[1]) - fixtureItem.value = value[0] + const userValue = value[0] + fixtureItem.value = fixtureItem.injected + ? (inject(prop) ?? userValue) + : userValue } fixtureItem.prop = prop diff --git a/packages/runner/src/suite.ts b/packages/runner/src/suite.ts index 7f69844a7407..d3c972794072 100644 --- a/packages/runner/src/suite.ts +++ b/packages/runner/src/suite.ts @@ -710,7 +710,11 @@ export function createTaskCollector( } taskFn.extend = function (fixtures: Fixtures>) { - const _context = mergeContextFixtures(fixtures, context) + const _context = mergeContextFixtures( + fixtures, + context || {}, + (key: string) => getRunner().injectValue?.(key), + ) return createTest(function fn( name: string | Function, diff --git a/packages/runner/src/types/runner.ts b/packages/runner/src/types/runner.ts index 76a29419d3bd..6e5e1dbd7d4c 100644 --- a/packages/runner/src/types/runner.ts +++ b/packages/runner/src/types/runner.ts @@ -147,6 +147,10 @@ export interface VitestRunner { * Called when test and setup files are imported. Can be called in two situations: when collecting tests and when importing 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. */ diff --git a/packages/vitest/src/runtime/runners/test.ts b/packages/vitest/src/runtime/runners/test.ts index 642fd526c8f2..952d27389981 100644 --- a/packages/vitest/src/runtime/runners/test.ts +++ b/packages/vitest/src/runtime/runners/test.ts @@ -16,6 +16,7 @@ import type { VitestExecutor } from '../execute' import { getState, GLOBAL_EXPECT, setState } from '@vitest/expect' import { getNames, getTestName, getTests } from '@vitest/runner/utils' import { createExpect } from '../../integrations/chai/index' +import { inject } from '../../integrations/inject' import { getSnapshotClient } from '../../integrations/snapshot/chai' import { vi } from '../../integrations/vi' import { rpc } from '../rpc' @@ -89,6 +90,12 @@ export class VitestTestRunner implements VitestRunner { this.cancelRun = true } + injectValue(key: string) { + // inject has a very limiting type controlled by ProvidedContext + // some tests override it which causes the build to fail + return (inject as any)(key) + } + async onBeforeRunTask(test: Task) { if (this.cancelRun) { test.mode = 'skip' diff --git a/test/workspaces/globalTest.ts b/test/workspaces/globalTest.ts index 0069891a748d..938d8dfb4f89 100644 --- a/test/workspaces/globalTest.ts +++ b/test/workspaces/globalTest.ts @@ -9,6 +9,8 @@ declare module 'vitest' { invalidValue: unknown projectConfigValue: boolean globalConfigValue: boolean + + providedConfigValue: string } } @@ -35,8 +37,8 @@ export async function teardown() { try { assert.ok(results.success) assert.equal(results.numTotalTestSuites, 28) - assert.equal(results.numTotalTests, 31) - assert.equal(results.numPassedTests, 31) + assert.equal(results.numTotalTests, 33) + assert.equal(results.numPassedTests, 33) assert.ok(results.coverageMap) const shared = results.testResults.filter((r: any) => r.name.includes('space_shared/test.spec.ts')) diff --git a/test/workspaces/space_shared/test.spec.ts b/test/workspaces/space_shared/test.spec.ts index c152931a1345..cbff4df5c2bf 100644 --- a/test/workspaces/space_shared/test.spec.ts +++ b/test/workspaces/space_shared/test.spec.ts @@ -4,6 +4,19 @@ declare global { const testValue: string } +const custom = it.extend({ + providedConfigValue: ['default value', { injected: true }], +}) + +custom('provided config value is injected', ({ providedConfigValue }) => { + expect(providedConfigValue).toBe( + // happy-dom provides the value in the workspace config + expect.getState().environment === 'node' + ? 'default value' + : 'actual config value', + ) +}) + it('the same file works with different projects', () => { expect(testValue).toBe(expect.getState().environment === 'node' ? 'node' : 'jsdom') }) diff --git a/test/workspaces/vitest.workspace.ts b/test/workspaces/vitest.workspace.ts index 49223e68f2bb..077de9814b7a 100644 --- a/test/workspaces/vitest.workspace.ts +++ b/test/workspaces/vitest.workspace.ts @@ -13,6 +13,9 @@ export default defineWorkspace([ root: './space_shared', environment: 'happy-dom', setupFiles: ['./setup.jsdom.ts'], + provide: { + providedConfigValue: 'actual config value', + }, }, }), Promise.resolve({ From 85e6f99f0719d9b5f9c37fe8b2497ca7d8260a6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Est=C3=A9ban?= Date: Wed, 13 Nov 2024 18:01:45 +0100 Subject: [PATCH 17/18] feat(expect): add `toHaveBeenCalledAfter` and `toHaveBeenCalledBefore` utility (#6056) Co-authored-by: Vladimir Sheremet --- docs/api/expect.md | 38 ++++++++ packages/expect/src/jest-expect.ts | 68 +++++++++++++ packages/expect/src/types.ts | 33 +++++++ pnpm-lock.yaml | 24 ++--- test/core/test/jest-expect.test.ts | 152 +++++++++++++++++++++++++++++ 5 files changed, 303 insertions(+), 12 deletions(-) diff --git a/docs/api/expect.md b/docs/api/expect.md index a511cbb36ee5..c4bbbd2e016c 100644 --- a/docs/api/expect.md +++ b/docs/api/expect.md @@ -876,6 +876,44 @@ test('spy function', () => { }) ``` +## toHaveBeenCalledBefore 2.2.0 {#tohavebeencalledbefore} + +- **Type**: `(mock: MockInstance, failIfNoFirstInvocation?: boolean) => Awaitable` + +This assertion checks if a `Mock` was called before another `Mock`. + +```ts +test('calls mock1 before mock2', () => { + const mock1 = vi.fn() + const mock2 = vi.fn() + + mock1() + mock2() + mock1() + + expect(mock1).toHaveBeenCalledBefore(mock2) +}) +``` + +## toHaveBeenCalledAfter 2.2.0 {#tohavebeencalledafter} + +- **Type**: `(mock: MockInstance, failIfNoFirstInvocation?: boolean) => Awaitable` + +This assertion checks if a `Mock` was called after another `Mock`. + +```ts +test('calls mock1 after mock2', () => { + const mock1 = vi.fn() + const mock2 = vi.fn() + + mock2() + mock1() + mock2() + + expect(mock1).toHaveBeenCalledAfter(mock2) +}) +``` + ## toHaveBeenCalledExactlyOnceWith 2.2.0 {#tohavebeencalledexactlyoncewith} - **Type**: `(...args: any[]) => Awaitable` diff --git a/packages/expect/src/jest-expect.ts b/packages/expect/src/jest-expect.ts index 6616e6f9d23a..c15f277edd9e 100644 --- a/packages/expect/src/jest-expect.ts +++ b/packages/expect/src/jest-expect.ts @@ -656,6 +656,74 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { ) }, ) + + /** + * Used for `toHaveBeenCalledBefore` and `toHaveBeenCalledAfter` to determine if the expected spy was called before the result spy. + */ + function isSpyCalledBeforeAnotherSpy(beforeSpy: MockInstance, afterSpy: MockInstance, failIfNoFirstInvocation: number): boolean { + const beforeInvocationCallOrder = beforeSpy.mock.invocationCallOrder + + const afterInvocationCallOrder = afterSpy.mock.invocationCallOrder + + if (beforeInvocationCallOrder.length === 0) { + return !failIfNoFirstInvocation + } + + if (afterInvocationCallOrder.length === 0) { + return false + } + + return beforeInvocationCallOrder[0] < afterInvocationCallOrder[0] + } + + def( + ['toHaveBeenCalledBefore'], + function (resultSpy: MockInstance, failIfNoFirstInvocation = true) { + const expectSpy = getSpy(this) + + if (!isMockFunction(resultSpy)) { + throw new TypeError( + `${utils.inspect(resultSpy)} is not a spy or a call to a spy`, + ) + } + + this.assert( + isSpyCalledBeforeAnotherSpy( + expectSpy, + resultSpy, + failIfNoFirstInvocation, + ), + `expected "${expectSpy.getMockName()}" to have been called before "${resultSpy.getMockName()}"`, + `expected "${expectSpy.getMockName()}" to not have been called before "${resultSpy.getMockName()}"`, + resultSpy, + expectSpy, + ) + }, + ) + def( + ['toHaveBeenCalledAfter'], + function (resultSpy: MockInstance, failIfNoFirstInvocation = true) { + const expectSpy = getSpy(this) + + if (!isMockFunction(resultSpy)) { + throw new TypeError( + `${utils.inspect(resultSpy)} is not a spy or a call to a spy`, + ) + } + + this.assert( + isSpyCalledBeforeAnotherSpy( + resultSpy, + expectSpy, + failIfNoFirstInvocation, + ), + `expected "${expectSpy.getMockName()}" to have been called after "${resultSpy.getMockName()}"`, + `expected "${expectSpy.getMockName()}" to not have been called after "${resultSpy.getMockName()}"`, + resultSpy, + expectSpy, + ) + }, + ) def( ['toThrow', 'toThrowError'], function (expected?: string | Constructable | RegExp | Error) { diff --git a/packages/expect/src/types.ts b/packages/expect/src/types.ts index e53b059e555e..5e1a76a897bd 100644 --- a/packages/expect/src/types.ts +++ b/packages/expect/src/types.ts @@ -6,6 +6,7 @@ * */ +import type { MockInstance } from '@vitest/spy' import type { Constructable } from '@vitest/utils' import type { Formatter } from 'tinyrainbow' import type { diff, getMatcherUtils, stringify } from './jest-matcher-utils' @@ -655,6 +656,38 @@ export interface Assertion */ toSatisfy: (matcher: (value: E) => boolean, message?: string) => void + /** + * This assertion checks if a `Mock` was called before another `Mock`. + * @param mock - A mock function created by `vi.spyOn` or `vi.fn` + * @param failIfNoFirstInvocation - Fail if the first mock was never called + * @example + * const mock1 = vi.fn() + * const mock2 = vi.fn() + * + * mock1() + * mock2() + * mock1() + * + * expect(mock1).toHaveBeenCalledBefore(mock2) + */ + toHaveBeenCalledBefore: (mock: MockInstance, failIfNoFirstInvocation?: boolean) => void + + /** + * This assertion checks if a `Mock` was called after another `Mock`. + * @param mock - A mock function created by `vi.spyOn` or `vi.fn` + * @param failIfNoFirstInvocation - Fail if the first mock was never called + * @example + * const mock1 = vi.fn() + * const mock2 = vi.fn() + * + * mock2() + * mock1() + * mock2() + * + * expect(mock1).toHaveBeenCalledAfter(mock2) + */ + toHaveBeenCalledAfter: (mock: MockInstance, failIfNoFirstInvocation?: boolean) => void + /** * Checks that a promise resolves successfully at least once. * diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e4a2cedd5393..5cbc1225559a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6007,8 +6007,8 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-east-asian-width@1.2.0: - resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} + get-east-asian-width@1.3.0: + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} engines: {node: '>=18'} get-intrinsic@1.2.4: @@ -6144,8 +6144,8 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - graphql@16.8.1: - resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==} + graphql@16.9.0: + resolution: {integrity: sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} gtoken@7.0.1: @@ -8393,8 +8393,8 @@ packages: streamx@2.15.1: resolution: {integrity: sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==} - streamx@2.18.0: - resolution: {integrity: sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==} + streamx@2.20.1: + resolution: {integrity: sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==} strict-event-emitter@0.5.1: resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} @@ -13079,7 +13079,7 @@ snapshots: bare-stream@2.1.3: dependencies: - streamx: 2.18.0 + streamx: 2.20.1 optional: true base64-js@1.5.1: {} @@ -14950,7 +14950,7 @@ snapshots: get-caller-file@2.0.5: {} - get-east-asian-width@1.2.0: {} + get-east-asian-width@1.3.0: {} get-intrinsic@1.2.4: dependencies: @@ -15141,7 +15141,7 @@ snapshots: graphemer@1.4.0: {} - graphql@16.8.1: {} + graphql@16.9.0: {} gtoken@7.0.1: dependencies: @@ -16464,7 +16464,7 @@ snapshots: '@types/cookie': 0.6.0 '@types/statuses': 2.0.5 chalk: 4.1.2 - graphql: 16.8.1 + graphql: 16.9.0 headers-polyfill: 4.0.3 is-node-process: 1.2.0 outvariant: 1.4.3 @@ -17726,7 +17726,7 @@ snapshots: fast-fifo: 1.3.2 queue-tick: 1.0.1 - streamx@2.18.0: + streamx@2.20.1: dependencies: fast-fifo: 1.3.2 queue-tick: 1.0.1 @@ -17754,7 +17754,7 @@ snapshots: string-width@7.0.0: dependencies: emoji-regex: 10.3.0 - get-east-asian-width: 1.2.0 + get-east-asian-width: 1.3.0 strip-ansi: 7.1.0 string.prototype.matchall@4.0.11: diff --git a/test/core/test/jest-expect.test.ts b/test/core/test/jest-expect.test.ts index 79ffc4874b39..4e230a1cff2d 100644 --- a/test/core/test/jest-expect.test.ts +++ b/test/core/test/jest-expect.test.ts @@ -698,6 +698,158 @@ describe('toHaveBeenCalledExactlyOnceWith', () => { }) }) +describe('toHaveBeenCalledBefore', () => { + it('success if expect mock is called before result mock', () => { + const expectMock = vi.fn() + const resultMock = vi.fn() + + expectMock() + resultMock() + + expect(expectMock).toHaveBeenCalledBefore(resultMock) + }) + + it('throws if expect is not a spy', () => { + expect(() => { + expect(1).toHaveBeenCalledBefore(vi.fn()) + }).toThrow(/^1 is not a spy or a call to a spy/) + }) + + it('throws if result is not a spy', () => { + expect(() => { + expect(vi.fn()).toHaveBeenCalledBefore(1 as any) + }).toThrow(/^1 is not a spy or a call to a spy/) + }) + + it('throws if expect mock is called after result mock', () => { + const expectMock = vi.fn() + const resultMock = vi.fn() + + resultMock() + expectMock() + + expect(() => { + expect(expectMock).toHaveBeenCalledBefore(resultMock) + }).toThrow(/^expected "spy" to have been called before "spy"/) + }) + + it('throws with correct mock name if failed', () => { + const mock1 = vi.fn().mockName('mock1') + const mock2 = vi.fn().mockName('mock2') + + mock2() + mock1() + + expect(() => { + expect(mock1).toHaveBeenCalledBefore(mock2) + }).toThrow(/^expected "mock1" to have been called before "mock2"/) + }) + + it('fails if expect mock is not called', () => { + const resultMock = vi.fn() + + resultMock() + + expect(() => { + expect(vi.fn()).toHaveBeenCalledBefore(resultMock) + }).toThrow(/^expected "spy" to have been called before "spy"/) + }) + + it('not fails if expect mock is not called with option `failIfNoFirstInvocation` set to false', () => { + const resultMock = vi.fn() + + resultMock() + + expect(vi.fn()).toHaveBeenCalledBefore(resultMock, false) + }) + + it('fails if result mock is not called', () => { + const expectMock = vi.fn() + + expectMock() + + expect(() => { + expect(expectMock).toHaveBeenCalledBefore(vi.fn()) + }).toThrow(/^expected "spy" to have been called before "spy"/) + }) +}) + +describe('toHaveBeenCalledAfter', () => { + it('success if expect mock is called after result mock', () => { + const resultMock = vi.fn() + const expectMock = vi.fn() + + resultMock() + expectMock() + + expect(expectMock).toHaveBeenCalledAfter(resultMock) + }) + + it('throws if expect is not a spy', () => { + expect(() => { + expect(1).toHaveBeenCalledAfter(vi.fn()) + }).toThrow(/^1 is not a spy or a call to a spy/) + }) + + it('throws if result is not a spy', () => { + expect(() => { + expect(vi.fn()).toHaveBeenCalledAfter(1 as any) + }).toThrow(/^1 is not a spy or a call to a spy/) + }) + + it('throws if expect mock is called before result mock', () => { + const resultMock = vi.fn() + const expectMock = vi.fn() + + expectMock() + resultMock() + + expect(() => { + expect(expectMock).toHaveBeenCalledAfter(resultMock) + }).toThrow(/^expected "spy" to have been called after "spy"/) + }) + + it('throws with correct mock name if failed', () => { + const mock1 = vi.fn().mockName('mock1') + const mock2 = vi.fn().mockName('mock2') + + mock1() + mock2() + + expect(() => { + expect(mock1).toHaveBeenCalledAfter(mock2) + }).toThrow(/^expected "mock1" to have been called after "mock2"/) + }) + + it('fails if result mock is not called', () => { + const expectMock = vi.fn() + + expectMock() + + expect(() => { + expect(expectMock).toHaveBeenCalledAfter(vi.fn()) + }).toThrow(/^expected "spy" to have been called after "spy"/) + }) + + it('not fails if result mock is not called with option `failIfNoFirstInvocation` set to false', () => { + const expectMock = vi.fn() + + expectMock() + + expect(expectMock).toHaveBeenCalledAfter(vi.fn(), false) + }) + + it('fails if expect mock is not called', () => { + const resultMock = vi.fn() + + resultMock() + + expect(() => { + expect(vi.fn()).toHaveBeenCalledAfter(resultMock) + }).toThrow(/^expected "spy" to have been called after "spy"/) + }) +}) + describe('async expect', () => { it('resolves', async () => { await expect((async () => 'true')()).resolves.toBe('true') From b915aa6807f3d2ae8b0a0dce52725400ef20e960 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 13 Nov 2024 18:14:59 +0100 Subject: [PATCH 18/18] chore: release v2.2.0-beta.1 --- package.json | 2 +- packages/browser/package.json | 2 +- packages/coverage-istanbul/package.json | 2 +- packages/coverage-v8/package.json | 2 +- packages/expect/package.json | 2 +- packages/mocker/package.json | 2 +- packages/pretty-format/package.json | 2 +- packages/runner/package.json | 2 +- packages/snapshot/package.json | 2 +- packages/spy/package.json | 2 +- packages/ui/package.json | 2 +- packages/utils/package.json | 2 +- packages/vite-node/package.json | 2 +- packages/vitest/package.json | 2 +- packages/web-worker/package.json | 2 +- packages/ws-client/package.json | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 9f63b997c17b..cb4e1fb49c3d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/monorepo", "type": "module", - "version": "2.1.5", + "version": "2.2.0-beta.1", "private": true, "packageManager": "pnpm@9.12.3", "description": "Next generation testing framework powered by Vite", diff --git a/packages/browser/package.json b/packages/browser/package.json index 1dcfebcd44c0..3d3ec401694f 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/browser", "type": "module", - "version": "2.1.5", + "version": "2.2.0-beta.1", "description": "Browser running for Vitest", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/coverage-istanbul/package.json b/packages/coverage-istanbul/package.json index b1a1f4dd0815..598b44da559f 100644 --- a/packages/coverage-istanbul/package.json +++ b/packages/coverage-istanbul/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/coverage-istanbul", "type": "module", - "version": "2.1.5", + "version": "2.2.0-beta.1", "description": "Istanbul coverage provider for Vitest", "author": "Anthony Fu ", "license": "MIT", diff --git a/packages/coverage-v8/package.json b/packages/coverage-v8/package.json index 93dd7c4c2a7b..0e4264462e8d 100644 --- a/packages/coverage-v8/package.json +++ b/packages/coverage-v8/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/coverage-v8", "type": "module", - "version": "2.1.5", + "version": "2.2.0-beta.1", "description": "V8 coverage provider for Vitest", "author": "Anthony Fu ", "license": "MIT", diff --git a/packages/expect/package.json b/packages/expect/package.json index d435e52a10c1..46679a311290 100644 --- a/packages/expect/package.json +++ b/packages/expect/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/expect", "type": "module", - "version": "2.1.5", + "version": "2.2.0-beta.1", "description": "Jest's expect matchers as a Chai plugin", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/mocker/package.json b/packages/mocker/package.json index d80a2f618903..e16bd0a354a6 100644 --- a/packages/mocker/package.json +++ b/packages/mocker/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/mocker", "type": "module", - "version": "2.1.5", + "version": "2.2.0-beta.1", "description": "Vitest module mocker implementation", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/pretty-format/package.json b/packages/pretty-format/package.json index 9a9deb37934b..09a962a8e855 100644 --- a/packages/pretty-format/package.json +++ b/packages/pretty-format/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/pretty-format", "type": "module", - "version": "2.1.5", + "version": "2.2.0-beta.1", "description": "Fork of pretty-format with support for ESM", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/runner/package.json b/packages/runner/package.json index 060c39af447c..92030116511d 100644 --- a/packages/runner/package.json +++ b/packages/runner/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/runner", "type": "module", - "version": "2.1.5", + "version": "2.2.0-beta.1", "description": "Vitest test runner", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/snapshot/package.json b/packages/snapshot/package.json index 048d5f53627b..fd9c277b9c50 100644 --- a/packages/snapshot/package.json +++ b/packages/snapshot/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/snapshot", "type": "module", - "version": "2.1.5", + "version": "2.2.0-beta.1", "description": "Vitest snapshot manager", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/spy/package.json b/packages/spy/package.json index e8a2b4fa2b00..2d0cf67ea6d2 100644 --- a/packages/spy/package.json +++ b/packages/spy/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/spy", "type": "module", - "version": "2.1.5", + "version": "2.2.0-beta.1", "description": "Lightweight Jest compatible spy implementation", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/ui/package.json b/packages/ui/package.json index a2ec2b133413..28a376f08f86 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/ui", "type": "module", - "version": "2.1.5", + "version": "2.2.0-beta.1", "description": "UI for Vitest", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/utils/package.json b/packages/utils/package.json index e4625ca44bd5..99772ff5e292 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/utils", "type": "module", - "version": "2.1.5", + "version": "2.2.0-beta.1", "description": "Shared Vitest utility functions", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/vite-node/package.json b/packages/vite-node/package.json index 39ec50c53052..c670b84499ca 100644 --- a/packages/vite-node/package.json +++ b/packages/vite-node/package.json @@ -1,7 +1,7 @@ { "name": "vite-node", "type": "module", - "version": "2.1.5", + "version": "2.2.0-beta.1", "description": "Vite as Node.js runtime", "author": "Anthony Fu ", "license": "MIT", diff --git a/packages/vitest/package.json b/packages/vitest/package.json index 2cd118d71d90..ab7ea0cc7954 100644 --- a/packages/vitest/package.json +++ b/packages/vitest/package.json @@ -1,7 +1,7 @@ { "name": "vitest", "type": "module", - "version": "2.1.5", + "version": "2.2.0-beta.1", "description": "Next generation testing framework powered by Vite", "author": "Anthony Fu ", "license": "MIT", diff --git a/packages/web-worker/package.json b/packages/web-worker/package.json index e971e1fb0a02..c3a720c6f531 100644 --- a/packages/web-worker/package.json +++ b/packages/web-worker/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/web-worker", "type": "module", - "version": "2.1.5", + "version": "2.2.0-beta.1", "description": "Web Worker support for testing in Vitest", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/ws-client/package.json b/packages/ws-client/package.json index fa905377ab6c..a09db9b5b9b1 100644 --- a/packages/ws-client/package.json +++ b/packages/ws-client/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/ws-client", "type": "module", - "version": "2.1.5", + "version": "2.2.0-beta.1", "description": "WebSocket client wrapper for communicating with Vitest", "author": "Anthony Fu ", "license": "MIT",