Skip to content

Commit 9e21f7a

Browse files
committed
[New] add t.capture and t.captureFn, modeled after tap
1 parent 135a952 commit 9e21f7a

File tree

5 files changed

+305
-1
lines changed

5 files changed

+305
-1
lines changed

lib/test.js

+71
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ var inspect = require('object-inspect');
1515
var is = require('object-is');
1616
var objectKeys = require('object-keys');
1717
var every = require('array.prototype.every');
18+
var mockProperty = require('mock-property');
1819

1920
var isEnumerable = callBound('Object.prototype.propertyIsEnumerable');
2021
var toLowerCase = callBound('String.prototype.toLowerCase');
@@ -26,6 +27,7 @@ var $replace = callBound('String.prototype.replace');
2627
var $strSlice = callBound('String.prototype.slice');
2728
var $push = callBound('Array.prototype.push');
2829
var $shift = callBound('Array.prototype.shift');
30+
var $slice = callBound('Array.prototype.slice');
2931

3032
var nextTick = typeof setImmediate !== 'undefined'
3133
? setImmediate
@@ -204,6 +206,75 @@ Test.prototype.teardown = function teardown(fn) {
204206
}
205207
};
206208

209+
function wrapFunction(original) {
210+
if (typeof original !== 'undefined' && typeof original !== 'function') {
211+
throw new TypeError('`original` must be a function or `undefined`');
212+
}
213+
214+
var bound = original && callBind.apply(original);
215+
216+
var calls = [];
217+
218+
var wrapObject = {
219+
__proto__: null,
220+
wrapped: function wrapped() {
221+
var args = $slice(arguments);
222+
var completed = false;
223+
try {
224+
var returned = original ? bound(this, arguments) : void undefined;
225+
$push(calls, { args: args, receiver: this, returned: returned });
226+
completed = true;
227+
return returned;
228+
} finally {
229+
if (!completed) {
230+
$push(calls, { args: args, receiver: this, threw: true });
231+
}
232+
}
233+
},
234+
calls: calls,
235+
results: function results() {
236+
try {
237+
return calls;
238+
} finally {
239+
calls = [];
240+
wrapObject.calls = calls;
241+
}
242+
}
243+
};
244+
return wrapObject;
245+
}
246+
247+
Test.prototype.capture = function capture(obj, method) {
248+
if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) {
249+
throw new TypeError('`obj` must be an object');
250+
}
251+
if (typeof method !== 'string' && typeof method !== 'symbol') {
252+
throw new TypeError('`method` must be a string or a symbol');
253+
}
254+
var implementation = arguments.length > 2 ? arguments[2] : void undefined;
255+
if (typeof implementation !== 'undefined' && typeof implementation !== 'function') {
256+
throw new TypeError('`implementation`, if provided, must be a function');
257+
}
258+
259+
var wrapper = wrapFunction(implementation);
260+
var restore = mockProperty(obj, method, { value: wrapper.wrapped });
261+
this.teardown(restore);
262+
263+
wrapper.results.restore = restore;
264+
265+
return wrapper.results;
266+
};
267+
268+
Test.prototype.captureFn = function captureFn(original) {
269+
if (typeof original !== 'function') {
270+
throw new TypeError('`original` must be a function');
271+
}
272+
273+
var wrapObject = wrapFunction(original);
274+
wrapObject.wrapped.calls = wrapObject.calls;
275+
return wrapObject.wrapped;
276+
};
277+
207278
Test.prototype._end = function _end(err) {
208279
var self = this;
209280

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"inherits": "^2.0.4",
4141
"is-regex": "^1.1.4",
4242
"minimist": "^1.2.8",
43+
"mock-property": "^1.0.0",
4344
"object-inspect": "^1.12.3",
4445
"object-is": "^1.1.5",
4546
"object-keys": "^1.1.1",
@@ -58,6 +59,7 @@
5859
"es-value-fixtures": "^1.4.2",
5960
"eslint": "=8.8.0",
6061
"falafel": "^2.2.5",
62+
"intl-fallback-symbol": "^1.0.0",
6163
"jackspeak": "=2.1.1",
6264
"js-yaml": "^3.14.0",
6365
"npm-run-posix-or-windows": "^2.0.2",

readme.markdown

+25-1
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,8 @@ You may pass the same options that [`test()`](#testname-opts-cb) accepts.
384384

385385
## t.comment(message)
386386

387-
Print a message without breaking the tap output. (Useful when using e.g. `tap-colorize` where output is buffered & `console.log` will print in incorrect order vis-a-vis tap output.)
387+
Print a message without breaking the tap output.
388+
(Useful when using e.g. `tap-colorize` where output is buffered & `console.log` will print in incorrect order vis-a-vis tap output.)
388389

389390
Multiline output will be split by `\n` characters, and each one printed as a comment.
390391

@@ -396,6 +397,29 @@ Assert that `string` matches the RegExp `regexp`. Will fail when the first two a
396397

397398
Assert that `string` does not match the RegExp `regexp`. Will fail when the first two arguments are the wrong type.
398399

400+
## t.capture(obj, method, implementation = () => {})
401+
402+
Replaces `obj[method]` with the supplied implementation.
403+
`obj` must be a non-primitive, `method` must be a valid property key (string or symbol), and `implementation`, if provided, must be a function.
404+
405+
Calling the returned `results()` function will return an array of call result objects.
406+
The array of calls will be reset whenever the function is called.
407+
Call result objects will match one of these forms:
408+
- `{ args: [x, y, z], receiver: o, returned: a }`
409+
- `{ args: [x, y, z], receiver: o, threw: true }`
410+
411+
The replacement will automatically be restored on test teardown.
412+
You can restore it manually, if desired, by calling `.restore()` on the returned results function.
413+
414+
Modeled after [tap](https://tapjs.github.io/tapjs/modules/_tapjs_intercept.html).
415+
416+
## t.captureFn(original)
417+
418+
Wraps the supplied function.
419+
The returned wrapper has a `.calls` property, which is an array that will be populated with call result objects, described under `t.capture()`.
420+
421+
Modeled after [tap](https://tapjs.github.io/tapjs/modules/_tapjs_intercept.html).
422+
399423
## var htest = test.createHarness()
400424

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

test/capture.js

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
'use strict';
2+
3+
var tape = require('../');
4+
var tap = require('tap');
5+
var concat = require('concat-stream');
6+
var inspect = require('object-inspect');
7+
var forEach = require('for-each');
8+
var v = require('es-value-fixtures');
9+
10+
var stripFullStack = require('./common').stripFullStack;
11+
12+
tap.test('capture: output', function (tt) {
13+
tt.plan(1);
14+
15+
var test = tape.createHarness();
16+
var count = 0;
17+
test.createStream().pipe(concat(function (body) {
18+
tt.same(stripFullStack(body.toString('utf8')), [].concat(
19+
'TAP version 13',
20+
'# argument validation',
21+
v.primitives.map(function (x) {
22+
return 'ok ' + ++count + ' ' + inspect(x) + ' is not an Object';
23+
}),
24+
v.nonPropertyKeys.map(function (x) {
25+
return 'ok ' + ++count + ' ' + inspect(x) + ' is not a valid property key';
26+
}),
27+
v.nonFunctions.filter(function (x) { return typeof x !== 'undefined'; }).map(function (x) {
28+
return 'ok ' + ++count + ' ' + inspect(x) + ' is not a function';
29+
}),
30+
'# captures calls',
31+
'ok ' + ++count + ' property has expected initial value',
32+
'# capturing',
33+
'ok ' + ++count + ' throwing implementation throws',
34+
'ok ' + ++count + ' should be deeply equivalent',
35+
'ok ' + ++count + ' should be deeply equivalent',
36+
'ok ' + ++count + ' should be deeply equivalent',
37+
'ok ' + ++count + ' should be deeply equivalent',
38+
'ok ' + ++count + ' should be deeply equivalent',
39+
'# post-capturing',
40+
'ok ' + ++count + ' property is restored',
41+
'ok ' + ++count + ' added property is removed',
42+
'',
43+
'1..' + count,
44+
'# tests ' + count,
45+
'# pass ' + count,
46+
'',
47+
'# ok',
48+
''
49+
));
50+
}));
51+
52+
test('argument validation', function (t) {
53+
forEach(v.primitives, function (primitive) {
54+
t.throws(
55+
function () { t.capture(primitive, ''); },
56+
TypeError,
57+
inspect(primitive) + ' is not an Object'
58+
);
59+
});
60+
61+
forEach(v.nonPropertyKeys, function (nonPropertyKey) {
62+
t.throws(
63+
function () { t.capture({}, nonPropertyKey); },
64+
TypeError,
65+
inspect(nonPropertyKey) + ' is not a valid property key'
66+
);
67+
});
68+
69+
forEach(v.nonFunctions, function (nonFunction) {
70+
if (typeof nonFunction !== 'undefined') {
71+
t.throws(
72+
function () { t.capture({}, '', nonFunction); },
73+
TypeError,
74+
inspect(nonFunction) + ' is not a function'
75+
);
76+
}
77+
});
78+
79+
t.end();
80+
});
81+
82+
test('captures calls', function (t) {
83+
var sentinel = { sentinel: true, inspect: function () { return '{ SENTINEL OBJECT }'; } };
84+
var o = { foo: sentinel, inspect: function () { return '{ o OBJECT }'; } };
85+
t.equal(o.foo, sentinel, 'property has expected initial value');
86+
87+
t.test('capturing', function (st) {
88+
var results = st.capture(o, 'foo', function () { return sentinel; });
89+
var results2 = st.capture(o, 'foo2');
90+
var up = new SyntaxError('foo');
91+
var resultsThrow = st.capture(o, 'fooThrow', function () { throw up; });
92+
93+
o.foo(1, 2, 3);
94+
o.foo(3, 4, 5);
95+
o.foo2.call(sentinel, 1);
96+
st.throws(
97+
function () { o.fooThrow(1, 2, 3); },
98+
SyntaxError,
99+
'throwing implementation throws'
100+
);
101+
102+
st.deepEqual(results(), [
103+
{ args: [1, 2, 3], receiver: o, returned: sentinel },
104+
{ args: [3, 4, 5], receiver: o, returned: sentinel }
105+
]);
106+
st.deepEqual(results(), []);
107+
108+
o.foo(6, 7, 8);
109+
st.deepEqual(results(), [
110+
{ args: [6, 7, 8], receiver: o, returned: sentinel }
111+
]);
112+
113+
st.deepEqual(results2(), [
114+
{ args: [1], receiver: sentinel, returned: undefined }
115+
]);
116+
st.deepEqual(resultsThrow(), [
117+
{ args: [1, 2, 3], receiver: o, threw: true }
118+
]);
119+
120+
st.end();
121+
});
122+
123+
t.test('post-capturing', function (st) {
124+
st.equal(o.foo, sentinel, 'property is restored');
125+
st.notOk('foo2' in o, 'added property is removed');
126+
127+
st.end();
128+
});
129+
130+
t.end();
131+
});
132+
});

test/captureFn.js

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
'use strict';
2+
3+
var tape = require('../');
4+
var tap = require('tap');
5+
var concat = require('concat-stream');
6+
var inspect = require('object-inspect');
7+
var forEach = require('for-each');
8+
var v = require('es-value-fixtures');
9+
10+
var stripFullStack = require('./common').stripFullStack;
11+
12+
tap.test('captureFn: output', function (tt) {
13+
tt.plan(1);
14+
15+
var test = tape.createHarness();
16+
var count = 0;
17+
test.createStream().pipe(concat(function (body) {
18+
tt.same(stripFullStack(body.toString('utf8')), [].concat(
19+
'TAP version 13',
20+
'# argument validation',
21+
v.nonFunctions.map(function (x) {
22+
return 'ok ' + ++count + ' ' + inspect(x) + ' is not a function';
23+
}),
24+
'# captured fn calls',
25+
'ok ' + ++count + ' return value is passed through',
26+
'ok ' + ++count + ' throwing implementation throws',
27+
'ok ' + ++count + ' should be deeply equivalent',
28+
'ok ' + ++count + ' should be deeply equivalent',
29+
'',
30+
'1..' + count,
31+
'# tests ' + count,
32+
'# pass ' + count,
33+
'',
34+
'# ok',
35+
''
36+
));
37+
}));
38+
39+
test('argument validation', function (t) {
40+
forEach(v.nonFunctions, function (nonFunction) {
41+
t.throws(
42+
function () { t.captureFn(nonFunction); },
43+
TypeError,
44+
inspect(nonFunction) + ' is not a function'
45+
);
46+
});
47+
48+
t.end();
49+
});
50+
51+
test('captured fn calls', function (t) {
52+
var sentinel = { sentinel: true, inspect: function () { return '{ SENTINEL OBJECT }'; } };
53+
54+
var wrappedSentinelThunk = t.captureFn(function () { return sentinel; });
55+
var up = new SyntaxError('foo');
56+
var wrappedThrower = t.captureFn(function () { throw up; });
57+
58+
t.equal(wrappedSentinelThunk(1, 2), sentinel, 'return value is passed through');
59+
t.throws(
60+
function () { wrappedThrower.call(sentinel, 1, 2, 3); },
61+
SyntaxError,
62+
'throwing implementation throws'
63+
);
64+
65+
t.deepEqual(wrappedSentinelThunk.calls, [
66+
{ args: [1, 2], receiver: undefined, returned: sentinel }
67+
]);
68+
69+
t.deepEqual(wrappedThrower.calls, [
70+
{ args: [1, 2, 3], receiver: sentinel, threw: true }
71+
]);
72+
73+
t.end();
74+
});
75+
});

0 commit comments

Comments
 (0)