Skip to content

Commit 5d37060

Browse files
committed
[New] add t.intercept()
1 parent 9e21f7a commit 5d37060

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
@@ -275,6 +275,103 @@ Test.prototype.captureFn = function captureFn(original) {
275275
return wrapObject.wrapped;
276276
};
277277

278+
Test.prototype.intercept = function intercept(obj, property) {
279+
if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) {
280+
throw new TypeError('`obj` must be an object');
281+
}
282+
if (typeof property !== 'string' && typeof property !== 'symbol') {
283+
throw new TypeError('`property` must be a string or a symbol');
284+
}
285+
var desc = arguments.length > 2 ? arguments[2] : { __proto__: null };
286+
if (typeof desc !== 'undefined' && (!desc || typeof desc !== 'object')) {
287+
throw new TypeError('`desc`, if provided, must be an object');
288+
}
289+
if ('configurable' in desc && !desc.configurable) {
290+
throw new TypeError('`desc.configurable`, if provided, must be `true`, so that the interception can be restored later');
291+
}
292+
var isData = 'writable' in desc || 'value' in desc;
293+
var isAccessor = 'get' in desc || 'set' in desc;
294+
if (isData && isAccessor) {
295+
throw new TypeError('`value` and `writable` can not be mixed with `get` and `set`');
296+
}
297+
var strictMode = arguments.length > 3 ? arguments[3] : true;
298+
if (typeof strictMode !== 'boolean') {
299+
throw new TypeError('`strictMode`, if provided, must be a boolean');
300+
}
301+
302+
var calls = [];
303+
var getter = desc.get && callBind.apply(desc.get);
304+
var setter = desc.set && callBind.apply(desc.set);
305+
var value = !isAccessor ? desc.value : void undefined;
306+
var writable = !!desc.writable;
307+
308+
function getInterceptor() {
309+
var args = $slice(arguments);
310+
if (isAccessor) {
311+
if (getter) {
312+
var completed = false;
313+
try {
314+
var returned = getter(this, arguments);
315+
completed = true;
316+
$push(calls, { type: 'get', success: true, value: returned, args: args, receiver: this });
317+
return returned;
318+
} finally {
319+
if (!completed) {
320+
$push(calls, { type: 'get', success: false, threw: true, args: args, receiver: this });
321+
}
322+
}
323+
}
324+
}
325+
$push(calls, { type: 'get', success: true, value: value, args: args, receiver: this });
326+
return value;
327+
}
328+
329+
function setInterceptor(v) {
330+
var args = $slice(arguments);
331+
if (isAccessor && setter) {
332+
var completed = false;
333+
try {
334+
var returned = setter(this, arguments);
335+
completed = true;
336+
$push(calls, { type: 'set', success: true, value: v, args: args, receiver: this });
337+
return returned;
338+
} finally {
339+
if (!completed) {
340+
$push(calls, { type: 'set', success: false, threw: true, args: args, receiver: this });
341+
}
342+
}
343+
}
344+
var canSet = isAccessor || writable;
345+
if (canSet) {
346+
value = v;
347+
}
348+
$push(calls, { type: 'set', success: !!canSet, value: value, args: args, receiver: this });
349+
350+
if (!canSet && strictMode) {
351+
throw new TypeError('Cannot assign to read only property \'' + property + '\' of object \'' + inspect(obj) + '\'');
352+
}
353+
return value;
354+
}
355+
356+
var restore = mockProperty(obj, property, {
357+
nonEnumerable: !!desc.enumerable,
358+
get: getInterceptor,
359+
set: setInterceptor
360+
});
361+
this.teardown(restore);
362+
363+
function results() {
364+
try {
365+
return calls;
366+
} finally {
367+
calls = [];
368+
}
369+
}
370+
results.restore = restore;
371+
372+
return results;
373+
};
374+
278375
Test.prototype._end = function _end(err) {
279376
var self = this;
280377

readme.markdown

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

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

423+
## t.intercept(obj, property, desc = {}, strictMode = true)
424+
425+
Similar to `t.capture()``, but can be used to track get/set operations for any arbitrary property.
426+
Calling the returned `results()` function will return an array of call result objects.
427+
The array of calls will be reset whenever the function is called.
428+
Call result objects will match one of these forms:
429+
- `{ type: 'get', value: '1.2.3', success: true, args: [x, y, z], receiver: o }`
430+
- `{ type: 'set', value: '2.4.6', success: false, args: [x, y, z], receiver: o }`
431+
432+
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.
433+
If `strictMode` is `false` in this scenario, nothing will be set, but the attempt will still be logged.
434+
435+
Providing both `desc.get` and `desc.set` are optional and can still be useful for logging get/set attempts.
436+
437+
`desc` must be a valid property descriptor, meaning that `get`/`set` are mutually exclusive with `writable`/`value`.
438+
Additionally, explicitly setting `configurable` to `false` is not permitted, so that the property can be restored.
439+
423440
## var htest = test.createHarness()
424441

425442
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)