Skip to content

Commit e60aeca

Browse files
committed
[New] add t.intercept()
1 parent 3d96d69 commit e60aeca

File tree

3 files changed

+430
-0
lines changed

3 files changed

+430
-0
lines changed

lib/test.js

+97
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,103 @@ Test.prototype.captureFn = function captureFn(original) {
245245
return wrapObject.wrapped;
246246
};
247247

248+
Test.prototype.intercept = function intercept(obj, property) {
249+
if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) {
250+
throw new TypeError('`obj` must be an object');
251+
}
252+
if (typeof property !== 'string' && typeof property !== 'symbol') {
253+
throw new TypeError('`property` must be a string or a symbol');
254+
}
255+
var desc = arguments.length > 2 ? arguments[2] : { __proto__: null };
256+
if (typeof desc !== 'undefined' && (!desc || typeof desc !== 'object')) {
257+
throw new TypeError('`desc`, if provided, must be an object');
258+
}
259+
if ('configurable' in desc && !desc.configurable) {
260+
throw new TypeError('`desc.configurable`, if provided, must be `true`, so that the interception can be restored later');
261+
}
262+
var isData = 'writable' in desc || 'value' in desc;
263+
var isAccessor = 'get' in desc || 'set' in desc;
264+
if (isData && isAccessor) {
265+
throw new TypeError('`value` and `writable` can not be mixed with `get` and `set`');
266+
}
267+
var strictMode = arguments.length > 3 ? arguments[3] : true;
268+
if (typeof strictMode !== 'boolean') {
269+
throw new TypeError('`strictMode`, if provided, must be a boolean');
270+
}
271+
272+
var calls = [];
273+
var getter = desc.get && callBind.apply(desc.get);
274+
var setter = desc.set && callBind.apply(desc.set);
275+
var value = !isAccessor ? desc.value : void undefined;
276+
var writable = !!desc.writable;
277+
278+
function getInterceptor() {
279+
var args = $slice(arguments);
280+
if (isAccessor) {
281+
if (getter) {
282+
var completed = false;
283+
try {
284+
var returned = getter(this, arguments);
285+
completed = true;
286+
$push(calls, { type: 'get', success: true, value: returned, args: args, receiver: this });
287+
return returned;
288+
} finally {
289+
if (!completed) {
290+
$push(calls, { type: 'get', success: false, threw: true, args: args, receiver: this });
291+
}
292+
}
293+
}
294+
}
295+
$push(calls, { type: 'get', success: true, value: value, args: args, receiver: this });
296+
return value;
297+
}
298+
299+
function setInterceptor(v) {
300+
var args = $slice(arguments);
301+
if (isAccessor && setter) {
302+
var completed = false;
303+
try {
304+
var returned = setter(this, arguments);
305+
completed = true;
306+
$push(calls, { type: 'set', success: true, value: v, args: args, receiver: this });
307+
return returned;
308+
} finally {
309+
if (!completed) {
310+
$push(calls, { type: 'set', success: false, threw: true, args: args, receiver: this });
311+
}
312+
}
313+
}
314+
var canSet = isAccessor || writable;
315+
if (canSet) {
316+
value = v;
317+
}
318+
$push(calls, { type: 'set', success: !!canSet, value: value, args: args, receiver: this });
319+
320+
if (!canSet && strictMode) {
321+
throw new TypeError('Cannot assign to read only property \'' + property + '\' of object \'' + inspect(obj) + '\'');
322+
}
323+
return value;
324+
}
325+
326+
var restore = mockProperty(obj, property, {
327+
nonEnumerable: !!desc.enumerable,
328+
get: getInterceptor,
329+
set: setInterceptor
330+
});
331+
this.teardown(restore);
332+
333+
function results() {
334+
try {
335+
return calls;
336+
} finally {
337+
calls = [];
338+
}
339+
}
340+
results.restore = restore;
341+
342+
return results;
343+
};
344+
248345
Test.prototype._end = function (err) {
249346
var self = this;
250347
if (this._progeny.length) {

readme.markdown

+17
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,23 @@ The returned wrapper has a `.calls` property, which is an array that will be pop
396396

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

399+
## t.intercept(obj, property, desc = {}, strictMode = true)
400+
401+
Similar to `t.capture()``, but can be used to track get/set operations for any arbitrary property.
402+
Calling the returned `results()` function will return an array of call result objects.
403+
The array of calls will be reset whenever the function is called.
404+
Call result objects will match one of these forms:
405+
- `{ type: 'get', value: '1.2.3', success: true, args: [x, y, z], receiver: o }`
406+
- `{ type: 'set', value: '2.4.6', success: false, args: [x, y, z], receiver: o }`
407+
408+
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.
409+
If `strictMode` is `false` in this scenario, nothing will be set, but the attempt will still be logged.
410+
411+
Providing both `desc.get` and `desc.set` are optional and can still be useful for logging get/set attempts.
412+
413+
`desc` must be a valid property descriptor, meaning that `get`/`set` are mutually exclusive with `writable`/`value`.
414+
Additionally, explicitly setting `configurable` to `false` is not permitted, so that the property can be restored.
415+
399416
## var htest = test.createHarness()
400417

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

0 commit comments

Comments
 (0)