diff --git a/docs/.vitepress/contributors.ts b/docs/.vitepress/contributors.ts index 31e67fbc5..191e2b12c 100644 --- a/docs/.vitepress/contributors.ts +++ b/docs/.vitepress/contributors.ts @@ -29,17 +29,21 @@ export const contributors = (contributorNames).reduce((acc, name) function createLinks(tm: CoreTeam): CoreTeam { tm.links = [{ icon: 'github', link: `https://github.com/${tm.github}` }] - if (tm.mastodon) + if (tm.mastodon) { tm.links.push({ icon: 'mastodon', link: tm.mastodon }) + } - if (tm.discord) + if (tm.discord) { tm.links.push({ icon: 'discord', link: tm.discord }) + } - if (tm.youtube) + if (tm.youtube) { tm.links.push({ icon: 'youtube', link: `https://www.youtube.com/@${tm.youtube}` }) + } - if (tm.twitter) + if (tm.twitter) { tm.links.push({ icon: 'x', link: `https://twitter.com/${tm.twitter}` }) + } return tm } diff --git a/docs/.vitepress/scripts/cli-generator.ts b/docs/.vitepress/scripts/cli-generator.ts index f6ea9e8f8..1223e3b24 100644 --- a/docs/.vitepress/scripts/cli-generator.ts +++ b/docs/.vitepress/scripts/cli-generator.ts @@ -19,18 +19,22 @@ function resolveOptions(options: CLIOptions, parentName?: string) { } function resolveCommand(name: string, config: CLIOption | null): any { - if (!config) + if (!config) { return null + } let title = '`' - if (config.shorthand) + if (config.shorthand) { title += `-${config.shorthand}, ` + } title += `--${config.alias || name}` - if ('argument' in config) + if ('argument' in config) { title += ` ${config.argument}` + } title += '`' - if ('subcommands' in config && config.subcommands) + if ('subcommands' in config && config.subcommands) { return resolveOptions(config.subcommands, name) + } return { title, diff --git a/docs/.vitepress/scripts/fetch-avatars.ts b/docs/.vitepress/scripts/fetch-avatars.ts index 4587a12e7..5d305582d 100644 --- a/docs/.vitepress/scripts/fetch-avatars.ts +++ b/docs/.vitepress/scripts/fetch-avatars.ts @@ -8,8 +8,9 @@ const dirAvatars = resolve(docsDir, 'public/user-avatars/') const dirSponsors = resolve(docsDir, 'public/sponsors/') async function download(url: string, fileName: string) { - if (existsSync(fileName)) + if (existsSync(fileName)) { return + } // eslint-disable-next-line no-console console.log('downloading', fileName) try { @@ -20,15 +21,17 @@ async function download(url: string, fileName: string) { } async function fetchAvatars() { - if (!existsSync(dirAvatars)) + if (!existsSync(dirAvatars)) { await fsp.mkdir(dirAvatars, { recursive: true }) + } await Promise.all([...teamEmeritiMembers, ...teamMembers].map(c => c.github).map(name => download(`https://github.com/${name}.png?size=100`, join(dirAvatars, `${name}.png`)))) } async function fetchSponsors() { - if (!existsSync(dirSponsors)) + if (!existsSync(dirSponsors)) { await fsp.mkdir(dirSponsors, { recursive: true }) + } await Promise.all([ download('https://cdn.jsdelivr.net/gh/antfu/static/sponsors.svg', join(dirSponsors, 'antfu.svg')), download('https://cdn.jsdelivr.net/gh/patak-dev/static/sponsors.svg', join(dirSponsors, 'patak-dev.svg')), diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index ff05dbe4d..97e274322 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -10,8 +10,9 @@ import HomePage from '../components/HomePage.vue' import Version from '../components/Version.vue' import '@shikijs/vitepress-twoslash/style.css' -if (inBrowser) +if (inBrowser) { import('./pwa') +} export default { ...Theme, diff --git a/docs/api/expect-typeof.md b/docs/api/expect-typeof.md index febf5072f..4857acd57 100644 --- a/docs/api/expect-typeof.md +++ b/docs/api/expect-typeof.md @@ -263,8 +263,9 @@ This matcher extracts assert value (e.g., `assert v is number`), so you can perf import { expectTypeOf } from 'vitest' function assertNumber(v: any): asserts v is number { - if (typeof v !== 'number') + if (typeof v !== 'number') { throw new TypeError('Nope !') + } } expectTypeOf(assertNumber).asserts.toBeNumber() diff --git a/docs/api/expect.md b/docs/api/expect.md index 32b9e6824..d61b99d8a 100644 --- a/docs/api/expect.md +++ b/docs/api/expect.md @@ -192,8 +192,9 @@ Opposite of `toBeDefined`, `toBeUndefined` asserts that the value _is_ equal to import { expect, test } from 'vitest' function getApplesFromStock(stock: string) { - if (stock === 'Bill') + if (stock === 'Bill') { return 13 + } } test('mary doesn\'t have a stock', () => { @@ -214,8 +215,9 @@ import { Stocks } from './stocks.js' const stocks = new Stocks() stocks.sync('Bill') -if (stocks.getInfo('Bill')) +if (stocks.getInfo('Bill')) { stocks.sell('apples', 'Bill') +} ``` So if you want to test that `stocks.getInfo` will be truthy, you could write: @@ -247,8 +249,9 @@ import { Stocks } from './stocks.js' const stocks = new Stocks() stocks.sync('Bill') -if (!stocks.stockFailed('Bill')) +if (!stocks.stockFailed('Bill')) { stocks.sell('apples', 'Bill') +} ``` So if you want to test that `stocks.stockFailed` will be falsy, you could write: @@ -660,8 +663,9 @@ For example, if we want to test that `getFruitStock('pineapples')` throws, we co import { expect, test } from 'vitest' function getFruitStock(type: string) { - if (type === 'pineapples') + if (type === 'pineapples') { throw new Error('Pineapples are not in stock') + } // Do some other stuff } @@ -1203,8 +1207,9 @@ For example, if you have a function that fails when you call it, you may use thi import { expect, test } from 'vitest' async function buyApples(id) { - if (!id) + if (!id) { throw new Error('no id') + } } test('buyApples throws an error when no id provided', async () => { @@ -1301,8 +1306,9 @@ For example, if we want to test that `build()` throws due to receiving directori import { expect, test } from 'vitest' async function build(dir) { - if (dir.includes('no-src')) + if (dir.includes('no-src')) { throw new Error(`${dir}/src does not exist`) + } } const errorDirs = [ @@ -1594,14 +1600,15 @@ function areAnagramsEqual(a: unknown, b: unknown): boolean | undefined { const isAAnagramComparator = isAnagramComparator(a) const isBAnagramComparator = isAnagramComparator(b) - if (isAAnagramComparator && isBAnagramComparator) + if (isAAnagramComparator && isBAnagramComparator) { return a.equals(b) - - else if (isAAnagramComparator === isBAnagramComparator) + } + else if (isAAnagramComparator === isBAnagramComparator) { return undefined - - else + } + else { return false + } } expect.addEqualityTesters([areAnagramsEqual]) diff --git a/docs/api/index.md b/docs/api/index.md index 3b6a98a3e..7af5f6c7d 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -618,8 +618,9 @@ You can also nest describe blocks if you have a hierarchy of tests or benchmarks import { describe, expect, test } from 'vitest' function numberToCurrency(value: number | string) { - if (typeof value !== 'number') - throw new Error('Value must be a number') + if (typeof value !== 'number') { + throw new TypeError('Value must be a number') + } return value.toFixed(2).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') } diff --git a/docs/api/vi.md b/docs/api/vi.md index 7d9f5f55d..4f2d967b1 100644 --- a/docs/api/vi.md +++ b/docs/api/vi.md @@ -673,8 +673,9 @@ let i = 0 setTimeout(() => console.log(++i)) const interval = setInterval(() => { console.log(++i) - if (i === 3) + if (i === 3) { clearInterval(interval) + } }, 50) vi.runAllTimers() @@ -818,8 +819,9 @@ test('Server started successfully', async () => { await vi.waitFor( () => { - if (!server.isReady) + if (!server.isReady) { throw new Error('Server not started') + } console.log('Server started') }, diff --git a/docs/config/index.md b/docs/config/index.md index 68c8019ab..8bf98b979 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -2131,12 +2131,14 @@ export default defineConfig({ test: { onStackTrace(error: Error, { file }: ParsedStack): boolean | void { // If we've encountered a ReferenceError, show the whole stack. - if (error.name === 'ReferenceError') + if (error.name === 'ReferenceError') { return + } // Reject all frames from third party libraries. - if (file.includes('node_modules')) + if (file.includes('node_modules')) { return false + } }, }, }) diff --git a/docs/guide/mocking.md b/docs/guide/mocking.md index 6d3e344a4..424053ae6 100644 --- a/docs/guide/mocking.md +++ b/docs/guide/mocking.md @@ -27,8 +27,9 @@ function purchase() { const currentHour = new Date().getHours() const [open, close] = businessHours - if (currentHour > open && currentHour < close) + if (currentHour > open && currentHour < close) { return { message: 'Success' } + } return { message: 'Error' } } @@ -194,8 +195,9 @@ export default { { name: 'virtual-modules', resolveId(id) { - if (id === '$app/forms') + if (id === '$app/forms') { return 'virtual:$app/forms' + } } } ] diff --git a/eslint.config.js b/eslint.config.js index 6d1d64076..df00d9b20 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -45,6 +45,7 @@ export default antfu( 'no-undef': 'off', 'ts/no-invalid-this': 'off', 'eslint-comments/no-unlimited-disable': 'off', + 'curly': ['error', 'all'], // TODO: migrate and turn it back on 'ts/ban-types': 'off', diff --git a/examples/sveltekit/src/lib/add.ts b/examples/sveltekit/src/lib/add.ts index 6c97930ad..ef7ac53dc 100644 --- a/examples/sveltekit/src/lib/add.ts +++ b/examples/sveltekit/src/lib/add.ts @@ -1,8 +1,9 @@ import { dev } from '$app/environment' export function add(a: number, b: number) { - if (dev) + if (dev) { console.warn(`Adding ${a} and ${b}`) + } return a + b } diff --git a/package.json b/package.json index 44a473e6c..14367d4b1 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "lint-staged": "^15.2.5", "magic-string": "^0.30.10", "pathe": "^1.1.2", + "prettier": "^2.8.8", "rimraf": "^5.0.7", "rollup": "^4.18.0", "rollup-plugin-dts": "^6.1.1", diff --git a/packages/browser/context.d.ts b/packages/browser/context.d.ts index fcd268255..7145cc264 100644 --- a/packages/browser/context.d.ts +++ b/packages/browser/context.d.ts @@ -19,12 +19,24 @@ export interface FsOptions { flag?: string | number } -export interface TypePayload { type: string } -export interface PressPayload { press: string } -export interface DownPayload { down: string } -export interface UpPayload { up: string } +export interface TypePayload { + type: string +} +export interface PressPayload { + press: string +} +export interface DownPayload { + down: string +} +export interface UpPayload { + up: string +} -export type SendKeysPayload = TypePayload | PressPayload | DownPayload | UpPayload +export type SendKeysPayload = + | TypePayload + | PressPayload + | DownPayload + | UpPayload export interface ScreenshotOptions { element?: Element @@ -35,8 +47,15 @@ export interface ScreenshotOptions { } export interface BrowserCommands { - readFile: (path: string, options?: BufferEncoding | FsOptions) => Promise - writeFile: (path: string, content: string, options?: BufferEncoding | FsOptions & { mode?: number | string }) => Promise + readFile: ( + path: string, + options?: BufferEncoding | FsOptions + ) => Promise + writeFile: ( + path: string, + content: string, + options?: BufferEncoding | (FsOptions & { mode?: number | string }) + ) => Promise removeFile: (path: string) => Promise sendKeys: (payload: SendKeysPayload) => Promise } diff --git a/packages/browser/providers/playwright.d.ts b/packages/browser/providers/playwright.d.ts index c74a5841c..ed58fc698 100644 --- a/packages/browser/providers/playwright.d.ts +++ b/packages/browser/providers/playwright.d.ts @@ -9,7 +9,10 @@ import type { declare module 'vitest/node' { interface BrowserProviderOptions { launch?: LaunchOptions - context?: Omit + context?: Omit< + BrowserContextOptions, + 'ignoreHTTPSErrors' | 'serviceWorkers' + > } export interface BrowserCommandContext { diff --git a/packages/browser/rollup.config.js b/packages/browser/rollup.config.js index f6327813e..c760fe9c2 100644 --- a/packages/browser/rollup.config.js +++ b/packages/browser/rollup.config.js @@ -33,35 +33,36 @@ const input = { providers: './src/node/providers/index.ts', } -export default () => defineConfig([ - { - input, - output: { - dir: 'dist', - format: 'esm', +export default () => + defineConfig([ + { + input, + output: { + dir: 'dist', + format: 'esm', + }, + external, + plugins, }, - external, - plugins, - }, - { - input: './src/client/context.ts', - output: { - file: 'dist/context.js', - format: 'esm', + { + input: './src/client/context.ts', + output: { + file: 'dist/context.js', + format: 'esm', + }, + plugins: [ + esbuild({ + target: 'node18', + }), + ], }, - plugins: [ - esbuild({ - target: 'node18', - }), - ], - }, - { - input: input.index, - output: { - file: 'dist/index.d.ts', - format: 'esm', + { + input: input.index, + output: { + file: 'dist/index.d.ts', + format: 'esm', + }, + external, + plugins: [dts()], }, - external, - plugins: [dts()], - }, -]) + ]) diff --git a/packages/browser/src/client/channel.ts b/packages/browser/src/client/channel.ts index b2d894248..d183be726 100644 --- a/packages/browser/src/client/channel.ts +++ b/packages/browser/src/client/channel.ts @@ -78,13 +78,20 @@ export type IframeChannelEvent = | IframeChannelIncomingEvent | IframeChannelOutgoingEvent -export const channel = new BroadcastChannel(`vitest:${getBrowserState().contextId}`) +export const channel = new BroadcastChannel( + `vitest:${getBrowserState().contextId}`, +) export function waitForChannel(event: IframeChannelOutgoingEvent['type']) { return new Promise((resolve) => { - channel.addEventListener('message', (e) => { - if (e.data?.type === event) - resolve() - }, { once: true }) + channel.addEventListener( + 'message', + (e) => { + if (e.data?.type === event) { + resolve() + } + }, + { once: true }, + ) }) } diff --git a/packages/browser/src/client/client.ts b/packages/browser/src/client/client.ts index 12f19d673..3a18135c8 100644 --- a/packages/browser/src/client/client.ts +++ b/packages/browser/src/client/client.ts @@ -9,9 +9,10 @@ const PAGE_TYPE = getBrowserState().type export const PORT = import.meta.hot ? '51204' : location.port export const HOST = [location.hostname, PORT].filter(Boolean).join(':') -export const SESSION_ID = PAGE_TYPE === 'orchestrator' - ? getBrowserState().contextId - : crypto.randomUUID() +export const SESSION_ID + = PAGE_TYPE === 'orchestrator' + ? getBrowserState().contextId + : crypto.randomUUID() export const ENTRY_URL = `${ location.protocol === 'https:' ? 'wss:' : 'ws:' }//${HOST}/__vitest_browser_api__?type=${PAGE_TYPE}&sessionId=${SESSION_ID}` @@ -27,7 +28,10 @@ export interface VitestBrowserClient { waitForConnection: () => Promise } -export type BrowserRPC = BirpcReturn +export type BrowserRPC = BirpcReturn< + WebSocketBrowserHandlers, + WebSocketBrowserEvents +> function createClient() { const autoReconnect = true @@ -44,46 +48,55 @@ function createClient() { let onMessage: Function - ctx.rpc = createBirpc({ - onCancel: setCancel, - async startMocking(id: string) { - // @ts-expect-error not typed global - if (typeof __vitest_mocker__ === 'undefined') - throw new Error(`Cannot mock modules in the orchestrator process`) - // @ts-expect-error not typed global - const mocker = __vitest_mocker__ as VitestBrowserClientMocker - const exports = await mocker.resolve(id) - return Object.keys(exports) - }, - async createTesters(files: string[]) { - if (PAGE_TYPE !== 'orchestrator') - return - getBrowserState().createTesters?.(files) - }, - }, { - post: msg => ctx.ws.send(msg), - on: fn => (onMessage = fn), - serialize: e => stringify(e, (_, v) => { - if (v instanceof Error) { - return { - name: v.name, - message: v.message, - stack: v.stack, + ctx.rpc = createBirpc( + { + onCancel: setCancel, + async startMocking(id: string) { + // @ts-expect-error not typed global + if (typeof __vitest_mocker__ === 'undefined') { + throw new TypeError( + `Cannot mock modules in the orchestrator process`, + ) } - } - return v - }), - deserialize: parse, - onTimeoutError(functionName) { - throw new Error(`[vitest-browser]: Timeout calling "${functionName}"`) + // @ts-expect-error not typed global + const mocker = __vitest_mocker__ as VitestBrowserClientMocker + const exports = await mocker.resolve(id) + return Object.keys(exports) + }, + async createTesters(files: string[]) { + if (PAGE_TYPE !== 'orchestrator') { + return + } + getBrowserState().createTesters?.(files) + }, }, - }) + { + post: msg => ctx.ws.send(msg), + on: fn => (onMessage = fn), + serialize: e => + stringify(e, (_, v) => { + if (v instanceof Error) { + return { + name: v.name, + message: v.message, + stack: v.stack, + } + } + return v + }), + deserialize: parse, + onTimeoutError(functionName) { + throw new Error(`[vitest-browser]: Timeout calling "${functionName}"`) + }, + }, + ) let openPromise: Promise function reconnect(reset = false) { - if (reset) + if (reset) { tries = reconnectTries + } ctx.ws = new WebSocket(ENTRY_URL) registerWS() } @@ -91,10 +104,15 @@ function createClient() { function registerWS() { openPromise = new Promise((resolve, reject) => { const timeout = setTimeout(() => { - reject(new Error(`Cannot connect to the server in ${connectTimeout / 1000} seconds`)) + reject( + new Error( + `Cannot connect to the server in ${connectTimeout / 1000} seconds`, + ), + ) }, connectTimeout)?.unref?.() - if (ctx.ws.OPEN === ctx.ws.readyState) + if (ctx.ws.OPEN === ctx.ws.readyState) { resolve() + } // still have a listener even if it's already open to update tries ctx.ws.addEventListener('open', () => { tries = reconnectTries @@ -107,8 +125,9 @@ function createClient() { }) ctx.ws.addEventListener('close', () => { tries -= 1 - if (autoReconnect && tries > 0) + if (autoReconnect && tries > 0) { setTimeout(reconnect, reconnectInterval) + } }) } diff --git a/packages/browser/src/client/context.ts b/packages/browser/src/client/context.ts index 675644242..776a70451 100644 --- a/packages/browser/src/client/context.ts +++ b/packages/browser/src/client/context.ts @@ -1,32 +1,48 @@ import type { Task, WorkerGlobalState } from 'vitest' -import type { BrowserPage, UserEvent, UserEventClickOptions } from '../../context' +import type { + BrowserPage, + UserEvent, + UserEventClickOptions, +} from '../../context' import type { BrowserRPC } from './client' import type { BrowserRunnerState } from './utils' // this file should not import anything directly, only types function convertElementToXPath(element: Element) { - if (!element || !(element instanceof Element)) - throw new Error(`Expected DOM element to be an instance of Element, received ${typeof element}`) + if (!element || !(element instanceof Element)) { + throw new Error( + `Expected DOM element to be an instance of Element, received ${typeof element}`, + ) + } return getPathTo(element) } function getPathTo(element: Element): string { - if (element.id !== '') + if (element.id !== '') { return `id("${element.id}")` + } - if (!element.parentNode || element === document.documentElement) + if (!element.parentNode || element === document.documentElement) { return element.tagName + } let ix = 0 const siblings = element.parentNode.childNodes for (let i = 0; i < siblings.length; i++) { const sibling = siblings[i] - if (sibling === element) - return `${getPathTo(element.parentNode as Element)}/${element.tagName}[${ix + 1}]` - if (sibling.nodeType === 1 && (sibling as Element).tagName === element.tagName) + if (sibling === element) { + return `${getPathTo(element.parentNode as Element)}/${element.tagName}[${ + ix + 1 + }]` + } + if ( + sibling.nodeType === 1 + && (sibling as Element).tagName === element.tagName + ) { ix++ + } } return 'invalid xpath' } @@ -35,7 +51,9 @@ function getPathTo(element: Element): string { const state = (): WorkerGlobalState => __vitest_worker__ // @ts-expect-error not typed global const runner = (): BrowserRunnerState => __vitest_browser_runner__ -const filepath = () => state().filepath || state().current?.file?.filepath || undefined +function filepath() { + return state().filepath || state().current?.file?.filepath || undefined +} const rpc = () => state().rpc as any as BrowserRPC const contextId = runner().contextId const channel = new BroadcastChannel(`vitest:${contextId}`) @@ -74,8 +92,9 @@ export const page: BrowserPage = { }, async screenshot(options = {}) { const currentTest = state().current - if (!currentTest) + if (!currentTest) { throw new Error('Cannot take a screenshot outside of a test.') + } if (currentTest.concurrent) { throw new Error( @@ -92,16 +111,15 @@ export const page: BrowserPage = { screenshotIds[repeatCount] ??= {} screenshotIds[repeatCount][taskName] = number + 1 - const name = options.path || `${taskName.replace(/[^a-z0-9]/g, '-')}-${number}.png` + const name + = options.path || `${taskName.replace(/[^a-z0-9]/g, '-')}-${number}.png` - return triggerCommand( - '__vitest_screenshot', - name, - { - ...options, - element: options.element ? convertElementToXPath(options.element) : undefined, - }, - ) + return triggerCommand('__vitest_screenshot', name, { + ...options, + element: options.element + ? convertElementToXPath(options.element) + : undefined, + }) }, } diff --git a/packages/browser/src/client/dialog.ts b/packages/browser/src/client/dialog.ts index f658dd006..7c807dc64 100644 --- a/packages/browser/src/client/dialog.ts +++ b/packages/browser/src/client/dialog.ts @@ -8,7 +8,9 @@ If needed, mock the \`${name}\` call manually like: \`\`\` import { expect, vi } from "vitest" -vi.spyOn(window, "${name}")${defaultValue ? `.mockReturnValue(${JSON.stringify(defaultValue)})` : ''} +vi.spyOn(window, "${name}")${ + defaultValue ? `.mockReturnValue(${JSON.stringify(defaultValue)})` : '' + } ${name}(${formatedParams}) expect(${name}).toHaveBeenCalledWith(${formatedParams}) \`\`\``) diff --git a/packages/browser/src/client/logger.ts b/packages/browser/src/client/logger.ts index 9de7ea758..dae709776 100644 --- a/packages/browser/src/client/logger.ts +++ b/packages/browser/src/client/logger.ts @@ -4,23 +4,46 @@ import { getConfig, importId } from './utils' const { Date, console } = globalThis export async function setupConsoleLogSpy() { - const { stringify, format, inspect } = await importId('vitest/utils') as typeof import('vitest/utils') - const { log, info, error, dir, dirxml, trace, time, timeEnd, timeLog, warn, debug, count, countReset } = console + const { stringify, format, inspect } = (await importId( + 'vitest/utils', + )) as typeof import('vitest/utils') + const { + log, + info, + error, + dir, + dirxml, + trace, + time, + timeEnd, + timeLog, + warn, + debug, + count, + countReset, + } = console const formatInput = (input: unknown) => { - if (input instanceof Node) + if (input instanceof Node) { return stringify(input) + } return format(input) } const processLog = (args: unknown[]) => args.map(formatInput).join(' ') - const sendLog = (type: 'stdout' | 'stderr', content: string, disableStack?: boolean) => { - if (content.startsWith('[vite]')) + const sendLog = ( + type: 'stdout' | 'stderr', + content: string, + disableStack?: boolean, + ) => { + if (content.startsWith('[vite]')) { return + } const unknownTestId = '__vitest__unknown_test__' // @ts-expect-error untyped global const taskId = globalThis.__vitest_worker__?.current?.id ?? unknownTestId - const origin = getConfig().printConsoleTrace && !disableStack - ? new Error('STACK_TRACE').stack?.split('\n').slice(1).join('\n') - : undefined + const origin + = getConfig().printConsoleTrace && !disableStack + ? new Error('STACK_TRACE').stack?.split('\n').slice(1).join('\n') + : undefined rpc().sendLog({ origin, content, @@ -31,14 +54,18 @@ export async function setupConsoleLogSpy() { size: content.length, }) } - const stdout = (base: (...args: unknown[]) => void) => (...args: unknown[]) => { - sendLog('stdout', processLog(args)) - return base(...args) - } - const stderr = (base: (...args: unknown[]) => void) => (...args: unknown[]) => { - sendLog('stderr', processLog(args)) - return base(...args) - } + const stdout + = (base: (...args: unknown[]) => void) => + (...args: unknown[]) => { + sendLog('stdout', processLog(args)) + return base(...args) + } + const stderr + = (base: (...args: unknown[]) => void) => + (...args: unknown[]) => { + sendLog('stderr', processLog(args)) + return base(...args) + } console.log = stdout(log) console.debug = stdout(debug) console.info = stdout(info) @@ -77,10 +104,12 @@ export async function setupConsoleLogSpy() { console.timeLog = (label = 'default') => { timeLog(label) - if (!(label in timeLabels)) + if (!(label in timeLabels)) { sendLog('stderr', `Timer "${label}" does not exist`) - else + } + else { sendLog('stdout', `${label}: ${timeLabels[label]} ms`) + } } console.timeEnd = (label = 'default') => { diff --git a/packages/browser/src/client/mocker.ts b/packages/browser/src/client/mocker.ts index 61c5a4c82..c04b9c7b6 100644 --- a/packages/browser/src/client/mocker.ts +++ b/packages/browser/src/client/mocker.ts @@ -21,25 +21,30 @@ export class VitestBrowserClientMocker { private spyModule!: SpyModule setupWorker() { - channel.addEventListener('message', async (e: MessageEvent) => { - if (e.data.type === 'mock-factory:request') { - try { - const module = await this.resolve(e.data.id) - const exports = Object.keys(module) - channel.postMessage({ - type: 'mock-factory:response', - exports, - }) - } - catch (err: any) { - const { processError } = await importId('vitest/browser') as typeof import('vitest/browser') - channel.postMessage({ - type: 'mock-factory:error', - error: processError(err), - }) + channel.addEventListener( + 'message', + async (e: MessageEvent) => { + if (e.data.type === 'mock-factory:request') { + try { + const module = await this.resolve(e.data.id) + const exports = Object.keys(module) + channel.postMessage({ + type: 'mock-factory:response', + exports, + }) + } + catch (err: any) { + const { processError } = (await importId( + 'vitest/browser', + )) as typeof import('vitest/browser') + channel.postMessage({ + type: 'mock-factory:error', + error: processError(err), + }) + } } - } - }) + }, + ) } public setSpyModule(mod: SpyModule) { @@ -48,8 +53,11 @@ export class VitestBrowserClientMocker { public async importActual(id: string, importer: string) { const resolved = await rpc().resolveId(id, importer) - if (resolved == null) - throw new Error(`[vitest] Cannot resolve ${id} imported from ${importer}`) + if (resolved == null) { + throw new Error( + `[vitest] Cannot resolve ${id} imported from ${importer}`, + ) + } const ext = extname(resolved.id) const url = new URL(`/@id/${resolved.id}`, location.href) const query = `_vitest_original&ext.${ext}` @@ -61,14 +69,20 @@ export class VitestBrowserClientMocker { public async importMock(rawId: string, importer: string) { await this.prepare() - const { resolvedId, type, mockPath } = await rpc().resolveMock(rawId, importer, false) + const { resolvedId, type, mockPath } = await rpc().resolveMock( + rawId, + importer, + false, + ) const factoryReturn = this.get(resolvedId) - if (factoryReturn) + if (factoryReturn) { return factoryReturn + } - if (this.factories[resolvedId]) + if (this.factories[resolvedId]) { return await this.resolve(resolvedId) + } if (type === 'redirect') { const url = new URL(`/@id/${mockPath}`, location.href) @@ -90,8 +104,9 @@ export class VitestBrowserClientMocker { public async invalidate() { const ids = Array.from(this.ids) - if (!ids.length) + if (!ids.length) { return + } await rpc().invalidate(ids) channel.postMessage({ type: 'mock:invalidate' }) this.ids.clear() @@ -102,8 +117,9 @@ export class VitestBrowserClientMocker { public async resolve(id: string) { const factory = this.factories[id] - if (!factory) + if (!factory) { throw new Error(`Cannot resolve ${id} mock: no factory provided`) + } try { this.mockObjects[id] = await factory() return this.mockObjects[id] @@ -120,13 +136,15 @@ export class VitestBrowserClientMocker { } public queueMock(id: string, importer: string, factory?: () => any) { - const promise = rpc().resolveMock(id, importer, !!factory) + const promise = rpc() + .resolveMock(id, importer, !!factory) .then(async ({ mockPath, resolvedId }) => { this.ids.add(resolvedId) const urlPaths = resolveMockPaths(resolvedId) - const resolvedMock = typeof mockPath === 'string' - ? new URL(resolvedMockedPath(mockPath), location.href).toString() - : mockPath + const resolvedMock + = typeof mockPath === 'string' + ? new URL(resolvedMockedPath(mockPath), location.href).toString() + : mockPath urlPaths.forEach((url) => { this.mocks[url] = resolvedMock this.factories[url] = factory! @@ -137,17 +155,20 @@ export class VitestBrowserClientMocker { mock: resolvedMock, }) await waitForChannel('mock:done') - }).finally(() => { + }) + .finally(() => { this.queue.delete(promise) }) this.queue.add(promise) } public queueUnmock(id: string, importer: string) { - const promise = rpc().resolveId(id, importer) + const promise = rpc() + .resolveId(id, importer) .then(async (resolved) => { - if (!resolved) + if (!resolved) { return + } this.ids.delete(resolved.id) const urlPaths = resolveMockPaths(resolved.id) urlPaths.forEach((url) => { @@ -168,15 +189,17 @@ export class VitestBrowserClientMocker { } public async prepare() { - if (!this.queue.size) + if (!this.queue.size) { return - await Promise.all([ - ...this.queue.values(), - ]) + } + await Promise.all([...this.queue.values()]) } // TODO: move this logic into a util(?) - public mockObject(object: Record, mockExports: Record = {}) { + public mockObject( + object: Record, + mockExports: Record = {}, + ) { const finalizers = new Array<() => void>() const refs = new RefTracker() @@ -190,10 +213,16 @@ export class VitestBrowserClientMocker { } } - const mockPropertiesOf = (container: Record, newContainer: Record) => { + const mockPropertiesOf = ( + container: Record, + newContainer: Record, + ) => { const containerType = /* #__PURE__ */ getType(container) const isModule = containerType === 'Module' || !!container.__esModule - for (const { key: property, descriptor } of getAllMockableProperties(container, isModule)) { + for (const { key: property, descriptor } of getAllMockableProperties( + container, + isModule, + )) { // Modules define their exports as getters. We want to process those. if (!isModule && descriptor.get) { try { @@ -206,8 +235,9 @@ export class VitestBrowserClientMocker { } // Skip special read-only props, we don't want to mess with those. - if (isSpecialProp(property, containerType)) + if (isSpecialProp(property, containerType)) { continue + } const value = container[property] @@ -215,7 +245,9 @@ export class VitestBrowserClientMocker { // recursion in circular objects. const refId = refs.getId(value) if (refId !== undefined) { - finalizers.push(() => define(newContainer, property, refs.getMockedValue(refId))) + finalizers.push(() => + define(newContainer, property, refs.getMockedValue(refId)), + ) continue } @@ -226,38 +258,54 @@ export class VitestBrowserClientMocker { continue } - const isFunction = type.includes('Function') && typeof value === 'function' - if ((!isFunction || value.__isMockFunction) && type !== 'Object' && type !== 'Module') { + const isFunction + = type.includes('Function') && typeof value === 'function' + if ( + (!isFunction || value.__isMockFunction) + && type !== 'Object' + && type !== 'Module' + ) { define(newContainer, property, value) continue } // Sometimes this assignment fails for some unknown reason. If it does, // just move along. - if (!define(newContainer, property, isFunction ? value : {})) + if (!define(newContainer, property, isFunction ? value : {})) { continue + } if (isFunction) { const spyModule = this.spyModule - if (!spyModule) - throw new Error('[vitest] `spyModule` is not defined. This is Vitest error. Please open a new issue with reproduction.') + if (!spyModule) { + throw new Error( + '[vitest] `spyModule` is not defined. This is Vitest error. Please open a new issue with reproduction.', + ) + } function mockFunction(this: any) { // detect constructor call and mock each instance's methods // so that mock states between prototype/instances don't affect each other // (jest reference https://github.com/jestjs/jest/blob/2c3d2409879952157433de215ae0eee5188a4384/packages/jest-mock/src/index.ts#L678-L691) if (this instanceof newContainer[property]) { - for (const { key, descriptor } of getAllMockableProperties(this, false)) { + for (const { key, descriptor } of getAllMockableProperties( + this, + false, + )) { // skip getter since it's not mocked on prototype as well - if (descriptor.get) + if (descriptor.get) { continue + } const value = this[key] const type = /* #__PURE__ */ getType(value) - const isFunction = type.includes('Function') && typeof value === 'function' + const isFunction + = type.includes('Function') && typeof value === 'function' if (isFunction) { // mock and delegate calls to original prototype method, which should be also mocked already const original = this[key] - const mock = spyModule.spyOn(this, key as string).mockImplementation(original) + const mock = spyModule + .spyOn(this, key as string) + .mockImplementation(original) mock.mockRestore = () => { mock.mockReset() mock.mockImplementation(original) @@ -267,7 +315,9 @@ export class VitestBrowserClientMocker { } } } - const mock = spyModule.spyOn(newContainer, property).mockImplementation(mockFunction) + const mock = spyModule + .spyOn(newContainer, property) + .mockImplementation(mockFunction) mock.mockRestore = () => { mock.mockReset() mock.mockImplementation(mockFunction) @@ -286,17 +336,20 @@ export class VitestBrowserClientMocker { mockPropertiesOf(object, mockedObject) // Plug together refs - for (const finalizer of finalizers) + for (const finalizer of finalizers) { finalizer() + } return mockedObject } } function isSpecialProp(prop: Key, parentType: string) { - return parentType.includes('Function') + return ( + parentType.includes('Function') && typeof prop === 'string' && ['arguments', 'callee', 'caller', 'length', 'name'].includes(prop) + ) } class RefTracker { @@ -322,39 +375,56 @@ class RefTracker { type Key = string | symbol export function getAllMockableProperties(obj: any, isModule: boolean) { - const allProps = new Map() + const allProps = new Map< + string | symbol, + { key: string | symbol; descriptor: PropertyDescriptor } + >() let curr = obj do { // we don't need properties from these - if (curr === Object.prototype || curr === Function.prototype || curr === RegExp.prototype) + if ( + curr === Object.prototype + || curr === Function.prototype + || curr === RegExp.prototype + ) { break + } collectOwnProperties(curr, (key) => { const descriptor = Object.getOwnPropertyDescriptor(curr, key) - if (descriptor) + if (descriptor) { allProps.set(key, { key, descriptor }) + } }) - // eslint-disable-next-line no-cond-assign - } while (curr = Object.getPrototypeOf(curr)) + // eslint-disable-next-line no-cond-assign + } while ((curr = Object.getPrototypeOf(curr))) // default is not specified in ownKeys, if module is interoped if (isModule && !allProps.has('default') && 'default' in obj) { const descriptor = Object.getOwnPropertyDescriptor(obj, 'default') - if (descriptor) + if (descriptor) { allProps.set('default', { key: 'default', descriptor }) + } } return Array.from(allProps.values()) } -function collectOwnProperties(obj: any, collector: Set | ((key: string | symbol) => void)) { - const collect = typeof collector === 'function' ? collector : (key: string | symbol) => collector.add(key) +function collectOwnProperties( + obj: any, + collector: Set | ((key: string | symbol) => void), +) { + const collect + = typeof collector === 'function' + ? collector + : (key: string | symbol) => collector.add(key) Object.getOwnPropertyNames(obj).forEach(collect) Object.getOwnPropertySymbols(obj).forEach(collect) } function resolvedMockedPath(path: string) { const config = getBrowserState().viteConfig - if (path.startsWith(config.root)) + if (path.startsWith(config.root)) { return path.slice(config.root.length) + } return path } @@ -365,11 +435,13 @@ function resolveMockPaths(path: string) { const paths = [path, join('/@fs/', path)] // URL can be /file/path.js, but path is resolved to /file/path - if (path.startsWith(config.root)) + if (path.startsWith(config.root)) { paths.push(path.slice(config.root.length)) + } - if (path.startsWith(fsRoot)) + if (path.startsWith(fsRoot)) { paths.push(path.slice(fsRoot.length)) + } return paths } diff --git a/packages/browser/src/client/msw.ts b/packages/browser/src/client/msw.ts index e170a67f0..e5dac3579 100644 --- a/packages/browser/src/client/msw.ts +++ b/packages/browser/src/client/msw.ts @@ -1,6 +1,11 @@ import { http } from 'msw/core/http' import { setupWorker } from 'msw/browser' -import type { IframeChannelEvent, IframeMockEvent, IframeMockingDoneEvent, IframeUnmockEvent } from './channel' +import type { + IframeChannelEvent, + IframeMockEvent, + IframeMockingDoneEvent, + IframeUnmockEvent, +} from './channel' import { channel } from './channel' import { client } from './client' @@ -10,8 +15,9 @@ export function createModuleMocker() { const worker = setupWorker( http.get(/.+/, async ({ request }) => { const path = removeTimestamp(request.url.slice(location.origin.length)) - if (!mocks.has(path)) + if (!mocks.has(path)) { return passthrough() + } const mock = mocks.get(path) @@ -20,11 +26,14 @@ export function createModuleMocker() { // TODO: check how the error looks const exports = await getFactoryExports(path) const module = `const module = __vitest_mocker__.get('${path}');` - const keys = exports.map((name) => { - if (name === 'default') - return `export default module['default'];` - return `export const ${name} = module['${name}'];` - }).join('\n') + const keys = exports + .map((name) => { + if (name === 'default') { + return `export default module['default'];` + } + return `export const ${name} = module['${name}'];` + }) + .join('\n') const text = `${module}\n${keys}` return new Response(text, { headers: { @@ -33,8 +42,9 @@ export function createModuleMocker() { }) } - if (typeof mock === 'string') + if (typeof mock === 'string') { return Response.redirect(mock) + } const content = await client.rpc.automock(path) return new Response(content, { @@ -49,19 +59,23 @@ export function createModuleMocker() { let startPromise: undefined | Promise async function init() { - if (started) + if (started) { return - if (startPromise) + } + if (startPromise) { return startPromise - startPromise = worker.start({ - serviceWorker: { - url: '/__virtual_vitest__:mocker-worker.js', - }, - quiet: true, - }).finally(() => { - started = true - startPromise = undefined - }) + } + startPromise = worker + .start({ + serviceWorker: { + url: '/__virtual_vitest__:mocker-worker.js', + }, + quiet: true, + }) + .finally(() => { + started = true + startPromise = undefined + }) await startPromise } @@ -88,16 +102,19 @@ function getFactoryExports(id: string) { id, }) return new Promise((resolve, reject) => { - channel.addEventListener('message', function onMessage(e: MessageEvent) { - if (e.data.type === 'mock-factory:response') { - resolve(e.data.exports) - channel.removeEventListener('message', onMessage) - } - if (e.data.type === 'mock-factory:error') { - reject(e.data.error) - channel.removeEventListener('message', onMessage) - } - }) + channel.addEventListener( + 'message', + function onMessage(e: MessageEvent) { + if (e.data.type === 'mock-factory:response') { + resolve(e.data.exports) + channel.removeEventListener('message', onMessage) + } + if (e.data.type === 'mock-factory:error') { + reject(e.data.error) + channel.removeEventListener('message', onMessage) + } + }, + ) }) } diff --git a/packages/browser/src/client/orchestrator.ts b/packages/browser/src/client/orchestrator.ts index 4e82157f0..9b1f0f9d8 100644 --- a/packages/browser/src/client/orchestrator.ts +++ b/packages/browser/src/client/orchestrator.ts @@ -25,8 +25,9 @@ getBrowserState().createTesters = async (files) => { function debug(...args: unknown[]) { const debug = getConfig().env.VITEST_BROWSER_DEBUG - if (debug && debug !== 'false') + if (debug && debug !== 'false') { client.rpc.debug(...args.map(String)) + } } function createIframe(container: HTMLDivElement, file: string) { @@ -37,7 +38,12 @@ function createIframe(container: HTMLDivElement, file: string) { const iframe = document.createElement('iframe') iframe.setAttribute('loading', 'eager') - iframe.setAttribute('src', `${url.pathname}__vitest_test__/__test__/${getBrowserState().contextId}/${encodeURIComponent(file)}`) + iframe.setAttribute( + 'src', + `${url.pathname}__vitest_test__/__test__/${ + getBrowserState().contextId + }/${encodeURIComponent(file)}`, + ) iframe.setAttribute('data-vitest', 'true') iframe.style.display = 'block' @@ -84,89 +90,106 @@ client.ws.addEventListener('open', async () => { const mocker = createModuleMocker() - channel.addEventListener('message', async (e: MessageEvent): Promise => { - debug('channel event', JSON.stringify(e.data)) - switch (e.data.type) { - case 'viewport': { - const { width, height, id } = e.data - const iframe = iframes.get(id) - if (!iframe) { - const error = new Error(`Cannot find iframe with id ${id}`) - channel.postMessage({ type: 'viewport:fail', id, error: error.message }) - await client.rpc.onUnhandledError({ - name: 'Teardown Error', - message: error.message, - }, 'Teardown Error') - return + channel.addEventListener( + 'message', + async (e: MessageEvent): Promise => { + debug('channel event', JSON.stringify(e.data)) + switch (e.data.type) { + case 'viewport': { + const { width, height, id } = e.data + const iframe = iframes.get(id) + if (!iframe) { + const error = new Error(`Cannot find iframe with id ${id}`) + channel.postMessage({ + type: 'viewport:fail', + id, + error: error.message, + }) + await client.rpc.onUnhandledError( + { + name: 'Teardown Error', + message: error.message, + }, + 'Teardown Error', + ) + return + } + await setIframeViewport(iframe, width, height) + channel.postMessage({ type: 'viewport:done', id }) + break } - await setIframeViewport(iframe, width, height) - channel.postMessage({ type: 'viewport:done', id }) - break - } - case 'done': { - const filenames = e.data.filenames - filenames.forEach(filename => runningFiles.delete(filename)) - - if (!runningFiles.size) { - const ui = getUiAPI() - // in isolated mode we don't change UI because it will slow down tests, - // so we only select it when the run is done - if (ui && filenames.length > 1) { - const id = generateFileId(filenames[filenames.length - 1]) - ui.setCurrentFileId(id) + case 'done': { + const filenames = e.data.filenames + filenames.forEach(filename => runningFiles.delete(filename)) + + if (!runningFiles.size) { + const ui = getUiAPI() + // in isolated mode we don't change UI because it will slow down tests, + // so we only select it when the run is done + if (ui && filenames.length > 1) { + const id = generateFileId(filenames[filenames.length - 1]) + ui.setCurrentFileId(id) + } + await done() } - await done() + else { + // keep the last iframe + const iframeId = e.data.id + iframes.get(iframeId)?.remove() + iframes.delete(iframeId) + } + break } - else { - // keep the last iframe + // error happened at the top level, this should never happen in user code, but it can trigger during development + case 'error': { const iframeId = e.data.id - iframes.get(iframeId)?.remove() iframes.delete(iframeId) + await client.rpc.onUnhandledError(e.data.error, e.data.errorType) + if (iframeId === ID_ALL) { + runningFiles.clear() + } + else { + runningFiles.delete(iframeId) + } + if (!runningFiles.size) { + await done() + } + break } - break - } - // error happened at the top level, this should never happen in user code, but it can trigger during development - case 'error': { - const iframeId = e.data.id - iframes.delete(iframeId) - await client.rpc.onUnhandledError(e.data.error, e.data.errorType) - if (iframeId === ID_ALL) - runningFiles.clear() - else - runningFiles.delete(iframeId) - if (!runningFiles.size) + case 'mock:invalidate': + mocker.invalidate() + break + case 'unmock': + await mocker.unmock(e.data) + break + case 'mock': + await mocker.mock(e.data) + break + case 'mock-factory:error': + case 'mock-factory:response': + // handled manually + break + default: { + e.data satisfies never + + await client.rpc.onUnhandledError( + { + name: 'Unexpected Event', + message: `Unexpected event: ${(e.data as any).type}`, + }, + 'Unexpected Event', + ) await done() - break - } - case 'mock:invalidate': - mocker.invalidate() - break - case 'unmock': - await mocker.unmock(e.data) - break - case 'mock': - await mocker.mock(e.data) - break - case 'mock-factory:error': - case 'mock-factory:response': - // handled manually - break - default: { - e.data satisfies never - - await client.rpc.onUnhandledError({ - name: 'Unexpected Event', - message: `Unexpected event: ${(e.data as any).type}`, - }, 'Unexpected Event') - await done() + } } - } - }) + }, + ) // if page was refreshed, there will be no test files // createTesters will be called again when tests are running in the UI - if (testFiles.length) + if (testFiles.length) { await createTesters(testFiles) + } }) async function createTesters(testFiles: string[]) { @@ -186,10 +209,7 @@ async function createTesters(testFiles: string[]) { iframes.clear() if (config.isolate === false) { - const iframe = createIframe( - container, - ID_ALL, - ) + const iframe = createIframe(container, ID_ALL) await setIframeViewport(iframe, width, height) } @@ -197,21 +217,21 @@ async function createTesters(testFiles: string[]) { // otherwise, we need to wait for each iframe to finish before creating the next one // this is the most stable way to run tests in the browser for (const file of testFiles) { - const iframe = createIframe( - container, - file, - ) + const iframe = createIframe(container, file) await setIframeViewport(iframe, width, height) await new Promise((resolve) => { - channel.addEventListener('message', function handler(e: MessageEvent) { - // done and error can only be triggered by the previous iframe - if (e.data.type === 'done' || e.data.type === 'error') { - channel.removeEventListener('message', handler) - resolve() - } - }) + channel.addEventListener( + 'message', + function handler(e: MessageEvent) { + // done and error can only be triggered by the previous iframe + if (e.data.type === 'done' || e.data.type === 'error') { + channel.removeEventListener('message', handler) + resolve() + } + }, + ) }) } } @@ -224,7 +244,11 @@ function generateFileId(file: string) { return generateHash(`${path}${project}`) } -async function setIframeViewport(iframe: HTMLIFrameElement, width: number, height: number) { +async function setIframeViewport( + iframe: HTMLIFrameElement, + width: number, + height: number, +) { const ui = getUiAPI() if (ui) { await ui.setIframeViewport(width, height) diff --git a/packages/browser/src/client/public/esm-client-injector.js b/packages/browser/src/client/public/esm-client-injector.js index a1365fc91..943c12b77 100644 --- a/packages/browser/src/client/public/esm-client-injector.js +++ b/packages/browser/src/client/public/esm-client-injector.js @@ -1,18 +1,18 @@ -const moduleCache = new Map() +const moduleCache = new Map(); function wrapModule(module) { - if (typeof module === 'function') { + if (typeof module === "function") { const promise = new Promise((resolve, reject) => { - if (typeof __vitest_mocker__ === 'undefined') - return module().then(resolve, reject) + if (typeof __vitest_mocker__ === "undefined") + return module().then(resolve, reject); __vitest_mocker__.prepare().finally(() => { - module().then(resolve, reject) - }) - }) - moduleCache.set(promise, { promise, evaluated: false }) - return promise.finally(() => moduleCache.delete(promise)) + module().then(resolve, reject); + }); + }); + moduleCache.set(promise, { promise, evaluated: false }); + return promise.finally(() => moduleCache.delete(promise)); } - return module + return module; } window.__vitest_browser_runner__ = { @@ -23,25 +23,24 @@ window.__vitest_browser_runner__ = { files: { __VITEST_FILES__ }, type: { __VITEST_TYPE__ }, contextId: { __VITEST_CONTEXT_ID__ }, -} +}; -const config = __vitest_browser_runner__.config +const config = __vitest_browser_runner__.config; if (config.testNamePattern) - config.testNamePattern = parseRegexp(config.testNamePattern) + config.testNamePattern = parseRegexp(config.testNamePattern); function parseRegexp(input) { // Parse input - const m = input.match(/(\/?)(.+)\1([a-z]*)/i) + const m = input.match(/(\/?)(.+)\1([a-z]*)/i); // match nothing - if (!m) - return /$^/ + if (!m) return /$^/; // Invalid flags if (m[3] && !/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(m[3])) - return RegExp(input) + return RegExp(input); // Create the regular expression - return new RegExp(m[2], m[3]) + return new RegExp(m[2], m[3]); } diff --git a/packages/browser/src/client/rpc.ts b/packages/browser/src/client/rpc.ts index 7611cecbf..f93b8e829 100644 --- a/packages/browser/src/client/rpc.ts +++ b/packages/browser/src/client/rpc.ts @@ -1,13 +1,12 @@ -import type { - getSafeTimers, -} from '@vitest/utils' +import type { getSafeTimers } from '@vitest/utils' import { importId } from './utils' import type { VitestBrowserClient } from './client' const { get } = Reflect function withSafeTimers(getTimers: typeof getSafeTimers, fn: () => void) { - const { setTimeout, clearTimeout, setImmediate, clearImmediate } = getTimers() + const { setTimeout, clearTimeout, setImmediate, clearImmediate } + = getTimers() const currentSetTimeout = globalThis.setTimeout const currentClearTimeout = globalThis.clearTimeout @@ -34,28 +33,34 @@ function withSafeTimers(getTimers: typeof getSafeTimers, fn: () => void) { const promises = new Set>() export async function rpcDone() { - if (!promises.size) + if (!promises.size) { return + } const awaitable = Array.from(promises) return Promise.all(awaitable) } -export function createSafeRpc(client: VitestBrowserClient, getTimers: () => any): VitestBrowserClient['rpc'] { +export function createSafeRpc( + client: VitestBrowserClient, + getTimers: () => any, +): VitestBrowserClient['rpc'] { return new Proxy(client.rpc, { get(target, p, handler) { - if (p === 'then') + if (p === 'then') { return + } const sendCall = get(target, p, handler) - const safeSendCall = (...args: any[]) => withSafeTimers(getTimers, async () => { - const result = sendCall(...args) - promises.add(result) - try { - return await result - } - finally { - promises.delete(result) - } - }) + const safeSendCall = (...args: any[]) => + withSafeTimers(getTimers, async () => { + const result = sendCall(...args) + promises.add(result) + try { + return await result + } + finally { + promises.delete(result) + } + }) safeSendCall.asEvent = sendCall.asEvent return safeSendCall }, @@ -64,7 +69,9 @@ export function createSafeRpc(client: VitestBrowserClient, getTimers: () => any) export async function loadSafeRpc(client: VitestBrowserClient) { // if importing /@id/ failed, we reload the page waiting until Vite prebundles it - const { getSafeTimers } = await importId('vitest/utils') as typeof import('vitest/utils') + const { getSafeTimers } = (await importId( + 'vitest/utils', + )) as typeof import('vitest/utils') return createSafeRpc(client, getSafeTimers) } diff --git a/packages/browser/src/client/runner.ts b/packages/browser/src/client/runner.ts index 3b64befd4..df34c9343 100644 --- a/packages/browser/src/client/runner.ts +++ b/packages/browser/src/client/runner.ts @@ -10,18 +10,21 @@ interface BrowserRunnerOptions { config: ResolvedConfig } -export const browserHashMap = new Map() +export const browserHashMap = new Map< + string, + [test: boolean, timstamp: string] +>() interface CoverageHandler { takeCoverage: () => Promise } export function createBrowserRunner( - runnerClass: { new(config: ResolvedConfig): VitestRunner }, + runnerClass: { new (config: ResolvedConfig): VitestRunner }, mocker: VitestBrowserClientMocker, state: WorkerGlobalState, coverageModule: CoverageHandler | null, -): { new(options: BrowserRunnerOptions): VitestRunner } { +): { new (options: BrowserRunnerOptions): VitestRunner } { return class BrowserTestRunner extends runnerClass implements VitestRunner { public config: ResolvedConfig hashMap = browserHashMap @@ -101,9 +104,14 @@ export function createBrowserRunner( let cachedRunner: VitestRunner | null = null -export async function initiateRunner(state: WorkerGlobalState, mocker: VitestBrowserClientMocker, config: ResolvedConfig) { - if (cachedRunner) +export async function initiateRunner( + state: WorkerGlobalState, + mocker: VitestBrowserClientMocker, + config: ResolvedConfig, +) { + if (cachedRunner) { return cachedRunner + } const [ { VitestTestRunner, NodeBenchmarkRunner }, { takeCoverageInsideWorker, loadDiffConfig, loadSnapshotSerializers }, @@ -111,12 +119,16 @@ export async function initiateRunner(state: WorkerGlobalState, mocker: VitestBro importId('vitest/runners') as Promise, importId('vitest/browser') as Promise, ]) - const runnerClass = config.mode === 'test' ? VitestTestRunner : NodeBenchmarkRunner + const runnerClass + = config.mode === 'test' ? VitestTestRunner : NodeBenchmarkRunner const BrowserRunner = createBrowserRunner(runnerClass, mocker, state, { - takeCoverage: () => takeCoverageInsideWorker(config.coverage, { executeId: importId }), + takeCoverage: () => + takeCoverageInsideWorker(config.coverage, { executeId: importId }), }) - if (!config.snapshotOptions.snapshotEnvironment) - config.snapshotOptions.snapshotEnvironment = new VitestBrowserSnapshotEnvironment() + if (!config.snapshotOptions.snapshotEnvironment) { + config.snapshotOptions.snapshotEnvironment + = new VitestBrowserSnapshotEnvironment() + } const runner = new BrowserRunner({ config, }) @@ -131,22 +143,27 @@ export async function initiateRunner(state: WorkerGlobalState, mocker: VitestBro } async function updateFilesLocations(files: File[]) { - const { loadSourceMapUtils } = await importId('vitest/utils') as typeof import('vitest/utils') + const { loadSourceMapUtils } = (await importId( + 'vitest/utils', + )) as typeof import('vitest/utils') const { TraceMap, originalPositionFor } = await loadSourceMapUtils() const promises = files.map(async (file) => { const result = await rpc().getBrowserFileSourceMap(file.filepath) - if (!result) + if (!result) { return null + } const traceMap = new TraceMap(result as any) function updateLocation(task: Task) { if (task.location) { const { line, column } = originalPositionFor(traceMap, task.location) - if (line != null && column != null) + if (line != null && column != null) { task.location = { line, column: task.each ? column : column + 1 } + } } - if ('tasks' in task) + if ('tasks' in task) { task.tasks.forEach(updateLocation) + } } file.tasks.forEach(updateLocation) return null diff --git a/packages/browser/src/client/tester.html b/packages/browser/src/client/tester.html index 140989f03..3136578ad 100644 --- a/packages/browser/src/client/tester.html +++ b/packages/browser/src/client/tester.html @@ -19,7 +19,14 @@ {__VITEST_SCRIPTS__} - + {__VITEST_APPEND__} diff --git a/packages/browser/src/client/tester.ts b/packages/browser/src/client/tester.ts index 9cd1c19d6..9f33f2a27 100644 --- a/packages/browser/src/client/tester.ts +++ b/packages/browser/src/client/tester.ts @@ -6,7 +6,11 @@ import { browserHashMap, initiateRunner } from './runner' import { getBrowserState, getConfig, importId } from './utils' import { loadSafeRpc } from './rpc' import { VitestBrowserClientMocker } from './mocker' -import { registerUnexpectedErrors, registerUnhandledErrors, serializeError } from './unhandled' +import { + registerUnexpectedErrors, + registerUnhandledErrors, + serializeError, +} from './unhandled' const stopErrorHandler = registerUnhandledErrors() @@ -15,26 +19,44 @@ const reloadStart = url.searchParams.get('__reloadStart') function debug(...args: unknown[]) { const debug = getConfig().env.VITEST_BROWSER_DEBUG - if (debug && debug !== 'false') + if (debug && debug !== 'false') { client.rpc.debug(...args.map(String)) + } } -async function tryCall(fn: () => Promise): Promise { +async function tryCall( + fn: () => Promise, +): Promise { try { return await fn() } catch (err: any) { const now = Date.now() // try for 30 seconds - const canTry = !reloadStart || (now - Number(reloadStart) < 30_000) + const canTry = !reloadStart || now - Number(reloadStart) < 30_000 const errorStack = (() => { - if (!err) + if (!err) { return null - return err.stack?.includes(err.message) ? err.stack : `${err.message}\n${err.stack}` + } + return err.stack?.includes(err.message) + ? err.stack + : `${err.message}\n${err.stack}` })() - debug('failed to resolve runner', 'trying again:', canTry, 'time is', now, 'reloadStart is', reloadStart, ':\n', errorStack) + debug( + 'failed to resolve runner', + 'trying again:', + canTry, + 'time is', + now, + 'reloadStart is', + reloadStart, + ':\n', + errorStack, + ) if (!canTry) { - const error = serializeError(new Error('Vitest failed to load its runner after 30 seconds.')) + const error = serializeError( + new Error('Vitest failed to load its runner after 30 seconds.'), + ) error.cause = serializeError(err) await client.rpc.onUnhandledError(error, 'Preload Error') @@ -114,14 +136,17 @@ async function prepareTestEnvironment(files: string[]) { const version = url.searchParams.get('browserv') || '' files.forEach((filename) => { const currentVersion = browserHashMap.get(filename) - if (!currentVersion || currentVersion[1] !== version) + if (!currentVersion || currentVersion[1] !== version) { browserHashMap.set(filename, [true, version]) + } }) - const [runner, { startTests, setupCommonEnv, SpyModule }] = await Promise.all([ - initiateRunner(state, mocker, config), - importId('vitest/browser') as Promise, - ]) + const [runner, { startTests, setupCommonEnv, SpyModule }] = await Promise.all( + [ + initiateRunner(state, mocker, config), + importId('vitest/browser') as Promise, + ], + ) mocker.setSpyModule(SpyModule) mocker.setupWorker() @@ -155,7 +180,10 @@ async function runTests(files: string[]) { debug('client is connected to ws server') - let preparedData: Awaited> | undefined | false + let preparedData: + | Awaited> + | undefined + | false // if importing /@id/ failed, we reload the page waiting until Vite prebundles it try { @@ -191,8 +219,9 @@ async function runTests(files: string[]) { try { await setupCommonEnv(config) - for (const file of files) + for (const file of files) { await startTests([file], runner) + } } finally { state.environmentTeardownRun = true diff --git a/packages/browser/src/client/unhandled.ts b/packages/browser/src/client/unhandled.ts index e94efe7c6..d9f049573 100644 --- a/packages/browser/src/client/unhandled.ts +++ b/packages/browser/src/client/unhandled.ts @@ -31,22 +31,30 @@ async function defaultErrorReport(type: string, unhandledError: any) { function catchWindowErrors(cb: (e: ErrorEvent) => void) { let userErrorListenerCount = 0 function throwUnhandlerError(e: ErrorEvent) { - if (userErrorListenerCount === 0 && e.error != null) + if (userErrorListenerCount === 0 && e.error != null) { cb(e) - else + } + else { console.error(e.error) + } } const addEventListener = window.addEventListener.bind(window) const removeEventListener = window.removeEventListener.bind(window) window.addEventListener('error', throwUnhandlerError) - window.addEventListener = function (...args: Parameters) { - if (args[0] === 'error') + window.addEventListener = function ( + ...args: Parameters + ) { + if (args[0] === 'error') { userErrorListenerCount++ + } return addEventListener.apply(this, args) } - window.removeEventListener = function (...args: Parameters) { - if (args[0] === 'error' && userErrorListenerCount) + window.removeEventListener = function ( + ...args: Parameters + ) { + if (args[0] === 'error' && userErrorListenerCount) { userErrorListenerCount-- + } return removeEventListener.apply(this, args) } return function clearErrorHandlers() { @@ -55,8 +63,11 @@ function catchWindowErrors(cb: (e: ErrorEvent) => void) { } export function registerUnhandledErrors() { - const stopErrorHandler = catchWindowErrors(e => defaultErrorReport('Error', e.error)) - const stopRejectionHandler = on('unhandledrejection', e => defaultErrorReport('Unhandled Rejection', e.reason)) + const stopErrorHandler = catchWindowErrors(e => + defaultErrorReport('Error', e.error), + ) + const stopRejectionHandler = on('unhandledrejection', e => + defaultErrorReport('Unhandled Rejection', e.reason)) return () => { stopErrorHandler() stopRejectionHandler() @@ -64,12 +75,21 @@ export function registerUnhandledErrors() { } export function registerUnexpectedErrors(rpc: typeof client.rpc) { - catchWindowErrors(event => reportUnexpectedError(rpc, 'Error', event.error)) - on('unhandledrejection', event => reportUnexpectedError(rpc, 'Unhandled Rejection', event.reason)) + catchWindowErrors(event => + reportUnexpectedError(rpc, 'Error', event.error), + ) + on('unhandledrejection', event => + reportUnexpectedError(rpc, 'Unhandled Rejection', event.reason)) } -async function reportUnexpectedError(rpc: typeof client.rpc, type: string, error: any) { - const { processError } = await importId('vitest/browser') as typeof import('vitest/browser') +async function reportUnexpectedError( + rpc: typeof client.rpc, + type: string, + error: any, +) { + const { processError } = (await importId( + 'vitest/browser', + )) as typeof import('vitest/browser') const processedError = processError(error) await rpc.onUnhandledError(processedError, type) } diff --git a/packages/browser/src/client/vite.config.ts b/packages/browser/src/client/vite.config.ts index dfa8fe446..9292b4b05 100644 --- a/packages/browser/src/client/vite.config.ts +++ b/packages/browser/src/client/vite.config.ts @@ -27,27 +27,35 @@ export default defineConfig({ name: 'virtual:msw', enforce: 'pre', resolveId(id) { - if (id.startsWith('msw')) + if (id.startsWith('msw')) { return `/__virtual_vitest__:${id}` + } }, }, { name: 'copy-ui-plugin', /* eslint-disable no-console */ closeBundle: async () => { - const root = resolve(fileURLToPath(import.meta.url), '../../../../../packages') + const root = resolve( + fileURLToPath(import.meta.url), + '../../../../../packages', + ) const ui = resolve(root, 'ui/dist/client') const browser = resolve(root, 'browser/dist/client/__vitest__/') - const timeout = setTimeout(() => console.log('[copy-ui-plugin] Waiting for UI to be built...'), 1000) + const timeout = setTimeout( + () => console.log('[copy-ui-plugin] Waiting for UI to be built...'), + 1000, + ) await waitFor(() => fs.existsSync(ui)) clearTimeout(timeout) const files = fg.sync('**/*', { cwd: ui }) - if (fs.existsSync(browser)) + if (fs.existsSync(browser)) { fs.rmSync(browser, { recursive: true }) + } fs.mkdirSync(browser, { recursive: true }) fs.mkdirSync(resolve(browser, 'assets')) @@ -63,11 +71,13 @@ export default defineConfig({ }) async function waitFor(method: () => boolean, retries = 100): Promise { - if (method()) + if (method()) { return + } - if (retries === 0) + if (retries === 0) { throw new Error('Timeout in waitFor') + } await new Promise(resolve => setTimeout(resolve, 500)) diff --git a/packages/browser/src/node/automocker.ts b/packages/browser/src/node/automocker.ts index fc6bb550d..551f67e96 100644 --- a/packages/browser/src/node/automocker.ts +++ b/packages/browser/src/node/automocker.ts @@ -1,4 +1,14 @@ -import type { Declaration, ExportDefaultDeclaration, ExportNamedDeclaration, Expression, Identifier, Literal, Pattern, Positioned, Program } from '@vitest/utils/ast' +import type { + Declaration, + ExportDefaultDeclaration, + ExportNamedDeclaration, + Expression, + Identifier, + Literal, + Pattern, + Positioned, + Program, +} from '@vitest/utils/ast' import MagicString from 'magic-string' // TODO: better source map replacement @@ -28,21 +38,25 @@ export function automockModule(code: string, parse: (code: string) => Program) { // export const [test, ...rest] = [1, 2, 3] else if (expression.type === 'ArrayPattern') { expression.elements.forEach((element) => { - if (!element) + if (!element) { return + } traversePattern(element) }) } else if (expression.type === 'ObjectPattern') { expression.properties.forEach((property) => { // export const { ...rest } = {} - if (property.type === 'RestElement') + if (property.type === 'RestElement') { traversePattern(property) + } // export const { test, test2: alias } = {} - else if (property.type === 'Property') + else if (property.type === 'Property') { traversePattern(property.value) - else + } + else { property satisfies never + } }) } else if (expression.type === 'RestElement') { @@ -51,12 +65,16 @@ export function automockModule(code: string, parse: (code: string) => Program) { // const [name[1], name[2]] = [] // cannot be used in export else if (expression.type === 'AssignmentPattern') { - throw new Error(`AssignmentPattern is not supported. Please open a new bug report.`) + throw new Error( + `AssignmentPattern is not supported. Please open a new bug report.`, + ) } // const test = thing.func() // cannot be used in export else if (expression.type === 'MemberExpression') { - throw new Error(`MemberExpression is not supported. Please open a new bug report.`) + throw new Error( + `MemberExpression is not supported. Please open a new bug report.`, + ) } else { expression satisfies never @@ -89,9 +107,7 @@ export function automockModule(code: string, parse: (code: string) => Program) { const exported = specifier.exported as Literal | Identifier allSpecifiers.push({ - alias: exported.type === 'Literal' - ? exported.raw! - : exported.name, + alias: exported.type === 'Literal' ? exported.raw! : exported.name, name: specifier.local.name, }) }) @@ -106,13 +122,13 @@ export function automockModule(code: string, parse: (code: string) => Program) { importNames.push([specifier.local.name, importedName]) allSpecifiers.push({ name: importedName, - alias: exported.type === 'Literal' - ? exported.raw! - : exported.name, + alias: exported.type === 'Literal' ? exported.raw! : exported.name, }) }) - const importString = `import { ${importNames.map(([name, alias]) => `${name} as ${alias}`).join(', ')} } from '${source.value}'` + const importString = `import { ${importNames + .map(([name, alias]) => `${name} as ${alias}`) + .join(', ')} } from '${source.value}'` m.overwrite(node.start, node.end, importString) } @@ -131,13 +147,17 @@ const __vitest_es_current_module__ = { } const __vitest_mocked_module__ = __vitest_mocker__.mockObject(__vitest_es_current_module__) ` - const assigning = allSpecifiers.map(({ name }, index) => { - return `const __vitest_mocked_${index}__ = __vitest_mocked_module__["${name}"]` - }).join('\n') + const assigning = allSpecifiers + .map(({ name }, index) => { + return `const __vitest_mocked_${index}__ = __vitest_mocked_module__["${name}"]` + }) + .join('\n') - const redeclarations = allSpecifiers.map(({ name, alias }, index) => { - return ` __vitest_mocked_${index}__ as ${alias || name},` - }).join('\n') + const redeclarations = allSpecifiers + .map(({ name, alias }, index) => { + return ` __vitest_mocked_${index}__ as ${alias || name},` + }) + .join('\n') const specifiersExports = ` export { ${redeclarations} diff --git a/packages/browser/src/node/commands/fs.ts b/packages/browser/src/node/commands/fs.ts index fc10e7a91..d9275b73f 100644 --- a/packages/browser/src/node/commands/fs.ts +++ b/packages/browser/src/node/commands/fs.ts @@ -5,29 +5,43 @@ import type { BrowserCommand, WorkspaceProject } from 'vitest/node' import type { BrowserCommands } from '../../../context' function assertFileAccess(path: string, project: WorkspaceProject) { - if (!isFileServingAllowed(path, project.server) && !isFileServingAllowed(path, project.ctx.server)) - throw new Error(`Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`) + if ( + !isFileServingAllowed(path, project.server) + && !isFileServingAllowed(path, project.ctx.server) + ) { + throw new Error( + `Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`, + ) + } } -export const readFile: BrowserCommand> = async ({ project, testPath = process.cwd() }, path, options = {}) => { +export const readFile: BrowserCommand< + Parameters +> = async ({ project, testPath = process.cwd() }, path, options = {}) => { const filepath = resolve(dirname(testPath), path) assertFileAccess(filepath, project) // never return a Buffer - if (typeof options === 'object' && !options.encoding) + if (typeof options === 'object' && !options.encoding) { options.encoding = 'utf-8' + } return fsp.readFile(filepath, options) } -export const writeFile: BrowserCommand> = async ({ project, testPath = process.cwd() }, path, data, options) => { +export const writeFile: BrowserCommand< + Parameters +> = async ({ project, testPath = process.cwd() }, path, data, options) => { const filepath = resolve(dirname(testPath), path) assertFileAccess(filepath, project) const dir = dirname(filepath) - if (!fs.existsSync(dir)) + if (!fs.existsSync(dir)) { await fsp.mkdir(dir, { recursive: true }) + } await fsp.writeFile(filepath, data, options) } -export const removeFile: BrowserCommand> = async ({ project, testPath = process.cwd() }, path) => { +export const removeFile: BrowserCommand< + Parameters +> = async ({ project, testPath = process.cwd() }, path) => { const filepath = resolve(dirname(testPath), path) assertFileAccess(filepath, project) await fsp.rm(filepath) diff --git a/packages/browser/src/node/commands/index.ts b/packages/browser/src/node/commands/index.ts index 0f6946e20..a38fb2f2e 100644 --- a/packages/browser/src/node/commands/index.ts +++ b/packages/browser/src/node/commands/index.ts @@ -1,9 +1,5 @@ import { click } from './click' -import { - readFile, - removeFile, - writeFile, -} from './fs' +import { readFile, removeFile, writeFile } from './fs' import { sendKeys } from './keyboard' import { screenshot } from './screenshot' diff --git a/packages/browser/src/node/commands/keyboard.ts b/packages/browser/src/node/commands/keyboard.ts index 6b7959971..cfab77868 100644 --- a/packages/browser/src/node/commands/keyboard.ts +++ b/packages/browser/src/node/commands/keyboard.ts @@ -19,13 +19,16 @@ function isObject(payload: unknown): payload is Record { function isSendKeysPayload(payload: unknown): boolean { const validOptions = ['type', 'press', 'down', 'up'] - if (!isObject(payload)) + if (!isObject(payload)) { throw new Error('You must provide a `SendKeysPayload` object') + } const numberOfValidOptions = Object.keys(payload).filter(key => validOptions.includes(key), ).length - const unknownOptions = Object.keys(payload).filter(key => !validOptions.includes(key)) + const unknownOptions = Object.keys(payload).filter( + key => !validOptions.includes(key), + ) if (numberOfValidOptions > 1) { throw new Error( @@ -41,8 +44,11 @@ function isSendKeysPayload(payload: unknown): boolean { )}.`, ) } - if (unknownOptions.length > 0) - throw new Error(`Unknown options \`${unknownOptions.join(', ')}\` present.`) + if (unknownOptions.length > 0) { + throw new Error( + `Unknown options \`${unknownOptions.join(', ')}\` present.`, + ) + } return true } @@ -63,31 +69,43 @@ function isUpPayload(payload: SendKeysPayload): payload is UpPayload { return 'up' in payload } -export const sendKeys: BrowserCommand> = async ({ provider, contextId }, payload) => { - if (!isSendKeysPayload(payload) || !payload) +export const sendKeys: BrowserCommand< + Parameters +> = async ({ provider, contextId }, payload) => { + if (!isSendKeysPayload(payload) || !payload) { throw new Error('You must provide a `SendKeysPayload` object') + } if (provider instanceof PlaywrightBrowserProvider) { const page = provider.getPage(contextId) - if (isTypePayload(payload)) + if (isTypePayload(payload)) { await page.keyboard.type(payload.type) - else if (isPressPayload(payload)) + } + else if (isPressPayload(payload)) { await page.keyboard.press(payload.press) - else if (isDownPayload(payload)) + } + else if (isDownPayload(payload)) { await page.keyboard.down(payload.down) - else if (isUpPayload(payload)) + } + else if (isUpPayload(payload)) { await page.keyboard.up(payload.up) + } } else if (provider instanceof WebdriverBrowserProvider) { const browser = provider.browser! - if (isTypePayload(payload)) + if (isTypePayload(payload)) { await browser.keys(payload.type.split('')) - else if (isPressPayload(payload)) + } + else if (isPressPayload(payload)) { await browser.keys([payload.press]) - else + } + else { throw new Error('Only "press" and "type" are supported by webdriverio.') + } } else { - throw new TypeError(`"sendKeys" is not supported for ${provider.name} browser provider.`) + throw new TypeError( + `"sendKeys" is not supported for ${provider.name} browser provider.`, + ) } } diff --git a/packages/browser/src/node/commands/screenshot.ts b/packages/browser/src/node/commands/screenshot.ts index c9e44fa33..33971b92a 100644 --- a/packages/browser/src/node/commands/screenshot.ts +++ b/packages/browser/src/node/commands/screenshot.ts @@ -8,11 +8,20 @@ import { PlaywrightBrowserProvider } from '../providers/playwright' import { WebdriverBrowserProvider } from '../providers/webdriver' // TODO: expose provider specific options in types -export const screenshot: BrowserCommand<[string, ScreenshotOptions]> = async (context, name: string, options = {}) => { - if (!context.testPath) +export const screenshot: BrowserCommand<[string, ScreenshotOptions]> = async ( + context, + name: string, + options = {}, +) => { + if (!context.testPath) { throw new Error(`Cannot take a screenshot without a test path`) + } - const path = resolveScreenshotPath(context.testPath, name, context.project.config) + const path = resolveScreenshotPath( + context.testPath, + name, + context.project.config, + ) const savePath = normalize(path) await mkdir(dirname(path), { recursive: true }) @@ -42,10 +51,16 @@ export const screenshot: BrowserCommand<[string, ScreenshotOptions]> = async (co return path } - throw new Error(`Provider "${context.provider.name}" does not support screenshots`) + throw new Error( + `Provider "${context.provider.name}" does not support screenshots`, + ) } -function resolveScreenshotPath(testPath: string, name: string, config: ResolvedConfig) { +function resolveScreenshotPath( + testPath: string, + name: string, + config: ResolvedConfig, +) { const dir = dirname(testPath) const base = basename(testPath) if (config.browser.screenshotDirectory) { diff --git a/packages/browser/src/node/commands/utils.ts b/packages/browser/src/node/commands/utils.ts index 6e4db8456..c95a31b8e 100644 --- a/packages/browser/src/node/commands/utils.ts +++ b/packages/browser/src/node/commands/utils.ts @@ -6,7 +6,7 @@ export type UserEventCommand any> = BrowserCommand< type ConvertElementToLocator = T extends Element ? string : T type ConvertUserEventParameters = { - [K in keyof T]: ConvertElementToLocator + [K in keyof T]: ConvertElementToLocator; } export function defineBrowserCommand( diff --git a/packages/browser/src/node/esmInjector.ts b/packages/browser/src/node/esmInjector.ts index 08d3e370e..c15198346 100644 --- a/packages/browser/src/node/esmInjector.ts +++ b/packages/browser/src/node/esmInjector.ts @@ -3,7 +3,11 @@ import type { PluginContext } from 'rollup' import { esmWalker } from '@vitest/utils/ast' import type { Expression, Positioned } from '@vitest/utils/ast' -export function injectDynamicImport(code: string, id: string, parse: PluginContext['parse']) { +export function injectDynamicImport( + code: string, + id: string, + parse: PluginContext['parse'], +) { const s = new MagicString(code) let ast: any @@ -23,7 +27,11 @@ export function injectDynamicImport(code: string, id: string, parse: PluginConte }, onDynamicImport(node) { const replace = '__vitest_browser_runner__.wrapModule(() => import(' - s.overwrite(node.start, (node.source as Positioned).start, replace) + s.overwrite( + node.start, + (node.source as Positioned).start, + replace, + ) s.overwrite(node.end - 1, node.end, '))') }, }) diff --git a/packages/browser/src/node/index.ts b/packages/browser/src/node/index.ts index 78b4e9f6a..9ab9d88bd 100644 --- a/packages/browser/src/node/index.ts +++ b/packages/browser/src/node/index.ts @@ -31,39 +31,54 @@ export default (project: WorkspaceProject, base = '/'): Plugin[] => { } }, async configureServer(server) { - const testerHtml = readFile(resolve(distRoot, 'client/tester.html'), 'utf8') + const testerHtml = readFile( + resolve(distRoot, 'client/tester.html'), + 'utf8', + ) const orchestratorHtml = project.config.browser.ui ? readFile(resolve(distRoot, 'client/__vitest__/index.html'), 'utf8') : readFile(resolve(distRoot, 'client/orchestrator.html'), 'utf8') - const injectorJs = readFile(resolve(distRoot, 'client/esm-client-injector.js'), 'utf8') + const injectorJs = readFile( + resolve(distRoot, 'client/esm-client-injector.js'), + 'utf8', + ) const manifest = (async () => { - return JSON.parse(await readFile(`${distRoot}/client/.vite/manifest.json`, 'utf8')) + return JSON.parse( + await readFile(`${distRoot}/client/.vite/manifest.json`, 'utf8'), + ) })() const favicon = `${base}favicon.svg` const testerPrefix = `${base}__vitest_test__/__test__/` server.middlewares.use((_req, res, next) => { const headers = server.config.server.headers if (headers) { - for (const name in headers) + for (const name in headers) { res.setHeader(name, headers[name]!) + } } next() }) let orchestratorScripts: string | undefined let testerScripts: string | undefined server.middlewares.use(async (req, res, next) => { - if (!req.url) + if (!req.url) { return next() + } const url = new URL(req.url, 'http://localhost') - if (!url.pathname.startsWith(testerPrefix) && url.pathname !== base) + if (!url.pathname.startsWith(testerPrefix) && url.pathname !== base) { return next() + } - res.setHeader('Cache-Control', 'no-cache, max-age=0, must-revalidate') + res.setHeader( + 'Cache-Control', + 'no-cache, max-age=0, must-revalidate', + ) res.setHeader('Content-Type', 'text/html; charset=utf-8') const config = wrapConfig(project.getSerializableConfig()) config.env ??= {} - config.env.VITEST_BROWSER_DEBUG = process.env.VITEST_BROWSER_DEBUG || '' + config.env.VITEST_BROWSER_DEBUG + = process.env.VITEST_BROWSER_DEBUG || '' // remove custom iframe related headers to allow the iframe to load res.removeHeader('X-Frame-Options') @@ -72,8 +87,9 @@ export default (project: WorkspaceProject, base = '/'): Plugin[] => { let contextId = url.searchParams.get('contextId') // it's possible to open the page without a context, // for now, let's assume it should be the first one - if (!contextId) + if (!contextId) { contextId = project.browserState.keys().next().value ?? 'none' + } const files = project.browserState.get(contextId!)?.files ?? [] @@ -83,15 +99,20 @@ export default (project: WorkspaceProject, base = '/'): Plugin[] => { root: project.browser!.config.root, }), __VITEST_FILES__: JSON.stringify(files), - __VITEST_TYPE__: url.pathname === base ? '"orchestrator"' : '"tester"', + __VITEST_TYPE__: + url.pathname === base ? '"orchestrator"' : '"tester"', __VITEST_CONTEXT_ID__: JSON.stringify(contextId), }) // disable CSP for the orchestrator as we are the ones controlling it res.removeHeader('Content-Security-Policy') - if (!orchestratorScripts) - orchestratorScripts = await formatScripts(project.config.browser.orchestratorScripts, server) + if (!orchestratorScripts) { + orchestratorScripts = await formatScripts( + project.config.browser.orchestratorScripts, + server, + ) + } let baseHtml = await orchestratorHtml @@ -99,14 +120,16 @@ export default (project: WorkspaceProject, base = '/'): Plugin[] => { if (project.config.browser.ui) { const manifestContent = await manifest const jsEntry = manifestContent['orchestrator.html'].file - baseHtml = baseHtml.replaceAll('./assets/', `${base}__vitest__/assets/`).replace( - '', - [ - '', - '{__VITEST_SCRIPTS__}', - ``, - ].join('\n'), - ) + baseHtml = baseHtml + .replaceAll('./assets/', `${base}__vitest__/assets/`) + .replace( + '', + [ + '', + '{__VITEST_SCRIPTS__}', + ``, + ].join('\n'), + ) } const html = replacer(baseHtml, { @@ -125,14 +148,23 @@ export default (project: WorkspaceProject, base = '/'): Plugin[] => { if (typeof csp === 'string') { // add frame-ancestors to allow the iframe to be loaded by Vitest, // but keep the rest of the CSP - res.setHeader('Content-Security-Policy', csp.replace(/frame-ancestors [^;]+/, 'frame-ancestors *')) + res.setHeader( + 'Content-Security-Policy', + csp.replace(/frame-ancestors [^;]+/, 'frame-ancestors *'), + ) } - const [contextId, testFile] = url.pathname.slice(testerPrefix.length).split('/') + const [contextId, testFile] = url.pathname + .slice(testerPrefix.length) + .split('/') const decodedTestFile = decodeURIComponent(testFile) const testFiles = await project.globTestFiles() // if decoded test file is "__vitest_all__" or not in the list of known files, run all tests - const tests = decodedTestFile === '__vitest_all__' || !testFiles.includes(decodedTestFile) ? '__vitest_browser_runner__.files' : JSON.stringify([decodedTestFile]) + const tests + = decodedTestFile === '__vitest_all__' + || !testFiles.includes(decodedTestFile) + ? '__vitest_browser_runner__.files' + : JSON.stringify([decodedTestFile]) const iframeId = JSON.stringify(decodedTestFile) const files = project.browserState.get(contextId)?.files ?? [] @@ -142,12 +174,17 @@ export default (project: WorkspaceProject, base = '/'): Plugin[] => { __VITEST_VITE_CONFIG__: JSON.stringify({ root: project.browser!.config.root, }), - __VITEST_TYPE__: url.pathname === base ? '"orchestrator"' : '"tester"', + __VITEST_TYPE__: + url.pathname === base ? '"orchestrator"' : '"tester"', __VITEST_CONTEXT_ID__: JSON.stringify(contextId), }) - if (!testerScripts) - testerScripts = await formatScripts(project.config.browser.testerScripts, server) + if (!testerScripts) { + testerScripts = await formatScripts( + project.config.browser.testerScripts, + server, + ) + } const html = replacer(await testerHtml, { __VITEST_FAVICON__: favicon, @@ -155,8 +192,8 @@ export default (project: WorkspaceProject, base = '/'): Plugin[] => { __VITEST_SCRIPTS__: testerScripts, __VITEST_INJECTOR__: injector, __VITEST_APPEND__: - // TODO: have only a single global variable to not pollute the global scope -`` - }) + } + const promises = scripts.map( + async ({ content, src, async, id, type = 'module' }, index) => { + const srcLink + = (src ? (await server.pluginContainer.resolveId(src))?.id : undefined) + || src + const transformId + = srcLink + || join(server.config.root, `virtual__${id || `injected-${index}.js`}`) + await server.moduleGraph.ensureEntryFromUrl(transformId) + const contentProcessed + = content && type === 'module' + ? (await server.pluginContainer.transform(content, transformId)).code + : content + return `` + }, + ) return (await Promise.all(promises)).join('\n') } diff --git a/packages/browser/src/node/plugins/pluginContext.ts b/packages/browser/src/node/plugins/pluginContext.ts index 146164999..108dc581d 100644 --- a/packages/browser/src/node/plugins/pluginContext.ts +++ b/packages/browser/src/node/plugins/pluginContext.ts @@ -13,39 +13,55 @@ const __dirname = dirname(fileURLToPath(import.meta.url)) export default function BrowserContext(project: WorkspaceProject): Plugin { project.config.browser.commands ??= {} - for (const [name, command] of Object.entries(builtinCommands)) + for (const [name, command] of Object.entries(builtinCommands)) { project.config.browser.commands[name] ??= command + } // validate names because they can't be used as identifiers for (const command in project.config.browser.commands) { - if (!/^[a-z_$][\w$]*$/i.test(command)) - throw new Error(`Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`) + if (!/^[a-z_$][\w$]*$/i.test(command)) { + throw new Error( + `Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`, + ) + } } return { name: 'vitest:browser:virtual-module:context', enforce: 'pre', resolveId(id) { - if (id === ID_CONTEXT) + if (id === ID_CONTEXT) { return VIRTUAL_ID_CONTEXT + } }, load(id) { - if (id === VIRTUAL_ID_CONTEXT) + if (id === VIRTUAL_ID_CONTEXT) { return generateContextFile.call(this, project) + } }, } } -async function generateContextFile(this: PluginContext, project: WorkspaceProject) { +async function generateContextFile( + this: PluginContext, + project: WorkspaceProject, +) { const commands = Object.keys(project.config.browser.commands ?? {}) - const filepathCode = '__vitest_worker__.filepath || __vitest_worker__.current?.file?.filepath || undefined' + const filepathCode + = '__vitest_worker__.filepath || __vitest_worker__.current?.file?.filepath || undefined' const provider = project.browserProvider! - const commandsCode = commands.filter(command => !command.startsWith('__vitest')).map((command) => { - return ` ["${command}"]: (...args) => rpc().triggerCommand(contextId, "${command}", filepath(), args),` - }).join('\n') + const commandsCode = commands + .filter(command => !command.startsWith('__vitest')) + .map((command) => { + return ` ["${command}"]: (...args) => rpc().triggerCommand(contextId, "${command}", filepath(), args),` + }) + .join('\n') - const userEventNonProviderImport = await getUserEventImport(provider, this.resolve.bind(this)) + const userEventNonProviderImport = await getUserEventImport( + provider, + this.resolve.bind(this), + ) const distContextPath = slash(`/@fs/${resolve(__dirname, 'context.js')}`) return ` @@ -65,16 +81,25 @@ export const server = { } } export const commands = server.commands -export const userEvent = ${provider.name === 'preview' ? '__vitest_user_event__' : '__userEvent_CDP__'} +export const userEvent = ${ + provider.name === 'preview' ? '__vitest_user_event__' : '__userEvent_CDP__' + } export { page } ` } -async function getUserEventImport(provider: BrowserProvider, resolve: (id: string, importer: string) => Promise) { - if (provider.name !== 'preview') +async function getUserEventImport( + provider: BrowserProvider, + resolve: (id: string, importer: string) => Promise, +) { + if (provider.name !== 'preview') { return '' + } const resolved = await resolve('@testing-library/user-event', __dirname) - if (!resolved) + if (!resolved) { throw new Error(`Failed to resolve user-event package from ${__dirname}`) - return `import { userEvent as __vitest_user_event__ } from '${slash(`/@fs/${resolved.id}`)}'` + } + return `import { userEvent as __vitest_user_event__ } from '${slash( + `/@fs/${resolved.id}`, + )}'` } diff --git a/packages/browser/src/node/plugins/pluginDynamicImport.ts b/packages/browser/src/node/plugins/pluginDynamicImport.ts index fe5ac3c74..bf3412dbd 100644 --- a/packages/browser/src/node/plugins/pluginDynamicImport.ts +++ b/packages/browser/src/node/plugins/pluginDynamicImport.ts @@ -9,8 +9,9 @@ export default (): Plugin => { enforce: 'post', transform(source, id) { // TODO: test is not called for static imports - if (!regexDynamicImport.test(source)) + if (!regexDynamicImport.test(source)) { return + } return injectDynamicImport(source, id, this.parse) }, } diff --git a/packages/browser/src/node/providers/playwright.ts b/packages/browser/src/node/providers/playwright.ts index cfb7b5261..3ba9b44ae 100644 --- a/packages/browser/src/node/providers/playwright.ts +++ b/packages/browser/src/node/providers/playwright.ts @@ -1,10 +1,21 @@ -import type { Browser, BrowserContext, BrowserContextOptions, LaunchOptions, Page } from 'playwright' -import type { BrowserProvider, BrowserProviderInitializationOptions, WorkspaceProject } from 'vitest/node' +import type { + Browser, + BrowserContext, + BrowserContextOptions, + LaunchOptions, + Page, +} from 'playwright' +import type { + BrowserProvider, + BrowserProviderInitializationOptions, + WorkspaceProject, +} from 'vitest/node' export const playwrightBrowsers = ['firefox', 'webkit', 'chromium'] as const -export type PlaywrightBrowser = typeof playwrightBrowsers[number] +export type PlaywrightBrowser = (typeof playwrightBrowsers)[number] -export interface PlaywrightProviderOptions extends BrowserProviderInitializationOptions { +export interface PlaywrightProviderOptions + extends BrowserProviderInitializationOptions { browser: PlaywrightBrowser } @@ -31,18 +42,23 @@ export class PlaywrightBrowserProvider implements BrowserProvider { return playwrightBrowsers } - initialize(project: WorkspaceProject, { browser, options }: PlaywrightProviderOptions) { + initialize( + project: WorkspaceProject, + { browser, options }: PlaywrightProviderOptions, + ) { this.ctx = project this.browserName = browser this.options = options as any } private async openBrowser() { - if (this.browserPromise) + if (this.browserPromise) { return this.browserPromise + } - if (this.browser) + if (this.browser) { return this.browser + } this.browserPromise = (async () => { const options = this.ctx.config.browser @@ -62,8 +78,9 @@ export class PlaywrightBrowserProvider implements BrowserProvider { } private async createContext(contextId: string) { - if (this.contexts.has(contextId)) + if (this.contexts.has(contextId)) { return this.contexts.get(contextId)! + } const browser = await this.openBrowser() const context = await browser.newContext({ @@ -77,8 +94,9 @@ export class PlaywrightBrowserProvider implements BrowserProvider { public getPage(contextId: string) { const page = this.pages.get(contextId) - if (!page) + if (!page) { throw new Error(`Page "${contextId}" not found`) + } return page } diff --git a/packages/browser/src/node/providers/preview.ts b/packages/browser/src/node/providers/preview.ts index 45bfde315..df2ebe522 100644 --- a/packages/browser/src/node/providers/preview.ts +++ b/packages/browser/src/node/providers/preview.ts @@ -22,14 +22,18 @@ export class PreviewBrowserProvider implements BrowserProvider { async initialize(ctx: WorkspaceProject) { this.ctx = ctx this.open = false - if (ctx.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') + if (ctx.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', + ) + } } async openPage(_contextId: string, url: string) { this.open = true - if (!this.ctx.browser) + if (!this.ctx.browser) { throw new Error('Browser is not initialized') + } const options = this.ctx.browser.config.server const _open = options.open options.open = url @@ -37,6 +41,5 @@ export class PreviewBrowserProvider implements BrowserProvider { options.open = _open } - async close() { - } + async close() {} } diff --git a/packages/browser/src/node/providers/webdriver.ts b/packages/browser/src/node/providers/webdriver.ts index e7a90fe53..074592ece 100644 --- a/packages/browser/src/node/providers/webdriver.ts +++ b/packages/browser/src/node/providers/webdriver.ts @@ -1,10 +1,15 @@ -import type { BrowserProvider, BrowserProviderInitializationOptions, WorkspaceProject } from 'vitest/node' +import type { + BrowserProvider, + BrowserProviderInitializationOptions, + WorkspaceProject, +} from 'vitest/node' import type { RemoteOptions } from 'webdriverio' const webdriverBrowsers = ['firefox', 'chrome', 'edge', 'safari'] as const -type WebdriverBrowser = typeof webdriverBrowsers[number] +type WebdriverBrowser = (typeof webdriverBrowsers)[number] -interface WebdriverProviderOptions extends BrowserProviderInitializationOptions { +interface WebdriverProviderOptions + extends BrowserProviderInitializationOptions { browser: WebdriverBrowser } @@ -23,7 +28,10 @@ export class WebdriverBrowserProvider implements BrowserProvider { return webdriverBrowsers } - async initialize(ctx: WorkspaceProject, { browser, options }: WebdriverProviderOptions) { + async initialize( + ctx: WorkspaceProject, + { browser, options }: WebdriverProviderOptions, + ) { this.ctx = ctx this.browserName = browser this.options = options as RemoteOptions @@ -31,7 +39,10 @@ export class WebdriverBrowserProvider implements BrowserProvider { async beforeCommand() { const page = this.browser! - const iframe = await page.findElement('css selector', 'iframe[data-vitest]') + const iframe = await page.findElement( + 'css selector', + 'iframe[data-vitest]', + ) await page.switchToFrame(iframe) } @@ -46,14 +57,18 @@ export class WebdriverBrowserProvider implements BrowserProvider { } async openBrowser() { - if (this.browser) + if (this.browser) { return this.browser + } const options = this.ctx.config.browser if (this.browserName === 'safari') { - if (options.headless) - throw new Error('You\'ve enabled headless mode for Safari but it doesn\'t currently support it.') + if (options.headless) { + throw new Error( + 'You\'ve enabled headless mode for Safari but it doesn\'t currently support it.', + ) + } } const { remote } = await import('webdriverio') @@ -85,7 +100,7 @@ export class WebdriverBrowserProvider implements BrowserProvider { if (browser !== 'safari' && options.headless) { const [key, args] = headlessMap[browser] const currentValues = (this.options?.capabilities as any)?.[key] || {} - const newArgs = [...currentValues.args || [], ...args] + const newArgs = [...(currentValues.args || []), ...args] capabilities[key] = { ...currentValues, args: newArgs as any } } diff --git a/packages/browser/tsconfig.json b/packages/browser/tsconfig.json index 5ca4cde0c..d146b51cf 100644 --- a/packages/browser/tsconfig.json +++ b/packages/browser/tsconfig.json @@ -1,10 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": [ - "node", - "vite/client" - ] + "types": ["node", "vite/client"] }, "exclude": ["dist", "node_modules"] } diff --git a/packages/coverage-istanbul/rollup.config.js b/packages/coverage-istanbul/rollup.config.js index ae5351536..9a0009501 100644 --- a/packages/coverage-istanbul/rollup.config.js +++ b/packages/coverage-istanbul/rollup.config.js @@ -48,8 +48,6 @@ export default () => [ format: 'esm', }, external, - plugins: [ - dts({ respectExternal: true }), - ], + plugins: [dts({ respectExternal: true })], }, ] diff --git a/packages/coverage-istanbul/src/index.ts b/packages/coverage-istanbul/src/index.ts index 9cf5c9e28..1d8662dc3 100644 --- a/packages/coverage-istanbul/src/index.ts +++ b/packages/coverage-istanbul/src/index.ts @@ -3,7 +3,9 @@ import { COVERAGE_STORE_KEY } from './constants' export async function getProvider() { // to not bundle the provider const providerPath = './provider.js' - const { IstanbulCoverageProvider } = await import(providerPath) as typeof import('./provider') + const { IstanbulCoverageProvider } = (await import( + providerPath + )) as typeof import('./provider') return new IstanbulCoverageProvider() } diff --git a/packages/coverage-istanbul/src/provider.ts b/packages/coverage-istanbul/src/provider.ts index 732973112..0311e0729 100644 --- a/packages/coverage-istanbul/src/provider.ts +++ b/packages/coverage-istanbul/src/provider.ts @@ -1,7 +1,23 @@ -import { existsSync, promises as fs, readdirSync, writeFileSync } from 'node:fs' +import { + existsSync, + promises as fs, + readdirSync, + writeFileSync, +} from 'node:fs' import { resolve } from 'pathe' -import type { AfterSuiteRunMeta, CoverageIstanbulOptions, CoverageProvider, ReportContext, ResolvedCoverageOptions, Vitest } from 'vitest' -import { coverageConfigDefaults, defaultExclude, defaultInclude } from 'vitest/config' +import type { + AfterSuiteRunMeta, + CoverageIstanbulOptions, + CoverageProvider, + ReportContext, + ResolvedCoverageOptions, + Vitest, +} from 'vitest' +import { + coverageConfigDefaults, + defaultExclude, + defaultInclude, +} from 'vitest/config' import { BaseCoverageProvider } from 'vitest/coverage' import c from 'picocolors' import { parseModule } from 'magicast' @@ -19,11 +35,16 @@ import { COVERAGE_STORE_KEY } from './constants' type Options = ResolvedCoverageOptions<'istanbul'> type Filename = string -type CoverageFilesByTransformMode = Record -type ProjectName = NonNullable | typeof DEFAULT_PROJECT +type CoverageFilesByTransformMode = Record< + AfterSuiteRunMeta['transformMode'], + Filename[] +> +type ProjectName = + | NonNullable + | typeof DEFAULT_PROJECT interface TestExclude { - new(opts: { + new (opts: { cwd?: string | string[] include?: string | string[] exclude?: string | string[] @@ -40,7 +61,9 @@ const DEFAULT_PROJECT = Symbol.for('default-project') const debug = createDebug('vitest:coverage') let uniqueId = 0 -export class IstanbulCoverageProvider extends BaseCoverageProvider implements CoverageProvider { +export class IstanbulCoverageProvider + extends BaseCoverageProvider + implements CoverageProvider { name = 'istanbul' ctx!: Vitest @@ -64,15 +87,22 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co // Resolved fields provider: 'istanbul', - reportsDirectory: resolve(ctx.config.root, config.reportsDirectory || coverageConfigDefaults.reportsDirectory), - reporter: this.resolveReporters(config.reporter || coverageConfigDefaults.reporter), + reportsDirectory: resolve( + ctx.config.root, + config.reportsDirectory || coverageConfigDefaults.reportsDirectory, + ), + reporter: this.resolveReporters( + config.reporter || coverageConfigDefaults.reporter, + ), thresholds: config.thresholds && { ...config.thresholds, lines: config.thresholds['100'] ? 100 : config.thresholds.lines, branches: config.thresholds['100'] ? 100 : config.thresholds.branches, functions: config.thresholds['100'] ? 100 : config.thresholds.functions, - statements: config.thresholds['100'] ? 100 : config.thresholds.statements, + statements: config.thresholds['100'] + ? 100 + : config.thresholds.statements, }, } @@ -90,7 +120,10 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co this.testExclude = new _TestExclude({ cwd: ctx.config.root, - include: typeof this.options.include === 'undefined' ? undefined : [...this.options.include], + include: + typeof this.options.include === 'undefined' + ? undefined + : [...this.options.include], exclude: [...defaultExclude, ...defaultInclude, ...this.options.exclude], excludeNodeModules: true, extension: this.options.extension, @@ -98,9 +131,14 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co }) const shard = this.ctx.config.shard - const tempDirectory = `.tmp${shard ? `-${shard.index}-${shard.count}` : ''}` + const tempDirectory = `.tmp${ + shard ? `-${shard.index}-${shard.count}` : '' + }` - this.coverageFilesDirectory = resolve(this.options.reportsDirectory, tempDirectory) + this.coverageFilesDirectory = resolve( + this.options.reportsDirectory, + tempDirectory, + ) } resolveOptions() { @@ -108,16 +146,24 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co } onFileTransform(sourceCode: string, id: string, pluginCtx: any) { - if (!this.testExclude.shouldInstrument(id)) + if (!this.testExclude.shouldInstrument(id)) { return + } const sourceMap = pluginCtx.getCombinedSourcemap() sourceMap.sources = sourceMap.sources.map(removeQueryParameters) // Exclude SWC's decorators that are left in source maps - sourceCode = sourceCode.replaceAll('_ts_decorate', '/* istanbul ignore next */_ts_decorate') + sourceCode = sourceCode.replaceAll( + '_ts_decorate', + '/* istanbul ignore next */_ts_decorate', + ) - const code = this.instrumenter.instrumentSync(sourceCode, id, sourceMap as any) + const code = this.instrumenter.instrumentSync( + sourceCode, + id, + sourceMap as any, + ) const map = this.instrumenter.lastSourceMap() as any return { code, map } @@ -129,11 +175,13 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co * backwards compatibility is a breaking change. */ onAfterSuiteRun({ coverage, transformMode, projectName }: AfterSuiteRunMeta) { - if (!coverage) + if (!coverage) { return + } - if (transformMode !== 'web' && transformMode !== 'ssr') + if (transformMode !== 'web' && transformMode !== 'ssr') { throw new Error(`Invalid transform mode: ${transformMode}`) + } let entry = this.coverageFiles.get(projectName || DEFAULT_PROJECT) @@ -142,7 +190,10 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co this.coverageFiles.set(projectName || DEFAULT_PROJECT, entry) } - const filename = resolve(this.coverageFilesDirectory, `coverage-${uniqueId++}.json`) + const filename = resolve( + this.coverageFilesDirectory, + `coverage-${uniqueId++}.json`, + ) entry[transformMode].push(filename) const promise = fs.writeFile(filename, JSON.stringify(coverage), 'utf-8') @@ -150,11 +201,21 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co } async clean(clean = true) { - if (clean && existsSync(this.options.reportsDirectory)) - await fs.rm(this.options.reportsDirectory, { recursive: true, force: true, maxRetries: 10 }) + if (clean && existsSync(this.options.reportsDirectory)) { + await fs.rm(this.options.reportsDirectory, { + recursive: true, + force: true, + maxRetries: 10, + }) + } - if (existsSync(this.coverageFilesDirectory)) - await fs.rm(this.coverageFilesDirectory, { recursive: true, force: true, maxRetries: 10 }) + if (existsSync(this.coverageFilesDirectory)) { + await fs.rm(this.coverageFilesDirectory, { + recursive: true, + force: true, + maxRetries: 10, + }) + } await fs.mkdir(this.coverageFilesDirectory, { recursive: true }) @@ -171,33 +232,45 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co this.pendingPromises = [] for (const coveragePerProject of this.coverageFiles.values()) { - for (const filenames of [coveragePerProject.ssr, coveragePerProject.web]) { + for (const filenames of [ + coveragePerProject.ssr, + coveragePerProject.web, + ]) { const coverageMapByTransformMode = libCoverage.createCoverageMap({}) - for (const chunk of this.toSlices(filenames, this.options.processingConcurrency)) { + for (const chunk of this.toSlices( + filenames, + this.options.processingConcurrency, + )) { if (debug.enabled) { index += chunk.length debug('Covered files %d/%d', index, total) } - await Promise.all(chunk.map(async (filename) => { - const contents = await fs.readFile(filename, 'utf-8') - const coverage = JSON.parse(contents) as CoverageMap + await Promise.all( + chunk.map(async (filename) => { + const contents = await fs.readFile(filename, 'utf-8') + const coverage = JSON.parse(contents) as CoverageMap - coverageMapByTransformMode.merge(coverage) - })) + coverageMapByTransformMode.merge(coverage) + }), + ) } // Source maps can change based on projectName and transform mode. // Coverage transform re-uses source maps so we need to separate transforms from each other. - const transformedCoverage = await transformCoverage(coverageMapByTransformMode) + const transformedCoverage = await transformCoverage( + coverageMapByTransformMode, + ) coverageMap.merge(transformedCoverage) } } if (this.options.all && allTestsRun) { const coveredFiles = coverageMap.files() - const uncoveredCoverage = await this.getCoverageMapForUncoveredFiles(coveredFiles) + const uncoveredCoverage = await this.getCoverageMapForUncoveredFiles( + coveredFiles, + ) coverageMap.merge(await transformCoverage(uncoveredCoverage)) } @@ -207,7 +280,7 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co async reportCoverage(coverageMap: unknown, { allTestsRun }: ReportContext) { await this.generateReports( - coverageMap as CoverageMap || libCoverage.createCoverageMap({}), + (coverageMap as CoverageMap) || libCoverage.createCoverageMap({}), allTestsRun, ) @@ -219,28 +292,37 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co await fs.rm(this.coverageFilesDirectory, { recursive: true }) // Remove empty reports directory, e.g. when only text-reporter is used - if (readdirSync(this.options.reportsDirectory).length === 0) + if (readdirSync(this.options.reportsDirectory).length === 0) { await fs.rm(this.options.reportsDirectory, { recursive: true }) + } } } - async generateReports(coverageMap: CoverageMap, allTestsRun: boolean | undefined) { + async generateReports( + coverageMap: CoverageMap, + allTestsRun: boolean | undefined, + ) { const context = libReport.createContext({ dir: this.options.reportsDirectory, coverageMap, watermarks: this.options.watermarks, }) - if (this.hasTerminalReporter(this.options.reporter)) - this.ctx.logger.log(c.blue(' % ') + c.dim('Coverage report from ') + c.yellow(this.name)) + if (this.hasTerminalReporter(this.options.reporter)) { + this.ctx.logger.log( + c.blue(' % ') + c.dim('Coverage report from ') + c.yellow(this.name), + ) + } for (const reporter of this.options.reporter) { // Type assertion required for custom reporters - reports.create(reporter[0] as Parameters[0], { - skipFull: this.options.skipFull, - projectRoot: this.ctx.config.root, - ...reporter[1], - }).execute(context) + reports + .create(reporter[0] as Parameters[0], { + skipFull: this.options.skipFull, + projectRoot: this.ctx.config.root, + ...reporter[1], + }) + .execute(context) } if (this.options.thresholds) { @@ -257,17 +339,27 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co }) if (this.options.thresholds.autoUpdate && allTestsRun) { - if (!this.ctx.server.config.configFile) - throw new Error('Missing configurationFile. The "coverage.thresholds.autoUpdate" can only be enabled when configuration file is used.') + if (!this.ctx.server.config.configFile) { + throw new Error( + 'Missing configurationFile. The "coverage.thresholds.autoUpdate" can only be enabled when configuration file is used.', + ) + } const configFilePath = this.ctx.server.config.configFile - const configModule = parseModule(await fs.readFile(configFilePath, 'utf8')) + const configModule = parseModule( + await fs.readFile(configFilePath, 'utf8'), + ) this.updateThresholds({ thresholds: resolvedThresholds, perFile: this.options.thresholds.perFile, configurationFile: configModule, - onUpdate: () => writeFileSync(configFilePath, configModule.generate().code, 'utf-8'), + onUpdate: () => + writeFileSync( + configFilePath, + configModule.generate().code, + 'utf-8', + ), }) } } @@ -276,18 +368,24 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co async mergeReports(coverageMaps: unknown[]) { const coverageMap = libCoverage.createCoverageMap({}) - for (const coverage of coverageMaps) + for (const coverage of coverageMaps) { coverageMap.merge(coverage as CoverageMap) + } await this.generateReports(coverageMap, true) } private async getCoverageMapForUncoveredFiles(coveredFiles: string[]) { const allFiles = await this.testExclude.glob(this.ctx.config.root) - let includedFiles = allFiles.map(file => resolve(this.ctx.config.root, file)) + let includedFiles = allFiles.map(file => + resolve(this.ctx.config.root, file), + ) - if (this.ctx.config.changed) - includedFiles = (this.ctx.config.related || []).filter(file => includedFiles.includes(file)) + if (this.ctx.config.changed) { + includedFiles = (this.ctx.config.related || []).filter(file => + includedFiles.includes(file), + ) + } const uncoveredFiles = includedFiles .filter(file => !coveredFiles.includes(file)) diff --git a/packages/coverage-v8/rollup.config.js b/packages/coverage-v8/rollup.config.js index eb5897fd6..577e9d2f6 100644 --- a/packages/coverage-v8/rollup.config.js +++ b/packages/coverage-v8/rollup.config.js @@ -49,8 +49,6 @@ export default () => [ format: 'esm', }, external, - plugins: [ - dts({ respectExternal: true }), - ], + plugins: [dts({ respectExternal: true })], }, ] diff --git a/packages/coverage-v8/src/index.ts b/packages/coverage-v8/src/index.ts index cbdedfa38..c7a0ee47a 100644 --- a/packages/coverage-v8/src/index.ts +++ b/packages/coverage-v8/src/index.ts @@ -5,7 +5,9 @@ export default { async getProvider() { // to not bundle the provider const name = './provider.js' - const { V8CoverageProvider } = await import(name) as typeof import('./provider') + const { V8CoverageProvider } = (await import( + name + )) as typeof import('./provider') return new V8CoverageProvider() }, } diff --git a/packages/coverage-v8/src/provider.ts b/packages/coverage-v8/src/provider.ts index 9b08b466c..c067b3f51 100644 --- a/packages/coverage-v8/src/provider.ts +++ b/packages/coverage-v8/src/provider.ts @@ -1,4 +1,9 @@ -import { existsSync, promises as fs, readdirSync, writeFileSync } from 'node:fs' +import { + existsSync, + promises as fs, + readdirSync, + writeFileSync, +} from 'node:fs' import type { Profiler } from 'node:inspector' import { fileURLToPath, pathToFileURL } from 'node:url' import v8ToIstanbul from 'v8-to-istanbul' @@ -18,16 +23,26 @@ import { stripLiteral } from 'strip-literal' import createDebug from 'debug' import { cleanUrl } from 'vite-node/utils' import type { EncodedSourceMap, FetchResult } from 'vite-node' -import { coverageConfigDefaults, defaultExclude, defaultInclude } from 'vitest/config' +import { + coverageConfigDefaults, + defaultExclude, + defaultInclude, +} from 'vitest/config' import { BaseCoverageProvider } from 'vitest/coverage' -import type { AfterSuiteRunMeta, CoverageProvider, CoverageV8Options, ReportContext, ResolvedCoverageOptions } from 'vitest' +import type { + AfterSuiteRunMeta, + CoverageProvider, + CoverageV8Options, + ReportContext, + ResolvedCoverageOptions, +} from 'vitest' import type { Vitest } from 'vitest/node' // @ts-expect-error missing types import _TestExclude from 'test-exclude' interface TestExclude { - new(opts: { + new (opts: { cwd?: string | string[] include?: string | string[] exclude?: string | string[] @@ -44,21 +59,30 @@ type Options = ResolvedCoverageOptions<'v8'> type TransformResults = Map type Filename = string type RawCoverage = Profiler.TakePreciseCoverageReturnType -type CoverageFilesByTransformMode = Record -type ProjectName = NonNullable | typeof DEFAULT_PROJECT +type CoverageFilesByTransformMode = Record< + AfterSuiteRunMeta['transformMode'], + Filename[] +> +type ProjectName = + | NonNullable + | typeof DEFAULT_PROJECT // TODO: vite-node should export this const WRAPPER_LENGTH = 185 // Note that this needs to match the line ending as well -const VITE_EXPORTS_LINE_PATTERN = /Object\.defineProperty\(__vite_ssr_exports__.*\n/g -const DECORATOR_METADATA_PATTERN = /_ts_metadata\("design:paramtypes", \[[^\]]*\]\),*/g +const VITE_EXPORTS_LINE_PATTERN + = /Object\.defineProperty\(__vite_ssr_exports__.*\n/g +const DECORATOR_METADATA_PATTERN + = /_ts_metadata\("design:paramtypes", \[[^\]]*\]\),*/g const DEFAULT_PROJECT = Symbol.for('default-project') const debug = createDebug('vitest:coverage') let uniqueId = 0 -export class V8CoverageProvider extends BaseCoverageProvider implements CoverageProvider { +export class V8CoverageProvider + extends BaseCoverageProvider + implements CoverageProvider { name = 'v8' ctx!: Vitest @@ -81,21 +105,31 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage // Resolved fields provider: 'v8', - reporter: this.resolveReporters(config.reporter || coverageConfigDefaults.reporter), - reportsDirectory: resolve(ctx.config.root, config.reportsDirectory || coverageConfigDefaults.reportsDirectory), + reporter: this.resolveReporters( + config.reporter || coverageConfigDefaults.reporter, + ), + reportsDirectory: resolve( + ctx.config.root, + config.reportsDirectory || coverageConfigDefaults.reportsDirectory, + ), thresholds: config.thresholds && { ...config.thresholds, lines: config.thresholds['100'] ? 100 : config.thresholds.lines, branches: config.thresholds['100'] ? 100 : config.thresholds.branches, functions: config.thresholds['100'] ? 100 : config.thresholds.functions, - statements: config.thresholds['100'] ? 100 : config.thresholds.statements, + statements: config.thresholds['100'] + ? 100 + : config.thresholds.statements, }, } this.testExclude = new _TestExclude({ cwd: ctx.config.root, - include: typeof this.options.include === 'undefined' ? undefined : [...this.options.include], + include: + typeof this.options.include === 'undefined' + ? undefined + : [...this.options.include], exclude: [...defaultExclude, ...defaultInclude, ...this.options.exclude], excludeNodeModules: true, extension: this.options.extension, @@ -103,9 +137,14 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage }) const shard = this.ctx.config.shard - const tempDirectory = `.tmp${shard ? `-${shard.index}-${shard.count}` : ''}` + const tempDirectory = `.tmp${ + shard ? `-${shard.index}-${shard.count}` : '' + }` - this.coverageFilesDirectory = resolve(this.options.reportsDirectory, tempDirectory) + this.coverageFilesDirectory = resolve( + this.options.reportsDirectory, + tempDirectory, + ) } resolveOptions() { @@ -113,11 +152,21 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage } async clean(clean = true) { - if (clean && existsSync(this.options.reportsDirectory)) - await fs.rm(this.options.reportsDirectory, { recursive: true, force: true, maxRetries: 10 }) + if (clean && existsSync(this.options.reportsDirectory)) { + await fs.rm(this.options.reportsDirectory, { + recursive: true, + force: true, + maxRetries: 10, + }) + } - if (existsSync(this.coverageFilesDirectory)) - await fs.rm(this.coverageFilesDirectory, { recursive: true, force: true, maxRetries: 10 }) + if (existsSync(this.coverageFilesDirectory)) { + await fs.rm(this.coverageFilesDirectory, { + recursive: true, + force: true, + maxRetries: 10, + }) + } await fs.mkdir(this.coverageFilesDirectory, { recursive: true }) @@ -131,8 +180,9 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage * backwards compatibility is a breaking change. */ onAfterSuiteRun({ coverage, transformMode, projectName }: AfterSuiteRunMeta) { - if (transformMode !== 'web' && transformMode !== 'ssr') + if (transformMode !== 'web' && transformMode !== 'ssr') { throw new Error(`Invalid transform mode: ${transformMode}`) + } let entry = this.coverageFiles.get(projectName || DEFAULT_PROJECT) @@ -141,7 +191,10 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage this.coverageFiles.set(projectName || DEFAULT_PROJECT, entry) } - const filename = resolve(this.coverageFilesDirectory, `coverage-${uniqueId++}.json`) + const filename = resolve( + this.coverageFilesDirectory, + `coverage-${uniqueId++}.json`, + ) entry[transformMode].push(filename) const promise = fs.writeFile(filename, JSON.stringify(coverage), 'utf-8') @@ -156,24 +209,38 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage await Promise.all(this.pendingPromises) this.pendingPromises = [] - for (const [projectName, coveragePerProject] of this.coverageFiles.entries()) { - for (const [transformMode, filenames] of Object.entries(coveragePerProject) as [AfterSuiteRunMeta['transformMode'], Filename[]][]) { + for (const [ + projectName, + coveragePerProject, + ] of this.coverageFiles.entries()) { + for (const [transformMode, filenames] of Object.entries( + coveragePerProject, + ) as [AfterSuiteRunMeta['transformMode'], Filename[]][]) { let merged: RawCoverage = { result: [] } - for (const chunk of this.toSlices(filenames, this.options.processingConcurrency)) { + for (const chunk of this.toSlices( + filenames, + this.options.processingConcurrency, + )) { if (debug.enabled) { index += chunk.length debug('Covered files %d/%d', index, total) } - await Promise.all(chunk.map(async (filename) => { - const contents = await fs.readFile(filename, 'utf-8') - const coverage = JSON.parse(contents) as RawCoverage - merged = mergeProcessCovs([merged, coverage]) - })) + await Promise.all( + chunk.map(async (filename) => { + const contents = await fs.readFile(filename, 'utf-8') + const coverage = JSON.parse(contents) as RawCoverage + merged = mergeProcessCovs([merged, coverage]) + }), + ) } - const converted = await this.convertCoverage(merged, projectName, transformMode) + const converted = await this.convertCoverage( + merged, + projectName, + transformMode, + ) // Source maps can change based on projectName and transform mode. // Coverage transform re-uses source maps so we need to separate transforms from each other. @@ -194,11 +261,17 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage } async reportCoverage(coverageMap: unknown, { allTestsRun }: ReportContext) { - if (provider === 'stackblitz') - this.ctx.logger.log(c.blue(' % ') + c.yellow('@vitest/coverage-v8 does not work on Stackblitz. Report will be empty.')) + if (provider === 'stackblitz') { + this.ctx.logger.log( + c.blue(' % ') + + c.yellow( + '@vitest/coverage-v8 does not work on Stackblitz. Report will be empty.', + ), + ) + } await this.generateReports( - coverageMap as CoverageMap || libCoverage.createCoverageMap({}), + (coverageMap as CoverageMap) || libCoverage.createCoverageMap({}), allTestsRun, ) @@ -210,8 +283,9 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage await fs.rm(this.coverageFilesDirectory, { recursive: true }) // Remove empty reports directory, e.g. when only text-reporter is used - if (readdirSync(this.options.reportsDirectory).length === 0) + if (readdirSync(this.options.reportsDirectory).length === 0) { await fs.rm(this.options.reportsDirectory, { recursive: true }) + } } } @@ -222,16 +296,21 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage watermarks: this.options.watermarks, }) - if (this.hasTerminalReporter(this.options.reporter)) - this.ctx.logger.log(c.blue(' % ') + c.dim('Coverage report from ') + c.yellow(this.name)) + if (this.hasTerminalReporter(this.options.reporter)) { + this.ctx.logger.log( + c.blue(' % ') + c.dim('Coverage report from ') + c.yellow(this.name), + ) + } for (const reporter of this.options.reporter) { // Type assertion required for custom reporters - reports.create(reporter[0] as Parameters[0], { - skipFull: this.options.skipFull, - projectRoot: this.ctx.config.root, - ...reporter[1], - }).execute(context) + reports + .create(reporter[0] as Parameters[0], { + skipFull: this.options.skipFull, + projectRoot: this.ctx.config.root, + ...reporter[1], + }) + .execute(context) } if (this.options.thresholds) { @@ -248,17 +327,27 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage }) if (this.options.thresholds.autoUpdate && allTestsRun) { - if (!this.ctx.server.config.configFile) - throw new Error('Missing configurationFile. The "coverage.thresholds.autoUpdate" can only be enabled when configuration file is used.') + if (!this.ctx.server.config.configFile) { + throw new Error( + 'Missing configurationFile. The "coverage.thresholds.autoUpdate" can only be enabled when configuration file is used.', + ) + } const configFilePath = this.ctx.server.config.configFile - const configModule = parseModule(await fs.readFile(configFilePath, 'utf8')) + const configModule = parseModule( + await fs.readFile(configFilePath, 'utf8'), + ) this.updateThresholds({ thresholds: resolvedThresholds, perFile: this.options.thresholds.perFile, configurationFile: configModule, - onUpdate: () => writeFileSync(configFilePath, configModule.generate().code, 'utf-8'), + onUpdate: () => + writeFileSync( + configFilePath, + configModule.generate().code, + 'utf-8', + ), }) } } @@ -267,20 +356,28 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage async mergeReports(coverageMaps: unknown[]) { const coverageMap = libCoverage.createCoverageMap({}) - for (const coverage of coverageMaps) + for (const coverage of coverageMaps) { coverageMap.merge(coverage as CoverageMap) + } await this.generateReports(coverageMap, true) } private async getUntestedFiles(testedFiles: string[]): Promise { - const transformResults = normalizeTransformResults(this.ctx.vitenode.fetchCache) + const transformResults = normalizeTransformResults( + this.ctx.vitenode.fetchCache, + ) const allFiles = await this.testExclude.glob(this.ctx.config.root) - let includedFiles = allFiles.map(file => resolve(this.ctx.config.root, file)) + let includedFiles = allFiles.map(file => + resolve(this.ctx.config.root, file), + ) - if (this.ctx.config.changed) - includedFiles = (this.ctx.config.related || []).filter(file => includedFiles.includes(file)) + if (this.ctx.config.changed) { + includedFiles = (this.ctx.config.related || []).filter(file => + includedFiles.includes(file), + ) + } const uncoveredFiles = includedFiles .map(file => pathToFileURL(file)) @@ -289,48 +386,67 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage let merged: RawCoverage = { result: [] } let index = 0 - for (const chunk of this.toSlices(uncoveredFiles, this.options.processingConcurrency)) { + for (const chunk of this.toSlices( + uncoveredFiles, + this.options.processingConcurrency, + )) { if (debug.enabled) { index += chunk.length debug('Uncovered files %d/%d', index, uncoveredFiles.length) } - const coverages = await Promise.all(chunk.map(async (filename) => { - const { originalSource, source } = await this.getSources(filename.href, transformResults) - - // Ignore empty files, e.g. files that contain only typescript types and no runtime code - if (source && stripLiteral(source).trim() === '') - return null - - const coverage = { - url: filename.href, - scriptId: '0', - // Create a made up function to mark whole file as uncovered. Note that this does not exist in source maps. - functions: [{ - ranges: [{ - startOffset: 0, - endOffset: originalSource.length, - count: 0, - }], - isBlockCoverage: true, - // This is magical value that indicates an empty report: https://github.com/istanbuljs/v8-to-istanbul/blob/fca5e6a9e6ef38a9cdc3a178d5a6cf9ef82e6cab/lib/v8-to-istanbul.js#LL131C40-L131C40 - functionName: '(empty-report)', - }], - } + const coverages = await Promise.all( + chunk.map(async (filename) => { + const { originalSource, source } = await this.getSources( + filename.href, + transformResults, + ) - return { result: [coverage] } - })) + // Ignore empty files, e.g. files that contain only typescript types and no runtime code + if (source && stripLiteral(source).trim() === '') { + return null + } + + const coverage = { + url: filename.href, + scriptId: '0', + // Create a made up function to mark whole file as uncovered. Note that this does not exist in source maps. + functions: [ + { + ranges: [ + { + startOffset: 0, + endOffset: originalSource.length, + count: 0, + }, + ], + isBlockCoverage: true, + // This is magical value that indicates an empty report: https://github.com/istanbuljs/v8-to-istanbul/blob/fca5e6a9e6ef38a9cdc3a178d5a6cf9ef82e6cab/lib/v8-to-istanbul.js#LL131C40-L131C40 + functionName: '(empty-report)', + }, + ], + } + + return { result: [coverage] } + }), + ) merged = mergeProcessCovs([ merged, - ...coverages.filter((cov): cov is NonNullable => cov != null), + ...coverages.filter( + (cov): cov is NonNullable => cov != null, + ), ]) } return merged } - private async getSources(url: string, transformResults: TransformResults, functions: Profiler.FunctionCoverage[] = []): Promise<{ + private async getSources( + url: string, + transformResults: TransformResults, + functions: Profiler.FunctionCoverage[] = [], + ): Promise<{ source: string originalSource: string sourceMap?: { sourcemap: EncodedSourceMap } @@ -339,21 +455,28 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage const filePath = normalize(fileURLToPath(url)) let isExecuted = true - let transformResult: FetchResult | Awaited> = transformResults.get(filePath) + let transformResult: + | FetchResult + | Awaited> + = transformResults.get(filePath) if (!transformResult) { isExecuted = false - transformResult = await this.ctx.vitenode.transformRequest(filePath).catch(() => null) + transformResult = await this.ctx.vitenode + .transformRequest(filePath) + .catch(() => null) } - const map = transformResult?.map as (EncodedSourceMap | undefined) + const map = transformResult?.map as EncodedSourceMap | undefined const code = transformResult?.code - const sourcesContent = map?.sourcesContent?.[0] || await fs.readFile(filePath, 'utf-8').catch(() => { - // If file does not exist construct a dummy source for it. - // These can be files that were generated dynamically during the test run and were removed after it. - const length = findLongestFunctionLength(functions) - return '.'.repeat(length) - }) + const sourcesContent + = map?.sourcesContent?.[0] + || (await fs.readFile(filePath, 'utf-8').catch(() => { + // If file does not exist construct a dummy source for it. + // These can be files that were generated dynamically during the test run and were removed after it. + const length = findLongestFunctionLength(functions) + return '.'.repeat(length) + })) // These can be uncovered files included by "all: true" or files that are loaded outside vite-node if (!map) { @@ -365,8 +488,9 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage } const sources = [url] - if (map.sources && map.sources[0] && !url.endsWith(map.sources[0])) + if (map.sources && map.sources[0] && !url.endsWith(map.sources[0])) { sources[0] = new URL(map.sources[0], url).href + } return { isExecuted, @@ -383,33 +507,58 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage } } - private async convertCoverage(coverage: RawCoverage, projectName?: ProjectName, transformMode?: 'web' | 'ssr'): Promise { - const viteNode = this.ctx.projects.find(project => project.getName() === projectName)?.vitenode || this.ctx.vitenode - const fetchCache = transformMode ? viteNode.fetchCaches[transformMode] : viteNode.fetchCache + private async convertCoverage( + coverage: RawCoverage, + projectName?: ProjectName, + transformMode?: 'web' | 'ssr', + ): Promise { + const viteNode + = this.ctx.projects.find(project => project.getName() === projectName) + ?.vitenode || this.ctx.vitenode + const fetchCache = transformMode + ? viteNode.fetchCaches[transformMode] + : viteNode.fetchCache const transformResults = normalizeTransformResults(fetchCache) - const scriptCoverages = coverage.result.filter(result => this.testExclude.shouldInstrument(fileURLToPath(result.url))) + const scriptCoverages = coverage.result.filter(result => + this.testExclude.shouldInstrument(fileURLToPath(result.url)), + ) const coverageMap = libCoverage.createCoverageMap({}) let index = 0 - for (const chunk of this.toSlices(scriptCoverages, this.options.processingConcurrency)) { + for (const chunk of this.toSlices( + scriptCoverages, + this.options.processingConcurrency, + )) { if (debug.enabled) { index += chunk.length debug('Converting %d/%d', index, scriptCoverages.length) } - await Promise.all(chunk.map(async ({ url, functions }) => { - const sources = await this.getSources(url, transformResults, functions) - - // If file was executed by vite-node we'll need to add its wrapper - const wrapperLength = sources.isExecuted ? WRAPPER_LENGTH : 0 - - const converter = v8ToIstanbul(url, wrapperLength, sources, undefined, this.options.ignoreEmptyLines) - await converter.load() - - converter.applyCoverage(functions) - coverageMap.merge(converter.toIstanbul()) - })) + await Promise.all( + chunk.map(async ({ url, functions }) => { + const sources = await this.getSources( + url, + transformResults, + functions, + ) + + // If file was executed by vite-node we'll need to add its wrapper + const wrapperLength = sources.isExecuted ? WRAPPER_LENGTH : 0 + + const converter = v8ToIstanbul( + url, + wrapperLength, + sources, + undefined, + this.options.ignoreEmptyLines, + ) + await converter.load() + + converter.applyCoverage(functions) + coverageMap.merge(converter.toIstanbul()) + }), + ) } return coverageMap @@ -426,16 +575,25 @@ async function transformCoverage(coverageMap: CoverageMap) { * - Vite's export helpers: e.g. `Object.defineProperty(__vite_ssr_exports__, "sum", { enumerable: true, configurable: true, get(){ return sum }});` * - SWC's decorator metadata: e.g. `_ts_metadata("design:paramtypes", [\ntypeof Request === "undefined" ? Object : Request\n]),` */ -function excludeGeneratedCode(source: string | undefined, map: EncodedSourceMap) { - if (!source) +function excludeGeneratedCode( + source: string | undefined, + map: EncodedSourceMap, +) { + if (!source) { return map + } - if (!source.match(VITE_EXPORTS_LINE_PATTERN) && !source.match(DECORATOR_METADATA_PATTERN)) + if ( + !source.match(VITE_EXPORTS_LINE_PATTERN) + && !source.match(DECORATOR_METADATA_PATTERN) + ) { return map + } const trimmed = new MagicString(source) trimmed.replaceAll(VITE_EXPORTS_LINE_PATTERN, '\n') - trimmed.replaceAll(DECORATOR_METADATA_PATTERN, match => '\n'.repeat(match.split('\n').length - 1)) + trimmed.replaceAll(DECORATOR_METADATA_PATTERN, match => + '\n'.repeat(match.split('\n').length - 1)) const trimmedMap = trimmed.generateMap({ hires: 'boundary' }) @@ -453,20 +611,26 @@ function excludeGeneratedCode(source: string | undefined, map: EncodedSourceMap) */ function findLongestFunctionLength(functions: Profiler.FunctionCoverage[]) { return functions.reduce((previous, current) => { - const maxEndOffset = current.ranges.reduce((endOffset, range) => Math.max(endOffset, range.endOffset), 0) + const maxEndOffset = current.ranges.reduce( + (endOffset, range) => Math.max(endOffset, range.endOffset), + 0, + ) return Math.max(previous, maxEndOffset) }, 0) } -function normalizeTransformResults(fetchCache: Map) { +function normalizeTransformResults( + fetchCache: Map, +) { const normalized: TransformResults = new Map() for (const [key, value] of fetchCache.entries()) { const cleanEntry = cleanUrl(key) - if (!normalized.has(cleanEntry)) + if (!normalized.has(cleanEntry)) { normalized.set(cleanEntry, value.result) + } } return normalized diff --git a/packages/coverage-v8/src/takeCoverage.ts b/packages/coverage-v8/src/takeCoverage.ts index ea49e52e1..0172ff0d8 100644 --- a/packages/coverage-v8/src/takeCoverage.ts +++ b/packages/coverage-v8/src/takeCoverage.ts @@ -1,6 +1,6 @@ /* * For details about the Profiler.* messages see https://chromedevtools.github.io/devtools-protocol/v8/Profiler/ -*/ + */ import inspector from 'node:inspector' import type { Profiler } from 'node:inspector' @@ -20,8 +20,9 @@ export function startCoverage() { export async function takeCoverage() { return new Promise((resolve, reject) => { session.post('Profiler.takePreciseCoverage', async (error, coverage) => { - if (error) + if (error) { return reject(error) + } // Reduce amount of data sent over rpc by doing some early result filtering const result = coverage.result.filter(filterResult) @@ -29,8 +30,9 @@ export async function takeCoverage() { resolve({ result }) }) - if (provider === 'stackblitz') + if (provider === 'stackblitz') { resolve({ result: [] }) + } }) } @@ -41,11 +43,13 @@ export function stopCoverage() { } function filterResult(coverage: Profiler.ScriptCoverage): boolean { - if (!coverage.url.startsWith('file://')) + if (!coverage.url.startsWith('file://')) { return false + } - if (coverage.url.includes('/node_modules/')) + if (coverage.url.includes('/node_modules/')) { return false + } return true } diff --git a/packages/expect/README.md b/packages/expect/README.md index da5d16d8c..55622ae13 100644 --- a/packages/expect/README.md +++ b/packages/expect/README.md @@ -6,7 +6,11 @@ Jest's expect matchers as a Chai plugin. ```js import * as chai from 'chai' -import { JestAsymmetricMatchers, JestChaiExpect, JestExtend } from '@vitest/expect' +import { + JestAsymmetricMatchers, + JestChaiExpect, + JestExtend, +} from '@vitest/expect' // allows using expect.extend instead of chai.use to extend plugins chai.use(JestExtend) diff --git a/packages/expect/rollup.config.js b/packages/expect/rollup.config.js index 901c03cef..f7c95d169 100644 --- a/packages/expect/rollup.config.js +++ b/packages/expect/rollup.config.js @@ -20,7 +20,11 @@ const plugins = [ }), copy({ targets: [ - { src: 'node_modules/@types/chai/index.d.ts', dest: 'dist', rename: 'chai.d.cts' }, + { + src: 'node_modules/@types/chai/index.d.ts', + dest: 'dist', + rename: 'chai.d.cts', + }, ], }), ] @@ -46,15 +50,14 @@ export default defineConfig([ format: 'esm', }, external, - plugins: [ - dts({ respectExternal: true }), - ], + plugins: [dts({ respectExternal: true })], onwarn, }, ]) function onwarn(message) { - if (['EMPTY_BUNDLE', 'CIRCULAR_DEPENDENCY'].includes(message.code)) + if (['EMPTY_BUNDLE', 'CIRCULAR_DEPENDENCY'].includes(message.code)) { return + } console.error(message) } diff --git a/packages/expect/src/constants.ts b/packages/expect/src/constants.ts index 70055a68b..ae615fb70 100644 --- a/packages/expect/src/constants.ts +++ b/packages/expect/src/constants.ts @@ -1,4 +1,6 @@ export const MATCHERS_OBJECT = Symbol.for('matchers-object') export const JEST_MATCHERS_OBJECT = Symbol.for('$$jest-matchers-object') export const GLOBAL_EXPECT = Symbol.for('expect-global') -export const ASYMMETRIC_MATCHERS_OBJECT = Symbol.for('asymmetric-matchers-object') +export const ASYMMETRIC_MATCHERS_OBJECT = Symbol.for( + 'asymmetric-matchers-object', +) diff --git a/packages/expect/src/jest-asymmetric-matchers.ts b/packages/expect/src/jest-asymmetric-matchers.ts index 2eebcddb2..da4e899d1 100644 --- a/packages/expect/src/jest-asymmetric-matchers.ts +++ b/packages/expect/src/jest-asymmetric-matchers.ts @@ -1,9 +1,20 @@ import type { ChaiPlugin, MatcherState } from './types' import { GLOBAL_EXPECT } from './constants' import { getState } from './state' -import { diff, getCustomEqualityTesters, getMatcherUtils, stringify } from './jest-matcher-utils' - -import { equals, isA, iterableEquality, pluralize, subsetEquality } from './jest-utils' +import { + diff, + getCustomEqualityTesters, + getMatcherUtils, + stringify, +} from './jest-matcher-utils' + +import { + equals, + isA, + iterableEquality, + pluralize, + subsetEquality, +} from './jest-utils' export interface AsymmetricMatcherInterface { asymmetricMatch: (other: unknown) => boolean @@ -40,23 +51,25 @@ export abstract class AsymmetricMatcher< abstract asymmetricMatch(other: unknown): boolean abstract toString(): string getExpectedType?(): string - toAsymmetricMatcher?(): string + toAsymmetricMatcher?(): string; // implement custom chai/loupe inspect for better AssertionError.message formatting // https://github.com/chaijs/loupe/blob/9b8a6deabcd50adc056a64fb705896194710c5c6/src/index.ts#L29 [Symbol.for('chai/inspect')](options: { depth: number; truncate: number }) { // minimal pretty-format with simple manual truncation const result = stringify(this, options.depth, { min: true }) - if (result.length <= options.truncate) + if (result.length <= options.truncate) { return result + } return `${this.toString()}{…}` } } export class StringContaining extends AsymmetricMatcher { constructor(sample: string, inverse = false) { - if (!isA('String', sample)) + if (!isA('String', sample)) { throw new Error('Expected is not a string') + } super(sample, inverse) } @@ -90,27 +103,33 @@ export class Anything extends AsymmetricMatcher { } } -export class ObjectContaining extends AsymmetricMatcher> { +export class ObjectContaining extends AsymmetricMatcher< + Record +> { constructor(sample: Record, inverse = false) { super(sample, inverse) } getPrototype(obj: object) { - if (Object.getPrototypeOf) + if (Object.getPrototypeOf) { return Object.getPrototypeOf(obj) + } - if (obj.constructor.prototype === obj) + if (obj.constructor.prototype === obj) { return null + } return obj.constructor.prototype } hasProperty(obj: object | null, property: string): boolean { - if (!obj) + if (!obj) { return false + } - if (Object.prototype.hasOwnProperty.call(obj, property)) + if (Object.prototype.hasOwnProperty.call(obj, property)) { return true + } return this.hasProperty(this.getPrototype(obj), property) } @@ -118,9 +137,8 @@ export class ObjectContaining extends AsymmetricMatcher> asymmetricMatch(other: any) { if (typeof this.sample !== 'object') { throw new TypeError( - `You must provide an object to ${this.toString()}, not '${ - typeof this.sample - }'.`, + `You must provide an object to ${this.toString()}, not '${typeof this + .sample}'.`, ) } @@ -128,7 +146,14 @@ export class ObjectContaining extends AsymmetricMatcher> const matcherContext = this.getMatcherContext() for (const property in this.sample) { - if (!this.hasProperty(other, property) || !equals(this.sample[property], other[property], matcherContext.customTesters)) { + if ( + !this.hasProperty(other, property) + || !equals( + this.sample[property], + other[property], + matcherContext.customTesters, + ) + ) { result = false break } @@ -154,9 +179,8 @@ export class ArrayContaining extends AsymmetricMatcher> { asymmetricMatch(other: Array) { if (!Array.isArray(this.sample)) { throw new TypeError( - `You must provide an array to ${this.toString()}, not '${ - typeof this.sample - }'.`, + `You must provide an array to ${this.toString()}, not '${typeof this + .sample}'.`, ) } @@ -165,7 +189,9 @@ export class ArrayContaining extends AsymmetricMatcher> { = this.sample.length === 0 || (Array.isArray(other) && this.sample.every(item => - other.some(another => equals(item, another, matcherContext.customTesters)), + other.some(another => + equals(item, another, matcherContext.customTesters), + ), )) return this.inverse ? !result : result @@ -192,8 +218,9 @@ export class Any extends AsymmetricMatcher { } fnNameFor(func: Function) { - if (func.name) + if (func.name) { return func.name + } const functionToString = Function.prototype.toString @@ -204,26 +231,33 @@ export class Any extends AsymmetricMatcher { } asymmetricMatch(other: unknown) { - if (this.sample === String) + if (this.sample === String) { return typeof other == 'string' || other instanceof String + } - if (this.sample === Number) + if (this.sample === Number) { return typeof other == 'number' || other instanceof Number + } - if (this.sample === Function) + if (this.sample === Function) { return typeof other == 'function' || other instanceof Function + } - if (this.sample === Boolean) + if (this.sample === Boolean) { return typeof other == 'boolean' || other instanceof Boolean + } - if (this.sample === BigInt) + if (this.sample === BigInt) { return typeof other == 'bigint' || other instanceof BigInt + } - if (this.sample === Symbol) + if (this.sample === Symbol) { return typeof other == 'symbol' || other instanceof Symbol + } - if (this.sample === Object) + if (this.sample === Object) { return typeof other == 'object' + } return other instanceof this.sample } @@ -233,20 +267,25 @@ export class Any extends AsymmetricMatcher { } getExpectedType() { - if (this.sample === String) + if (this.sample === String) { return 'string' + } - if (this.sample === Number) + if (this.sample === Number) { return 'number' + } - if (this.sample === Function) + if (this.sample === Function) { return 'function' + } - if (this.sample === Object) + if (this.sample === Object) { return 'object' + } - if (this.sample === Boolean) + if (this.sample === Boolean) { return 'boolean' + } return this.fnNameFor(this.sample) } @@ -258,8 +297,9 @@ export class Any extends AsymmetricMatcher { export class StringMatching extends AsymmetricMatcher { constructor(sample: string | RegExp, inverse = false) { - if (!isA('String', sample) && !isA('RegExp', sample)) + if (!isA('String', sample) && !isA('RegExp', sample)) { throw new Error('Expected is not a String or a RegExp') + } super(new RegExp(sample), inverse) } @@ -283,11 +323,13 @@ class CloseTo extends AsymmetricMatcher { private readonly precision: number constructor(sample: number, precision = 2, inverse = false) { - if (!isA('Number', sample)) + if (!isA('Number', sample)) { throw new Error('Expected is not a Number') + } - if (!isA('Number', precision)) + if (!isA('Number', precision)) { throw new Error('Precision is not a Number') + } super(sample) this.inverse = inverse @@ -295,19 +337,25 @@ class CloseTo extends AsymmetricMatcher { } asymmetricMatch(other: number) { - if (!isA('Number', other)) + if (!isA('Number', other)) { return false + } let result = false - if (other === Number.POSITIVE_INFINITY && this.sample === Number.POSITIVE_INFINITY) { + if ( + other === Number.POSITIVE_INFINITY + && this.sample === Number.POSITIVE_INFINITY + ) { result = true // Infinity - Infinity is NaN } - else if (other === Number.NEGATIVE_INFINITY && this.sample === Number.NEGATIVE_INFINITY) { + else if ( + other === Number.NEGATIVE_INFINITY + && this.sample === Number.NEGATIVE_INFINITY + ) { result = true // -Infinity - -Infinity is NaN } else { - result - = Math.abs(this.sample - other) < 10 ** -this.precision / 2 + result = Math.abs(this.sample - other) < 10 ** -this.precision / 2 } return this.inverse ? !result : result } @@ -330,17 +378,9 @@ class CloseTo extends AsymmetricMatcher { } export const JestAsymmetricMatchers: ChaiPlugin = (chai, utils) => { - utils.addMethod( - chai.expect, - 'anything', - () => new Anything(), - ) + utils.addMethod(chai.expect, 'anything', () => new Anything()) - utils.addMethod( - chai.expect, - 'any', - (expected: unknown) => new Any(expected), - ) + utils.addMethod(chai.expect, 'any', (expected: unknown) => new Any(expected)) utils.addMethod( chai.expect, @@ -370,14 +410,18 @@ export const JestAsymmetricMatchers: ChaiPlugin = (chai, utils) => { chai.expect, 'closeTo', (expected: any, precision?: number) => new CloseTo(expected, precision), - ) + ); // defineProperty does not work - ;(chai.expect as any).not = { - stringContaining: (expected: string) => new StringContaining(expected, true), + (chai.expect as any).not = { + stringContaining: (expected: string) => + new StringContaining(expected, true), objectContaining: (expected: any) => new ObjectContaining(expected, true), - arrayContaining: (expected: Array) => new ArrayContaining(expected, true), - stringMatching: (expected: string | RegExp) => new StringMatching(expected, true), - closeTo: (expected: any, precision?: number) => new CloseTo(expected, precision, true), + arrayContaining: (expected: Array) => + new ArrayContaining(expected, true), + stringMatching: (expected: string | RegExp) => + new StringMatching(expected, true), + closeTo: (expected: any, precision?: number) => + new CloseTo(expected, precision, true), } } diff --git a/packages/expect/src/jest-expect.ts b/packages/expect/src/jest-expect.ts index 5bd03978c..cbf299cbc 100644 --- a/packages/expect/src/jest-expect.ts +++ b/packages/expect/src/jest-expect.ts @@ -4,9 +4,22 @@ import type { MockInstance, MockResult, MockSettledResult } from '@vitest/spy' import { isMockFunction } from '@vitest/spy' import type { Test } from '@vitest/runner' import type { Assertion, ChaiPlugin } from './types' -import { arrayBufferEquality, generateToBeMessage, getObjectSubset, iterableEquality, equals as jestEquals, sparseArrayEquality, subsetEquality, typeEquality } from './jest-utils' +import { + arrayBufferEquality, + generateToBeMessage, + getObjectSubset, + iterableEquality, + equals as jestEquals, + sparseArrayEquality, + subsetEquality, + typeEquality, +} from './jest-utils' import type { AsymmetricMatcher } from './jest-asymmetric-matchers' -import { diff, getCustomEqualityTesters, stringify } from './jest-matcher-utils' +import { + diff, + getCustomEqualityTesters, + stringify, +} from './jest-matcher-utils' import { JEST_MATCHERS_OBJECT } from './constants' import { recordAsyncExpect, wrapSoft } from './utils' @@ -25,23 +38,34 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { const c = () => getColors() const customTesters = getCustomEqualityTesters() - function def(name: keyof Assertion | (keyof Assertion)[], fn: ((this: Chai.AssertionStatic & Assertion, ...args: any[]) => any)) { + function def( + name: keyof Assertion | (keyof Assertion)[], + fn: (this: Chai.AssertionStatic & Assertion, ...args: any[]) => any, + ) { const addMethod = (n: keyof Assertion) => { const softWrapper = wrapSoft(utils, fn) utils.addMethod(chai.Assertion.prototype, n, softWrapper) - utils.addMethod((globalThis as any)[JEST_MATCHERS_OBJECT].matchers, n, softWrapper) + utils.addMethod( + (globalThis as any)[JEST_MATCHERS_OBJECT].matchers, + n, + softWrapper, + ) } - if (Array.isArray(name)) + if (Array.isArray(name)) { name.forEach(n => addMethod(n)) - - else + } + else { addMethod(name) + } } (['throw', 'throws', 'Throw'] as const).forEach((m) => { utils.overwriteMethod(chai.Assertion.prototype, m, (_super: any) => { - return function (this: Chai.Assertion & Chai.AssertionStatic, ...args: any[]) { + return function ( + this: Chai.Assertion & Chai.AssertionStatic, + ...args: any[] + ) { const promise = utils.flag(this, 'promise') const object = utils.flag(this, 'object') const isNot = utils.flag(this, 'negate') as boolean @@ -55,7 +79,9 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { // called as '.resolves[.not].toThrow()` else if (promise === 'resolves' && typeof object !== 'function') { if (!isNot) { - const message = utils.flag(this, 'message') || 'expected promise to throw an error, but it didn\'t' + const message + = utils.flag(this, 'message') + || 'expected promise to throw an error, but it didn\'t' const error = { showDiff: false, } @@ -78,11 +104,10 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { def('toEqual', function (expected) { const actual = utils.flag(this, 'object') - const equal = jestEquals( - actual, - expected, - [...customTesters, iterableEquality], - ) + const equal = jestEquals(actual, expected, [ + ...customTesters, + iterableEquality, + ]) return this.assert( equal, @@ -140,14 +165,14 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { deepEqualityName = 'toStrictEqual' } else { - const toEqualPass = jestEquals( - actual, - expected, - [...customTesters, iterableEquality], - ) + const toEqualPass = jestEquals(actual, expected, [ + ...customTesters, + iterableEquality, + ]) - if (toEqualPass) + if (toEqualPass) { deepEqualityName = 'toEqual' + } } } @@ -161,29 +186,45 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { }) def('toMatchObject', function (expected) { const actual = this._obj - const pass = jestEquals(actual, expected, [...customTesters, iterableEquality, subsetEquality]) + const pass = jestEquals(actual, expected, [ + ...customTesters, + iterableEquality, + subsetEquality, + ]) const isNot = utils.flag(this, 'negate') as boolean - const { subset: actualSubset, stripped } = getObjectSubset(actual, expected) + const { subset: actualSubset, stripped } = getObjectSubset( + actual, + expected, + ) if ((pass && isNot) || (!pass && !isNot)) { - const msg = utils.getMessage( - this, - [ - pass, - 'expected #{this} to match object #{exp}', - 'expected #{this} to not match object #{exp}', - expected, - actualSubset, - false, - ], - ) - const message = stripped === 0 ? msg : `${msg}\n(${stripped} matching ${stripped === 1 ? 'property' : 'properties'} omitted from actual)` - throw new AssertionError(message, { showDiff: true, expected, actual: actualSubset }) + const msg = utils.getMessage(this, [ + pass, + 'expected #{this} to match object #{exp}', + 'expected #{this} to not match object #{exp}', + expected, + actualSubset, + false, + ]) + const message + = stripped === 0 + ? msg + : `${msg}\n(${stripped} matching ${ + stripped === 1 ? 'property' : 'properties' + } omitted from actual)` + throw new AssertionError(message, { + showDiff: true, + expected, + actual: actualSubset, + }) } }) def('toMatch', function (expected: string | RegExp) { const actual = this._obj as string - if (typeof actual !== 'string') - throw new TypeError(`.toMatch() expects to receive a string, but got ${typeof actual}`) + if (typeof actual !== 'string') { + throw new TypeError( + `.toMatch() expects to receive a string, but got ${typeof actual}`, + ) + } return this.assert( typeof expected === 'string' @@ -196,11 +237,18 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { ) }) def('toContain', function (item) { - const actual = this._obj as Iterable | string | Node | DOMTokenList + const actual = this._obj as + | Iterable + | string + | Node + | DOMTokenList if (typeof Node !== 'undefined' && actual instanceof Node) { - if (!(item instanceof Node)) - throw new TypeError(`toContain() expected a DOM node as the argument, but got ${typeof item}`) + if (!(item instanceof Node)) { + throw new TypeError( + `toContain() expected a DOM node as the argument, but got ${typeof item}`, + ) + } return this.assert( actual.contains(item), @@ -214,7 +262,9 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { if (typeof DOMTokenList !== 'undefined' && actual instanceof DOMTokenList) { assertTypes(item, 'class name', ['string']) const isNot = utils.flag(this, 'negate') as boolean - const expectedClassList = isNot ? actual.value.replace(item, '').trim() : `${actual.value} ${item}` + const expectedClassList = isNot + ? actual.value.replace(item, '').trim() + : `${actual.value} ${item}` return this.assert( actual.contains(item), `expected "${actual.value}" to contain "${item}"`, @@ -234,8 +284,9 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { ) } // make "actual" indexable to have compatibility with jest - if (actual != null && typeof actual !== 'string') + if (actual != null && typeof actual !== 'string') { utils.flag(this, 'object', Array.from(actual as Iterable)) + } return this.contain(item) }) def('toContainEqual', function (expected) { @@ -336,22 +387,36 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { const negate = utils.flag(this, 'negate') utils.flag(this, 'negate', false) - if (negate) + if (negate) { return this.be.undefined + } return this.not.be.undefined }) - def('toBeTypeOf', function (expected: 'bigint' | 'boolean' | 'function' | 'number' | 'object' | 'string' | 'symbol' | 'undefined') { - const actual = typeof this._obj - const equal = expected === actual - return this.assert( - equal, - 'expected #{this} to be type of #{exp}', - 'expected #{this} not to be type of #{exp}', - expected, - actual, - ) - }) + def( + 'toBeTypeOf', + function ( + expected: + | 'bigint' + | 'boolean' + | 'function' + | 'number' + | 'object' + | 'string' + | 'symbol' + | 'undefined', + ) { + const actual = typeof this._obj + const equal = expected === actual + return this.assert( + equal, + 'expected #{this} to be type of #{exp}', + 'expected #{this} not to be type of #{exp}', + expected, + actual, + ) + }, + ) def('toBeInstanceOf', function (obj: any) { return this.instanceOf(obj) }) @@ -359,41 +424,60 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { return this.have.length(length) }) // destructuring, because it checks `arguments` inside, and value is passing as `undefined` - def('toHaveProperty', function (...args: [property: string | (string | number)[], value?: any]) { - if (Array.isArray(args[0])) - args[0] = args[0].map(key => String(key).replace(/([.[\]])/g, '\\$1')).join('.') - - const actual = this._obj as any - const [propertyName, expected] = args - const getValue = () => { - const hasOwn = Object.prototype.hasOwnProperty.call(actual, propertyName) - if (hasOwn) - return { value: actual[propertyName], exists: true } - return utils.getPathInfo(actual, propertyName) - } - const { value, exists } = getValue() - const pass = exists && (args.length === 1 || jestEquals(expected, value, customTesters)) + def( + 'toHaveProperty', + function (...args: [property: string | (string | number)[], value?: any]) { + if (Array.isArray(args[0])) { + args[0] = args[0] + .map(key => String(key).replace(/([.[\]])/g, '\\$1')) + .join('.') + } - const valueString = args.length === 1 ? '' : ` with value ${utils.objDisplay(expected)}` + const actual = this._obj as any + const [propertyName, expected] = args + const getValue = () => { + const hasOwn = Object.prototype.hasOwnProperty.call( + actual, + propertyName, + ) + if (hasOwn) { + return { value: actual[propertyName], exists: true } + } + return utils.getPathInfo(actual, propertyName) + } + const { value, exists } = getValue() + const pass + = exists + && (args.length === 1 || jestEquals(expected, value, customTesters)) - return this.assert( - pass, - `expected #{this} to have property "${propertyName}"${valueString}`, - `expected #{this} to not have property "${propertyName}"${valueString}`, - expected, - exists ? value : undefined, - ) - }) + const valueString + = args.length === 1 ? '' : ` with value ${utils.objDisplay(expected)}` + + return this.assert( + pass, + `expected #{this} to have property "${propertyName}"${valueString}`, + `expected #{this} to not have property "${propertyName}"${valueString}`, + expected, + exists ? value : undefined, + ) + }, + ) def('toBeCloseTo', function (received: number, precision = 2) { const expected = this._obj let pass = false let expectedDiff = 0 let receivedDiff = 0 - if (received === Number.POSITIVE_INFINITY && expected === Number.POSITIVE_INFINITY) { + if ( + received === Number.POSITIVE_INFINITY + && expected === Number.POSITIVE_INFINITY + ) { pass = true } - else if (received === Number.NEGATIVE_INFINITY && expected === Number.NEGATIVE_INFINITY) { + else if ( + received === Number.NEGATIVE_INFINITY + && expected === Number.NEGATIVE_INFINITY + ) { pass = true } else { @@ -412,8 +496,11 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { }) const assertIsMock = (assertion: any) => { - if (!isMockFunction(assertion._obj)) - throw new TypeError(`${utils.inspect(assertion._obj)} is not a spy or a call to a spy!`) + if (!isMockFunction(assertion._obj)) { + throw new TypeError( + `${utils.inspect(assertion._obj)} is not a spy or a call to a spy!`, + ) + } } const getSpy = (assertion: any) => { assertIsMock(assertion) @@ -423,45 +510,87 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { const j = i % 10 const k = i % 100 - if (j === 1 && k !== 11) + if (j === 1 && k !== 11) { return `${i}st` + } - if (j === 2 && k !== 12) + if (j === 2 && k !== 12) { return `${i}nd` + } - if (j === 3 && k !== 13) + if (j === 3 && k !== 13) { return `${i}rd` + } return `${i}th` } - const formatCalls = (spy: MockInstance, msg: string, showActualCall?: any) => { + const formatCalls = ( + spy: MockInstance, + msg: string, + showActualCall?: any, + ) => { if (spy.mock.calls) { - msg += c().gray(`\n\nReceived: \n\n${spy.mock.calls.map((callArg, i) => { - let methodCall = c().bold(` ${ordinalOf(i + 1)} ${spy.getMockName()} call:\n\n`) - if (showActualCall) - methodCall += diff(showActualCall, callArg, { omitAnnotationLines: true }) - else - methodCall += stringify(callArg).split('\n').map(line => ` ${line}`).join('\n') - - methodCall += '\n' - return methodCall - }).join('\n')}`) + msg += c().gray( + `\n\nReceived: \n\n${spy.mock.calls + .map((callArg, i) => { + let methodCall = c().bold( + ` ${ordinalOf(i + 1)} ${spy.getMockName()} call:\n\n`, + ) + if (showActualCall) { + methodCall += diff(showActualCall, callArg, { + omitAnnotationLines: true, + }) + } + else { + methodCall += stringify(callArg) + .split('\n') + .map(line => ` ${line}`) + .join('\n') + } + + methodCall += '\n' + return methodCall + }) + .join('\n')}`, + ) } - msg += c().gray(`\n\nNumber of calls: ${c().bold(spy.mock.calls.length)}\n`) + msg += c().gray( + `\n\nNumber of calls: ${c().bold(spy.mock.calls.length)}\n`, + ) return msg } - const formatReturns = (spy: MockInstance, results: MockResult[] | MockSettledResult[], msg: string, showActualReturn?: any) => { - msg += c().gray(`\n\nReceived: \n\n${results.map((callReturn, i) => { - let methodCall = c().bold(` ${ordinalOf(i + 1)} ${spy.getMockName()} call return:\n\n`) - if (showActualReturn) - methodCall += diff(showActualReturn, callReturn.value, { omitAnnotationLines: true }) - else - methodCall += stringify(callReturn).split('\n').map(line => ` ${line}`).join('\n') - - methodCall += '\n' - return methodCall - }).join('\n')}`) - msg += c().gray(`\n\nNumber of calls: ${c().bold(spy.mock.calls.length)}\n`) + const formatReturns = ( + spy: MockInstance, + results: MockResult[] | MockSettledResult[], + msg: string, + showActualReturn?: any, + ) => { + msg += c().gray( + `\n\nReceived: \n\n${results + .map((callReturn, i) => { + let methodCall = c().bold( + ` ${ordinalOf(i + 1)} ${spy.getMockName()} call return:\n\n`, + ) + if (showActualReturn) { + methodCall += diff(showActualReturn, callReturn.value, { + omitAnnotationLines: true, + }) + } + else { + methodCall += stringify(callReturn) + .split('\n') + .map(line => ` ${line}`) + .join('\n') + } + + methodCall += '\n' + return methodCall + }) + .join('\n')}`, + ) + msg += c().gray( + `\n\nNumber of calls: ${c().bold(spy.mock.calls.length)}\n`, + ) return msg } def(['toHaveBeenCalledTimes', 'toBeCalledTimes'], function (number: number) { @@ -496,150 +625,178 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { const callCount = spy.mock.calls.length const called = callCount > 0 const isNot = utils.flag(this, 'negate') as boolean - let msg = utils.getMessage( - this, - [ - called, - `expected "${spyName}" to be called at least once`, - `expected "${spyName}" to not be called at all, but actually been called ${callCount} times`, - true, - called, - ], - ) - if (called && isNot) + let msg = utils.getMessage(this, [ + called, + `expected "${spyName}" to be called at least once`, + `expected "${spyName}" to not be called at all, but actually been called ${callCount} times`, + true, + called, + ]) + if (called && isNot) { msg = formatCalls(spy, msg) + } - if ((called && isNot) || (!called && !isNot)) + if ((called && isNot) || (!called && !isNot)) { throw new AssertionError(msg) + } }) def(['toHaveBeenCalledWith', 'toBeCalledWith'], function (...args) { const spy = getSpy(this) const spyName = spy.getMockName() - const pass = spy.mock.calls.some(callArg => jestEquals(callArg, args, [...customTesters, iterableEquality])) + const pass = spy.mock.calls.some(callArg => + jestEquals(callArg, args, [...customTesters, iterableEquality]), + ) const isNot = utils.flag(this, 'negate') as boolean - const msg = utils.getMessage( - this, - [ - pass, - `expected "${spyName}" to be called with arguments: #{exp}`, - `expected "${spyName}" to not be called with arguments: #{exp}`, - args, - ], - ) + const msg = utils.getMessage(this, [ + pass, + `expected "${spyName}" to be called with arguments: #{exp}`, + `expected "${spyName}" to not be called with arguments: #{exp}`, + args, + ]) - if ((pass && isNot) || (!pass && !isNot)) + if ((pass && isNot) || (!pass && !isNot)) { throw new AssertionError(formatCalls(spy, msg, args)) + } }) - def(['toHaveBeenNthCalledWith', 'nthCalledWith'], function (times: number, ...args: any[]) { - const spy = getSpy(this) - const spyName = spy.getMockName() - const nthCall = spy.mock.calls[times - 1] - const callCount = spy.mock.calls.length - const isCalled = times <= callCount - this.assert( - jestEquals(nthCall, args, [...customTesters, iterableEquality]), - `expected ${ordinalOf(times)} "${spyName}" call to have been called with #{exp}${ - isCalled ? `` : `, but called only ${callCount} times`}`, - `expected ${ordinalOf(times)} "${spyName}" call to not have been called with #{exp}`, - args, - nthCall, - isCalled, - ) - }) - def(['toHaveBeenLastCalledWith', 'lastCalledWith'], function (...args: any[]) { - const spy = getSpy(this) - const spyName = spy.getMockName() - const lastCall = spy.mock.calls[spy.mock.calls.length - 1] + def( + ['toHaveBeenNthCalledWith', 'nthCalledWith'], + function (times: number, ...args: any[]) { + const spy = getSpy(this) + const spyName = spy.getMockName() + const nthCall = spy.mock.calls[times - 1] + const callCount = spy.mock.calls.length + const isCalled = times <= callCount + this.assert( + jestEquals(nthCall, args, [...customTesters, iterableEquality]), + `expected ${ordinalOf( + times, + )} "${spyName}" call to have been called with #{exp}${ + isCalled ? `` : `, but called only ${callCount} times` + }`, + `expected ${ordinalOf( + times, + )} "${spyName}" call to not have been called with #{exp}`, + args, + nthCall, + isCalled, + ) + }, + ) + def( + ['toHaveBeenLastCalledWith', 'lastCalledWith'], + function (...args: any[]) { + const spy = getSpy(this) + const spyName = spy.getMockName() + const lastCall = spy.mock.calls[spy.mock.calls.length - 1] - this.assert( - jestEquals(lastCall, args, [...customTesters, iterableEquality]), - `expected last "${spyName}" call to have been called with #{exp}`, - `expected last "${spyName}" call to not have been called with #{exp}`, - args, - lastCall, - ) - }) - def(['toThrow', 'toThrowError'], function (expected?: string | Constructable | RegExp | Error) { - if (typeof expected === 'string' || typeof expected === 'undefined' || expected instanceof RegExp) - return this.throws(expected) + this.assert( + jestEquals(lastCall, args, [...customTesters, iterableEquality]), + `expected last "${spyName}" call to have been called with #{exp}`, + `expected last "${spyName}" call to not have been called with #{exp}`, + args, + lastCall, + ) + }, + ) + def( + ['toThrow', 'toThrowError'], + function (expected?: string | Constructable | RegExp | Error) { + if ( + typeof expected === 'string' + || typeof expected === 'undefined' + || expected instanceof RegExp + ) { + return this.throws(expected) + } - const obj = this._obj - const promise = utils.flag(this, 'promise') - const isNot = utils.flag(this, 'negate') as boolean - let thrown: any = null + const obj = this._obj + const promise = utils.flag(this, 'promise') + const isNot = utils.flag(this, 'negate') as boolean + let thrown: any = null - if (promise === 'rejects') { - thrown = obj - } - // if it got here, it's already resolved - // unless it tries to resolve to a function that should throw - // called as .resolves.toThrow(Error) - else if (promise === 'resolves' && typeof obj !== 'function') { - if (!isNot) { - const message = utils.flag(this, 'message') || 'expected promise to throw an error, but it didn\'t' - const error = { - showDiff: false, + if (promise === 'rejects') { + thrown = obj + } + // if it got here, it's already resolved + // unless it tries to resolve to a function that should throw + // called as .resolves.toThrow(Error) + else if (promise === 'resolves' && typeof obj !== 'function') { + if (!isNot) { + const message + = utils.flag(this, 'message') + || 'expected promise to throw an error, but it didn\'t' + const error = { + showDiff: false, + } + throw new AssertionError(message, error, utils.flag(this, 'ssfi')) + } + else { + return } - throw new AssertionError(message, error, utils.flag(this, 'ssfi')) } else { - return - } - } - else { - let isThrow = false - try { - obj() - } - catch (err) { - isThrow = true - thrown = err - } + let isThrow = false + try { + obj() + } + catch (err) { + isThrow = true + thrown = err + } - if (!isThrow && !isNot) { - const message = utils.flag(this, 'message') || 'expected function to throw an error, but it didn\'t' - const error = { - showDiff: false, + if (!isThrow && !isNot) { + const message + = utils.flag(this, 'message') + || 'expected function to throw an error, but it didn\'t' + const error = { + showDiff: false, + } + throw new AssertionError(message, error, utils.flag(this, 'ssfi')) } - throw new AssertionError(message, error, utils.flag(this, 'ssfi')) } - } - if (typeof expected === 'function') { - const name = expected.name || expected.prototype.constructor.name - return this.assert( - thrown && thrown instanceof expected, - `expected error to be instance of ${name}`, - `expected error not to be instance of ${name}`, - expected, - thrown, - ) - } + if (typeof expected === 'function') { + const name = expected.name || expected.prototype.constructor.name + return this.assert( + thrown && thrown instanceof expected, + `expected error to be instance of ${name}`, + `expected error not to be instance of ${name}`, + expected, + thrown, + ) + } - if (expected instanceof Error) { - return this.assert( - thrown && expected.message === thrown.message, - `expected error to have message: ${expected.message}`, - `expected error not to have message: ${expected.message}`, - expected.message, - thrown && thrown.message, - ) - } + if (expected instanceof Error) { + return this.assert( + thrown && expected.message === thrown.message, + `expected error to have message: ${expected.message}`, + `expected error not to have message: ${expected.message}`, + expected.message, + thrown && thrown.message, + ) + } - if (typeof expected === 'object' && 'asymmetricMatch' in expected && typeof (expected as any).asymmetricMatch === 'function') { - const matcher = expected as any as AsymmetricMatcher - return this.assert( - thrown && matcher.asymmetricMatch(thrown), - 'expected error to match asymmetric matcher', - 'expected error not to match asymmetric matcher', - matcher, - thrown, - ) - } + if ( + typeof expected === 'object' + && 'asymmetricMatch' in expected + && typeof (expected as any).asymmetricMatch === 'function' + ) { + const matcher = expected as any as AsymmetricMatcher + return this.assert( + thrown && matcher.asymmetricMatch(thrown), + 'expected error to match asymmetric matcher', + 'expected error not to match asymmetric matcher', + matcher, + thrown, + ) + } - throw new Error(`"toThrow" expects string, RegExp, function, Error instance or asymmetric matcher, got "${typeof expected}"`) - }) + throw new Error( + `"toThrow" expects string, RegExp, function, Error instance or asymmetric matcher, got "${typeof expected}"`, + ) + }, + ) interface ReturnMatcher { name: keyof Assertion | (keyof Assertion)[] @@ -647,20 +804,24 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { action: string } - ;([ - { - name: 'toHaveResolved', - condition: spy => - spy.mock.settledResults.length > 0 - && spy.mock.settledResults.some(({ type }) => type === 'fulfilled'), - action: 'resolved', - }, - { - name: ['toHaveReturned', 'toReturn'], - condition: spy => spy.mock.calls.length > 0 && spy.mock.results.some(({ type }) => type !== 'throw'), - action: 'called', - }, - ] satisfies ReturnMatcher[]).forEach(({ name, condition, action }) => { + ( + [ + { + name: 'toHaveResolved', + condition: spy => + spy.mock.settledResults.length > 0 + && spy.mock.settledResults.some(({ type }) => type === 'fulfilled'), + action: 'resolved', + }, + { + name: ['toHaveReturned', 'toReturn'], + condition: spy => + spy.mock.calls.length > 0 + && spy.mock.results.some(({ type }) => type !== 'throw'), + action: 'called', + }, + ] satisfies ReturnMatcher[] + ).forEach(({ name, condition, action }) => { def(name, function () { const spy = getSpy(this) const spyName = spy.getMockName() @@ -674,21 +835,29 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { false, ) }) - }) - ;([ - { - name: 'toHaveResolvedTimes', - condition: (spy, times) => - spy.mock.settledResults.reduce((s, { type }) => type === 'fulfilled' ? ++s : s, 0) === times, - action: 'resolved', - }, - { - name: ['toHaveReturnedTimes', 'toReturnTimes'], - condition: (spy, times) => - spy.mock.results.reduce((s, { type }) => type === 'throw' ? s : ++s, 0) === times, - action: 'called', - }, - ] satisfies ReturnMatcher<[number]>[]).forEach(({ name, condition, action }) => { + }); + ( + [ + { + name: 'toHaveResolvedTimes', + condition: (spy, times) => + spy.mock.settledResults.reduce( + (s, { type }) => (type === 'fulfilled' ? ++s : s), + 0, + ) === times, + action: 'resolved', + }, + { + name: ['toHaveReturnedTimes', 'toReturnTimes'], + condition: (spy, times) => + spy.mock.results.reduce( + (s, { type }) => (type === 'throw' ? s : ++s), + 0, + ) === times, + action: 'called', + }, + ] satisfies ReturnMatcher<[number]>[] + ).forEach(({ name, condition, action }) => { def(name, function (times: number) { const spy = getSpy(this) const spyName = spy.getMockName() @@ -702,21 +871,29 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { false, ) }) - }) - ;([ - { - name: 'toHaveResolvedWith', - condition: (spy, value) => - spy.mock.settledResults.some(({ type, value: result }) => type === 'fulfilled' && jestEquals(value, result)), - action: 'resolve', - }, - { - name: ['toHaveReturnedWith', 'toReturnWith'], - condition: (spy, value) => - spy.mock.results.some(({ type, value: result }) => type === 'return' && jestEquals(value, result)), - action: 'return', - }, - ] satisfies ReturnMatcher<[any]>[]).forEach(({ name, condition, action }) => { + }); + ( + [ + { + name: 'toHaveResolvedWith', + condition: (spy, value) => + spy.mock.settledResults.some( + ({ type, value: result }) => + type === 'fulfilled' && jestEquals(value, result), + ), + action: 'resolve', + }, + { + name: ['toHaveReturnedWith', 'toReturnWith'], + condition: (spy, value) => + spy.mock.results.some( + ({ type, value: result }) => + type === 'return' && jestEquals(value, result), + ), + action: 'return', + }, + ] satisfies ReturnMatcher<[any]>[] + ).forEach(({ name, condition, action }) => { def(name, function (value: any) { const spy = getSpy(this) const pass = condition(spy, value) @@ -724,42 +901,52 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { if ((pass && isNot) || (!pass && !isNot)) { const spyName = spy.getMockName() - const msg = utils.getMessage( - this, - [ - pass, + const msg = utils.getMessage(this, [ + pass, `expected "${spyName}" to ${action} with: #{exp} at least once`, `expected "${spyName}" to not ${action} with: #{exp}`, value, - ], - ) + ]) - const results = action === 'return' ? spy.mock.results : spy.mock.settledResults + const results + = action === 'return' ? spy.mock.results : spy.mock.settledResults throw new AssertionError(formatReturns(spy, results, msg, value)) } }) - }) - ;([ - { - name: 'toHaveLastResolvedWith', - condition: (spy, value) => { - const result = spy.mock.settledResults[spy.mock.settledResults.length - 1] - return result && result.type === 'fulfilled' && jestEquals(result.value, value) + }); + ( + [ + { + name: 'toHaveLastResolvedWith', + condition: (spy, value) => { + const result + = spy.mock.settledResults[spy.mock.settledResults.length - 1] + return ( + result + && result.type === 'fulfilled' + && jestEquals(result.value, value) + ) + }, + action: 'resolve', }, - action: 'resolve', - }, - { - name: ['toHaveLastReturnedWith', 'lastReturnedWith'], - condition: (spy, value) => { - const result = spy.mock.results[spy.mock.results.length - 1] - return result && result.type === 'return' && jestEquals(result.value, value) + { + name: ['toHaveLastReturnedWith', 'lastReturnedWith'], + condition: (spy, value) => { + const result = spy.mock.results[spy.mock.results.length - 1] + return ( + result + && result.type === 'return' + && jestEquals(result.value, value) + ) + }, + action: 'return', }, - action: 'return', - }, - ] satisfies ReturnMatcher<[any]>[]).forEach(({ name, condition, action }) => { + ] satisfies ReturnMatcher<[any]>[] + ).forEach(({ name, condition, action }) => { def(name, function (value: any) { const spy = getSpy(this) - const results = action === 'return' ? spy.mock.results : spy.mock.settledResults + const results + = action === 'return' ? spy.mock.results : spy.mock.settledResults const result = results[results.length - 1] const spyName = spy.getMockName() this.assert( @@ -770,29 +957,40 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { result?.value, ) }) - }) - ;([ - { - name: 'toHaveNthResolvedWith', - condition: (spy, index, value) => { - const result = spy.mock.settledResults[index - 1] - return result && result.type === 'fulfilled' && jestEquals(result.value, value) + }); + ( + [ + { + name: 'toHaveNthResolvedWith', + condition: (spy, index, value) => { + const result = spy.mock.settledResults[index - 1] + return ( + result + && result.type === 'fulfilled' + && jestEquals(result.value, value) + ) + }, + action: 'resolve', }, - action: 'resolve', - }, - { - name: ['toHaveNthReturnedWith', 'nthReturnedWith'], - condition: (spy, index, value) => { - const result = spy.mock.results[index - 1] - return result && result.type === 'return' && jestEquals(result.value, value) + { + name: ['toHaveNthReturnedWith', 'nthReturnedWith'], + condition: (spy, index, value) => { + const result = spy.mock.results[index - 1] + return ( + result + && result.type === 'return' + && jestEquals(result.value, value) + ) + }, + action: 'return', }, - action: 'return', - }, - ] satisfies ReturnMatcher<[number, any]>[]).forEach(({ name, condition, action }) => { + ] satisfies ReturnMatcher<[number, any]>[] + ).forEach(({ name, condition, action }) => { def(name, function (nthCall: number, value: any) { const spy = getSpy(this) const spyName = spy.getMockName() - const results = action === 'return' ? spy.mock.results : spy.mock.settledResults + const results + = action === 'return' ? spy.mock.results : spy.mock.settledResults const result = results[nthCall - 1] const ordinalCall = `${ordinalOf(nthCall)} call` @@ -811,98 +1009,135 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { // @ts-expect-error @internal def('withContext', function (this: any, context: Record) { - for (const key in context) + for (const key in context) { utils.flag(this, key, context[key]) + } return this }) - utils.addProperty(chai.Assertion.prototype, 'resolves', function __VITEST_RESOLVES__(this: any) { - const error = new Error('resolves') - utils.flag(this, 'promise', 'resolves') - utils.flag(this, 'error', error) - const test: Test = utils.flag(this, 'vitest-test') - const obj = utils.flag(this, 'object') + utils.addProperty( + chai.Assertion.prototype, + 'resolves', + function __VITEST_RESOLVES__(this: any) { + const error = new Error('resolves') + utils.flag(this, 'promise', 'resolves') + utils.flag(this, 'error', error) + const test: Test = utils.flag(this, 'vitest-test') + const obj = utils.flag(this, 'object') + + if (utils.flag(this, 'poll')) { + throw new SyntaxError( + `expect.poll() is not supported in combination with .resolves`, + ) + } - if (utils.flag(this, 'poll')) - throw new SyntaxError(`expect.poll() is not supported in combination with .resolves`) - - if (typeof obj?.then !== 'function') - throw new TypeError(`You must provide a Promise to expect() when using .resolves, not '${typeof obj}'.`) - - const proxy: any = new Proxy(this, { - get: (target, key, receiver) => { - const result = Reflect.get(target, key, receiver) - - if (typeof result !== 'function') - return result instanceof chai.Assertion ? proxy : result - - return async (...args: any[]) => { - const promise = obj.then( - (value: any) => { - utils.flag(this, 'object', value) - return result.call(this, ...args) - }, - (err: any) => { - const _error = new AssertionError( - `promise rejected "${utils.inspect(err)}" instead of resolving`, - { showDiff: false }, - ) as Error - _error.cause = err - _error.stack = (error.stack as string).replace(error.message, _error.message) - throw _error - }, - ) + if (typeof obj?.then !== 'function') { + throw new TypeError( + `You must provide a Promise to expect() when using .resolves, not '${typeof obj}'.`, + ) + } - return recordAsyncExpect(test, promise) - } - }, - }) + const proxy: any = new Proxy(this, { + get: (target, key, receiver) => { + const result = Reflect.get(target, key, receiver) - return proxy - }) + if (typeof result !== 'function') { + return result instanceof chai.Assertion ? proxy : result + } - utils.addProperty(chai.Assertion.prototype, 'rejects', function __VITEST_REJECTS__(this: any) { - const error = new Error('rejects') - utils.flag(this, 'promise', 'rejects') - utils.flag(this, 'error', error) - const test: Test = utils.flag(this, 'vitest-test') - const obj = utils.flag(this, 'object') - const wrapper = typeof obj === 'function' ? obj() : obj // for jest compat - - if (utils.flag(this, 'poll')) - throw new SyntaxError(`expect.poll() is not supported in combination with .rejects`) - - if (typeof wrapper?.then !== 'function') - throw new TypeError(`You must provide a Promise to expect() when using .rejects, not '${typeof wrapper}'.`) - - const proxy: any = new Proxy(this, { - get: (target, key, receiver) => { - const result = Reflect.get(target, key, receiver) - - if (typeof result !== 'function') - return result instanceof chai.Assertion ? proxy : result - - return async (...args: any[]) => { - const promise = wrapper.then( - (value: any) => { - const _error = new AssertionError( - `promise resolved "${utils.inspect(value)}" instead of rejecting`, - { showDiff: true, expected: new Error('rejected promise'), actual: value }, - ) as any - _error.stack = (error.stack as string).replace(error.message, _error.message) - throw _error - }, - (err: any) => { - utils.flag(this, 'object', err) - return result.call(this, ...args) - }, - ) + return async (...args: any[]) => { + const promise = obj.then( + (value: any) => { + utils.flag(this, 'object', value) + return result.call(this, ...args) + }, + (err: any) => { + const _error = new AssertionError( + `promise rejected "${utils.inspect( + err, + )}" instead of resolving`, + { showDiff: false }, + ) as Error + _error.cause = err + _error.stack = (error.stack as string).replace( + error.message, + _error.message, + ) + throw _error + }, + ) + + return recordAsyncExpect(test, promise) + } + }, + }) - return recordAsyncExpect(test, promise) - } - }, - }) + return proxy + }, + ) + + utils.addProperty( + chai.Assertion.prototype, + 'rejects', + function __VITEST_REJECTS__(this: any) { + const error = new Error('rejects') + utils.flag(this, 'promise', 'rejects') + utils.flag(this, 'error', error) + const test: Test = utils.flag(this, 'vitest-test') + const obj = utils.flag(this, 'object') + const wrapper = typeof obj === 'function' ? obj() : obj // for jest compat + + if (utils.flag(this, 'poll')) { + throw new SyntaxError( + `expect.poll() is not supported in combination with .rejects`, + ) + } - return proxy - }) + if (typeof wrapper?.then !== 'function') { + throw new TypeError( + `You must provide a Promise to expect() when using .rejects, not '${typeof wrapper}'.`, + ) + } + + const proxy: any = new Proxy(this, { + get: (target, key, receiver) => { + const result = Reflect.get(target, key, receiver) + + if (typeof result !== 'function') { + return result instanceof chai.Assertion ? proxy : result + } + + return async (...args: any[]) => { + const promise = wrapper.then( + (value: any) => { + const _error = new AssertionError( + `promise resolved "${utils.inspect( + value, + )}" instead of rejecting`, + { + showDiff: true, + expected: new Error('rejected promise'), + actual: value, + }, + ) as any + _error.stack = (error.stack as string).replace( + error.message, + _error.message, + ) + throw _error + }, + (err: any) => { + utils.flag(this, 'object', err) + return result.call(this, ...args) + }, + ) + + return recordAsyncExpect(test, promise) + } + }, + }) + + return proxy + }, + ) } diff --git a/packages/expect/src/jest-extend.ts b/packages/expect/src/jest-extend.ts index 3acb0ee46..116a93e62 100644 --- a/packages/expect/src/jest-extend.ts +++ b/packages/expect/src/jest-extend.ts @@ -10,16 +10,20 @@ import { ASYMMETRIC_MATCHERS_OBJECT, JEST_MATCHERS_OBJECT } from './constants' import { AsymmetricMatcher } from './jest-asymmetric-matchers' import { getState } from './state' -import { diff, getCustomEqualityTesters, getMatcherUtils, stringify } from './jest-matcher-utils' - import { - equals, - iterableEquality, - subsetEquality, -} from './jest-utils' + diff, + getCustomEqualityTesters, + getMatcherUtils, + stringify, +} from './jest-matcher-utils' + +import { equals, iterableEquality, subsetEquality } from './jest-utils' import { wrapSoft } from './utils' -function getMatcherState(assertion: Chai.AssertionStatic & Chai.Assertion, expect: ExpectStatic) { +function getMatcherState( + assertion: Chai.AssertionStatic & Chai.Assertion, + expect: ExpectStatic, +) { const obj = assertion._obj const isNot = util.flag(assertion, 'negate') as boolean const promise = util.flag(assertion, 'promise') || '' @@ -57,90 +61,123 @@ class JestExtendError extends Error { } } -function JestExtendPlugin(c: Chai.ChaiStatic, expect: ExpectStatic, matchers: MatchersObject): ChaiPlugin { +function JestExtendPlugin( + c: Chai.ChaiStatic, + expect: ExpectStatic, + matchers: MatchersObject, +): ChaiPlugin { return (_, utils) => { - Object.entries(matchers).forEach(([expectAssertionName, expectAssertion]) => { - function expectWrapper(this: Chai.AssertionStatic & Chai.Assertion, ...args: any[]) { - const { state, isNot, obj } = getMatcherState(this, expect) - - // @ts-expect-error args wanting tuple - const result = expectAssertion.call(state, obj, ...args) - - if (result && typeof result === 'object' && result instanceof Promise) { - return result.then(({ pass, message, actual, expected }) => { - if ((pass && isNot) || (!pass && !isNot)) - throw new JestExtendError(message(), actual, expected) - }) + Object.entries(matchers).forEach( + ([expectAssertionName, expectAssertion]) => { + function expectWrapper( + this: Chai.AssertionStatic & Chai.Assertion, + ...args: any[] + ) { + const { state, isNot, obj } = getMatcherState(this, expect) + + // @ts-expect-error args wanting tuple + const result = expectAssertion.call(state, obj, ...args) + + if ( + result + && typeof result === 'object' + && result instanceof Promise + ) { + return result.then(({ pass, message, actual, expected }) => { + if ((pass && isNot) || (!pass && !isNot)) { + throw new JestExtendError(message(), actual, expected) + } + }) + } + + const { pass, message, actual, expected } = result + + if ((pass && isNot) || (!pass && !isNot)) { + throw new JestExtendError(message(), actual, expected) + } } - const { pass, message, actual, expected } = result - - if ((pass && isNot) || (!pass && !isNot)) - throw new JestExtendError(message(), actual, expected) - } - - const softWrapper = wrapSoft(utils, expectWrapper) - utils.addMethod((globalThis as any)[JEST_MATCHERS_OBJECT].matchers, expectAssertionName, softWrapper) - utils.addMethod(c.Assertion.prototype, expectAssertionName, softWrapper) - - class CustomMatcher extends AsymmetricMatcher<[unknown, ...unknown[]]> { - constructor(inverse = false, ...sample: [unknown, ...unknown[]]) { - super(sample, inverse) + const softWrapper = wrapSoft(utils, expectWrapper) + utils.addMethod( + (globalThis as any)[JEST_MATCHERS_OBJECT].matchers, + expectAssertionName, + softWrapper, + ) + utils.addMethod( + c.Assertion.prototype, + expectAssertionName, + softWrapper, + ) + + class CustomMatcher extends AsymmetricMatcher<[unknown, ...unknown[]]> { + constructor(inverse = false, ...sample: [unknown, ...unknown[]]) { + super(sample, inverse) + } + + asymmetricMatch(other: unknown) { + const { pass } = expectAssertion.call( + this.getMatcherContext(expect), + other, + ...this.sample, + ) as SyncExpectationResult + + return this.inverse ? !pass : pass + } + + toString() { + return `${this.inverse ? 'not.' : ''}${expectAssertionName}` + } + + getExpectedType() { + return 'any' + } + + toAsymmetricMatcher() { + return `${this.toString()}<${this.sample.map(String).join(', ')}>` + } } - asymmetricMatch(other: unknown) { - const { pass } = expectAssertion.call( - this.getMatcherContext(expect), - other, - ...this.sample, - ) as SyncExpectationResult - - return this.inverse ? !pass : pass - } - - toString() { - return `${this.inverse ? 'not.' : ''}${expectAssertionName}` - } - - getExpectedType() { - return 'any' - } - - toAsymmetricMatcher() { - return `${this.toString()}<${this.sample.map(String).join(', ')}>` - } - } - - const customMatcher = (...sample: [unknown, ...unknown[]]) => new CustomMatcher(false, ...sample) - - Object.defineProperty(expect, expectAssertionName, { - configurable: true, - enumerable: true, - value: customMatcher, - writable: true, - }) - - Object.defineProperty(expect.not, expectAssertionName, { - configurable: true, - enumerable: true, - value: (...sample: [unknown, ...unknown[]]) => new CustomMatcher(true, ...sample), - writable: true, - }) - - // keep track of asymmetric matchers on global so that it can be copied over to local context's `expect`. - // note that the negated variant is automatically shared since it's assigned on the single `expect.not` object. - Object.defineProperty(((globalThis as any)[ASYMMETRIC_MATCHERS_OBJECT]), expectAssertionName, { - configurable: true, - enumerable: true, - value: customMatcher, - writable: true, - }) - }) + const customMatcher = (...sample: [unknown, ...unknown[]]) => + new CustomMatcher(false, ...sample) + + Object.defineProperty(expect, expectAssertionName, { + configurable: true, + enumerable: true, + value: customMatcher, + writable: true, + }) + + Object.defineProperty(expect.not, expectAssertionName, { + configurable: true, + enumerable: true, + value: (...sample: [unknown, ...unknown[]]) => + new CustomMatcher(true, ...sample), + writable: true, + }) + + // keep track of asymmetric matchers on global so that it can be copied over to local context's `expect`. + // note that the negated variant is automatically shared since it's assigned on the single `expect.not` object. + Object.defineProperty( + (globalThis as any)[ASYMMETRIC_MATCHERS_OBJECT], + expectAssertionName, + { + configurable: true, + enumerable: true, + value: customMatcher, + writable: true, + }, + ) + }, + ) } } export const JestExtend: ChaiPlugin = (chai, utils) => { - utils.addMethod(chai.expect, 'extend', (expect: ExpectStatic, expects: MatchersObject) => { - use(JestExtendPlugin(chai, expect, expects)) - }) + utils.addMethod( + chai.expect, + 'extend', + (expect: ExpectStatic, expects: MatchersObject) => { + use(JestExtendPlugin(chai, expect, expects)) + }, + ) } diff --git a/packages/expect/src/jest-matcher-utils.ts b/packages/expect/src/jest-matcher-utils.ts index ad2267d96..acf05fd05 100644 --- a/packages/expect/src/jest-matcher-utils.ts +++ b/packages/expect/src/jest-matcher-utils.ts @@ -49,12 +49,12 @@ export function getMatcherUtils() { } if (matcherName.includes('.')) { - // Old format: for backward compatibility, - // especially without promise or isNot options + // Old format: for backward compatibility, + // especially without promise or isNot options dimString += matcherName } else { - // New format: omit period from matcherName arg + // New format: omit period from matcherName arg hint += DIM_COLOR(`${dimString}.`) + matcherName dimString = '' } @@ -64,16 +64,19 @@ export function getMatcherUtils() { } else { hint += DIM_COLOR(`${dimString}(`) + expectedColor(expected) - if (secondArgument) + if (secondArgument) { hint += DIM_COLOR(', ') + secondArgumentColor(secondArgument) + } dimString = ')' } - if (comment !== '') + if (comment !== '') { dimString += ` // ${comment}` + } - if (dimString !== '') + if (dimString !== '') { hint += DIM_COLOR(dimString) + } return hint } diff --git a/packages/expect/src/jest-utils.ts b/packages/expect/src/jest-utils.ts index 03d6f063f..08d5687df 100644 --- a/packages/expect/src/jest-utils.ts +++ b/packages/expect/src/jest-utils.ts @@ -39,21 +39,31 @@ export function equals( const functionToString = Function.prototype.toString export function isAsymmetric(obj: any) { - return !!obj && typeof obj === 'object' && 'asymmetricMatch' in obj && isA('Function', obj.asymmetricMatch) + return ( + !!obj + && typeof obj === 'object' + && 'asymmetricMatch' in obj + && isA('Function', obj.asymmetricMatch) + ) } export function hasAsymmetric(obj: any, seen = new Set()): boolean { - if (seen.has(obj)) + if (seen.has(obj)) { return false + } seen.add(obj) - if (isAsymmetric(obj)) + if (isAsymmetric(obj)) { return true - if (Array.isArray(obj)) + } + if (Array.isArray(obj)) { return obj.some(i => hasAsymmetric(i, seen)) - if (obj instanceof Set) + } + if (obj instanceof Set) { return Array.from(obj).some(i => hasAsymmetric(i, seen)) - if (isObject(obj)) + } + if (isObject(obj)) { return Object.values(obj).some(v => hasAsymmetric(v, seen)) + } return false } @@ -61,14 +71,17 @@ function asymmetricMatch(a: any, b: any) { const asymmetricA = isAsymmetric(a) const asymmetricB = isAsymmetric(b) - if (asymmetricA && asymmetricB) + if (asymmetricA && asymmetricB) { return undefined + } - if (asymmetricA) + if (asymmetricA) { return a.asymmetricMatch(b) + } - if (asymmetricB) + if (asymmetricB) { return b.asymmetricMatch(a) + } } // Equality function lovingly adapted from isEqual in @@ -84,32 +97,44 @@ function eq( let result = true const asymmetricResult = asymmetricMatch(a, b) - if (asymmetricResult !== undefined) + if (asymmetricResult !== undefined) { return asymmetricResult + } const testerContext: TesterContext = { equals } for (let i = 0; i < customTesters.length; i++) { - const customTesterResult = customTesters[i].call(testerContext, a, b, customTesters) - if (customTesterResult !== undefined) + const customTesterResult = customTesters[i].call( + testerContext, + a, + b, + customTesters, + ) + if (customTesterResult !== undefined) { return customTesterResult + } } - if (a instanceof Error && b instanceof Error) + if (a instanceof Error && b instanceof Error) { return a.message === b.message + } - if (typeof URL === 'function' && a instanceof URL && b instanceof URL) + if (typeof URL === 'function' && a instanceof URL && b instanceof URL) { return a.href === b.href + } - if (Object.is(a, b)) + if (Object.is(a, b)) { return true + } // A strict comparison is necessary because `null == undefined`. - if (a === null || b === null) + if (a === null || b === null) { return a === b + } const className = Object.prototype.toString.call(a) - if (className !== Object.prototype.toString.call(b)) + if (className !== Object.prototype.toString.call(b)) { return false + } switch (className) { case '[object Boolean]': @@ -133,18 +158,20 @@ function eq( // Coerce dates to numeric primitive values. Dates are compared by their // millisecond representations. Note that invalid dates with millisecond representations // of `NaN` are equivalent. - return (numA === numB) || (Number.isNaN(numA) && Number.isNaN(numB)) + return numA === numB || (Number.isNaN(numA) && Number.isNaN(numB)) } // RegExps are compared by their source patterns and flags. case '[object RegExp]': return a.source === b.source && a.flags === b.flags } - if (typeof a !== 'object' || typeof b !== 'object') + if (typeof a !== 'object' || typeof b !== 'object') { return false + } // Use DOM3 method isEqualNode (IE>=9) - if (isDomNode(a) && isDomNode(b)) + if (isDomNode(a) && isDomNode(b)) { return a.isEqualNode(b) + } // Used to detect circular references. let length = aStack.length @@ -153,19 +180,21 @@ function eq( // unique nested structures. // circular references at same depth are equal // circular reference is not equal to non-circular one - if (aStack[length] === a) + if (aStack[length] === a) { return bStack[length] === b - - else if (bStack[length] === b) + } + else if (bStack[length] === b) { return false + } } // Add the first object to the stack of traversed objects. aStack.push(a) bStack.push(b) // Recursively compare objects and arrays. // Compare array lengths to determine if a deep comparison is necessary. - if (className === '[object Array]' && a.length !== b.length) + if (className === '[object Array]' && a.length !== b.length) { return false + } // Deep compare objects. const aKeys = keys(a, hasKey) @@ -173,8 +202,9 @@ function eq( let size = aKeys.length // Ensure that both objects contain the same number of properties before comparing deep equality. - if (keys(b, hasKey).length !== size) + if (keys(b, hasKey).length !== size) { return false + } while (size--) { key = aKeys[size] @@ -184,8 +214,9 @@ function eq( = hasKey(b, key) && eq(a[key], b[key], aStack, bStack, customTesters, hasKey) - if (!result) + if (!result) { return false + } } // Remove the first object from the stack of traversed objects. aStack.pop() @@ -198,8 +229,9 @@ function keys(obj: object, hasKey: (obj: object, key: string) => boolean) { const keys = [] for (const key in obj) { - if (hasKey(obj, key)) + if (hasKey(obj, key)) { keys.push(key) + } } return keys.concat( (Object.getOwnPropertySymbols(obj) as Array).filter( @@ -236,8 +268,9 @@ function isDomNode(obj: any): boolean { } export function fnNameFor(func: Function) { - if (func.name) + if (func.name) { return func.name + } const matches = functionToString .call(func) @@ -246,21 +279,25 @@ export function fnNameFor(func: Function) { } function getPrototype(obj: object) { - if (Object.getPrototypeOf) + if (Object.getPrototypeOf) { return Object.getPrototypeOf(obj) + } - if (obj.constructor.prototype === obj) + if (obj.constructor.prototype === obj) { return null + } return obj.constructor.prototype } export function hasProperty(obj: object | null, property: string): boolean { - if (!obj) + if (!obj) { return false + } - if (Object.prototype.hasOwnProperty.call(obj, property)) + if (Object.prototype.hasOwnProperty.call(obj, property)) { return true + } return hasProperty(getPrototype(obj), property) } @@ -331,7 +368,13 @@ function hasIterator(object: any) { return !!(object != null && object[IteratorSymbol]) } -export function iterableEquality(a: any, b: any, customTesters: Array = [], aStack: Array = [], bStack: Array = []): boolean | undefined { +export function iterableEquality( + a: any, + b: any, + customTesters: Array = [], + aStack: Array = [], + bStack: Array = [], +): boolean | undefined { if ( typeof a !== 'object' || typeof b !== 'object' @@ -343,8 +386,9 @@ export function iterableEquality(a: any, b: any, customTesters: Array = return undefined } - if (a.constructor !== b.constructor) + if (a.constructor !== b.constructor) { return false + } let length = aStack.length while (length--) { @@ -352,8 +396,9 @@ export function iterableEquality(a: any, b: any, customTesters: Array = // unique nested structures. // circular references at same depth are equal // circular reference is not equal to non-circular one - if (aStack[length] === a) + if (aStack[length] === a) { return bStack[length] === b + } } aStack.push(a) bStack.push(b) @@ -364,13 +409,7 @@ export function iterableEquality(a: any, b: any, customTesters: Array = ] function iterableEqualityWithStack(a: any, b: any) { - return iterableEquality( - a, - b, - [...customTesters], - [...aStack], - [...bStack], - ) + return iterableEquality(a, b, [...customTesters], [...aStack], [...bStack]) } if (a.size !== undefined) { @@ -384,8 +423,9 @@ export function iterableEquality(a: any, b: any, customTesters: Array = let has = false for (const bValue of b) { const isEqual = equals(aValue, bValue, filteredCustomTesters) - if (isEqual === true) + if (isEqual === true) { has = true + } } if (has === false) { @@ -408,14 +448,24 @@ export function iterableEquality(a: any, b: any, customTesters: Array = ) { let has = false for (const bEntry of b) { - const matchedKey = equals(aEntry[0], bEntry[0], filteredCustomTesters) + const matchedKey = equals( + aEntry[0], + bEntry[0], + filteredCustomTesters, + ) let matchedValue = false - if (matchedKey === true) - matchedValue = equals(aEntry[1], bEntry[1], filteredCustomTesters) - - if (matchedValue === true) + if (matchedKey === true) { + matchedValue = equals( + aEntry[1], + bEntry[1], + filteredCustomTesters, + ) + } + + if (matchedValue === true) { has = true + } } if (has === false) { @@ -435,15 +485,13 @@ export function iterableEquality(a: any, b: any, customTesters: Array = for (const aValue of a) { const nextB = bIterator.next() - if ( - nextB.done - || !equals(aValue, nextB.value, filteredCustomTesters) - ) { + if (nextB.done || !equals(aValue, nextB.value, filteredCustomTesters)) { return false } } - if (!bIterator.next().done) + if (!bIterator.next().done) { return false + } if ( !isImmutableList(a) @@ -453,8 +501,9 @@ export function iterableEquality(a: any, b: any, customTesters: Array = ) { const aEntries = Object.entries(a) const bEntries = Object.entries(b) - if (!equals(aEntries, bEntries)) + if (!equals(aEntries, bEntries)) { return false + } } // Remove the first value from the stack of traversed values. @@ -470,8 +519,9 @@ function hasPropertyInObject(object: object, key: string | symbol): boolean { const shouldTerminate = !object || typeof object !== 'object' || object === Object.prototype - if (shouldTerminate) + if (shouldTerminate) { return false + } return ( Object.prototype.hasOwnProperty.call(object, key) @@ -480,37 +530,47 @@ function hasPropertyInObject(object: object, key: string | symbol): boolean { } function isObjectWithKeys(a: any) { - return isObject(a) + return ( + isObject(a) && !(a instanceof Error) - && !(Array.isArray(a)) + && !Array.isArray(a) && !(a instanceof Date) + ) } -export function subsetEquality(object: unknown, subset: unknown, customTesters: Array = []): boolean | undefined { - const filteredCustomTesters = customTesters.filter(t => t !== subsetEquality) +export function subsetEquality( + object: unknown, + subset: unknown, + customTesters: Array = [], +): boolean | undefined { + const filteredCustomTesters = customTesters.filter( + t => t !== subsetEquality, + ) // subsetEquality needs to keep track of the references // it has already visited to avoid infinite loops in case // there are circular references in the subset passed to it. const subsetEqualityWithContext = (seenReferences: WeakMap = new WeakMap()) => (object: any, subset: any): boolean | undefined => { - if (!isObjectWithKeys(subset)) + if (!isObjectWithKeys(subset)) { return undefined + } return Object.keys(subset).every((key) => { if (subset[key] != null && typeof subset[key] === 'object') { - if (seenReferences.has(subset[key])) + if (seenReferences.has(subset[key])) { return equals(object[key], subset[key], filteredCustomTesters) + } seenReferences.set(subset[key], true) } const result - = object != null - && hasPropertyInObject(object, key) - && equals(object[key], subset[key], [ - ...filteredCustomTesters, - subsetEqualityWithContext(seenReferences), - ]) + = object != null + && hasPropertyInObject(object, key) + && equals(object[key], subset[key], [ + ...filteredCustomTesters, + subsetEqualityWithContext(seenReferences), + ]) // The main goal of using seenReference is to avoid circular node on tree. // It will only happen within a parent and its child, not a node and nodes next to it (same level) // We should keep the reference for a parent and its child only @@ -525,19 +585,24 @@ export function subsetEquality(object: unknown, subset: unknown, customTesters: } export function typeEquality(a: any, b: any): boolean | undefined { - if (a == null || b == null || a.constructor === b.constructor) + if (a == null || b == null || a.constructor === b.constructor) { return undefined + } return false } -export function arrayBufferEquality(a: unknown, b: unknown): boolean | undefined { +export function arrayBufferEquality( + a: unknown, + b: unknown, +): boolean | undefined { let dataViewA = a as DataView let dataViewB = b as DataView if (!(a instanceof DataView && b instanceof DataView)) { - if (!(a instanceof ArrayBuffer) || !(b instanceof ArrayBuffer)) + if (!(a instanceof ArrayBuffer) || !(b instanceof ArrayBuffer)) { return undefined + } try { dataViewA = new DataView(a) @@ -549,36 +614,48 @@ export function arrayBufferEquality(a: unknown, b: unknown): boolean | undefined } // Buffers are not equal when they do not have the same byte length - if (dataViewA.byteLength !== dataViewB.byteLength) + if (dataViewA.byteLength !== dataViewB.byteLength) { return false + } // Check if every byte value is equal to each other for (let i = 0; i < dataViewA.byteLength; i++) { - if (dataViewA.getUint8(i) !== dataViewB.getUint8(i)) + if (dataViewA.getUint8(i) !== dataViewB.getUint8(i)) { return false + } } return true } -export function sparseArrayEquality(a: unknown, b: unknown, customTesters: Array = []): boolean | undefined { - if (!Array.isArray(a) || !Array.isArray(b)) +export function sparseArrayEquality( + a: unknown, + b: unknown, + customTesters: Array = [], +): boolean | undefined { + if (!Array.isArray(a) || !Array.isArray(b)) { return undefined + } // A sparse array [, , 1] will have keys ["2"] whereas [undefined, undefined, 1] will have keys ["0", "1", "2"] const aKeys = Object.keys(a) const bKeys = Object.keys(b) - const filteredCustomTesters = customTesters.filter(t => t !== sparseArrayEquality) - return ( - equals(a, b, filteredCustomTesters, true) && equals(aKeys, bKeys) + const filteredCustomTesters = customTesters.filter( + t => t !== sparseArrayEquality, ) + return equals(a, b, filteredCustomTesters, true) && equals(aKeys, bKeys) } -export function generateToBeMessage(deepEqualityName: string, expected = '#{this}', actual = '#{exp}') { +export function generateToBeMessage( + deepEqualityName: string, + expected = '#{this}', + actual = '#{exp}', +) { const toBeMessage = `expected ${expected} to be ${actual} // Object.is equality` - if (['toStrictEqual', 'toEqual'].includes(deepEqualityName)) + if (['toStrictEqual', 'toEqual'].includes(deepEqualityName)) { return `${toBeMessage}\n\nIf it should pass with deep equality, replace "toBe" with "${deepEqualityName}"\n\nExpected: ${expected}\nReceived: serializes to the same string\n` + } return toBeMessage } @@ -596,59 +673,73 @@ export function getObjectKeys(object: object): Array { ] } -export function getObjectSubset(object: any, subset: any, customTesters: Array = []): { subset: any; stripped: number } { +export function getObjectSubset( + object: any, + subset: any, + customTesters: Array = [], +): { subset: any; stripped: number } { let stripped = 0 - const getObjectSubsetWithContext = (seenReferences: WeakMap = new WeakMap()) => (object: any, subset: any): any => { - if (Array.isArray(object)) { - if (Array.isArray(subset) && subset.length === object.length) { - // The map method returns correct subclass of subset. - return subset.map((sub: any, i: number) => - getObjectSubsetWithContext(seenReferences)(object[i], sub), - ) - } - } - else if (object instanceof Date) { - return object - } - else if (isObject(object) && isObject(subset)) { - if ( - equals(object, subset, [ - ...customTesters, - iterableEquality, - subsetEquality, - ]) - ) { - // Avoid unnecessary copy which might return Object instead of subclass. - return subset - } - - const trimmed: any = {} - seenReferences.set(object, trimmed) - - for (const key of getObjectKeys(object)) { - if (hasPropertyInObject(subset, key)) { - trimmed[key] = seenReferences.has(object[key]) - ? seenReferences.get(object[key]) - : getObjectSubsetWithContext(seenReferences)(object[key], subset[key]) + const getObjectSubsetWithContext + = (seenReferences: WeakMap = new WeakMap()) => + (object: any, subset: any): any => { + if (Array.isArray(object)) { + if (Array.isArray(subset) && subset.length === object.length) { + // The map method returns correct subclass of subset. + return subset.map((sub: any, i: number) => + getObjectSubsetWithContext(seenReferences)(object[i], sub), + ) + } } - else { - if (!seenReferences.has(object[key])) { - stripped += 1 - if (isObject(object[key])) - stripped += getObjectKeys(object[key]).length + else if (object instanceof Date) { + return object + } + else if (isObject(object) && isObject(subset)) { + if ( + equals(object, subset, [ + ...customTesters, + iterableEquality, + subsetEquality, + ]) + ) { + // Avoid unnecessary copy which might return Object instead of subclass. + return subset + } - getObjectSubsetWithContext(seenReferences)(object[key], subset[key]) + const trimmed: any = {} + seenReferences.set(object, trimmed) + + for (const key of getObjectKeys(object)) { + if (hasPropertyInObject(subset, key)) { + trimmed[key] = seenReferences.has(object[key]) + ? seenReferences.get(object[key]) + : getObjectSubsetWithContext(seenReferences)( + object[key], + subset[key], + ) + } + else { + if (!seenReferences.has(object[key])) { + stripped += 1 + if (isObject(object[key])) { + stripped += getObjectKeys(object[key]).length + } + + getObjectSubsetWithContext(seenReferences)( + object[key], + subset[key], + ) + } + } } - } - } - if (getObjectKeys(trimmed).length > 0) - return trimmed - } + if (getObjectKeys(trimmed).length > 0) { + return trimmed + } + } - return object - } + return object + } return { subset: getObjectSubsetWithContext()(object, subset), stripped } } diff --git a/packages/expect/src/state.ts b/packages/expect/src/state.ts index ee116f5b4..efa26c692 100644 --- a/packages/expect/src/state.ts +++ b/packages/expect/src/state.ts @@ -1,5 +1,10 @@ import type { ExpectStatic, MatcherState, Tester } from './types' -import { ASYMMETRIC_MATCHERS_OBJECT, GLOBAL_EXPECT, JEST_MATCHERS_OBJECT, MATCHERS_OBJECT } from './constants' +import { + ASYMMETRIC_MATCHERS_OBJECT, + GLOBAL_EXPECT, + JEST_MATCHERS_OBJECT, + MATCHERS_OBJECT, +} from './constants' if (!Object.prototype.hasOwnProperty.call(globalThis, MATCHERS_OBJECT)) { const globalState = new WeakMap() @@ -22,7 +27,9 @@ if (!Object.prototype.hasOwnProperty.call(globalThis, MATCHERS_OBJECT)) { }) } -export function getState(expect: ExpectStatic): State { +export function getState( + expect: ExpectStatic, +): State { return (globalThis as any)[MATCHERS_OBJECT].get(expect) } diff --git a/packages/expect/src/types.ts b/packages/expect/src/types.ts index 1df31519e..8929c8583 100644 --- a/packages/expect/src/types.ts +++ b/packages/expect/src/types.ts @@ -16,7 +16,7 @@ export type Tester = ( this: TesterContext, a: any, b: any, - customTesters: Array, + customTesters: Array ) => boolean | undefined export interface TesterContext { @@ -24,7 +24,7 @@ export interface TesterContext { a: unknown, b: unknown, customTesters?: Array, - strictCheck?: boolean, + strictCheck?: boolean ) => boolean } export type { DiffOptions } from '@vitest/utils/diff' @@ -50,7 +50,7 @@ export interface MatcherState { a: unknown, b: unknown, customTesters?: Array, - strictCheck?: boolean, + strictCheck?: boolean ) => boolean expand?: boolean expectedAssertionsNumber?: number | null @@ -88,9 +88,14 @@ export interface RawMatcherFn { (this: T, received: any, expected: any, options?: any): ExpectationResult } -export type MatchersObject = Record> +export type MatchersObject = Record< + string, + RawMatcherFn +> -export interface ExpectStatic extends Chai.ExpectStatic, AsymmetricMatchersContaining { +export interface ExpectStatic + extends Chai.ExpectStatic, + AsymmetricMatchersContaining { (actual: T, message?: string): Assertion extend: (expects: MatchersObject) => void anything: () => any @@ -130,7 +135,10 @@ export interface JestAssertion extends jest.Matchers { toBeInstanceOf: (expected: E) => void toBeCalledTimes: (times: number) => void toHaveLength: (length: number) => void - toHaveProperty: (property: string | (string | number)[], value?: E) => void + toHaveProperty: ( + property: string | (string | number)[], + value?: E + ) => void toBeCloseTo: (number: number, numDigits?: number) => void toHaveBeenCalledTimes: (times: number) => void toHaveBeenCalled: () => void @@ -160,7 +168,7 @@ type VitestAssertion = { ? Assertion : A[K] extends (...args: any[]) => any ? A[K] // not converting function since they may contain overload - : VitestAssertion + : VitestAssertion; } & ((type: string, message?: string) => Assertion) type Promisify = { @@ -168,13 +176,25 @@ type Promisify = { ? O extends R ? Promisify : (...args: A) => Promise - : O[K] + : O[K]; } export type PromisifyAssertion = Promisify> -export interface Assertion extends VitestAssertion, JestAssertion { - toBeTypeOf: (expected: 'bigint' | 'boolean' | 'function' | 'number' | 'object' | 'string' | 'symbol' | 'undefined') => void +export interface Assertion + extends VitestAssertion, + JestAssertion { + toBeTypeOf: ( + expected: + | 'bigint' + | 'boolean' + | 'function' + | 'number' + | 'object' + | 'string' + | 'symbol' + | 'undefined' + ) => void toHaveBeenCalledOnce: () => void toSatisfy: (matcher: (value: E) => boolean, message?: string) => void @@ -192,7 +212,6 @@ declare global { // support augmenting jest.Matchers by other libraries // eslint-disable-next-line ts/no-namespace namespace jest { - // eslint-disable-next-line unused-imports/no-unused-vars interface Matchers {} } diff --git a/packages/expect/src/utils.ts b/packages/expect/src/utils.ts index 9b9c0a05e..085980c71 100644 --- a/packages/expect/src/utils.ts +++ b/packages/expect/src/utils.ts @@ -2,34 +2,44 @@ import { processError } from '@vitest/utils/error' import type { Test } from '@vitest/runner/types' import type { Assertion } from './types' -export function recordAsyncExpect(test: any, promise: Promise | PromiseLike) { +export function recordAsyncExpect( + test: any, + promise: Promise | PromiseLike, +) { // 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(() => { const index = test.promises.indexOf(promise) - if (index !== -1) + if (index !== -1) { test.promises.splice(index, 1) + } }) // record promise - if (!test.promises) + if (!test.promises) { test.promises = [] + } test.promises.push(promise) } return promise } -export function wrapSoft(utils: Chai.ChaiUtils, fn: (this: Chai.AssertionStatic & Assertion, ...args: any[]) => void) { +export function wrapSoft( + utils: Chai.ChaiUtils, + fn: (this: Chai.AssertionStatic & Assertion, ...args: any[]) => void, +) { return function (this: Chai.AssertionStatic & Assertion, ...args: any[]) { - if (!utils.flag(this, 'soft')) + if (!utils.flag(this, 'soft')) { return fn.apply(this, args) + } const test: Test = utils.flag(this, 'vitest-test') - if (!test) + if (!test) { throw new Error('expect.soft() can only be used inside a test') + } try { return fn.apply(this, args) diff --git a/packages/runner/rollup.config.js b/packages/runner/rollup.config.js index 428029800..5b5fb6e1d 100644 --- a/packages/runner/rollup.config.js +++ b/packages/runner/rollup.config.js @@ -48,15 +48,14 @@ export default defineConfig([ format: 'esm', }, external, - plugins: [ - dts({ respectExternal: true }), - ], + plugins: [dts({ respectExternal: true })], onwarn, }, ]) function onwarn(message) { - if (['EMPTY_BUNDLE', 'CIRCULAR_DEPENDENCY'].includes(message.code)) + if (['EMPTY_BUNDLE', 'CIRCULAR_DEPENDENCY'].includes(message.code)) { return + } console.error(message) } diff --git a/packages/runner/src/collect.ts b/packages/runner/src/collect.ts index e567ba520..c114e1979 100644 --- a/packages/runner/src/collect.ts +++ b/packages/runner/src/collect.ts @@ -1,15 +1,27 @@ import { processError } from '@vitest/utils/error' import type { File, SuiteHooks } from './types' import type { VitestRunner } from './types/runner' -import { calculateSuiteHash, createFileTask, interpretTaskModes, someTasksAreOnly } from './utils/collect' -import { clearCollectorContext, createSuiteHooks, getDefaultSuite } from './suite' +import { + calculateSuiteHash, + createFileTask, + interpretTaskModes, + someTasksAreOnly, +} from './utils/collect' +import { + clearCollectorContext, + createSuiteHooks, + getDefaultSuite, +} from './suite' import { getHooks, setHooks } from './map' import { collectorContext } from './context' import { runSetupFiles } from './setup' const now = Date.now -export async function collectTests(paths: string[], runner: VitestRunner): Promise { +export async function collectTests( + paths: string[], + runner: VitestRunner, +): Promise { const files: File[] = [] const config = runner.config @@ -66,13 +78,20 @@ export async function collectTests(paths: string[], runner: VitestRunner): Promi calculateSuiteHash(file) const hasOnlyTasks = someTasksAreOnly(file) - interpretTaskModes(file, config.testNamePattern, hasOnlyTasks, false, config.allowOnly) + interpretTaskModes( + file, + config.testNamePattern, + hasOnlyTasks, + false, + config.allowOnly, + ) file.tasks.forEach((task) => { // task.suite refers to the internal default suite object // it should not be reported - if (task.suite?.id === '') + if (task.suite?.id === '') { delete task.suite + } }) files.push(file) } @@ -83,7 +102,7 @@ export async function collectTests(paths: string[], runner: VitestRunner): Promi function mergeHooks(baseHooks: SuiteHooks, hooks: SuiteHooks): SuiteHooks { for (const _key in hooks) { const key = _key as keyof SuiteHooks - baseHooks[key].push(...hooks[key] as any) + baseHooks[key].push(...(hooks[key] as any)) } return baseHooks diff --git a/packages/runner/src/context.ts b/packages/runner/src/context.ts index 456052cf4..a4b77e38c 100644 --- a/packages/runner/src/context.ts +++ b/packages/runner/src/context.ts @@ -1,6 +1,13 @@ import type { Awaitable } from '@vitest/utils' import { getSafeTimers } from '@vitest/utils' -import type { Custom, ExtendedContext, RuntimeContext, SuiteCollector, TaskContext, Test } from './types' +import type { + Custom, + ExtendedContext, + RuntimeContext, + SuiteCollector, + TaskContext, + Test, +} from './types' import type { VitestRunner } from './types/runner' import { PendingError } from './errors' @@ -13,36 +20,46 @@ export function collectTask(task: SuiteCollector) { collectorContext.currentSuite?.tasks.push(task) } -export async function runWithSuite(suite: SuiteCollector, fn: (() => Awaitable)) { +export async function runWithSuite( + suite: SuiteCollector, + fn: () => Awaitable, +) { const prev = collectorContext.currentSuite collectorContext.currentSuite = suite await fn() collectorContext.currentSuite = prev } -export function withTimeout any)>( +export function withTimeout any>( fn: T, timeout: number, isHook = false, ): T { - if (timeout <= 0 || timeout === Number.POSITIVE_INFINITY) + if (timeout <= 0 || timeout === Number.POSITIVE_INFINITY) { return fn + } const { setTimeout, clearTimeout } = getSafeTimers() - return ((...args: (T extends ((...args: infer A) => any) ? A : never)) => { - return Promise.race([fn(...args), new Promise((resolve, reject) => { - const timer = setTimeout(() => { - clearTimeout(timer) - reject(new Error(makeTimeoutMsg(isHook, timeout))) - }, timeout) - // `unref` might not exist in browser - timer.unref?.() - })]) as Awaitable + return ((...args: T extends (...args: infer A) => any ? A : never) => { + return Promise.race([ + fn(...args), + new Promise((resolve, reject) => { + const timer = setTimeout(() => { + clearTimeout(timer) + reject(new Error(makeTimeoutMsg(isHook, timeout))) + }, timeout) + // `unref` might not exist in browser + timer.unref?.() + }), + ]) as Awaitable }) as T } -export function createTestContext(test: T, runner: VitestRunner): ExtendedContext { +export function createTestContext( + test: T, + runner: VitestRunner, +): ExtendedContext { const context = function () { throw new Error('done() callback is deprecated, use promise instead') } as unknown as TaskContext @@ -64,9 +81,15 @@ export function createTestContext(test: T, runner: Vite test.onFinished.push(fn) } - return runner.extendTaskContext?.(context) as ExtendedContext || context + return (runner.extendTaskContext?.(context) as ExtendedContext) || context } function makeTimeoutMsg(isHook: boolean, timeout: number) { - return `${isHook ? 'Hook' : 'Test'} timed out in ${timeout}ms.\nIf this is a long-running ${isHook ? 'hook' : 'test'}, pass a timeout value as the last argument or configure it globally with "${isHook ? 'hookTimeout' : 'testTimeout'}".` + return `${ + isHook ? 'Hook' : 'Test' + } timed out in ${timeout}ms.\nIf this is a long-running ${ + isHook ? 'hook' : 'test' + }, pass a timeout value as the last argument or configure it globally with "${ + isHook ? 'hookTimeout' : 'testTimeout' + }".` } diff --git a/packages/runner/src/fixture.ts b/packages/runner/src/fixture.ts index 0d9d0a635..79f4a5d89 100644 --- a/packages/runner/src/fixture.ts +++ b/packages/runner/src/fixture.ts @@ -15,14 +15,18 @@ export interface FixtureItem extends FixtureOptions { deps?: FixtureItem[] } -export function mergeContextFixtures(fixtures: Record, context: { fixtures?: FixtureItem[] } = {}) { +export function mergeContextFixtures( + fixtures: Record, + context: { fixtures?: FixtureItem[] } = {}, +) { const fixtureOptionKeys = ['auto'] - const fixtureArray: FixtureItem[] = Object.entries(fixtures) - .map(([prop, value]) => { + const fixtureArray: FixtureItem[] = Object.entries(fixtures).map( + ([prop, value]) => { const fixtureItem = { value } as FixtureItem if ( - Array.isArray(value) && value.length >= 2 + Array.isArray(value) + && value.length >= 2 && isObject(value[1]) && Object.keys(value[1]).some(key => fixtureOptionKeys.includes(key)) ) { @@ -34,19 +38,25 @@ export function mergeContextFixtures(fixtures: Record, context: { f fixtureItem.prop = prop fixtureItem.isFn = typeof fixtureItem.value === 'function' return fixtureItem - }) + }, + ) - if (Array.isArray(context.fixtures)) + if (Array.isArray(context.fixtures)) { context.fixtures = context.fixtures.concat(fixtureArray) - else + } + else { context.fixtures = fixtureArray + } // Update dependencies of fixture functions fixtureArray.forEach((fixture) => { if (fixture.isFn) { const usedProps = getUsedProps(fixture.value) - if (usedProps.length) - fixture.deps = context.fixtures!.filter(({ prop }) => prop !== fixture.prop && usedProps.includes(prop)) + if (usedProps.length) { + fixture.deps = context.fixtures!.filter( + ({ prop }) => prop !== fixture.prop && usedProps.includes(prop), + ) + } } }) @@ -54,52 +64,69 @@ export function mergeContextFixtures(fixtures: Record, context: { f } const fixtureValueMaps = new Map>() -const cleanupFnArrayMap = new Map void | Promise>>() +const cleanupFnArrayMap = new Map< + TestContext, + Array<() => void | Promise> +>() export async function callFixtureCleanup(context: TestContext) { const cleanupFnArray = cleanupFnArrayMap.get(context) ?? [] - for (const cleanup of cleanupFnArray.reverse()) + for (const cleanup of cleanupFnArray.reverse()) { await cleanup() + } cleanupFnArrayMap.delete(context) } export function withFixtures(fn: Function, testContext?: TestContext) { return (hookContext?: TestContext) => { - const context: TestContext & { [key: string]: any } | undefined = hookContext || testContext + const context: (TestContext & { [key: string]: any }) | undefined + = hookContext || testContext - if (!context) + if (!context) { return fn({}) + } const fixtures = getFixture(context) - if (!fixtures?.length) + if (!fixtures?.length) { return fn(context) + } const usedProps = getUsedProps(fn) const hasAutoFixture = fixtures.some(({ auto }) => auto) - if (!usedProps.length && !hasAutoFixture) + if (!usedProps.length && !hasAutoFixture) { return fn(context) + } - if (!fixtureValueMaps.get(context)) + if (!fixtureValueMaps.get(context)) { fixtureValueMaps.set(context, new Map()) - const fixtureValueMap: Map = fixtureValueMaps.get(context)! + } + const fixtureValueMap: Map + = fixtureValueMaps.get(context)! - if (!cleanupFnArrayMap.has(context)) + if (!cleanupFnArrayMap.has(context)) { cleanupFnArrayMap.set(context, []) + } const cleanupFnArray = cleanupFnArrayMap.get(context)! - const usedFixtures = fixtures.filter(({ prop, auto }) => auto || usedProps.includes(prop)) + const usedFixtures = fixtures.filter( + ({ prop, auto }) => auto || usedProps.includes(prop), + ) const pendingFixtures = resolveDeps(usedFixtures) - if (!pendingFixtures.length) + if (!pendingFixtures.length) { return fn(context) + } async function resolveFixtures() { for (const fixture of pendingFixtures) { // fixture could be already initialized during "before" hook - if (fixtureValueMap.has(fixture)) + if (fixtureValueMap.has(fixture)) { continue + } - const resolvedValue = fixture.isFn ? await resolveFixtureFunction(fixture.value, context, cleanupFnArray) : fixture.value + const resolvedValue = fixture.isFn + ? await resolveFixtureFunction(fixture.value, context, cleanupFnArray) + : fixture.value context![fixture.prop] = resolvedValue fixtureValueMap.set(fixture, resolvedValue) cleanupFnArray.unshift(() => { @@ -113,9 +140,12 @@ export function withFixtures(fn: Function, testContext?: TestContext) { } async function resolveFixtureFunction( - fixtureFn: (context: unknown, useFn: (arg: unknown) => Promise) => Promise, + fixtureFn: ( + context: unknown, + useFn: (arg: unknown) => Promise + ) => Promise, context: unknown, - cleanupFnArray: (() => (void | Promise))[], + cleanupFnArray: (() => void | Promise)[], ): Promise { // wait for `use` call to extract fixture value const useFnArgPromise = createDefer() @@ -148,16 +178,27 @@ async function resolveFixtureFunction( return useFnArgPromise } -function resolveDeps(fixtures: FixtureItem[], depSet = new Set(), pendingFixtures: FixtureItem[] = []) { +function resolveDeps( + fixtures: FixtureItem[], + depSet = new Set(), + pendingFixtures: FixtureItem[] = [], +) { fixtures.forEach((fixture) => { - if (pendingFixtures.includes(fixture)) + if (pendingFixtures.includes(fixture)) { return + } if (!fixture.isFn || !fixture.deps) { pendingFixtures.push(fixture) return } - if (depSet.has(fixture)) - throw new Error(`Circular fixture dependency detected: ${fixture.prop} <- ${[...depSet].reverse().map(d => d.prop).join(' <- ')}`) + if (depSet.has(fixture)) { + throw new Error( + `Circular fixture dependency detected: ${fixture.prop} <- ${[...depSet] + .reverse() + .map(d => d.prop) + .join(' <- ')}`, + ) + } depSet.add(fixture) resolveDeps(fixture.deps, depSet, pendingFixtures) @@ -170,22 +211,28 @@ function resolveDeps(fixtures: FixtureItem[], depSet = new Set(), p function getUsedProps(fn: Function) { const match = fn.toString().match(/[^(]*\(([^)]*)/) - if (!match) + if (!match) { return [] + } const args = splitByComma(match[1]) - if (!args.length) + if (!args.length) { return [] + } let first = args[0] if ('__VITEST_FIXTURE_INDEX__' in fn) { first = args[(fn as any).__VITEST_FIXTURE_INDEX__] - if (!first) + if (!first) { return [] + } } - if (!(first.startsWith('{') && first.endsWith('}'))) - throw new Error(`The first argument inside a fixture must use object destructuring pattern, e.g. ({ test } => {}). Instead, received "${first}".`) + if (!(first.startsWith('{') && first.endsWith('}'))) { + throw new Error( + `The first argument inside a fixture must use object destructuring pattern, e.g. ({ test } => {}). Instead, received "${first}".`, + ) + } const _first = first.slice(1, -1).replace(/\s/g, '') const props = splitByComma(_first).map((prop) => { @@ -193,8 +240,11 @@ function getUsedProps(fn: Function) { }) const last = props.at(-1) - if (last && last.startsWith('...')) - throw new Error(`Rest parameters are not supported in fixtures, received "${last}".`) + if (last && last.startsWith('...')) { + throw new Error( + `Rest parameters are not supported in fixtures, received "${last}".`, + ) + } return props } @@ -212,13 +262,15 @@ function splitByComma(s: string) { } else if (!stack.length && s[i] === ',') { const token = s.substring(start, i).trim() - if (token) + if (token) { result.push(token) + } start = i + 1 } } const lastToken = s.substring(start).trim() - if (lastToken) + if (lastToken) { result.push(lastToken) + } return result } diff --git a/packages/runner/src/hooks.ts b/packages/runner/src/hooks.ts index 96aa09256..694ecfc1d 100644 --- a/packages/runner/src/hooks.ts +++ b/packages/runner/src/hooks.ts @@ -1,4 +1,9 @@ -import type { OnTestFailedHandler, OnTestFinishedHandler, SuiteHooks, TaskPopulated } from './types' +import type { + OnTestFailedHandler, + OnTestFinishedHandler, + SuiteHooks, + TaskPopulated, +} from './types' import { getCurrentSuite, getRunner } from './suite' import { getCurrentTest } from './test-state' import { withTimeout } from './context' @@ -10,34 +15,62 @@ function getDefaultHookTimeout() { // suite hooks export function beforeAll(fn: SuiteHooks['beforeAll'][0], timeout?: number) { - return getCurrentSuite().on('beforeAll', withTimeout(fn, timeout ?? getDefaultHookTimeout(), true)) + return getCurrentSuite().on( + 'beforeAll', + withTimeout(fn, timeout ?? getDefaultHookTimeout(), true), + ) } export function afterAll(fn: SuiteHooks['afterAll'][0], timeout?: number) { - return getCurrentSuite().on('afterAll', withTimeout(fn, timeout ?? getDefaultHookTimeout(), true)) + return getCurrentSuite().on( + 'afterAll', + withTimeout(fn, timeout ?? getDefaultHookTimeout(), true), + ) } -export function beforeEach(fn: SuiteHooks['beforeEach'][0], timeout?: number) { - return getCurrentSuite().on('beforeEach', withTimeout(withFixtures(fn), timeout ?? getDefaultHookTimeout(), true)) +export function beforeEach( + fn: SuiteHooks['beforeEach'][0], + timeout?: number, +) { + return getCurrentSuite().on( + 'beforeEach', + withTimeout(withFixtures(fn), timeout ?? getDefaultHookTimeout(), true), + ) } -export function afterEach(fn: SuiteHooks['afterEach'][0], timeout?: number) { - return getCurrentSuite().on('afterEach', withTimeout(withFixtures(fn), timeout ?? getDefaultHookTimeout(), true)) +export function afterEach( + fn: SuiteHooks['afterEach'][0], + timeout?: number, +) { + return getCurrentSuite().on( + 'afterEach', + withTimeout(withFixtures(fn), timeout ?? getDefaultHookTimeout(), true), + ) } -export const onTestFailed = createTestHook('onTestFailed', (test, handler) => { - test.onFailed ||= [] - test.onFailed.push(handler) -}) +export const onTestFailed = createTestHook( + 'onTestFailed', + (test, handler) => { + test.onFailed ||= [] + test.onFailed.push(handler) + }, +) -export const onTestFinished = createTestHook('onTestFinished', (test, handler) => { - test.onFinished ||= [] - test.onFinished.push(handler) -}) +export const onTestFinished = createTestHook( + 'onTestFinished', + (test, handler) => { + test.onFinished ||= [] + test.onFinished.push(handler) + }, +) -function createTestHook(name: string, handler: (test: TaskPopulated, handler: T) => void) { +function createTestHook( + name: string, + handler: (test: TaskPopulated, handler: T) => void, +) { return (fn: T) => { const current = getCurrentTest() - if (!current) + if (!current) { throw new Error(`Hook ${name}() can only be called inside a test`) + } return handler(current, fn) } diff --git a/packages/runner/src/index.ts b/packages/runner/src/index.ts index 52e06cf8c..0d8cd9064 100644 --- a/packages/runner/src/index.ts +++ b/packages/runner/src/index.ts @@ -1,6 +1,20 @@ export { startTests, updateTask } from './run' -export { test, it, describe, suite, getCurrentSuite, createTaskCollector } from './suite' -export { beforeAll, beforeEach, afterAll, afterEach, onTestFailed, onTestFinished } from './hooks' +export { + test, + it, + describe, + suite, + getCurrentSuite, + createTaskCollector, +} from './suite' +export { + beforeAll, + beforeEach, + afterAll, + afterEach, + onTestFailed, + onTestFinished, +} from './hooks' export { setFn, getFn, getHooks, setHooks } from './map' export { getCurrentTest } from './test-state' export { processError } from '@vitest/utils/error' diff --git a/packages/runner/src/map.ts b/packages/runner/src/map.ts index 32540d2a0..91964b16f 100644 --- a/packages/runner/src/map.ts +++ b/packages/runner/src/map.ts @@ -7,15 +7,18 @@ const fnMap = new WeakMap() const fixtureMap = new WeakMap() const hooksMap = new WeakMap() -export function setFn(key: Test | Custom, fn: (() => Awaitable)) { +export function setFn(key: Test | Custom, fn: () => Awaitable) { fnMap.set(key, fn) } -export function getFn(key: Task): (() => Awaitable) { +export function getFn(key: Task): () => Awaitable { return fnMap.get(key as any) } -export function setFixture(key: TestContext, fixture: FixtureItem[] | undefined) { +export function setFixture( + key: TestContext, + fixture: FixtureItem[] | undefined, +) { fixtureMap.set(key, fixture) } diff --git a/packages/runner/src/run.ts b/packages/runner/src/run.ts index 75077c9f0..9fa34b818 100644 --- a/packages/runner/src/run.ts +++ b/packages/runner/src/run.ts @@ -4,7 +4,21 @@ import { getSafeTimers, shuffle } from '@vitest/utils' import { processError } from '@vitest/utils/error' import type { DiffOptions } from '@vitest/utils/diff' import type { VitestRunner } from './types/runner' -import type { Custom, File, HookCleanupCallback, HookListener, SequenceHooks, Suite, SuiteHooks, Task, TaskMeta, TaskResult, TaskResultPack, TaskState, Test } from './types' +import type { + Custom, + File, + HookCleanupCallback, + HookListener, + SequenceHooks, + Suite, + SuiteHooks, + Task, + TaskMeta, + TaskResult, + TaskResultPack, + TaskState, + Test, +} from './types' import { partitionSuiteChildren } from './utils/suite' import { getFn, getHooks } from './map' import { collectTests } from './collect' @@ -15,11 +29,18 @@ import { callFixtureCleanup } from './fixture' const now = Date.now -function updateSuiteHookState(suite: Task, name: keyof SuiteHooks, state: TaskState, runner: VitestRunner) { - if (!suite.result) +function updateSuiteHookState( + suite: Task, + name: keyof SuiteHooks, + state: TaskState, + runner: VitestRunner, +) { + if (!suite.result) { suite.result = { state: 'run' } - if (!suite.result?.hooks) + } + if (!suite.result?.hooks) { suite.result.hooks = {} + } const suiteHooks = suite.result.hooks if (suiteHooks) { suiteHooks[name] = state @@ -27,23 +48,34 @@ function updateSuiteHookState(suite: Task, name: keyof SuiteHooks, state: TaskSt } } -function getSuiteHooks(suite: Suite, name: keyof SuiteHooks, sequence: SequenceHooks) { +function getSuiteHooks( + suite: Suite, + name: keyof SuiteHooks, + sequence: SequenceHooks, +) { const hooks = getHooks(suite)[name] - if (sequence === 'stack' && (name === 'afterAll' || name === 'afterEach')) + if (sequence === 'stack' && (name === 'afterAll' || name === 'afterEach')) { return hooks.slice().reverse() + } return hooks } -async function callTaskHooks(task: Task, hooks: ((result: TaskResult) => Awaitable)[], sequence: SequenceHooks) { - if (sequence === 'stack') +async function callTaskHooks( + task: Task, + hooks: ((result: TaskResult) => Awaitable)[], + sequence: SequenceHooks, +) { + if (sequence === 'stack') { hooks = hooks.slice().reverse() + } if (sequence === 'parallel') { await Promise.all(hooks.map(fn => fn(task.result!))) } else { - for (const fn of hooks) + for (const fn of hooks) { await fn(task.result!) + } } } @@ -58,13 +90,12 @@ export async function callSuiteHook( const callbacks: HookCleanupCallback[] = [] // stop at file level - const parentSuite: Suite | null = 'filepath' in suite - ? null - : (suite.suite || suite.file) + const parentSuite: Suite | null + = 'filepath' in suite ? null : suite.suite || suite.file if (name === 'beforeEach' && parentSuite) { callbacks.push( - ...await callSuiteHook(parentSuite, currentTask, name, runner, args), + ...(await callSuiteHook(parentSuite, currentTask, name, runner, args)), ) } @@ -73,18 +104,21 @@ export async function callSuiteHook( const hooks = getSuiteHooks(suite, name, sequence) if (sequence === 'parallel') { - callbacks.push(...await Promise.all(hooks.map(fn => fn(...args as any)))) + callbacks.push( + ...(await Promise.all(hooks.map(fn => fn(...(args as any))))), + ) } else { - for (const hook of hooks) - callbacks.push(await hook(...args as any)) + for (const hook of hooks) { + callbacks.push(await hook(...(args as any))) + } } updateSuiteHookState(currentTask, name, 'pass', runner) if (name === 'afterEach' && parentSuite) { callbacks.push( - ...await callSuiteHook(parentSuite, currentTask, name, runner, args), + ...(await callSuiteHook(parentSuite, currentTask, name, runner, args)), ) } @@ -113,11 +147,7 @@ async function sendTasksUpdate(runner: VitestRunner) { if (packs.size) { const taskPacks = Array.from(packs).map(([id, task]) => { - return [ - id, - task[0], - task[1], - ] + return [id, task[0], task[1]] }) const p = runner.onTaskUpdate?.(taskPacks) packs.clear() @@ -126,18 +156,22 @@ async function sendTasksUpdate(runner: VitestRunner) { } async function callCleanupHooks(cleanups: HookCleanupCallback[]) { - await Promise.all(cleanups.map(async (fn) => { - if (typeof fn !== 'function') - return - await fn() - })) + await Promise.all( + cleanups.map(async (fn) => { + if (typeof fn !== 'function') { + return + } + await fn() + }), + ) } export async function runTest(test: Test | Custom, runner: VitestRunner) { await runner.onBeforeRunTask?.(test) - if (test.mode !== 'run') + if (test.mode !== 'run') { return + } if (test.result?.state === 'fail') { updateTask(test, runner) @@ -163,36 +197,56 @@ export async function runTest(test: Test | Custom, runner: VitestRunner) { for (let retryCount = 0; retryCount <= retry; retryCount++) { let beforeEachCleanups: HookCleanupCallback[] = [] try { - await runner.onBeforeTryTask?.(test, { retry: retryCount, repeats: repeatCount }) + await runner.onBeforeTryTask?.(test, { + retry: retryCount, + repeats: repeatCount, + }) test.result.repeatCount = repeatCount - beforeEachCleanups = await callSuiteHook(suite, test, 'beforeEach', runner, [test.context, suite]) + beforeEachCleanups = await callSuiteHook( + suite, + test, + 'beforeEach', + runner, + [test.context, suite], + ) if (runner.runTask) { await runner.runTask(test) } else { const fn = getFn(test) - if (!fn) - throw new Error('Test function is not found. Did you add it using `setFn`?') + if (!fn) { + throw new Error( + 'Test function is not found. Did you add it using `setFn`?', + ) + } await fn() } // some async expect will be added to this array, in case user forget to await theme if (test.promises) { const result = await Promise.allSettled(test.promises) - const errors = result.map(r => r.status === 'rejected' ? r.reason : undefined).filter(Boolean) - if (errors.length) + const errors = result + .map(r => (r.status === 'rejected' ? r.reason : undefined)) + .filter(Boolean) + if (errors.length) { throw errors + } } - await runner.onAfterTryTask?.(test, { retry: retryCount, repeats: repeatCount }) + await runner.onAfterTryTask?.(test, { + retry: retryCount, + repeats: repeatCount, + }) if (test.result.state !== 'fail') { - if (!test.repeats) + if (!test.repeats) { test.result.state = 'pass' - else if (test.repeats && retry === retryCount) + } + else if (test.repeats && retry === retryCount) { test.result.state = 'pass' + } } } catch (e) { @@ -209,7 +263,10 @@ export async function runTest(test: Test | Custom, runner: VitestRunner) { } try { - await callSuiteHook(suite, test, 'afterEach', runner, [test.context, suite]) + await callSuiteHook(suite, test, 'afterEach', runner, [ + test.context, + suite, + ]) await callCleanupHooks(beforeEachCleanups) await callFixtureCleanup(test.context) } @@ -217,8 +274,9 @@ export async function runTest(test: Test | Custom, runner: VitestRunner) { failTask(test.result, e, runner.config.diffOptions) } - if (test.result.state === 'pass') + if (test.result.state === 'pass') { break + } if (retryCount < retry) { // reset state when retry test @@ -240,7 +298,11 @@ export async function runTest(test: Test | Custom, runner: VitestRunner) { if (test.result.state === 'fail') { try { - await callTaskHooks(test, test.onFailed || [], runner.config.sequence.hooks) + await callTaskHooks( + test, + test.onFailed || [], + runner.config.sequence.hooks, + ) } catch (e) { failTask(test.result, e, runner.config.diffOptions) @@ -276,9 +338,7 @@ function failTask(result: TaskResult, err: unknown, diffOptions?: DiffOptions) { } result.state = 'fail' - const errors = Array.isArray(err) - ? err - : [err] + const errors = Array.isArray(err) ? err : [err] for (const e of errors) { const error = processError(e, diffOptions) result.errors ??= [] @@ -291,8 +351,9 @@ function markTasksAsSkipped(suite: Suite, runner: VitestRunner) { t.mode = 'skip' t.result = { ...t.result, state: 'skip' } updateTask(t, runner) - if (t.type === 'suite') + if (t.type === 'suite') { markTasksAsSkipped(t, runner) + } }) } @@ -324,7 +385,13 @@ export async function runSuite(suite: Suite, runner: VitestRunner) { } else { try { - beforeAllCleanups = await callSuiteHook(suite, suite, 'beforeAll', runner, [suite]) + beforeAllCleanups = await callSuiteHook( + suite, + suite, + 'beforeAll', + runner, + [suite], + ) if (runner.runSuite) { await runner.runSuite(suite) @@ -338,13 +405,18 @@ export async function runSuite(suite: Suite, runner: VitestRunner) { const { sequence } = runner.config if (sequence.shuffle || suite.shuffle) { // run describe block independently from tests - const suites = tasksGroup.filter(group => group.type === 'suite') + const suites = tasksGroup.filter( + group => group.type === 'suite', + ) const tests = tasksGroup.filter(group => group.type === 'test') const groups = shuffle([suites, tests], sequence.seed) - tasksGroup = groups.flatMap(group => shuffle(group, sequence.seed)) + tasksGroup = groups.flatMap(group => + shuffle(group, sequence.seed), + ) } - for (const c of tasksGroup) + for (const c of tasksGroup) { await runSuiteChild(c, runner) + } } } } @@ -365,7 +437,9 @@ export async function runSuite(suite: Suite, runner: VitestRunner) { if (!runner.config.passWithNoTests && !hasTests(suite)) { suite.result.state = 'fail' if (!suite.result.errors?.length) { - const error = processError(new Error(`No test found in suite ${suite.name}`)) + const error = processError( + new Error(`No test found in suite ${suite.name}`), + ) suite.result.errors = [error] } } @@ -388,11 +462,12 @@ export async function runSuite(suite: Suite, runner: VitestRunner) { let limitMaxConcurrency: ReturnType async function runSuiteChild(c: Task, runner: VitestRunner) { - if (c.type === 'test' || c.type === 'custom') + if (c.type === 'test' || c.type === 'custom') { return limitMaxConcurrency(() => runTest(c, runner)) - - else if (c.type === 'suite') + } + else if (c.type === 'suite') { return runSuite(c, runner) + } } export async function runFiles(files: File[], runner: VitestRunner) { @@ -401,7 +476,9 @@ export async function runFiles(files: File[], runner: VitestRunner) { for (const file of files) { if (!file.tasks.length && !runner.config.passWithNoTests) { if (!file.result?.errors?.length) { - const error = processError(new Error(`No test suite found in file ${file.filepath}`)) + const error = processError( + new Error(`No test suite found in file ${file.filepath}`), + ) file.result = { state: 'fail', errors: [error], diff --git a/packages/runner/src/setup.ts b/packages/runner/src/setup.ts index 32c365b30..f54a8b033 100644 --- a/packages/runner/src/setup.ts +++ b/packages/runner/src/setup.ts @@ -1,7 +1,10 @@ import { toArray } from '@vitest/utils' import type { VitestRunner, VitestRunnerConfig } from './types' -export async function runSetupFiles(config: VitestRunnerConfig, runner: VitestRunner) { +export async function runSetupFiles( + config: VitestRunnerConfig, + runner: VitestRunner, +) { const files = toArray(config.setupFiles) if (config.sequence.setupFiles === 'parallel') { await Promise.all( @@ -11,7 +14,8 @@ export async function runSetupFiles(config: VitestRunnerConfig, runner: VitestRu ) } else { - for (const fsPath of files) + for (const fsPath of files) { await runner.importFile(fsPath, 'setup') + } } } diff --git a/packages/runner/src/suite.ts b/packages/runner/src/suite.ts index 4a07c0901..ea768e723 100644 --- a/packages/runner/src/suite.ts +++ b/packages/runner/src/suite.ts @@ -1,9 +1,39 @@ -import { format, isNegativeNaN, isObject, objDisplay, objectAttr, toArray } from '@vitest/utils' +import { + format, + isNegativeNaN, + isObject, + objDisplay, + objectAttr, + toArray, +} from '@vitest/utils' import { parseSingleStack } from '@vitest/utils/source-map' -import type { Custom, CustomAPI, File, Fixtures, RunMode, Suite, SuiteAPI, SuiteCollector, SuiteFactory, SuiteHooks, Task, TaskCustomOptions, Test, TestAPI, TestFunction, TestOptions } from './types' +import type { + Custom, + CustomAPI, + File, + Fixtures, + RunMode, + Suite, + SuiteAPI, + SuiteCollector, + SuiteFactory, + SuiteHooks, + Task, + TaskCustomOptions, + Test, + TestAPI, + TestFunction, + TestOptions, +} from './types' import type { VitestRunner } from './types/runner' import { createChainable } from './utils/chain' -import { collectTask, collectorContext, createTestContext, runWithSuite, withTimeout } from './context' +import { + collectTask, + collectorContext, + createTestContext, + runWithSuite, + withTimeout, +} from './context' import { getHooks, setFixture, setFn, setHooks } from './map' import type { FixtureItem } from './fixture' import { mergeContextFixtures, withFixtures } from './fixture' @@ -11,14 +41,24 @@ import { getCurrentTest } from './test-state' // apis export const suite = createSuite() -export const test = createTest( - function (name: string | Function, optionsOrFn?: TestOptions | TestFunction, optionsOrTest?: number | TestOptions | TestFunction) { - if (getCurrentTest()) - throw new Error('Calling the test function inside another test function is not allowed. Please put it inside "describe" or "suite" so it can be properly collected.') +export const test = createTest(function ( + name: string | Function, + optionsOrFn?: TestOptions | TestFunction, + optionsOrTest?: number | TestOptions | TestFunction, +) { + if (getCurrentTest()) { + throw new Error( + 'Calling the test function inside another test function is not allowed. Please put it inside "describe" or "suite" so it can be properly collected.', + ) + } - getCurrentSuite().test.fn.call(this, formatName(name), optionsOrFn as TestOptions, optionsOrTest as TestFunction) - }, -) + getCurrentSuite().test.fn.call( + this, + formatName(name), + optionsOrFn as TestOptions, + optionsOrTest as TestFunction, + ) +}) // alias export const describe = suite @@ -46,9 +86,13 @@ function createDefaultSuite(runner: VitestRunner) { return api('', { concurrent: config.concurrent }, () => {}) } -export function clearCollectorContext(filepath: string, currentRunner: VitestRunner) { - if (!defaultSuite) +export function clearCollectorContext( + filepath: string, + currentRunner: VitestRunner, +) { + if (!defaultSuite) { defaultSuite = createDefaultSuite(currentRunner) + } runner = currentRunner currentTestFilepath = filepath collectorContext.tasks.length = 0 @@ -57,7 +101,8 @@ export function clearCollectorContext(filepath: string, currentRunner: VitestRun } export function getCurrentSuite() { - return (collectorContext.currentSuite || defaultSuite) as SuiteCollector + return (collectorContext.currentSuite + || defaultSuite) as SuiteCollector } export function createSuiteHooks() { @@ -79,10 +124,13 @@ function parseArguments any>( // it('', () => {}, { retry: 2 }) if (typeof optionsOrTest === 'object') { // it('', { retry: 2 }, { retry: 3 }) - if (typeof optionsOrFn === 'object') - throw new TypeError('Cannot use two objects as arguments. Please provide options and a function callback in that order.') - // TODO: more info, add a name - // console.warn('The third argument is deprecated. Please use the second argument for options.') + if (typeof optionsOrFn === 'object') { + throw new TypeError( + 'Cannot use two objects as arguments. Please provide options and a function callback in that order.', + ) + } + // TODO: more info, add a name + // console.warn('The third argument is deprecated. Please use the second argument for options.') options = optionsOrTest } // it('', () => {}, 1000) @@ -95,8 +143,11 @@ function parseArguments any>( } if (typeof optionsOrFn === 'function') { - if (typeof optionsOrTest === 'function') - throw new TypeError('Cannot use two functions as arguments. Please use the second argument for options.') + if (typeof optionsOrTest === 'function') { + throw new TypeError( + 'Cannot use two functions as arguments. Please use the second argument for options.', + ) + } fn = optionsOrFn as T } else if (typeof optionsOrTest === 'function') { @@ -110,7 +161,14 @@ function parseArguments any>( } // implementations -function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, mode: RunMode, shuffle?: boolean, each?: boolean, suiteOptions?: TestOptions) { +function createSuiteCollector( + name: string, + factory: SuiteFactory = () => {}, + mode: RunMode, + shuffle?: boolean, + each?: boolean, + suiteOptions?: TestOptions, +) { const tasks: (Test | Custom | Suite | SuiteCollector)[] = [] const factoryQueue: (Test | Suite | SuiteCollector)[] = [] @@ -130,14 +188,25 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m file: undefined!, retry: options.retry ?? runner.config.retry, repeats: options.repeats, - mode: options.only ? 'only' : options.skip ? 'skip' : options.todo ? 'todo' : 'run', + mode: options.only + ? 'only' + : options.skip + ? 'skip' + : options.todo + ? 'todo' + : 'run', meta: options.meta ?? Object.create(null), } const handler = options.handler - if (options.concurrent || (!options.sequential && runner.config.sequence.concurrent)) + if ( + options.concurrent + || (!options.sequential && runner.config.sequence.concurrent) + ) { task.concurrent = true - if (shuffle) + } + if (shuffle) { task.shuffle = true + } const context = createTestContext(task, runner) // create test context @@ -148,10 +217,13 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m setFixture(context, options.fixtures) if (handler) { - setFn(task, withTimeout( - withFixtures(handler, context), - options?.timeout ?? runner.config.testTimeout, - )) + setFn( + task, + withTimeout( + withFixtures(handler, context), + options?.timeout ?? runner.config.testTimeout, + ), + ) } if (runner.config.includeTaskLocation) { @@ -161,32 +233,38 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m const error = new Error('stacktrace').stack! Error.stackTraceLimit = limit const stack = findTestFileStackTrace(error, task.each ?? false) - if (stack) + if (stack) { task.location = stack + } } tasks.push(task) return task } - const test = createTest(function (name: string | Function, optionsOrFn?: TestOptions | TestFunction, optionsOrTest?: number | TestOptions | TestFunction) { - let { options, handler } = parseArguments( - optionsOrFn, - optionsOrTest, - ) + const test = createTest(function ( + name: string | Function, + optionsOrFn?: TestOptions | TestFunction, + optionsOrTest?: number | TestOptions | TestFunction, + ) { + let { options, handler } = parseArguments(optionsOrFn, optionsOrTest) // inherit repeats, retry, timeout from suite - if (typeof suiteOptions === 'object') + if (typeof suiteOptions === 'object') { options = Object.assign({}, suiteOptions, options) + } // inherit concurrent / sequential from suite - options.concurrent = this.concurrent || (!this.sequential && options?.concurrent) - options.sequential = this.sequential || (!this.concurrent && options?.sequential) + options.concurrent + = this.concurrent || (!this.sequential && options?.concurrent) + options.sequential + = this.sequential || (!this.concurrent && options?.sequential) - const test = task( - formatName(name), - { ...this, ...options, handler }, - ) as unknown as Test + const test = task(formatName(name), { + ...this, + ...options, + handler, + }) as unknown as Test test.type = 'test' }) @@ -205,12 +283,13 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m } function addHook(name: T, ...fn: SuiteHooks[T]) { - getHooks(suite)[name].push(...fn as any) + getHooks(suite)[name].push(...(fn as any)) } function initSuite(includeLocation: boolean) { - if (typeof suiteOptions === 'number') + if (typeof suiteOptions === 'number') { suiteOptions = { timeout: suiteOptions } + } suite = { id: '', @@ -231,8 +310,9 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m const error = new Error('stacktrace').stack! Error.stackTraceLimit = limit const stack = findTestFileStackTrace(error, suite.each ?? false) - if (stack) + if (stack) { suite.location = stack + } } setHooks(suite, createSuiteHooks()) @@ -245,17 +325,20 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m } async function collect(file: File) { - if (!file) + if (!file) { throw new TypeError('File is required to collect tasks.') + } factoryQueue.length = 0 - if (factory) + if (factory) { await runWithSuite(collector, () => factory(test)) + } const allChildren: Task[] = [] - for (const i of [...factoryQueue, ...tasks]) + for (const i of [...factoryQueue, ...tasks]) { allChildren.push(i.type === 'collector' ? await i.collect(file) : i) + } suite.file = file suite.tasks = allChildren @@ -273,8 +356,19 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m } function createSuite() { - function suiteFn(this: Record, name: string | Function, factoryOrOptions?: SuiteFactory | TestOptions, optionsOrFactory: number | TestOptions | SuiteFactory = {}) { - const mode: RunMode = this.only ? 'only' : this.skip ? 'skip' : this.todo ? 'todo' : 'run' + function suiteFn( + this: Record, + name: string | Function, + factoryOrOptions?: SuiteFactory | TestOptions, + optionsOrFactory: number | TestOptions | SuiteFactory = {}, + ) { + const mode: RunMode = this.only + ? 'only' + : this.skip + ? 'skip' + : this.todo + ? 'todo' + : 'run' const currentSuite = getCurrentSuite() let { options, handler: factory } = parseArguments( @@ -283,33 +377,52 @@ function createSuite() { ) // inherit options from current suite - if (currentSuite?.options) + if (currentSuite?.options) { options = { ...currentSuite.options, ...options } + } // inherit concurrent / sequential from suite - const isConcurrent = options.concurrent || (this.concurrent && !this.sequential) - const isSequential = options.sequential || (this.sequential && !this.concurrent) + const isConcurrent + = options.concurrent || (this.concurrent && !this.sequential) + const isSequential + = options.sequential || (this.sequential && !this.concurrent) options.concurrent = isConcurrent && !isSequential options.sequential = isSequential && !isConcurrent - return createSuiteCollector(formatName(name), factory, mode, this.shuffle, this.each, options) + return createSuiteCollector( + formatName(name), + factory, + mode, + this.shuffle, + this.each, + options, + ) } - suiteFn.each = function(this: { withContext: () => SuiteAPI; setContext: (key: string, value: boolean | undefined) => SuiteAPI }, cases: ReadonlyArray, ...args: any[]) { + suiteFn.each = function ( + this: { + withContext: () => SuiteAPI + setContext: (key: string, value: boolean | undefined) => SuiteAPI + }, + cases: ReadonlyArray, + ...args: any[] + ) { const suite = this.withContext() this.setContext('each', true) - if (Array.isArray(cases) && args.length) + if (Array.isArray(cases) && args.length) { cases = formatTemplateString(cases, args) + } - return (name: string | Function, optionsOrFn: ((...args: T[]) => void) | TestOptions, fnOrOptions?: ((...args: T[]) => void) | number | TestOptions) => { + return ( + name: string | Function, + optionsOrFn: ((...args: T[]) => void) | TestOptions, + fnOrOptions?: ((...args: T[]) => void) | number | TestOptions, + ) => { const _name = formatName(name) const arrayOnlyCases = cases.every(Array.isArray) - const { options, handler } = parseArguments( - optionsOrFn, - fnOrOptions, - ) + const { options, handler } = parseArguments(optionsOrFn, fnOrOptions) const fnFirst = typeof optionsOrFn === 'function' @@ -317,12 +430,17 @@ function createSuite() { const items = Array.isArray(i) ? i : [i] if (fnFirst) { arrayOnlyCases - ? suite(formatTitle(_name, items, idx), () => handler(...items), options) + ? suite( + formatTitle(_name, items, idx), + () => handler(...items), + options, + ) : suite(formatTitle(_name, items, idx), () => handler(i), options) } else { arrayOnlyCases - ? suite(formatTitle(_name, items, idx), options, () => handler(...items)) + ? suite(formatTitle(_name, items, idx), options, () => + handler(...items)) : suite(formatTitle(_name, items, idx), options, () => handler(i)) } }) @@ -331,8 +449,10 @@ function createSuite() { } } - suiteFn.skipIf = (condition: any) => (condition ? suite.skip : suite) as SuiteAPI - suiteFn.runIf = (condition: any) => (condition ? suite : suite.skip) as SuiteAPI + suiteFn.skipIf = (condition: any) => + (condition ? suite.skip : suite) as SuiteAPI + suiteFn.runIf = (condition: any) => + (condition ? suite : suite.skip) as SuiteAPI return createChainable( ['concurrent', 'sequential', 'shuffle', 'skip', 'only', 'todo'], @@ -346,21 +466,30 @@ export function createTaskCollector( ) { const taskFn = fn as any - taskFn.each = function(this: { withContext: () => SuiteAPI; setContext: (key: string, value: boolean | undefined) => SuiteAPI }, cases: ReadonlyArray, ...args: any[]) { + taskFn.each = function ( + this: { + withContext: () => SuiteAPI + setContext: (key: string, value: boolean | undefined) => SuiteAPI + }, + cases: ReadonlyArray, + ...args: any[] + ) { const test = this.withContext() this.setContext('each', true) - if (Array.isArray(cases) && args.length) + if (Array.isArray(cases) && args.length) { cases = formatTemplateString(cases, args) + } - return (name: string | Function, optionsOrFn: ((...args: T[]) => void) | TestOptions, fnOrOptions?: ((...args: T[]) => void) | number | TestOptions) => { + return ( + name: string | Function, + optionsOrFn: ((...args: T[]) => void) | TestOptions, + fnOrOptions?: ((...args: T[]) => void) | number | TestOptions, + ) => { const _name = formatName(name) const arrayOnlyCases = cases.every(Array.isArray) - const { options, handler } = parseArguments( - optionsOrFn, - fnOrOptions, - ) + const { options, handler } = parseArguments(optionsOrFn, fnOrOptions) const fnFirst = typeof optionsOrFn === 'function' @@ -369,12 +498,17 @@ export function createTaskCollector( if (fnFirst) { arrayOnlyCases - ? test(formatTitle(_name, items, idx), () => handler(...items), options) + ? test( + formatTitle(_name, items, idx), + () => handler(...items), + options, + ) : test(formatTitle(_name, items, idx), () => handler(i), options) } else { arrayOnlyCases - ? test(formatTitle(_name, items, idx), options, () => handler(...items)) + ? test(formatTitle(_name, items, idx), options, () => + handler(...items)) : test(formatTitle(_name, items, idx), options, () => handler(i)) } }) @@ -393,8 +527,9 @@ export function createTaskCollector( ) { const test = this.withContext() - if (Array.isArray(cases) && args.length) + if (Array.isArray(cases) && args.length) { cases = formatTemplateString(cases, args) + } return ( name: string | Function, @@ -423,8 +558,17 @@ export function createTaskCollector( taskFn.extend = function (fixtures: Fixtures>) { const _context = mergeContextFixtures(fixtures, context) - return createTest(function fn(name: string | Function, optionsOrFn?: TestOptions | TestFunction, optionsOrTest?: number | TestOptions | TestFunction) { - getCurrentSuite().test.fn.call(this, formatName(name), optionsOrFn as TestOptions, optionsOrTest as TestFunction) + return createTest(function fn( + name: string | Function, + optionsOrFn?: TestOptions | TestFunction, + optionsOrTest?: number | TestOptions | TestFunction, + ) { + getCurrentSuite().test.fn.call( + this, + formatName(name), + optionsOrFn as TestOptions, + optionsOrTest as TestFunction, + ) }, _context) } @@ -433,25 +577,34 @@ export function createTaskCollector( taskFn, ) as CustomAPI - if (context) + if (context) { (_test as any).mergeContext(context) + } return _test } -function createTest(fn: ( - ( - this: Record<'concurrent' | 'sequential' | 'skip' | 'only' | 'todo' | 'fails' | 'each', boolean | undefined> & { fixtures?: FixtureItem[] }, +function createTest( + fn: ( + this: Record< + 'concurrent' | 'sequential' | 'skip' | 'only' | 'todo' | 'fails' | 'each', + boolean | undefined + > & { fixtures?: FixtureItem[] }, title: string, optionsOrFn?: TestOptions | TestFunction, - optionsOrTest?: number | TestOptions | TestFunction, - ) => void -), context?: Record) { + optionsOrTest?: number | TestOptions | TestFunction + ) => void, + context?: Record, +) { return createTaskCollector(fn, context) as TestAPI } function formatName(name: string | Function) { - return typeof name === 'string' ? name : name instanceof Function ? (name.name || '') : String(name) + return typeof name === 'string' + ? name + : name instanceof Function + ? name.name || '' + : String(name) } function formatTitle(template: string, items: any[], idx: number) { @@ -483,19 +636,28 @@ function formatTitle(template: string, items: any[], idx: number) { formatted = formatted.replace( /\$([$\w.]+)/g, // https://github.com/chaijs/chai/pull/1490 - (_, key) => objDisplay(objectAttr(items[0], key), { truncate: runner?.config?.chaiConfig?.truncateThreshold }) as unknown as string, + (_, key) => + objDisplay(objectAttr(items[0], key), { + truncate: runner?.config?.chaiConfig?.truncateThreshold, + }) as unknown as string, ) } return formatted } function formatTemplateString(cases: any[], args: any[]): any[] { - const header = cases.join('').trim().replace(/ /g, '').split('\n').map(i => i.split('|'))[0] + const header = cases + .join('') + .trim() + .replace(/ /g, '') + .split('\n') + .map(i => i.split('|'))[0] const res: any[] = [] - for (let i = 0; i < Math.floor((args.length) / header.length); i++) { + for (let i = 0; i < Math.floor(args.length / header.length); i++) { const oneCase: Record = {} - for (let j = 0; j < header.length; j++) + for (let j = 0; j < header.length; j++) { oneCase[header[j]] = args[i * header.length + j] as any + } res.push(oneCase) } return res diff --git a/packages/runner/src/types/runner.ts b/packages/runner/src/types/runner.ts index 633277afb..49ec0198f 100644 --- a/packages/runner/src/types/runner.ts +++ b/packages/runner/src/types/runner.ts @@ -40,13 +40,13 @@ export interface VitestRunnerConfig { export type VitestRunnerImportSource = 'collect' | 'setup' export interface VitestRunnerConstructor { - new(config: VitestRunnerConfig): VitestRunner + new (config: VitestRunnerConfig): VitestRunner } export type CancelReason = | 'keyboard-input' | 'test-failure' - | string & Record + | (string & Record) export interface VitestRunner { /** @@ -76,7 +76,10 @@ export interface VitestRunner { /** * Called before actually running the test function. Already has "result" with "state" and "startTime". */ - onBeforeTryTask?: (test: Task, options: { retry: number; repeats: number }) => unknown + onBeforeTryTask?: ( + test: Task, + options: { retry: number; repeats: number } + ) => unknown /** * Called after result and state are set. */ diff --git a/packages/runner/src/types/tasks.ts b/packages/runner/src/types/tasks.ts index 9aa0c2226..c1b9c19be 100644 --- a/packages/runner/src/types/tasks.ts +++ b/packages/runner/src/types/tasks.ts @@ -50,7 +50,11 @@ export interface TaskResult { repeatCount?: number } -export type TaskResultPack = [id: string, result: TaskResult | undefined, meta: TaskMeta] +export type TaskResultPack = [ + id: string, + result: TaskResult | undefined, + meta: TaskMeta, +] export interface Suite extends TaskBase { file: File @@ -78,7 +82,9 @@ export interface Custom extends TaskPopulated { export type Task = Test | Suite | Custom | File export type DoneCallback = (error?: any) => void -export type TestFunction = (context: ExtendedContext & ExtraContext) => Awaitable | void +export type TestFunction = ( + context: ExtendedContext & ExtraContext +) => Awaitable | void // jest's ExtractEachCallbackArgs type ExtractEachCallbackArgs> = { @@ -122,23 +128,25 @@ interface EachFunctionReturn { ( name: string | Function, fn: (...args: T) => Awaitable, - options: TestOptions, + options: TestOptions ): void ( name: string | Function, fn: (...args: T) => Awaitable, - options?: number | TestOptions, + options?: number | TestOptions ): void ( name: string | Function, options: TestOptions, - fn: (...args: T) => Awaitable, + fn: (...args: T) => Awaitable ): void } interface TestEachFunction { (cases: ReadonlyArray): EachFunctionReturn - >(cases: ReadonlyArray): EachFunctionReturn> + >(cases: ReadonlyArray): EachFunctionReturn< + ExtractEachCallbackArgs + > (cases: ReadonlyArray): EachFunctionReturn (...args: [TemplateStringsArray, ...any]): EachFunctionReturn } @@ -146,35 +154,53 @@ interface TestEachFunction { interface TestForFunctionReturn { ( name: string | Function, - fn: (arg: Arg, context: Context) => Awaitable, + fn: (arg: Arg, context: Context) => Awaitable ): void ( name: string | Function, options: TestOptions, - fn: (args: Arg, context: Context) => Awaitable, + fn: (args: Arg, context: Context) => Awaitable ): void } interface TestForFunction { // test.for([1, 2, 3]) // test.for([[1, 2], [3, 4, 5]]) - (cases: ReadonlyArray): TestForFunctionReturn & ExtraContext> + (cases: ReadonlyArray): TestForFunctionReturn< + T, + ExtendedContext & ExtraContext + > // test.for` // a | b // {1} | {2} // {3} | {4} // ` - (strings: TemplateStringsArray, ...values: any[]): TestForFunctionReturn & ExtraContext> + (strings: TemplateStringsArray, ...values: any[]): TestForFunctionReturn< + any, + ExtendedContext & ExtraContext + > } interface TestCollectorCallable { /** * @deprecated Use options as the second argument instead */ - (name: string | Function, fn: TestFunction, options: TestOptions): void - (name: string | Function, fn?: TestFunction, options?: number | TestOptions): void - (name: string | Function, options?: TestOptions, fn?: TestFunction): void + ( + name: string | Function, + fn: TestFunction, + options: TestOptions + ): void + ( + name: string | Function, + fn?: TestFunction, + options?: number | TestOptions + ): void + ( + name: string | Function, + options?: TestOptions, + fn?: TestFunction + ): void } type ChainableTestAPI = ChainableFunction< @@ -238,19 +264,31 @@ interface ExtendedAPI { runIf: (condition: any) => ChainableTestAPI } -export type CustomAPI = ChainableTestAPI & ExtendedAPI & { - extend: = {}>(fixtures: Fixtures) => CustomAPI<{ - [K in keyof T | keyof ExtraContext]: - K extends keyof T ? T[K] : - K extends keyof ExtraContext ? ExtraContext[K] : never }> -} +export type CustomAPI = ChainableTestAPI & + ExtendedAPI & { + extend: = {}>( + fixtures: Fixtures + ) => CustomAPI<{ + [K in keyof T | keyof ExtraContext]: K extends keyof T + ? T[K] + : K extends keyof ExtraContext + ? ExtraContext[K] + : never; + }> + } -export type TestAPI = ChainableTestAPI & ExtendedAPI & { - extend: = {}>(fixtures: Fixtures) => TestAPI<{ - [K in keyof T | keyof ExtraContext]: - K extends keyof T ? T[K] : - K extends keyof ExtraContext ? ExtraContext[K] : never }> -} +export type TestAPI = ChainableTestAPI & + ExtendedAPI & { + extend: = {}>( + fixtures: Fixtures + ) => TestAPI<{ + [K in keyof T | keyof ExtraContext]: K extends keyof T + ? T[K] + : K extends keyof ExtraContext + ? ExtraContext[K] + : never; + }> + } export interface FixtureOptions { /** @@ -260,14 +298,25 @@ export interface FixtureOptions { } export type Use = (value: T) => Promise -export type FixtureFn = - (context: Omit & ExtraContext, use: Use) => Promise -export type Fixture = - ((...args: any) => any) extends T[K] - ? (T[K] extends any ? FixtureFn>> : never) - : T[K] | (T[K] extends any ? FixtureFn>> : never) +export type FixtureFn = ( + context: Omit & ExtraContext, + use: Use +) => Promise +export type Fixture = (( + ...args: any +) => any) extends T[K] + ? T[K] extends any + ? FixtureFn>> + : never + : + | T[K] + | (T[K] extends any + ? FixtureFn>> + : never) export type Fixtures, ExtraContext = {}> = { - [K in keyof T]: Fixture> | [Fixture>, FixtureOptions?] + [K in keyof T]: + | Fixture> + | [Fixture>, FixtureOptions?]; } export type InferFixturesTypes = T extends TestAPI ? C : T @@ -276,9 +325,21 @@ interface SuiteCollectorCallable { /** * @deprecated Use options as the second argument instead */ - (name: string | Function, fn: SuiteFactory, options: TestOptions): SuiteCollector - (name: string | Function, fn?: SuiteFactory, options?: number | TestOptions): SuiteCollector - (name: string | Function, options: TestOptions, fn?: SuiteFactory): SuiteCollector + ( + name: string | Function, + fn: SuiteFactory, + options: TestOptions + ): SuiteCollector + ( + name: string | Function, + fn?: SuiteFactory, + options?: number | TestOptions + ): SuiteCollector + ( + name: string | Function, + options: TestOptions, + fn?: SuiteFactory + ): SuiteCollector } type ChainableSuiteAPI = ChainableFunction< @@ -294,15 +355,22 @@ export type SuiteAPI = ChainableSuiteAPI & { runIf: (condition: any) => ChainableSuiteAPI } -export type HookListener = (...args: T) => Awaitable +export type HookListener = ( + ...args: T +) => Awaitable export type HookCleanupCallback = (() => Awaitable) | void export interface SuiteHooks { beforeAll: HookListener<[Readonly], HookCleanupCallback>[] afterAll: HookListener<[Readonly]>[] - beforeEach: HookListener<[ExtendedContext & ExtraContext, Readonly], HookCleanupCallback>[] - afterEach: HookListener<[ExtendedContext & ExtraContext, Readonly]>[] + beforeEach: HookListener< + [ExtendedContext & ExtraContext, Readonly], + HookCleanupCallback + >[] + afterEach: HookListener< + [ExtendedContext & ExtraContext, Readonly] + >[] } export interface TaskCustomOptions extends TestOptions { @@ -324,14 +392,24 @@ export interface SuiteCollector { options?: TestOptions type: 'collector' test: TestAPI - tasks: (Suite | Custom | Test | SuiteCollector)[] + tasks: ( + | Suite + | Custom + | Test + | SuiteCollector + )[] task: (name: string, options?: TaskCustomOptions) => Custom collect: (file: File) => Promise clear: () => void - on: >(name: T, ...fn: SuiteHooks[T]) => void + on: >( + name: T, + ...fn: SuiteHooks[T] + ) => void } -export type SuiteFactory = (test: TestAPI) => Awaitable +export type SuiteFactory = ( + test: TestAPI +) => Awaitable export interface RuntimeContext { tasks: (SuiteCollector | Test)[] @@ -362,7 +440,8 @@ export interface TaskContext { skip: () => void } -export type ExtendedContext = TaskContext & TestContext +export type ExtendedContext = TaskContext & + TestContext export type OnTestFailedHandler = (result: TaskResult) => Awaitable export type OnTestFinishedHandler = (result: TaskResult) => Awaitable diff --git a/packages/runner/src/utils/chain.ts b/packages/runner/src/utils/chain.ts index 79da93447..dbfe9ab69 100644 --- a/packages/runner/src/utils/chain.ts +++ b/packages/runner/src/utils/chain.ts @@ -1,5 +1,9 @@ -export type ChainableFunction any, C = {}> = F & { - [x in T]: ChainableFunction +export type ChainableFunction< + T extends string, + F extends (...args: any) => any, + C = {}, +> = F & { + [x in T]: ChainableFunction; } & { fn: (this: Record, ...args: Parameters) => ReturnType } & C diff --git a/packages/runner/src/utils/collect.ts b/packages/runner/src/utils/collect.ts index eef88cc29..a2d5e6325 100644 --- a/packages/runner/src/utils/collect.ts +++ b/packages/runner/src/utils/collect.ts @@ -5,7 +5,13 @@ import type { File, Suite, TaskBase } from '../types' /** * If any tasks been marked as `only`, mark all other tasks as `skip`. */ -export function interpretTaskModes(suite: Suite, namePattern?: string | RegExp, onlyMode?: boolean, parentIsOnly?: boolean, allowOnly?: boolean) { +export function interpretTaskModes( + suite: Suite, + namePattern?: string | RegExp, + onlyMode?: boolean, + parentIsOnly?: boolean, + allowOnly?: boolean, +) { const suiteIsOnly = parentIsOnly || suite.mode === 'only' suite.tasks.forEach((t) => { @@ -28,21 +34,25 @@ export function interpretTaskModes(suite: Suite, namePattern?: string | RegExp, } } if (t.type === 'test') { - if (namePattern && !getTaskFullName(t).match(namePattern)) + if (namePattern && !getTaskFullName(t).match(namePattern)) { t.mode = 'skip' + } } else if (t.type === 'suite') { - if (t.mode === 'skip') + if (t.mode === 'skip') { skipAllTasks(t) - else + } + else { interpretTaskModes(t, namePattern, onlyMode, includeTask, allowOnly) + } } }) // if all subtasks are skipped, mark as skip if (suite.mode === 'run') { - if (suite.tasks.length && suite.tasks.every(i => i.mode !== 'run')) + if (suite.tasks.length && suite.tasks.every(i => i.mode !== 'run')) { suite.mode = 'skip' + } } } @@ -51,23 +61,31 @@ function getTaskFullName(task: TaskBase): string { } export function someTasksAreOnly(suite: Suite): boolean { - return suite.tasks.some(t => t.mode === 'only' || (t.type === 'suite' && someTasksAreOnly(t))) + return suite.tasks.some( + t => t.mode === 'only' || (t.type === 'suite' && someTasksAreOnly(t)), + ) } function skipAllTasks(suite: Suite) { suite.tasks.forEach((t) => { if (t.mode === 'run') { t.mode = 'skip' - if (t.type === 'suite') + if (t.type === 'suite') { skipAllTasks(t) + } } }) } function checkAllowOnly(task: TaskBase, allowOnly?: boolean) { - if (allowOnly) + if (allowOnly) { return - const error = processError(new Error('[Vitest] Unexpected .only modifier. Remove it or pass --allowOnly argument to bypass this error')) + } + const error = processError( + new Error( + '[Vitest] Unexpected .only modifier. Remove it or pass --allowOnly argument to bypass this error', + ), + ) task.result = { state: 'fail', errors: [error], @@ -76,8 +94,9 @@ function checkAllowOnly(task: TaskBase, allowOnly?: boolean) { export function generateHash(str: string): string { let hash = 0 - if (str.length === 0) + if (str.length === 0) { return `${hash}` + } for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i) hash = (hash << 5) - hash + char @@ -89,12 +108,17 @@ export function generateHash(str: string): string { export function calculateSuiteHash(parent: Suite) { parent.tasks.forEach((t, idx) => { t.id = `${parent.id}_${idx}` - if (t.type === 'suite') + if (t.type === 'suite') { calculateSuiteHash(t) + } }) } -export function createFileTask(filepath: string, root: string, projectName: string) { +export function createFileTask( + filepath: string, + root: string, + projectName: string, +) { const path = relative(root, filepath) const file: File = { id: generateHash(`${path}${projectName || ''}`), diff --git a/packages/runner/src/utils/suite.ts b/packages/runner/src/utils/suite.ts index c60a71aaa..1c9d89aad 100644 --- a/packages/runner/src/utils/suite.ts +++ b/packages/runner/src/utils/suite.ts @@ -15,8 +15,9 @@ export function partitionSuiteChildren(suite: Suite) { tasksGroup = [c] } } - if (tasksGroup.length > 0) + if (tasksGroup.length > 0) { tasksGroups.push(tasksGroup) + } return tasksGroups } diff --git a/packages/runner/src/utils/tasks.ts b/packages/runner/src/utils/tasks.ts index 8898fa1cf..82f25919c 100644 --- a/packages/runner/src/utils/tasks.ts +++ b/packages/runner/src/utils/tasks.ts @@ -19,8 +19,9 @@ export function getTests(suite: Arrayable): (Test | Custom)[] { } else { const taskTests = getTests(task) - for (const test of taskTests) + for (const test of taskTests) { tests.push(test) + } } } } @@ -29,19 +30,28 @@ export function getTests(suite: Arrayable): (Test | Custom)[] { } export function getTasks(tasks: Arrayable = []): Task[] { - return toArray(tasks).flatMap(s => isAtomTest(s) ? [s] : [s, ...getTasks(s.tasks)]) + return toArray(tasks).flatMap(s => + isAtomTest(s) ? [s] : [s, ...getTasks(s.tasks)], + ) } export function getSuites(suite: Arrayable): Suite[] { - return toArray(suite).flatMap(s => s.type === 'suite' ? [s, ...getSuites(s.tasks)] : []) + return toArray(suite).flatMap(s => + s.type === 'suite' ? [s, ...getSuites(s.tasks)] : [], + ) } export function hasTests(suite: Arrayable): boolean { - return toArray(suite).some(s => s.tasks.some(c => isAtomTest(c) || hasTests(c))) + return toArray(suite).some(s => + s.tasks.some(c => isAtomTest(c) || hasTests(c)), + ) } export function hasFailed(suite: Arrayable): boolean { - return toArray(suite).some(s => s.result?.state === 'fail' || (s.type === 'suite' && hasFailed(s.tasks))) + return toArray(suite).some( + s => + s.result?.state === 'fail' || (s.type === 'suite' && hasFailed(s.tasks)), + ) } export function getNames(task: Task) { @@ -50,12 +60,14 @@ export function getNames(task: Task) { while (current?.suite) { current = current.suite - if (current?.name) + if (current?.name) { names.unshift(current.name) + } } - if (current !== task.file) + if (current !== task.file) { names.unshift(task.file.name) + } return names } diff --git a/packages/snapshot/README.md b/packages/snapshot/README.md index 4c4ecda22..edf081739 100644 --- a/packages/snapshot/README.md +++ b/packages/snapshot/README.md @@ -12,7 +12,8 @@ import { SnapshotManager } from '@vitest/snapshot/manager' const client = new SnapshotClient({ // you need to provide your own equality check implementation if you use it // this function is called when `.toMatchSnapshot({ property: 1 })` is called - isEqual: (received, expected) => equals(received, expected, [iterableEquality, subsetEquality]), + isEqual: (received, expected) => + equals(received, expected, [iterableEquality, subsetEquality]), }) // class that implements snapshot saving and reading @@ -53,7 +54,11 @@ const options = { snapshotEnvironment: environment, } -await client.startCurrentRun(getCurrentFilepath(), getCurrentTestName(), options) +await client.startCurrentRun( + getCurrentFilepath(), + getCurrentTestName(), + options +) // this will save snapshot to a file which is returned by "snapshotEnvironment.resolvePath" client.assert({ diff --git a/packages/snapshot/rollup.config.js b/packages/snapshot/rollup.config.js index 32d75de37..90c493d3d 100644 --- a/packages/snapshot/rollup.config.js +++ b/packages/snapshot/rollup.config.js @@ -51,15 +51,14 @@ export default defineConfig([ format: 'esm', }, external, - plugins: [ - dts({ respectExternal: true }), - ], + plugins: [dts({ respectExternal: true })], onwarn, }, ]) function onwarn(message) { - if (['EMPTY_BUNDLE', 'CIRCULAR_DEPENDENCY'].includes(message.code)) + if (['EMPTY_BUNDLE', 'CIRCULAR_DEPENDENCY'].includes(message.code)) { return + } console.error(message) } diff --git a/packages/snapshot/src/client.ts b/packages/snapshot/src/client.ts index e6add4e99..dcbdff37b 100644 --- a/packages/snapshot/src/client.ts +++ b/packages/snapshot/src/client.ts @@ -3,7 +3,12 @@ import SnapshotState from './port/state' import type { SnapshotStateOptions } from './types' import type { RawSnapshotInfo } from './port/rawSnapshot' -function createMismatchError(message: string, expand: boolean | undefined, actual: unknown, expected: unknown) { +function createMismatchError( + message: string, + expand: boolean | undefined, + actual: unknown, + expected: unknown, +) { const error = new Error(message) Object.defineProperty(error, 'actual', { value: actual, @@ -52,7 +57,11 @@ export class SnapshotClient { constructor(private options: SnapshotClientOptions = {}) {} - async startCurrentRun(filepath: string, name: string, options: SnapshotStateOptions) { + async startCurrentRun( + filepath: string, + name: string, + options: SnapshotStateOptions, + ) { this.filepath = filepath this.name = name @@ -62,10 +71,7 @@ export class SnapshotClient { if (!this.getSnapshotState(filepath)) { this.snapshotStateMap.set( filepath, - await SnapshotState.create( - filepath, - options, - ), + await SnapshotState.create(filepath, options), ) } this.snapshotState = this.getSnapshotState(filepath) @@ -99,20 +105,31 @@ export class SnapshotClient { } = options let { received } = options - if (!filepath) + if (!filepath) { throw new Error('Snapshot cannot be used outside of test') + } if (typeof properties === 'object') { - if (typeof received !== 'object' || !received) - throw new Error('Received value must be an object when the matcher has properties') + if (typeof received !== 'object' || !received) { + throw new Error( + 'Received value must be an object when the matcher has properties', + ) + } try { const pass = this.options.isEqual?.(received, properties) ?? false // const pass = equals(received, properties, [iterableEquality, subsetEquality]) - if (!pass) - throw createMismatchError('Snapshot properties mismatched', this.snapshotState?.expand, received, properties) - else + if (!pass) { + throw createMismatchError( + 'Snapshot properties mismatched', + this.snapshotState?.expand, + received, + properties, + ) + } + else { received = deepMergeSnapshot(received, properties) + } } catch (err: any) { err.message = errorMessage || 'Snapshot mismatched' @@ -120,10 +137,7 @@ export class SnapshotClient { } } - const testName = [ - name, - ...(message ? [message] : []), - ].join(' > ') + const testName = [name, ...(message ? [message] : [])].join(' > ') const snapshotState = this.getSnapshotState(filepath) @@ -136,38 +150,49 @@ export class SnapshotClient { rawSnapshot, }) - if (!pass) - throw createMismatchError(`Snapshot \`${key || 'unknown'}\` mismatched`, this.snapshotState?.expand, actual?.trim(), expected?.trim()) + if (!pass) { + throw createMismatchError( + `Snapshot \`${key || 'unknown'}\` mismatched`, + this.snapshotState?.expand, + actual?.trim(), + expected?.trim(), + ) + } } async assertRaw(options: AssertOptions): Promise { - if (!options.rawSnapshot) + if (!options.rawSnapshot) { throw new Error('Raw snapshot is required') + } - const { - filepath = this.filepath, - rawSnapshot, - } = options + const { filepath = this.filepath, rawSnapshot } = options if (rawSnapshot.content == null) { - if (!filepath) + if (!filepath) { throw new Error('Snapshot cannot be used outside of test') + } const snapshotState = this.getSnapshotState(filepath) // save the filepath, so it don't lose even if the await make it out-of-context options.filepath ||= filepath // resolve and read the raw snapshot file - rawSnapshot.file = await snapshotState.environment.resolveRawPath(filepath, rawSnapshot.file) - rawSnapshot.content = await snapshotState.environment.readSnapshotFile(rawSnapshot.file) ?? undefined + rawSnapshot.file = await snapshotState.environment.resolveRawPath( + filepath, + rawSnapshot.file, + ) + rawSnapshot.content + = (await snapshotState.environment.readSnapshotFile(rawSnapshot.file)) + ?? undefined } return this.assert(options) } async finishCurrentRun() { - if (!this.snapshotState) + if (!this.snapshotState) { return null + } const result = await this.snapshotState.pack() this.snapshotState = undefined diff --git a/packages/snapshot/src/env/node.ts b/packages/snapshot/src/env/node.ts index 57231cfe0..32a61795d 100644 --- a/packages/snapshot/src/env/node.ts +++ b/packages/snapshot/src/env/node.ts @@ -14,17 +14,12 @@ export class NodeSnapshotEnvironment implements SnapshotEnvironment { } async resolveRawPath(testPath: string, rawPath: string) { - return isAbsolute(rawPath) - ? rawPath - : resolve(dirname(testPath), rawPath) + return isAbsolute(rawPath) ? rawPath : resolve(dirname(testPath), rawPath) } async resolvePath(filepath: string): Promise { return join( - join( - dirname(filepath), - this.options.snapshotsDirName ?? '__snapshots__', - ), + join(dirname(filepath), this.options.snapshotsDirName ?? '__snapshots__'), `${basename(filepath)}.snap`, ) } @@ -39,13 +34,15 @@ export class NodeSnapshotEnvironment implements SnapshotEnvironment { } async readSnapshotFile(filepath: string): Promise { - if (!existsSync(filepath)) + if (!existsSync(filepath)) { return null + } return fs.readFile(filepath, 'utf-8') } async removeSnapshotFile(filepath: string): Promise { - if (existsSync(filepath)) + if (existsSync(filepath)) { await fs.unlink(filepath) + } } } diff --git a/packages/snapshot/src/manager.ts b/packages/snapshot/src/manager.ts index c01244e83..16c1f0fe9 100644 --- a/packages/snapshot/src/manager.ts +++ b/packages/snapshot/src/manager.ts @@ -1,11 +1,17 @@ import { basename, dirname, isAbsolute, join, resolve } from 'pathe' -import type { SnapshotResult, SnapshotStateOptions, SnapshotSummary } from './types' +import type { + SnapshotResult, + SnapshotStateOptions, + SnapshotSummary, +} from './types' export class SnapshotManager { summary: SnapshotSummary = undefined! extension = '.snap' - constructor(public options: Omit) { + constructor( + public options: Omit, + ) { this.clear() } @@ -18,28 +24,27 @@ export class SnapshotManager { } resolvePath(testPath: string) { - const resolver = this.options.resolveSnapshotPath || (() => { - return join( - join( - dirname(testPath), - '__snapshots__', - ), - `${basename(testPath)}${this.extension}`, - ) - }) + const resolver + = this.options.resolveSnapshotPath + || (() => { + return join( + join(dirname(testPath), '__snapshots__'), + `${basename(testPath)}${this.extension}`, + ) + }) const path = resolver(testPath, this.extension) return path } resolveRawPath(testPath: string, rawPath: string) { - return isAbsolute(rawPath) - ? rawPath - : resolve(dirname(testPath), rawPath) + return isAbsolute(rawPath) ? rawPath : resolve(dirname(testPath), rawPath) } } -export function emptySummary(options: Omit): SnapshotSummary { +export function emptySummary( + options: Omit, +): SnapshotSummary { const summary = { added: 0, failure: false, @@ -59,15 +64,22 @@ export function emptySummary(options: Omit i.file)) - await Promise.all(Array.from(files).map(async (file) => { - const snaps = snapshots.filter(i => i.file === file) - const code = await environment.readSnapshotFile(file) as string - const s = new MagicString(code) - - for (const snap of snaps) { - const index = positionToOffset(code, snap.line, snap.column) - replaceInlineSnap(code, s, index, snap.snapshot) - } + await Promise.all( + Array.from(files).map(async (file) => { + const snaps = snapshots.filter(i => i.file === file) + const code = (await environment.readSnapshotFile(file)) as string + const s = new MagicString(code) + + for (const snap of snaps) { + const index = positionToOffset(code, snap.line, snap.column) + replaceInlineSnap(code, s, index, snap.snapshot) + } - const transformed = s.toString() - if (transformed !== code) - await environment.saveSnapshotFile(file, transformed) - })) + const transformed = s.toString() + if (transformed !== code) { + await environment.saveSnapshotFile(file, transformed) + } + }), + ) } -const startObjectRegex = /(?:toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot)\s*\(\s*(?:\/\*[\s\S]*\*\/\s*|\/\/.*(?:[\n\r\u2028\u2029]\s*|[\t\v\f \xA0\u1680\u2000-\u200A\u202F\u205F\u3000\uFEFF]))*\{/ +const startObjectRegex + = /(?:toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot)\s*\(\s*(?:\/\*[\s\S]*\*\/\s*|\/\/.*(?:[\n\r\u2028\u2029]\s*|[\t\v\f \xA0\u1680\u2000-\u200A\u202F\u205F\u3000\uFEFF]))*\{/ -function replaceObjectSnap(code: string, s: MagicString, index: number, newSnap: string) { +function replaceObjectSnap( + code: string, + s: MagicString, + index: number, + newSnap: string, +) { let _code = code.slice(index) const startMatch = startObjectRegex.exec(_code) - if (!startMatch) + if (!startMatch) { return false + } _code = _code.slice(startMatch.index) let callEnd = getCallLastIndex(_code) - if (callEnd === null) + if (callEnd === null) { return false + } callEnd += index + startMatch.index const shapeStart = index + startMatch.index + startMatch[0].length @@ -67,10 +83,12 @@ function getObjectShapeEndIndex(code: string, index: number) { let endBraces = 0 while (startBraces !== endBraces && index < code.length) { const s = code[index++] - if (s === '{') + if (s === '{') { startBraces++ - else if (s === '}') + } + else if (s === '}') { endBraces++ + } } return index } @@ -81,28 +99,43 @@ function prepareSnapString(snap: string, source: string, index: number) { const indent = line.match(/^\s*/)![0] || '' const indentNext = indent.includes('\t') ? `${indent}\t` : `${indent} ` - const lines = snap - .trim() - .replace(/\\/g, '\\\\') - .split(/\n/g) + const lines = snap.trim().replace(/\\/g, '\\\\').split(/\n/g) const isOneline = lines.length <= 1 const quote = '`' - if (isOneline) - return `${quote}${lines.join('\n').replace(/`/g, '\\`').replace(/\$\{/g, '\\${')}${quote}` - return `${quote}\n${lines.map(i => i ? indentNext + i : '').join('\n').replace(/`/g, '\\`').replace(/\$\{/g, '\\${')}\n${indent}${quote}` + if (isOneline) { + return `${quote}${lines + .join('\n') + .replace(/`/g, '\\`') + .replace(/\$\{/g, '\\${')}${quote}` + } + return `${quote}\n${lines + .map(i => (i ? indentNext + i : '')) + .join('\n') + .replace(/`/g, '\\`') + .replace(/\$\{/g, '\\${')}\n${indent}${quote}` } -const startRegex = /(?:toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot)\s*\(\s*(?:\/\*[\s\S]*\*\/\s*|\/\/.*(?:[\n\r\u2028\u2029]\s*|[\t\v\f \xA0\u1680\u2000-\u200A\u202F\u205F\u3000\uFEFF]))*[\w$]*(['"`)])/ -export function replaceInlineSnap(code: string, s: MagicString, index: number, newSnap: string) { +const startRegex + = /(?:toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot)\s*\(\s*(?:\/\*[\s\S]*\*\/\s*|\/\/.*(?:[\n\r\u2028\u2029]\s*|[\t\v\f \xA0\u1680\u2000-\u200A\u202F\u205F\u3000\uFEFF]))*[\w$]*(['"`)])/ +export function replaceInlineSnap( + code: string, + s: MagicString, + index: number, + newSnap: string, +) { const codeStartingAtIndex = code.slice(index) const startMatch = startRegex.exec(codeStartingAtIndex) - const firstKeywordMatch = /toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot/.exec(codeStartingAtIndex) + const firstKeywordMatch + = /toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot/.exec( + codeStartingAtIndex, + ) - if (!startMatch || startMatch.index !== firstKeywordMatch?.index) + if (!startMatch || startMatch.index !== firstKeywordMatch?.index) { return replaceObjectSnap(code, s, index, newSnap) + } const quote = startMatch[1] const startIndex = index + startMatch.index + startMatch[0].length @@ -115,8 +148,9 @@ export function replaceInlineSnap(code: string, s: MagicString, index: number, n const quoteEndRE = new RegExp(`(?:^|[^\\\\])${quote}`) const endMatch = quoteEndRE.exec(code.slice(startIndex)) - if (!endMatch) + if (!endMatch) { return false + } const endIndex = startIndex + endMatch.index! + endMatch[0].length s.overwrite(startIndex - 1, endIndex, snapString) diff --git a/packages/snapshot/src/port/mockSerializer.ts b/packages/snapshot/src/port/mockSerializer.ts index 23eda1f2d..f7b68f91d 100644 --- a/packages/snapshot/src/port/mockSerializer.ts +++ b/packages/snapshot/src/port/mockSerializer.ts @@ -24,21 +24,21 @@ export const serialize: NewPlugin['serialize'] = ( let callsString = '' if (val.mock.calls.length !== 0) { const indentationNext = indentation + config.indent - callsString - = ` {${ - config.spacingOuter - }${indentationNext - }"calls": ${ - printer(val.mock.calls, config, indentationNext, depth, refs) - }${config.min ? ', ' : ',' - }${config.spacingOuter - }${indentationNext - }"results": ${ - printer(val.mock.results, config, indentationNext, depth, refs) - }${config.min ? '' : ',' - }${config.spacingOuter - }${indentation - }}` + callsString = ` {${config.spacingOuter}${indentationNext}"calls": ${printer( + val.mock.calls, + config, + indentationNext, + depth, + refs, + )}${config.min ? ', ' : ','}${ + config.spacingOuter + }${indentationNext}"results": ${printer( + val.mock.results, + config, + indentationNext, + depth, + refs, + )}${config.min ? '' : ','}${config.spacingOuter}${indentation}}` } return `[MockFunction${nameString}]${callsString}` diff --git a/packages/snapshot/src/port/plugins.ts b/packages/snapshot/src/port/plugins.ts index 7657a64a4..597888bec 100644 --- a/packages/snapshot/src/port/plugins.ts +++ b/packages/snapshot/src/port/plugins.ts @@ -9,9 +9,7 @@ import type { Plugin as PrettyFormatPlugin, Plugins as PrettyFormatPlugins, } from 'pretty-format' -import { - plugins as prettyFormatPlugins, -} from 'pretty-format' +import { plugins as prettyFormatPlugins } from 'pretty-format' import MockSerializer from './mockSerializer' diff --git a/packages/snapshot/src/port/rawSnapshot.ts b/packages/snapshot/src/port/rawSnapshot.ts index 5863c0d91..f544c5532 100644 --- a/packages/snapshot/src/port/rawSnapshot.ts +++ b/packages/snapshot/src/port/rawSnapshot.ts @@ -15,8 +15,11 @@ export async function saveRawSnapshots( environment: SnapshotEnvironment, snapshots: Array, ) { - await Promise.all(snapshots.map(async (snap) => { - if (!snap.readonly) - await environment.saveSnapshotFile(snap.file, snap.snapshot) - })) + await Promise.all( + snapshots.map(async (snap) => { + if (!snap.readonly) { + await environment.saveSnapshotFile(snap.file, snap.snapshot) + } + }), + ) } diff --git a/packages/snapshot/src/port/state.ts b/packages/snapshot/src/port/state.ts index b54139b53..e168dd974 100644 --- a/packages/snapshot/src/port/state.ts +++ b/packages/snapshot/src/port/state.ts @@ -8,7 +8,14 @@ import type { OptionsReceived as PrettyFormatOptions } from 'pretty-format' import type { ParsedStack } from '../../../utils/src/index' import { parseErrorStacktrace } from '../../../utils/src/source-map' -import type { SnapshotData, SnapshotEnvironment, SnapshotMatchOptions, SnapshotResult, SnapshotStateOptions, SnapshotUpdateState } from '../types' +import type { + SnapshotData, + SnapshotEnvironment, + SnapshotMatchOptions, + SnapshotResult, + SnapshotStateOptions, + SnapshotUpdateState, +} from '../types' import type { InlineSnapshot } from './inlineSnapshot' import { saveInlineSnapshots } from './inlineSnapshot' import type { RawSnapshot, RawSnapshotInfo } from './rawSnapshot' @@ -64,10 +71,7 @@ export default class SnapshotState { snapshotContent: string | null, options: SnapshotStateOptions, ) { - const { data, dirty } = getSnapshotData( - snapshotContent, - options, - ) + const { data, dirty } = getSnapshotData(snapshotContent, options) this._fileExists = snapshotContent != null // TODO: update on watch? this._initialData = data this._snapshotData = data @@ -90,12 +94,13 @@ export default class SnapshotState { this._environment = options.snapshotEnvironment } - static async create( - testFilePath: string, - options: SnapshotStateOptions, - ) { - const snapshotPath = await options.snapshotEnvironment.resolvePath(testFilePath) - const content = await options.snapshotEnvironment.readSnapshotFile(snapshotPath) + static async create(testFilePath: string, options: SnapshotStateOptions) { + const snapshotPath = await options.snapshotEnvironment.resolvePath( + testFilePath, + ) + const content = await options.snapshotEnvironment.readSnapshotFile( + snapshotPath, + ) return new SnapshotState(testFilePath, snapshotPath, content, options) } @@ -105,20 +110,26 @@ export default class SnapshotState { markSnapshotsAsCheckedForTest(testName: string): void { this._uncheckedKeys.forEach((uncheckedKey) => { - if (keyToTestName(uncheckedKey) === testName) + if (keyToTestName(uncheckedKey) === testName) { this._uncheckedKeys.delete(uncheckedKey) + } }) } protected _inferInlineSnapshotStack(stacks: ParsedStack[]) { // if called inside resolves/rejects, stacktrace is different - const promiseIndex = stacks.findIndex(i => i.method.match(/__VITEST_(RESOLVES|REJECTS)__/)) - if (promiseIndex !== -1) + const promiseIndex = stacks.findIndex(i => + i.method.match(/__VITEST_(RESOLVES|REJECTS)__/), + ) + if (promiseIndex !== -1) { return stacks[promiseIndex + 3] + } // inline snapshot function is called __INLINE_SNAPSHOT__ // in integrations/snapshot/chai.ts - const stackIndex = stacks.findIndex(i => i.method.includes('__INLINE_SNAPSHOT__')) + const stackIndex = stacks.findIndex(i => + i.method.includes('__INLINE_SNAPSHOT__'), + ) return stackIndex !== -1 ? stacks[stackIndex + 2] : null } @@ -129,11 +140,16 @@ export default class SnapshotState { ): void { this._dirty = true if (options.isInline) { - const stacks = parseErrorStacktrace(options.error || new Error('snapshot'), { ignoreStackEntries: [] }) + const stacks = parseErrorStacktrace( + options.error || new Error('snapshot'), + { ignoreStackEntries: [] }, + ) const stack = this._inferInlineSnapshotStack(stacks) if (!stack) { throw new Error( - `@vitest/snapshot: Couldn't infer stack frame for inline snapshot.\n${JSON.stringify(stacks)}`, + `@vitest/snapshot: Couldn't infer stack frame for inline snapshot.\n${JSON.stringify( + stacks, + )}`, ) } // removing 1 column, because source map points to the wrong @@ -171,7 +187,8 @@ export default class SnapshotState { const hasExternalSnapshots = Object.keys(this._snapshotData).length const hasInlineSnapshots = this._inlineSnapshots.length const hasRawSnapshots = this._rawSnapshots.length - const isEmpty = !hasExternalSnapshots && !hasInlineSnapshots && !hasRawSnapshots + const isEmpty + = !hasExternalSnapshots && !hasInlineSnapshots && !hasRawSnapshots const status: SaveStatus = { deleted: false, @@ -180,13 +197,19 @@ export default class SnapshotState { if ((this._dirty || this._uncheckedKeys.size) && !isEmpty) { if (hasExternalSnapshots) { - await saveSnapshotFile(this._environment, this._snapshotData, this.snapshotPath) + await saveSnapshotFile( + this._environment, + this._snapshotData, + this.snapshotPath, + ) this._fileExists = true } - if (hasInlineSnapshots) + if (hasInlineSnapshots) { await saveInlineSnapshots(this._environment, this._inlineSnapshots) - if (hasRawSnapshots) + } + if (hasRawSnapshots) { await saveRawSnapshots(this._environment, this._rawSnapshots) + } status.saved = true } @@ -230,26 +253,35 @@ export default class SnapshotState { this._counters.set(testName, (this._counters.get(testName) || 0) + 1) const count = Number(this._counters.get(testName)) - if (!key) + if (!key) { key = testNameToKey(testName, count) + } // Do not mark the snapshot as "checked" if the snapshot is inline and // there's an external snapshot. This way the external snapshot can be // removed with `--updateSnapshot`. - if (!(isInline && this._snapshotData[key] !== undefined)) + if (!(isInline && this._snapshotData[key] !== undefined)) { this._uncheckedKeys.delete(key) + } - let receivedSerialized = (rawSnapshot && typeof received === 'string') - ? received as string - : serialize(received, undefined, this._snapshotFormat) + let receivedSerialized + = rawSnapshot && typeof received === 'string' + ? (received as string) + : serialize(received, undefined, this._snapshotFormat) - if (!rawSnapshot) + if (!rawSnapshot) { receivedSerialized = addExtraLineBreaks(receivedSerialized) + } if (rawSnapshot) { // normalize EOL when snapshot contains CRLF but received is LF - if (rawSnapshot.content && rawSnapshot.content.match(/\r\n/) && !receivedSerialized.match(/\r\n/)) + if ( + rawSnapshot.content + && rawSnapshot.content.match(/\r\n/) + && !receivedSerialized.match(/\r\n/) + ) { rawSnapshot.content = normalizeNewlines(rawSnapshot.content) + } } const expected = isInline @@ -260,7 +292,10 @@ export default class SnapshotState { const expectedTrimmed = prepareExpected(expected) const pass = expectedTrimmed === prepareExpected(receivedSerialized) const hasSnapshot = expected !== undefined - const snapshotIsPersisted = isInline || this._fileExists || (rawSnapshot && rawSnapshot.content != null) + const snapshotIsPersisted + = isInline + || this._fileExists + || (rawSnapshot && rawSnapshot.content != null) if (pass && !isInline && !rawSnapshot) { // Executing a snapshot file as JavaScript and writing the strings back @@ -286,19 +321,29 @@ export default class SnapshotState { ) { if (this._updateSnapshot === 'all') { if (!pass) { - if (hasSnapshot) + if (hasSnapshot) { this.updated++ - else + } + else { this.added++ + } - this._addSnapshot(key, receivedSerialized, { error, isInline, rawSnapshot }) + this._addSnapshot(key, receivedSerialized, { + error, + isInline, + rawSnapshot, + }) } else { this.matched++ } } else { - this._addSnapshot(key, receivedSerialized, { error, isInline, rawSnapshot }) + this._addSnapshot(key, receivedSerialized, { + error, + isInline, + rawSnapshot, + }) this.added++ } @@ -317,9 +362,9 @@ export default class SnapshotState { actual: removeExtraLineBreaks(receivedSerialized), count, expected: - expectedTrimmed !== undefined - ? removeExtraLineBreaks(expectedTrimmed) - : undefined, + expectedTrimmed !== undefined + ? removeExtraLineBreaks(expectedTrimmed) + : undefined, key, pass: false, } @@ -350,8 +395,9 @@ export default class SnapshotState { } const uncheckedCount = this.getUncheckedCount() const uncheckedKeys = this.getUncheckedKeys() - if (uncheckedCount) + if (uncheckedCount) { this.removeUncheckedKeys() + } const status = await this.save() snapshot.fileDeleted = status.deleted diff --git a/packages/snapshot/src/port/utils.ts b/packages/snapshot/src/port/utils.ts index c89dcf7d9..4dc4d84f3 100644 --- a/packages/snapshot/src/port/utils.ts +++ b/packages/snapshot/src/port/utils.ts @@ -7,9 +7,7 @@ import naturalCompare from 'natural-compare' import type { OptionsReceived as PrettyFormatOptions } from 'pretty-format' -import { - format as prettyFormat, -} from 'pretty-format' +import { format as prettyFormat } from 'pretty-format' import { isObject } from '../../../utils/src/index' import type { SnapshotData, SnapshotStateOptions } from '../types' import type { SnapshotEnvironment } from '../types/environment' @@ -22,16 +20,20 @@ export function testNameToKey(testName: string, count: number): string { } export function keyToTestName(key: string): string { - if (!/ \d+$/.test(key)) + if (!/ \d+$/.test(key)) { throw new Error('Snapshot keys must end with a number.') + } return key.replace(/ \d+$/, '') } -export function getSnapshotData(content: string | null, options: SnapshotStateOptions): { - data: SnapshotData - dirty: boolean -} { +export function getSnapshotData( + content: string | null, + options: SnapshotStateOptions, +): { + data: SnapshotData + dirty: boolean + } { const update = options.updateSnapshot const data = Object.create(null) let snapshotContents = '' @@ -53,8 +55,9 @@ export function getSnapshotData(content: string | null, options: SnapshotStateOp // if (update === 'none' && isInvalid) // throw validationResult - if ((update === 'all' || update === 'new') && isInvalid) + if ((update === 'all' || update === 'new') && isInvalid) { dirty = true + } return { data, dirty } } @@ -69,7 +72,7 @@ export function addExtraLineBreaks(string: string): string { // Instead of trim, which can remove additional newlines or spaces // at beginning or end of the content from a custom serializer. export function removeExtraLineBreaks(string: string): string { - return (string.length > 2 && string.startsWith('\n') && string.endsWith('\n')) + return string.length > 2 && string.startsWith('\n') && string.endsWith('\n') ? string.slice(1, -1) : string } @@ -90,7 +93,11 @@ export function removeExtraLineBreaks(string: string): string { const escapeRegex = true const printFunctionName = false -export function serialize(val: unknown, indent = 2, formatOverrides: PrettyFormatOptions = {}): string { +export function serialize( + val: unknown, + indent = 2, + formatOverrides: PrettyFormatOptions = {}, +): string { return normalizeNewlines( prettyFormat(val, { escapeRegex, @@ -136,20 +143,21 @@ export async function saveSnapshotFile( const snapshots = Object.keys(snapshotData) .sort(naturalCompare) .map( - key => `exports[${printBacktickString(key)}] = ${printBacktickString(normalizeNewlines(snapshotData[key]))};`, + key => + `exports[${printBacktickString(key)}] = ${printBacktickString( + normalizeNewlines(snapshotData[key]), + )};`, ) const content = `${environment.getHeader()}\n\n${snapshots.join('\n\n')}\n` const oldContent = await environment.readSnapshotFile(snapshotPath) const skipWriting = oldContent != null && oldContent === content - if (skipWriting) + if (skipWriting) { return + } - await environment.saveSnapshotFile( - snapshotPath, - content, - ) + await environment.saveSnapshotFile(snapshotPath, content) } export async function saveSnapshotFileRaw( @@ -160,13 +168,11 @@ export async function saveSnapshotFileRaw( const oldContent = await environment.readSnapshotFile(snapshotPath) const skipWriting = oldContent != null && oldContent === content - if (skipWriting) + if (skipWriting) { return + } - await environment.saveSnapshotFile( - snapshotPath, - content, - ) + await environment.saveSnapshotFile(snapshotPath, content) } export function prepareExpected(expected?: string) { @@ -176,8 +182,9 @@ export function prepareExpected(expected?: string) { const matchObject = /^( +)\}\s+$/m.exec(expected || '') const objectIndent = matchObject?.[1]?.length - if (objectIndent) + if (objectIndent) { return objectIndent + } // Attempts to find indentation for texts. // Matches the quote of first line. @@ -191,7 +198,8 @@ export function prepareExpected(expected?: string) { if (startIndent) { expectedTrimmed = expectedTrimmed - ?.replace(new RegExp(`^${' '.repeat(startIndent)}`, 'gm'), '').replace(/ +\}$/, '}') + ?.replace(new RegExp(`^${' '.repeat(startIndent)}`, 'gm'), '') + .replace(/ +\}$/, '}') } return expectedTrimmed @@ -235,9 +243,12 @@ export function deepMergeSnapshot(target: any, source: any): any { const mergedOutput = { ...target } Object.keys(source).forEach((key) => { if (isObject(source[key]) && !source[key].$$typeof) { - if (!(key in target)) + if (!(key in target)) { Object.assign(mergedOutput, { [key]: source[key] }) - else mergedOutput[key] = deepMergeSnapshot(target[key], source[key]) + } + else { + mergedOutput[key] = deepMergeSnapshot(target[key], source[key]) + } } else if (Array.isArray(source[key])) { mergedOutput[key] = deepMergeArray(target[key], source[key]) diff --git a/packages/snapshot/src/types/index.ts b/packages/snapshot/src/types/index.ts index 66e7baf98..0a11f6733 100644 --- a/packages/snapshot/src/types/index.ts +++ b/packages/snapshot/src/types/index.ts @@ -1,6 +1,12 @@ -import type { OptionsReceived as PrettyFormatOptions, Plugin as PrettyFormatPlugin } from 'pretty-format' +import type { + OptionsReceived as PrettyFormatOptions, + Plugin as PrettyFormatPlugin, +} from 'pretty-format' import type { RawSnapshotInfo } from '../port/rawSnapshot' -import type { SnapshotEnvironment, SnapshotEnvironmentOptions } from './environment' +import type { + SnapshotEnvironment, + SnapshotEnvironmentOptions, +} from './environment' export type { SnapshotEnvironment, SnapshotEnvironmentOptions } export type SnapshotData = Record diff --git a/packages/spy/rollup.config.js b/packages/spy/rollup.config.js index fa89fdadc..3f2803087 100644 --- a/packages/spy/rollup.config.js +++ b/packages/spy/rollup.config.js @@ -39,15 +39,14 @@ export default defineConfig([ format: 'esm', }, external, - plugins: [ - dts({ respectExternal: true }), - ], + plugins: [dts({ respectExternal: true })], onwarn, }, ]) function onwarn(message) { - if (['EMPTY_BUNDLE', 'CIRCULAR_DEPENDENCY'].includes(message.code)) + if (['EMPTY_BUNDLE', 'CIRCULAR_DEPENDENCY'].includes(message.code)) { return + } console.error(message) } diff --git a/packages/spy/src/index.ts b/packages/spy/src/index.ts index fad7c9dff..6cb34d980 100644 --- a/packages/spy/src/index.ts +++ b/packages/spy/src/index.ts @@ -30,8 +30,13 @@ interface MockSettledResultRejected { value: any } -export type MockResult = MockResultReturn | MockResultThrow | MockResultIncomplete -export type MockSettledResult = MockSettledResultFulfilled | MockSettledResultRejected +export type MockResult = + | MockResultReturn + | MockResultThrow + | MockResultIncomplete +export type MockSettledResult = + | MockSettledResultFulfilled + | MockSettledResultRejected export interface MockContext { /** @@ -137,16 +142,19 @@ type Methods = keyof { [K in keyof T as T[K] extends Procedure ? K : never]: T[K]; } type Properties = { - [K in keyof T]: T[K] extends Procedure ? never : K -}[keyof T] & (string | symbol) + [K in keyof T]: T[K] extends Procedure ? never : K; +}[keyof T] & +(string | symbol) type Classes = { - [K in keyof T]: T[K] extends new (...args: any[]) => any ? K : never -}[keyof T] & (string | symbol) + [K in keyof T]: T[K] extends new (...args: any[]) => any ? K : never; +}[keyof T] & +(string | symbol) /** * @deprecated Use MockInstance instead */ -export interface SpyInstance extends MockInstance {} +export interface SpyInstance + extends MockInstance {} export interface MockInstance { /** @@ -193,7 +201,7 @@ export interface MockInstance { * const increment = vi.fn().mockImplementation(count => count + 1); * expect(increment(3)).toBe(4); */ - mockImplementation: (fn: ((...args: TArgs) => TReturns)) => this + mockImplementation: (fn: (...args: TArgs) => TReturns) => this /** * Accepts a function that will be used as a mock implementation during the next call. Can be chained so that multiple function calls produce different results. * @example @@ -201,7 +209,7 @@ export interface MockInstance { * expect(fn(3)).toBe(4); * expect(fn(3)).toBe(3); */ - mockImplementationOnce: (fn: ((...args: TArgs) => TReturns)) => this + mockImplementationOnce: (fn: (...args: TArgs) => TReturns) => this /** * Overrides the original mock implementation temporarily while the callback is being executed. * @example @@ -213,7 +221,10 @@ export interface MockInstance { * * myMockFn() // 'original' */ - withImplementation: (fn: ((...args: TArgs) => TReturns), cb: () => T) => T extends Promise ? Promise : this + withImplementation: ( + fn: (...args: TArgs) => TReturns, + cb: () => T + ) => T extends Promise ? Promise : this /** * Use this if you need to return `this` context from the method without invoking actual implementation. */ @@ -278,11 +289,18 @@ export interface MockInstance { mockRejectedValueOnce: (obj: any) => this } -export interface Mock extends MockInstance { +export interface Mock + extends MockInstance { new (...args: TArgs): TReturns (...args: TArgs): TReturns } -export interface PartialMock extends MockInstance> ? Promise>> : Partial> { +export interface PartialMock + extends MockInstance< + TArgs, + TReturns extends Promise> + ? Promise>> + : Partial + > { new (...args: TArgs): TReturns (...args: TArgs): TReturns } @@ -292,23 +310,33 @@ export type MaybeMockedConstructor = T extends new ( ) => infer R ? Mock, R> : T -export type MockedFunction = Mock, ReturnType> & { +export type MockedFunction = Mock< + Parameters, + ReturnType +> & { [K in keyof T]: T[K]; } -export type PartiallyMockedFunction = PartialMock, ReturnType> & { +export type PartiallyMockedFunction = PartialMock< + Parameters, + ReturnType +> & { [K in keyof T]: T[K]; } -export type MockedFunctionDeep = Mock, ReturnType> & MockedObjectDeep -export type PartiallyMockedFunctionDeep = PartialMock, ReturnType> & MockedObjectDeep +export type MockedFunctionDeep = Mock< + Parameters, + ReturnType +> & +MockedObjectDeep +export type PartiallyMockedFunctionDeep = PartialMock< + Parameters, + ReturnType +> & +MockedObjectDeep export type MockedObject = MaybeMockedConstructor & { - [K in Methods]: T[K] extends Procedure - ? MockedFunction - : T[K]; + [K in Methods]: T[K] extends Procedure ? MockedFunction : T[K]; } & { [K in Properties]: T[K] } export type MockedObjectDeep = MaybeMockedConstructor & { - [K in Methods]: T[K] extends Procedure - ? MockedFunctionDeep - : T[K]; + [K in Methods]: T[K] extends Procedure ? MockedFunctionDeep : T[K]; } & { [K in Properties]: MaybeMockedDeep } export type MaybeMockedDeep = T extends Procedure @@ -340,8 +368,8 @@ interface Constructable { } export type MockedClass = MockInstance< - T extends new (...args: infer P) => any ? P : never, - InstanceType + T extends new (...args: infer P) => any ? P : never, + InstanceType > & { prototype: T extends { prototype: any } ? Mocked : never } & T @@ -351,32 +379,35 @@ export type Mocked = { ? MockInstance : T[P] extends Constructable ? MockedClass - : T[P] -} & -T + : T[P]; +} & T export const mocks = new Set() export function isMockFunction(fn: any): fn is MockInstance { - return typeof fn === 'function' - && '_isMockFunction' in fn - && fn._isMockFunction + return ( + typeof fn === 'function' && '_isMockFunction' in fn && fn._isMockFunction + ) } export function spyOn>>( obj: T, methodName: S, - accessType: 'get', + accessType: 'get' ): MockInstance<[], T[S]> export function spyOn>>( obj: T, methodName: G, - accessType: 'set', + accessType: 'set' ): MockInstance<[T[G]], void> -export function spyOn> | Methods>)>( +export function spyOn> | Methods>>( obj: T, - methodName: M, -): Required[M] extends ({ new (...args: infer A): infer R }) | ((...args: infer A) => infer R) ? MockInstance : never + methodName: M +): Required[M] extends +| { new (...args: infer A): infer R } +| ((...args: infer A) => infer R) + ? MockInstance + : never export function spyOn( obj: T, method: K, @@ -419,13 +450,15 @@ function enhanceSpy( }, get results() { return state.results.map(([callType, value]) => { - const type = callType === 'error' ? 'throw' as const : 'return' as const + const type + = callType === 'error' ? ('throw' as const) : ('return' as const) return { type, value } }) }, get settledResults() { return state.resolves.map(([callType, value]) => { - const type = callType === 'error' ? 'rejected' as const : 'fulfilled' as const + const type + = callType === 'error' ? ('rejected' as const) : ('fulfilled' as const) return { type, value } }) }, @@ -440,7 +473,12 @@ function enhanceSpy( function mockCall(this: unknown, ...args: any) { instances.push(this) invocations.push(++callOrder) - const impl = implementationChangedTemporarily ? implementation! : (onceImplementations.shift() || implementation || state.getOriginal() || (() => {})) + const impl = implementationChangedTemporarily + ? implementation! + : onceImplementations.shift() + || implementation + || state.getOriginal() + || (() => {}) return impl.apply(this, args) } @@ -485,9 +523,18 @@ function enhanceSpy( return stub } - function withImplementation(fn: (...args: TArgs) => TReturns, cb: () => void): MockInstance - function withImplementation(fn: (...args: TArgs) => TReturns, cb: () => Promise): Promise> - function withImplementation(fn: (...args: TArgs) => TReturns, cb: () => void | Promise): MockInstance | Promise> { + function withImplementation( + fn: (...args: TArgs) => TReturns, + cb: () => void + ): MockInstance + function withImplementation( + fn: (...args: TArgs) => TReturns, + cb: () => Promise + ): Promise> + function withImplementation( + fn: (...args: TArgs) => TReturns, + cb: () => void | Promise, + ): MockInstance | Promise> { const originalImplementation = implementation implementation = fn @@ -521,7 +568,8 @@ function enhanceSpy( }) stub.mockReturnValue = (val: TReturns) => stub.mockImplementation(() => val) - stub.mockReturnValueOnce = (val: TReturns) => stub.mockImplementationOnce(() => val) + stub.mockReturnValueOnce = (val: TReturns) => + stub.mockImplementationOnce(() => val) stub.mockResolvedValue = (val: Awaited) => stub.mockImplementation(() => Promise.resolve(val as TReturns) as any) @@ -553,9 +601,12 @@ export function fn( export function fn( implementation?: (...args: TArgs) => R, ): Mock { - const enhancedSpy = enhanceSpy(tinyspy.internalSpyOn({ spy: implementation || (() => {}) }, 'spy')) - if (implementation) + const enhancedSpy = enhanceSpy( + tinyspy.internalSpyOn({ spy: implementation || (() => {}) }, 'spy'), + ) + if (implementation) { enhancedSpy.mockImplementation(implementation) + } return enhancedSpy as Mock } diff --git a/packages/ui/client/auto-imports.d.ts b/packages/ui/client/auto-imports.d.ts index af3b710de..d0043d4d0 100644 --- a/packages/ui/client/auto-imports.d.ts +++ b/packages/ui/client/auto-imports.d.ts @@ -99,7 +99,7 @@ declare global { const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] const provide: typeof import('vue')['provide'] const provideLocal: typeof import('@vueuse/core')['provideLocal'] - const provideResizing: typeof import('./composables/browser')['provideResizing'] + const provideResizing: typeof import("./composables/browser")["provideResizing"] const reactify: typeof import('@vueuse/core')['reactify'] const reactifyObject: typeof import('@vueuse/core')['reactifyObject'] const reactive: typeof import('vue')['reactive'] @@ -107,14 +107,14 @@ declare global { const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit'] const reactivePick: typeof import('@vueuse/core')['reactivePick'] const readonly: typeof import('vue')['readonly'] - const recalculateDetailPanels: typeof import('./composables/browser')['recalculateDetailPanels'] + const recalculateDetailPanels: typeof import("./composables/browser")["recalculateDetailPanels"] const ref: typeof import('vue')['ref'] const refAutoReset: typeof import('@vueuse/core')['refAutoReset'] const refDebounced: typeof import('@vueuse/core')['refDebounced'] const refDefault: typeof import('@vueuse/core')['refDefault'] const refThrottled: typeof import('@vueuse/core')['refThrottled'] const refWithControl: typeof import('@vueuse/core')['refWithControl'] - const registerResizingListener: typeof import('./composables/browser')['registerResizingListener'] + const registerResizingListener: typeof import("./composables/browser")["registerResizingListener"] const resolveComponent: typeof import('vue')['resolveComponent'] const resolveRef: typeof import('@vueuse/core')['resolveRef'] const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] @@ -150,7 +150,7 @@ declare global { const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted'] const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose'] const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted'] - const unifiedDiff: typeof import('./composables/diff')['unifiedDiff'] + const unifiedDiff: typeof import("./composables/diff")["unifiedDiff"] const unref: typeof import('vue')['unref'] const unrefElement: typeof import('@vueuse/core')['unrefElement'] const until: typeof import('@vueuse/core')['until'] @@ -245,7 +245,7 @@ declare global { const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver'] const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage'] const useNetwork: typeof import('@vueuse/core')['useNetwork'] - const useNotifyResizing: typeof import('./composables/browser')['useNotifyResizing'] + const useNotifyResizing: typeof import("./composables/browser")["useNotifyResizing"] const useNow: typeof import('@vueuse/core')['useNow'] const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl'] const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination'] @@ -267,7 +267,7 @@ declare global { const useRafFn: typeof import('@vueuse/core')['useRafFn'] const useRefHistory: typeof import('@vueuse/core')['useRefHistory'] const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver'] - const useResizing: typeof import('./composables/browser')['useResizing'] + const useResizing: typeof import("./composables/browser")["useResizing"] const useRoute: typeof import('vue-router')['useRoute'] const useRouter: typeof import('vue-router')['useRouter'] const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation'] diff --git a/packages/ui/client/components/BrowserIframe.vue b/packages/ui/client/components/BrowserIframe.vue index fad8eba67..1ad3a4de9 100644 --- a/packages/ui/client/components/BrowserIframe.vue +++ b/packages/ui/client/components/BrowserIframe.vue @@ -1,65 +1,47 @@