Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: gracefully handle unsettable keys during automocking #1786

Merged
merged 1 commit into from Aug 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 20 additions & 8 deletions packages/vitest/src/runtime/mocker.ts
Expand Up @@ -3,7 +3,7 @@ import { isNodeBuiltin } from 'mlly'
import { basename, dirname, join, resolve } from 'pathe'
import { normalizeRequestId, toFilePath } from 'vite-node/utils'
import type { ModuleCacheMap } from 'vite-node/client'
import { getAllProperties, getType, getWorkerState, isWindows, mergeSlashes, slash } from '../utils'
import { getAllMockableProperties, getType, getWorkerState, isWindows, mergeSlashes, slash } from '../utils'
import { distDir } from '../constants'
import type { PendingSuiteMock } from '../types/mocker'
import type { ExecuteOptions } from './execute'
Expand Down Expand Up @@ -172,15 +172,24 @@ export class VitestMocker {
const finalizers = new Array<() => void>()
const refs = new RefTracker()

const define = (container: Record<Key, any>, key: Key, value: any) => {
try {
container[key] = value
return true
}
catch {
return false
}
}

const mockPropertiesOf = (container: Record<Key, any>, newContainer: Record<Key, any>) => {
const containerType = getType(container)
const isModule = containerType === 'Module' || !!container.__esModule
for (const property of getAllProperties(container)) {
for (const { key: property, descriptor } of getAllMockableProperties(container)) {
// Modules define their exports as getters. We want to process those.
if (!isModule) {
// TODO: Mock getters/setters somehow?
const descriptor = Object.getOwnPropertyDescriptor(container, property)
if (descriptor?.get || descriptor?.set)
if (descriptor.get || descriptor.set)
continue
}

Expand All @@ -194,24 +203,27 @@ export class VitestMocker {
// recursion in circular objects.
const refId = refs.getId(value)
if (refId) {
finalizers.push(() => newContainer[property] = refs.getMockedValue(refId))
finalizers.push(() => define(newContainer, property, refs.getMockedValue(refId)))
continue
}

const type = getType(value)

if (Array.isArray(value)) {
newContainer[property] = []
define(newContainer, property, [])
continue
}

const isFunction = type.includes('Function') && typeof value === 'function'
if ((!isFunction || value.__isMockFunction) && type !== 'Object' && type !== 'Module') {
newContainer[property] = value
define(newContainer, property, value)
continue
}

newContainer[property] = isFunction ? value : {}
// Sometimes this assignment fails for some unknown reason. If it does,
// just move along.
if (!define(newContainer, property, isFunction ? value : {}))
continue

if (isFunction) {
spyModule.spyOn(newContainer, property).mockImplementation(() => undefined)
Expand Down
23 changes: 13 additions & 10 deletions packages/vitest/src/utils/base.ts
Expand Up @@ -5,22 +5,25 @@ function isFinalObj(obj: any) {
return obj === Object.prototype || obj === Function.prototype || obj === RegExp.prototype
}

function collectOwnProperties(obj: any, collector: Set<string | symbol>) {
const props = Object.getOwnPropertyNames(obj)
const symbols = Object.getOwnPropertySymbols(obj)

props.forEach(prop => collector.add(prop))
symbols.forEach(symbol => collector.add(symbol))
function collectOwnProperties(obj: any, collector: Set<string | symbol> | ((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)
}

export function getAllProperties(obj: any) {
const allProps = new Set<string | symbol>()
export function getAllMockableProperties(obj: any) {
const allProps = new Set<{ key: string | symbol; descriptor: PropertyDescriptor }>()
let curr = obj
do {
// we don't need propterties from these
// we don't need properties from these
if (isFinalObj(curr))
break
collectOwnProperties(curr, allProps)

collectOwnProperties(curr, (key) => {
const descriptor = Object.getOwnPropertyDescriptor(curr, key)
if (descriptor)
allProps.add({ key, descriptor })
})
// eslint-disable-next-line no-cond-assign
} while (curr = Object.getPrototypeOf(curr))
return Array.from(allProps)
Expand Down
3 changes: 3 additions & 0 deletions test/core/src/mockedC.ts
Expand Up @@ -22,3 +22,6 @@ export async function asyncFunc(): Promise<string> {
await new Promise<void>(resolve => resolve())
return '1234'
}

// This is here because mocking streams previously caused some problems (#1671).
export const exportedStream = process.stderr
7 changes: 6 additions & 1 deletion test/core/test/mocked.test.ts
Expand Up @@ -4,7 +4,7 @@ import { value as virtualValue } from 'virtual-module'
import { two } from '../src/submodule'
import * as mocked from '../src/mockedA'
import { mockedB } from '../src/mockedB'
import { MockedC, asyncFunc } from '../src/mockedC'
import { MockedC, asyncFunc, exportedStream } from '../src/mockedC'
import * as globalMock from '../src/global-mock'

vitest.mock('../src/submodule')
Expand Down Expand Up @@ -63,3 +63,8 @@ test('async functions should be mocked', () => {
vi.mocked(asyncFunc).mockResolvedValue('foo')
expect(asyncFunc()).resolves.toBe('foo')
})

// This is here because mocking streams previously caused some problems (#1671).
test('streams', () => {
expect(exportedStream).toBeDefined()
})