Skip to content

Commit

Permalink
[New] add t.intercept()
Browse files Browse the repository at this point in the history
  • Loading branch information
ljharb committed Sep 20, 2023
1 parent 3d96d69 commit e60aeca
Show file tree
Hide file tree
Showing 3 changed files with 430 additions and 0 deletions.
97 changes: 97 additions & 0 deletions lib/test.js
Expand Up @@ -245,6 +245,103 @@ Test.prototype.captureFn = function captureFn(original) {
return wrapObject.wrapped;
};

Test.prototype.intercept = function intercept(obj, property) {
if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) {
throw new TypeError('`obj` must be an object');
}
if (typeof property !== 'string' && typeof property !== 'symbol') {
throw new TypeError('`property` must be a string or a symbol');
}
var desc = arguments.length > 2 ? arguments[2] : { __proto__: null };
if (typeof desc !== 'undefined' && (!desc || typeof desc !== 'object')) {
throw new TypeError('`desc`, if provided, must be an object');
}
if ('configurable' in desc && !desc.configurable) {
throw new TypeError('`desc.configurable`, if provided, must be `true`, so that the interception can be restored later');
}
var isData = 'writable' in desc || 'value' in desc;
var isAccessor = 'get' in desc || 'set' in desc;
if (isData && isAccessor) {
throw new TypeError('`value` and `writable` can not be mixed with `get` and `set`');
}
var strictMode = arguments.length > 3 ? arguments[3] : true;
if (typeof strictMode !== 'boolean') {
throw new TypeError('`strictMode`, if provided, must be a boolean');
}

var calls = [];
var getter = desc.get && callBind.apply(desc.get);
var setter = desc.set && callBind.apply(desc.set);
var value = !isAccessor ? desc.value : void undefined;
var writable = !!desc.writable;

function getInterceptor() {
var args = $slice(arguments);
if (isAccessor) {
if (getter) {
var completed = false;
try {
var returned = getter(this, arguments);
completed = true;
$push(calls, { type: 'get', success: true, value: returned, args: args, receiver: this });
return returned;
} finally {
if (!completed) {
$push(calls, { type: 'get', success: false, threw: true, args: args, receiver: this });
}
}
}
}
$push(calls, { type: 'get', success: true, value: value, args: args, receiver: this });
return value;
}

function setInterceptor(v) {
var args = $slice(arguments);
if (isAccessor && setter) {
var completed = false;
try {
var returned = setter(this, arguments);
completed = true;
$push(calls, { type: 'set', success: true, value: v, args: args, receiver: this });
return returned;
} finally {
if (!completed) {
$push(calls, { type: 'set', success: false, threw: true, args: args, receiver: this });
}
}
}
var canSet = isAccessor || writable;
if (canSet) {
value = v;
}
$push(calls, { type: 'set', success: !!canSet, value: value, args: args, receiver: this });

if (!canSet && strictMode) {
throw new TypeError('Cannot assign to read only property \'' + property + '\' of object \'' + inspect(obj) + '\'');
}
return value;
}

var restore = mockProperty(obj, property, {
nonEnumerable: !!desc.enumerable,
get: getInterceptor,
set: setInterceptor
});
this.teardown(restore);

function results() {
try {
return calls;
} finally {
calls = [];
}
}
results.restore = restore;

return results;
};

Test.prototype._end = function (err) {
var self = this;
if (this._progeny.length) {
Expand Down
17 changes: 17 additions & 0 deletions readme.markdown
Expand Up @@ -396,6 +396,23 @@ The returned wrapper has a `.calls` property, which is an array that will be pop

Modeled after [tap](https://tapjs.github.io/tapjs/modules/_tapjs_intercept.html).

## t.intercept(obj, property, desc = {}, strictMode = true)

Similar to `t.capture()``, but can be used to track get/set operations for any arbitrary property.
Calling the returned `results()` function will return an array of call result objects.
The array of calls will be reset whenever the function is called.
Call result objects will match one of these forms:
- `{ type: 'get', value: '1.2.3', success: true, args: [x, y, z], receiver: o }`
- `{ type: 'set', value: '2.4.6', success: false, args: [x, y, z], receiver: o }`

If `strictMode` is `true`, and `writable` is `false`, and no `get` or `set` is provided, an exception will be thrown when `obj[property]` is assigned to.
If `strictMode` is `false` in this scenario, nothing will be set, but the attempt will still be logged.

Providing both `desc.get` and `desc.set` are optional and can still be useful for logging get/set attempts.

`desc` must be a valid property descriptor, meaning that `get`/`set` are mutually exclusive with `writable`/`value`.
Additionally, explicitly setting `configurable` to `false` is not permitted, so that the property can be restored.

## var htest = test.createHarness()

Create a new test harness instance, which is a function like `test()`, but with a new pending stack and test state.
Expand Down

0 comments on commit e60aeca

Please sign in to comment.