test
Version:
Node.js 18's node:test, as an npm package
372 lines (315 loc) • 8.98 kB
JavaScript
// https://github.com/nodejs/node/blob/929aada39d0f418193ca03cc360ced8c5b4ce553/lib/internal/test_runner/mock.js
'use strict'
const {
ArrayPrototypePush,
ArrayPrototypeSlice,
Error,
FunctionPrototypeCall,
ObjectDefineProperty,
ObjectGetOwnPropertyDescriptor,
ObjectGetPrototypeOf,
Proxy,
ReflectApply,
ReflectConstruct,
ReflectGet,
SafeMap
} = require('#internal/per_context/primordials')
const {
codes: {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE
}
} = require('#internal/errors')
const { kEmptyObject } = require('#internal/util')
const {
validateBoolean,
validateFunction,
validateInteger,
validateObject
} = require('#internal/validators')
function kDefaultFunction () {}
class MockFunctionContext {
#calls
#mocks
#implementation
#restore
#times
constructor (implementation, restore, times) {
this.#calls = []
this.#mocks = new SafeMap()
this.#implementation = implementation
this.#restore = restore
this.#times = times
}
get calls () {
return ArrayPrototypeSlice(this.#calls, 0)
}
callCount () {
return this.#calls.length
}
mockImplementation (implementation) {
validateFunction(implementation, 'implementation')
this.#implementation = implementation
}
mockImplementationOnce (implementation, onCall) {
validateFunction(implementation, 'implementation')
const nextCall = this.#calls.length
const call = onCall ?? nextCall
validateInteger(call, 'onCall', nextCall)
this.#mocks.set(call, implementation)
}
restore () {
const { descriptor, object, original, methodName } = this.#restore
if (typeof methodName === 'string') {
// This is an object method spy.
ObjectDefineProperty(object, methodName, descriptor)
} else {
// This is a bare function spy. There isn't much to do here but make
// the mock call the original function.
this.#implementation = original
}
}
trackCall (call) {
ArrayPrototypePush(this.#calls, call)
}
nextImpl () {
const nextCall = this.#calls.length
const mock = this.#mocks.get(nextCall)
const impl = mock ?? this.#implementation
if (nextCall + 1 === this.#times) {
this.restore()
}
this.#mocks.delete(nextCall)
return impl
}
}
const { nextImpl, restore, trackCall } = MockFunctionContext.prototype
delete MockFunctionContext.prototype.trackCall
delete MockFunctionContext.prototype.nextImpl
class MockTracker {
#mocks = []
fn (
original = function () {},
implementation = original,
options = kEmptyObject
) {
if (original !== null && typeof original === 'object') {
options = original
original = function () {}
implementation = original
} else if (implementation !== null && typeof implementation === 'object') {
options = implementation
implementation = original
}
validateFunction(original, 'original')
validateFunction(implementation, 'implementation')
validateObject(options, 'options')
const { times = Infinity } = options
validateTimes(times, 'options.times')
const ctx = new MockFunctionContext(implementation, { original }, times)
return this.#setupMock(ctx, original)
}
method (
objectOrFunction,
methodName,
implementation = kDefaultFunction,
options = kEmptyObject
) {
validateStringOrSymbol(methodName, 'methodName')
if (typeof objectOrFunction !== 'function') {
validateObject(objectOrFunction, 'object')
}
if (implementation !== null && typeof implementation === 'object') {
options = implementation
implementation = kDefaultFunction
}
validateFunction(implementation, 'implementation')
validateObject(options, 'options')
const {
getter = false,
setter = false,
times = Infinity
} = options
validateBoolean(getter, 'options.getter')
validateBoolean(setter, 'options.setter')
validateTimes(times, 'options.times')
if (setter && getter) {
throw new ERR_INVALID_ARG_VALUE(
'options.setter', setter, "cannot be used with 'options.getter'"
)
}
const descriptor = findMethodOnPrototypeChain(objectOrFunction, methodName)
let original
if (getter) {
original = descriptor?.get
} else if (setter) {
original = descriptor?.set
} else {
original = descriptor?.value
}
if (typeof original !== 'function') {
throw new ERR_INVALID_ARG_VALUE(
'methodName', original, 'must be a method'
)
}
const restore = { descriptor, object: objectOrFunction, methodName }
const impl = implementation === kDefaultFunction
? original
: implementation
const ctx = new MockFunctionContext(impl, restore, times)
const mock = this.#setupMock(ctx, original)
const mockDescriptor = {
__proto__: null,
configurable: descriptor.configurable,
enumerable: descriptor.enumerable
}
if (getter) {
mockDescriptor.get = mock
mockDescriptor.set = descriptor.set
} else if (setter) {
mockDescriptor.get = descriptor.get
mockDescriptor.set = mock
} else {
mockDescriptor.writable = descriptor.writable
mockDescriptor.value = mock
}
ObjectDefineProperty(objectOrFunction, methodName, mockDescriptor)
return mock
}
getter (
object,
methodName,
implementation = kDefaultFunction,
options = kEmptyObject
) {
if (implementation !== null && typeof implementation === 'object') {
options = implementation
implementation = kDefaultFunction
} else {
validateObject(options, 'options')
}
const { getter = true } = options
if (getter === false) {
throw new ERR_INVALID_ARG_VALUE(
'options.getter', getter, 'cannot be false'
)
}
return this.method(object, methodName, implementation, {
...options,
getter
})
}
setter (
object,
methodName,
implementation = kDefaultFunction,
options = kEmptyObject
) {
if (implementation !== null && typeof implementation === 'object') {
options = implementation
implementation = kDefaultFunction
} else {
validateObject(options, 'options')
}
const { setter = true } = options
if (setter === false) {
throw new ERR_INVALID_ARG_VALUE(
'options.setter', setter, 'cannot be false'
)
}
return this.method(object, methodName, implementation, {
...options,
setter
})
}
reset () {
this.restoreAll()
this.#mocks = []
}
restoreAll () {
for (let i = 0; i < this.#mocks.length; i++) {
FunctionPrototypeCall(restore, this.#mocks[i])
}
}
#setupMock (ctx, fnToMatch) {
const mock = new Proxy(fnToMatch, {
__proto__: null,
apply (_fn, thisArg, argList) {
const fn = FunctionPrototypeCall(nextImpl, ctx)
let result
let error
try {
result = ReflectApply(fn, thisArg, argList)
} catch (err) {
error = err
throw err
} finally {
FunctionPrototypeCall(trackCall, ctx, {
arguments: argList,
error,
result,
// eslint-disable-next-line no-restricted-syntax
stack: new Error(),
target: undefined,
this: thisArg
})
}
return result
},
construct (target, argList, newTarget) {
const realTarget = FunctionPrototypeCall(nextImpl, ctx)
let result
let error
try {
result = ReflectConstruct(realTarget, argList, newTarget)
} catch (err) {
error = err
throw err
} finally {
FunctionPrototypeCall(trackCall, ctx, {
arguments: argList,
error,
result,
// eslint-disable-next-line no-restricted-syntax
stack: new Error(),
target,
this: result
})
}
return result
},
get (target, property, receiver) {
if (property === 'mock') {
return ctx
}
return ReflectGet(target, property, receiver)
}
})
ArrayPrototypePush(this.#mocks, ctx)
return mock
}
}
function validateStringOrSymbol (value, name) {
if (typeof value !== 'string' && typeof value !== 'symbol') {
throw new ERR_INVALID_ARG_TYPE(name, ['string', 'symbol'], value)
}
}
function validateTimes (value, name) {
if (value === Infinity) {
return
}
validateInteger(value, name, 1)
}
function findMethodOnPrototypeChain (instance, methodName) {
let host = instance
let descriptor
while (host !== null) {
descriptor = ObjectGetOwnPropertyDescriptor(host, methodName)
if (descriptor) {
break
}
host = ObjectGetPrototypeOf(host)
}
return descriptor
}
module.exports = { MockTracker }